Mom's Helper — дизайн-система
Единая страница дизайн-системы mamkin-mini-app: цвета, типографика, foundations, layout, компоненты, иконки — из реального кода
src/. Контекст и метрики продукта — в кейсе Mom’s Helper.
Цветовые токены проекта
Компонент не знает своего цвета — он знает роль («фон карточки», «вторичный текст»), а значение подставляет тема. Поэтому имя одно, а значений два: светлое и тёмное. У акцента ребёнка — семь.
Образец — реальный цвет из src/index.css, на диагональной подложке: иначе полупрозрачные *-bg не видно ни в одной теме. Примитив и HEX — в разделе Figma ↔ Код.
Semantic tokens — Light mode
Двенадцать переменных задают всю светлую тему — фоны, текст, границы.
| Образец | CSS-переменная | Semantic | Где используется |
|---|---|---|---|
--bg-page | bg/page | Фон страницы | |
--bg-card | bg/card | Фон карточки/шита | |
--bg-card2 | bg/card2 | Фон вложенной группы (settings) | |
--bg-input | bg/input | Фон полей ввода, неактивные кнопки | |
--border-card | border/card | Обводка карточек/полей | |
--text-primary | text/primary | Основной текст | |
--text-secondary | text/secondary | Вторичный текст | |
--text-placeholder | text/placeholder | Плейсхолдеры | |
--row-divider | row/divider | Разделители строк списков | |
--nav-bg | nav/bg | Фон нижней навигации | |
--nav-border | nav/border | Обводка нижней навигации | |
--icon-btn-bg | icon-btn/bg | Фон Button variant=subtle |
Semantic tokens — Dark mode
Те же двенадцать имён, другие значения. В Figma это Modes одной переменной, не вторая переменная.
| Образец | CSS-переменная | Semantic | Где используется |
|---|---|---|---|
--bg-page | bg/page | Фон страницы | |
--bg-card | bg/card | Фон карточки/шита | |
--bg-card2 | bg/card2 | Фон вложенной группы (settings) | |
--bg-input | bg/input | Фон полей ввода, неактивные кнопки | |
--border-card | border/card | Обводка карточек/полей | |
--text-primary | text/primary | Основной текст | |
--text-secondary | text/secondary | Вторичный текст | |
--text-placeholder | text/placeholder | Плейсхолдеры | |
--row-divider | row/divider | Разделители строк списков | |
--nav-bg | nav/bg | Фон нижней навигации | |
--nav-border | nav/border | Обводка нижней навигации | |
--icon-btn-bg | icon-btn/bg | Фон Button variant=subtle |
Semantic tokens — Status & CTA
Четыре статуса, у каждого пара: насыщенный цвет для текста и иконки плюс фон плашки — тот же цвет, 15% в тёмной теме и 12% в светлой.
Dark
| Образец | CSS-переменная | Semantic | Где используется |
|---|---|---|---|
--success | success | Подтверждение, идёт/активно | |
--success-bg | success-bg | Подтверждение, идёт/активно — фон плашки | |
--warning | warning | Предупреждение, error-баннер Dashboard | |
--warning-bg | warning-bg | Предупреждение, error-баннер Dashboard — фон плашки | |
--danger | danger | Удаление, ошибки ввода | |
--danger-bg | danger-bg | Удаление, ошибки ввода — фон плашки | |
--info | info | Кормление в EventRow/History | |
--info-bg | info-bg | Кормление в EventRow/History — фон плашки |
Light
| Образец | CSS-переменная | Semantic | Где используется |
|---|---|---|---|
--success | success | Подтверждение, идёт/активно | |
--success-bg | success-bg | Подтверждение, идёт/активно — фон плашки | |
--warning | warning | Предупреждение, error-баннер Dashboard | |
--warning-bg | warning-bg | Предупреждение, error-баннер Dashboard — фон плашки | |
--danger | danger | Удаление, ошибки ввода | |
--danger-bg | danger-bg | Удаление, ошибки ввода — фон плашки | |
--info | info | Кормление в EventRow/History | |
--info-bg | info-bg | Кормление в EventRow/History — фон плашки |
Semantic tokens — Accent ребёнка
Не зависит от темы — выбирается в настройках ребёнка. Строка — вариант, колонка — роль: --accent красит CTA и аватар, --accent-light — подложку, --accent-text — текст поверх неё (светлее: основной на подложке не проходит по контрасту), --accent-border — обводку и кольцо фокуса. В коде это четыре переменные на семь вариантов, значение переключает [data-accent]; в Figma придётся завести все 28, и нейтральный вариант там зовётся …/default, а не gray.
| Вариант | --accent | --accent-light | --accent-text | --accent-border |
|---|---|---|---|---|
grayБез цвета — имя в Figma …/default | accent/default | accent-light/default | accent-text/default | accent-border/default |
purpleФиолетовый | accent/purple | accent-light/purple | accent-text/purple | accent-border/purple |
blueСиний | accent/blue | accent-light/blue | accent-text/blue | accent-border/blue |
greenЗелёный | accent/green | accent-light/green | accent-text/green | accent-border/green |
pinkРозовый | accent/pink | accent-light/pink | accent-text/pink | accent-border/pink |
orangeОранжевый | accent/orange | accent-light/orange | accent-text/orange | accent-border/orange |
tealБирюзовый | accent/teal | accent-light/teal | accent-text/teal | accent-border/teal |
Типографика
Четырнадцать текстовых стилей на всё приложение. Стиль — это не «размер шрифта», а связка размер + вес + интерлиньяж + трекинг: применяется одним кликом и одинаково выглядит везде.
Шрифт
Своего веб-шрифта нет — стек -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif. Интерфейс берёт шрифт платформы: на iOS это SF Pro, на Android — Roboto. Плюс — ноль килобайт загрузки и родное для системы начертание; минус — метрики на разных платформах не совпадают, поэтому нигде нет вёрстки, которая держится на точной ширине строки.
Насыщенность — четыре веса
Иерархию держит вес, а не размер: на 14px три стиля и на 12px ещё три — внутри пары размеров они различаются только весом. Поэтому шкала размеров такая короткая.
Интерлиньяж — часть стиля, а не следствие размера
leading-noneмл
text-smза сегодня
У крупных цифр интерлиньяж сжат до 100%, иначе число «висит» в пустоте и ломает высоту карточки. У текста — 143%, иначе абзац не читается. Одно и то же значение обоим не подходит, поэтому оно и вшито в стиль.
Text Styles 14 стилей
«Образец» — как стиль выглядит на самом деле, «Semantic» — имя Text Style для Figma, «Реализация» — класс, которым он набран в коде. Переменные, из которых стиль собран внутри Figma (font-size/*, weight/*), — в разделе Figma ↔ Код.
| Образец | Semantic | Реализация | Где используется |
|---|---|---|---|
| 254 мл | Number | text-2xl font-bold leading-none | Крупные цифры статистики (StatCard) |
| Статистика | PageTitle | text-xl font-bold | Заголовки экранов и разделов Главной |
| Добавить ребёнка | ModalTitle | text-lg font-bold | Заголовки модалок и разделов (AddChildModal, Settings, Premium) |
| Кормление | SheetHeader | text-base font-semibold | Заголовок шторки и карточки ребёнка |
| 3 ч 40 мин | TileValue | text-sm font-semibold leading-tight | StatusTile — есть в коде, в проде не используется |
| Ночной режим | BodyPrimary | text-sm font-medium | Заголовок строки списка (ListRow); в EventRow — semibold |
| Пока нет записей за сегодня | Body | text-sm | Загрузка, ошибка, пусто, описания форм — самый частый паттерн, около 40 мест |
| Изменить | AccentCaption | text-xs font-semibold | Кнопки статус-бара, бейдж Premium, дельта в Статистике |
| Кормление | ButtonCaption | text-xs font-medium | Подпись под иконкой действия (secondary-кнопки) |
| 2 часа назад | BodySecondary | text-xs | Вторая строка в списках, подписи без явного веса |
| Уведомления | SectionLabel | text-[11px] font-semibold + inline | Заголовки секций, UPPERCASE |
| Главная | TabLabel | text-[11px] font-medium leading-none | Подпись таба нижней навигации |
| PREMIUM | SmallBadge | text-[10px] | Маленький бейдж (Badge size=sm, вес — из базового класса) |
| скоро | MicroBadge | text-[8px] | Бейдж «скоро» (Badge size=xs) |
Layout & grid
Одноколоночный мобильный контейнер с вертикальным скроллом, без классической сетки колонок и без брейкпоинтов — это Telegram Mini App с одним форм-фактором, а не адаптивный сайт под разные вьюпорты. Правила ниже — из кода, а не из макета.
Модуль
Базовая единица пространства — 4px, в коде это --space-unit: 0.25rem. Из неё выведены все отступы, зазоры, радиусы и высоты контролов. Есть и микромодуль 2px — половина шага, только для отбивки строк текста внутри блока, где полный шаг уже слишком много.
В Figma шкала выгружена целиком, 33 значения вплоть до 448px — но это справочник, а не рабочий набор. Реально в коде живут десять ступеней:
| Токен | В коде | Где применяется |
|---|---|---|
space/2 | p-0.5 · gap-0.5 | Отбивка подписи от заголовка, зазор внутри бейджа |
space/4 | p-1 · gap-1 | Зазор между иконкой и подписью в плитке |
space/6 | p-1.5 · gap-1.5 | Вертикальный паддинг мелкой кнопки |
space/8 | p-2 · gap-2 | Основной зазор: сетки карточек, хвост строки |
space/10 | p-2.5 · gap-2.5 | Зазор между крупными плитками Быстрой записи |
space/12 | p-3 · gap-3 | Вертикальный паддинг строки, зазор иконка ↔ текст |
space/14 | p-3.5 | Вертикальный паддинг крупной кнопки |
space/16 | p-4 · gap-4 | Поля страницы, паддинг карточки — самое частое значение |
space/20 | p-5 · mb-5 | Отступ перед заголовком секции |
space/24 | p-6 | Боковой паддинг крупной кнопки, поля экрана-заглушки |
Три вида отступа
Одна и та же ступень значит разное в зависимости от того, что она разделяет.
| Вид | В коде | Правило |
|---|---|---|
| Внутренний — от края блока до содержимого | p- · px- · py- | Принадлежит компоненту: карточка и строка задают его сами, снаружи не переопределяется |
| Между соседями внутри блока | gap- | Тоже принадлежит компоненту — это его внутреннее устройство, а не вёрстка страницы |
| Внешний — до следующего блока | mb- | Принадлежит странице, а не компоненту: сколько воздуха между блоками — решает экран. Поэтому у карточки списка своего внешнего отступа нет |
Отступы
Всё кратно четырём: базовая единица --space-unit: 0.25rem, то есть 4px. Ниже — как это разложено по уровням, от контейнера страницы до зазора между иконкой и текстом.
| Уровень | Чем задан | Значение и где |
|---|---|---|
| Контейнер страницы | max-w-md · mx-auto | 448px, по центру. Один на все страницы — это Mini App с одним форм-фактором |
| Поля страницы | page-content · px-4 | 16px по бокам. Так открываются все семь экранов |
| Низ страницы | safe-area-inset-bottom + 96px | Запас под плавающую навигацию, чтобы последний блок не уезжал под неё |
| Плавающая навигация | safe-area-inset-bottom + 10px | Отсчёт от системной области, а не от края окна |
| Паддинг карточки | p-4 | 16px со всех сторон |
| Паддинг строки списка | px-4 · py-3 | 16 × 12px, минимум 52px высоты. Задаёт сам компонент, см. ListRow |
| Зазор в сетке | gap-2 | 8px: сетки карточек, плиток, хвост строки |
| Зазор иконка ↔ текст | gap-3 | 12px |
| Между блоками | mb-2 · mb-3 · mb-4 · mb-5 | 8 / 12 / 16 / 20px — шаг выбирается по весу блока |
Компоненты
Компоненты приложения по одному — реальные варианты, размеры и состояния из кода, не выдуманные заново. Разделены на примитивы (неделимый контрол, одна роль) и составные (несколько слотов, повторяющиеся элементы или прямой вызов другого компонента внутри).
Все компоненты разом 23 компонента
Чем компоненты отличаются друг от друга и какие вообще ни на что не реагируют. Статичный — состояний нет вовсе: компонент только рисует, нажатие и фокус живут на элементе вокруг. Состояния названы здесь по смыслу и одинаково, чтобы строки сравнивались; в самих блоках имена повторяют код — checked у Switch, selected у ChipSelect, active у Button и BottomNav.
| Компонент | Тип | Состав | Состояния |
|---|---|---|---|
Button | примитив | Иконка и подпись; 5 вариантов × 3 размера | нажат · фокус · выключен · загрузка · выбран |
Switch | примитив | Трек и бегунок, без слотов | вкл · выкл · фокус · выключен |
Badge | примитив | Только текст; 6 тонов × 3 размера | состояний нет |
Avatar | примитив | Эмодзи, фото или силуэт; 3 размера | состояний нет |
Field | примитив | Подпись и поверхность — основа для Input, SelectField и TimeSelect | состояний нет |
Input | примитив | Field плюс поле ввода | фокус · выключен · ошибка |
Slider | примитив | Дорожка и бегунок | фокус · выключен |
Skeleton | примитив | Заглушка загрузки; 3 варианта формы | состояний нет |
AccentPlate | примитив | Акцентная поверхность и слот под содержимое | состояний нет |
PopoverPanel | примитив | Только поверхность попапа, содержимое передаётся снаружи | состояний нет |
Notice | составной | Текст, заголовок, слот действий и крестик; 3 тона. Внутри Button | состояний нет |
ScreenState | составной | Эмодзи, заголовок, текст и слот действия. Занимает весь экран | загрузка · ошибка |
ChipSelect | составной | Ряд пилюль: иконка и подпись; обычный и compact | выбран · нажат · фокус · выключен |
QuickActionButton | составной | Эмодзи, подпись, бейдж; primary и secondary | выбран · нажат · фокус · выключен |
ListRow | составной | Заголовок секции, карточка и строка: иконка или аватар, заголовок, подпись, заметка, хвост | нажат · фокус · погашен · выбран · опасный |
BottomNav | составной | Четыре таба: иконка и подпись | активный · нажат |
StatCard | составной | Заголовок, значение с единицей, вторая строка, бейдж | нет данных · погашен |
SelectField | составной | Field плюс список опций | открыт · фокус · выключен |
TimeSelect | составной | Field плюс колёса часов и минут | открыт · выключен |
SliderField | составной | Подпись, Slider и текстовая подсказка | состояния приходят из Slider |
Sheet | составной | Шторка снизу: ручка, шапка, тело, секции, подвал | открыта · закрыта · свайп |
Примитивы
Собраны сами по себе — из токенов и разметки, без других компонентов внутри. Критерий тот же, что в сводной таблице: тип определяется тем, из чего компонент собран, а не тем, что он импортирует. Поэтому PopoverPanel — примитив, хотя стоит поверх Radix: своя у него только поверхность.
Button ui-kit/Button.tsx
Кнопка действия — текст, иконка или и то и другое. Без children рендерится как квадратная icon-only кнопка.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Подпись | BodyPrimary · ButtonCaption | 14/500 — md и lg; 12/500 — sm |
| Скругление | rounded-control | 10px |
| Обводка | stroke/thin · border/card | 1px у secondary и subtle; прозрачная (не отсутствует — иначе active сдвигал бы кнопку на 1px) у primary и danger; нет у ghost |
| Высота — минимум, не фикс | min-h-8 · min-h-11 · min-h-13 | 32 / 44 / 52px |
| Отступы по бокам | space/12 · space/20 · space/24 | 12 / 20 / 24px |
| Зазор иконка ↔ подпись | gap-1-5 | 6px |
| Заливка | --accent · --bg-card · --danger · --icon-btn-bg | акцент / карточка / danger / прозрачная / icon-btn |
| Цвет подписи | --text-primary · --text-secondary | белый на заливке, иначе основной или вторичный |
Варианты
| Вариант | Заливка | Обводка | Выключено |
|---|---|---|---|
primary | var(--accent), заливка | transparent (плейсхолдер) | полная смена палитры → нейтральный серый, не притушенный акцент |
secondary | var(--bg-card), заливка | var(--border-card), 1px | просто opacity-50 — и так нейтральный |
danger | var(--danger), заливка | transparent (плейсхолдер) | та же смена палитры, что у primary |
ghost | transparent | нет | opacity-50 |
subtle | var(--icon-btn-bg), полупрозрачная | var(--border-card), 1px | opacity-50 · для кнопок на цветном/акцентном фоне |
Состояния
| Состояние | Токен | Что меняется |
|---|---|---|
default | — | Базовый вид |
pressed | opacity/70 | Кнопка гаснет |
active | --accent | Свойство подменяет вариант на primary — «выбрано» у переключателей вида графика. То же свойство и тот же смысл, что у QuickActionButton и BottomNav |
focus-visible | focus-ring · --accent-border | Кольцо 2px с зазором 2px, бледный акцент |
disabled | --bg-input · opacity/50 | primary и danger получают нейтральную заливку вместо акцента; остальные просто гаснут |
loading | — | Спиннер вместо содержимого, кнопка не нажимается |
Свойства
| Свойство | Назначение |
|---|---|
variant | primary · secondary · danger · ghost · subtle |
size | sm · md (по умолчанию) · lg |
icon | Иконка перед подписью; без подписи — квадратная icon-only кнопка |
active | Подменяет вариант на primary — «выбрано» у переключателей вида графика |
loading | Спиннер вместо содержимого, кнопка не нажимается |
fullWidth | Растянуть на всю ширину контейнера |
Switch ui/switch.tsx
Бинарный переключатель вкл/выкл — обёртка над Radix Switch.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Трек | w-11 · h-6 | 44 × 24px |
| Бегунок | h-5 · space/20 | 20 × 20px, белый |
| Сдвиг включённого | translate-x-5 | 20px |
| Отступы | border-2 | нет — инсет под бегунок создаёт прозрачная рамка: 24 − 2×2 = 20px |
| Скругление | rounded-full | полное — и трек, и бегунок |
| Заливка вкл / выкл | --accent · --bg-input | акцент / фон поля |
| Тень бегунка | shadow-lg · opacity/25 | мягкая, 25% чёрного |
focus-visible | --accent-border | кольцо 2px внутрь |
disabled | opacity/50 | гаснет целиком |
pressed | — | нет — его не было ни у одного из двух прежних компонентов, при объединении не добавляли |
Состояния
| Состояние | Токен | Что меняется |
|---|---|---|
checked / unchecked | --accent · --bg-input | Заливка трека и сдвиг бегунка на 20px |
focus-visible | --accent-border | Кольцо 2px внутрь |
disabled | opacity/50 | Гаснет целиком |
pressed | — | Нет — не было ни у одного из двух прежних компонентов, при объединении не добавляли |
Свойства
| Свойство | Назначение |
|---|---|
checked | Состояние переключателя |
onCheckedChange | Обработчик переключения |
disabled | Заблокирован |
Badge ui-kit/Badge.tsx
Пилюля со статусом или тегом: «скоро», «Premium», счётчик. Не нажимается, и об этом говорит форма — всегда rounded-full, тогда как всё нажимаемое рядом прямоугольное. Поэтому переопределять форму по месту нельзя.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Подпись — md | ButtonCaption | 12/500 |
| Подпись — sm | SmallBadge | 10/500 |
| Подпись — xs | MicroBadge | 8/500 |
| Отступы | space/10 · space/8 · space/6 | 10 × 4px у md · 8 × 2px у sm · 6 × 2px у xs |
| Скругление | rounded-full | полное, всегда |
| Зазор иконка ↔ подпись | gap-1 | 4px |
| Тона (6) | --accent-light · --success-bg · --warning-bg · --danger-bg · --info-bg · --bg-input | полупрозрачная заливка и насыщенный текст того же цвета |
Свойства
| Свойство | Назначение |
|---|---|
tone | neutral (по умолчанию) · accent · success · warning · danger · info |
size | xs (8px) · sm (10px) · md (12px, по умолчанию) |
Avatar ui-kit/Avatar.tsx
Кружок с эмодзи или фото ребёнка. Свойство onAccent — полупрозрачно-белая заливка, когда аватар сидит на акцентной плашке. Не интерактивен.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Размеры | h-8 · h-10 · h-14 | 32 / 40 / 56px |
| Эмодзи внутри | font-size/14 · font-size/20 | 14 / 20 / 30px — по размеру |
| Скругление | rounded-full | полное |
| Заливка | --accent-light · opacity/25 | акцентная подложка ребёнка; на акцентной плашке — полупрозрачно-белая |
Свойства
| Свойство | Назначение |
|---|---|
size | sm · md (по умолчанию) · lg |
emoji | Эмодзи ребёнка |
photoUrl | Фото вместо эмодзи |
background | Заливка кружка — акцентный цвет ребёнка |
onAccent | Полупрозрачно-белая заливка: аватар сидит на акцентной плашке |
Input и Field ui-kit/Input.tsx · ui-kit/Field.tsx
Input — готовое поле ввода, текст или число. Field под ним — не поле, а основа: подпись над контролом и общая строка классов поверхности FIELD_SURFACE. Сам по себе Field ничего не рисует — без label он возвращает содержимое как есть. Тот же Field используют SelectField и TimeSelect, поэтому подпись и высота у всех трёх одинаковые.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Поле — высота | h-11 | 44px |
| Поле — отступ по бокам | space/16 | 16px |
| Поле — текст | BodyPrimary | 14/400 |
| Поле — обводка и заливка | border/card · --bg-input | 1px, фон поля |
| Подпись над полем | ButtonCaption · space/6 | 12/500, отступ снизу 6px |
| Скругление | rounded-control | 10px |
Состояния
| Состояние | Токен | Что меняется |
|---|---|---|
default | — | Базовый вид |
focus-visible | --accent-border | Кольцо 2px без зазора — у поля уже есть своя обводка. |
disabled | opacity/60 | Гаснет, курсор запрета |
error | --danger | Обводка поля становится danger. Сообщение поле не показывает — его рисует Notice под формой; обводка нужна, чтобы было видно, какое именно поле чинить |
Свойства
| Свойство | Назначение |
|---|---|
label | Подпись над полем — только у Input |
value | Значение |
onChange | Обработчик ввода |
placeholder | Подсказка в пустом поле — только у Input |
disabled | Заблокировано |
error | Не прошло проверку — обводка danger. Есть и у SelectField |
Что содержит
| Экспорт | Назначение |
|---|---|
FIELD_SURFACE | Строка классов поверхности: высота, отступы, радиус, обводка, кегль. Без w-full и фона — их добавляет конкретное поле |
Field | Обёртка с подписью над контролом или группой контролов — например, «Дата рождения» над тремя списками или «Тип питания» над ChipSelect. Нет label — контрол возвращается как есть, без лишнего <div> |
Skeleton ui-kit/Skeleton.tsx
Пульсирующая заглушка на время загрузки. Отдельно есть ListRowSkeleton — загрузочное состояние строки списка, живёт в одном файле с ней. Не интерактивен.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Заливка | --bg-card | фон карточки |
| Анимация | animate-pulse | пульсация, бесконечная |
| Скругление | rounded-xs · rounded-full · rounded-control | 4px text · полное circle · 10px block |
| Высота текстовой строки | h-3-5 · h-3 | по кеглю: 14px заголовок, 12px подпись |
Варианты
| Вариант | Скругление | Под что |
|---|---|---|
text | 4px | Строка текста. Высота повторяет кегль: заголовок 14px, подпись 12px |
circle | 999px | Аватар, точка, трек переключателя |
block | 10px | Карточка, кнопка, крупный блок — по умолчанию |
Свойства
| Свойство | Назначение |
|---|---|
variant | text · circle · block (по умолчанию) |
className | Размеры заглушки — задаются по месту |
AccentPlate ui-kit/AccentPlate.tsx
Акцентная плашка: скруглённый блок с градиентом поверх акцентного цвета. Сам по себе — только фон и отступы, все состояния на элементах внутри.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Скругление | rounded-plate | 28px |
| Отступы | space/16 | 16px со всех сторон |
| Отступ снизу | space/20 | 20px |
| Заливка | --accent-gradient | градиент поверх акцентного цвета |
Свойства
| Свойство | Назначение |
|---|---|
children | Содержимое плашки |
className | Раскладка внутри — задаётся по месту |
ACCENT_PLATE_BG | Не свойство, а экспорт константы градиента — им же красится ChildSwitcherPopover, когда стоит не на плашке |
PopoverPanel «поверхность выпадающего списка» ui-kit/PopoverPanel.tsx
Стилизованная поверхность попапа — рамка, фон, тень — поверх Radix PopoverContent. Содержимое передаётся снаружи. Примитив, а не составной: критерий — из чего собран сам, а не что импортирует.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Заливка | --bg-card | фон карточки |
| Обводка | --border-card | 1px |
| Тень | shadow/popover | 0 8px 24px rgba(0,0,0,.18) |
| Скругление | rounded-control | 10px |
Свойства
| Свойство | Назначение |
|---|---|
align, sideOffset | Позиционирование — те же свойства Radix PopoverContent, компонент их только принимает и пробрасывает дальше |
className | Внутренние отступы/ширина — по месту (p-0 overflow-hidden w-56 у списка детей, p-1.5 w-56 у метрик) |
children | Содержимое — своё в каждом случае, компонент ничего не знает про список внутри |
Slider и SliderField ui-kit/Slider.tsx · ui-kit/SliderField.tsx
Обвязка вокруг Slider: подпись слева, текущее значение справа, ползунок, необязательная подсказка снизу. Не слит с Input и SelectField: у тех общая 44px-коробка с обводкой и фоном, у ползунка своей поверхности нет.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Дорожка ползунка | h-1-5 · rounded-full | 6px, полное скругление |
| Кружок ползунка | --accent | 18×18px — вне шкалы space, инлайн CSS (::-webkit-slider-thumb/::-moz-range-thumb в index.css, не Tailwind-класс) |
| Фокус-кольцо кружка | --accent-border | 2px, с зазором 2px от кружка |
| Подпись | --text-secondary | 12/400, text-secondary |
| Значение | --text-primary | 14/700, text-primary |
| Отступ строки подписи до ползунка | mb-1-5 | 6px |
| Ползунок | — | как отдельный Slider |
Состояния
Показаны на голом Slider: у SliderField своих состояний нет, он их только оборачивает.
| Состояние | Токен | Что меняется |
|---|---|---|
default | --accent | Заполненная часть дорожки и бегунок — акцентом, через accent-color. Вид бегунка отдан браузеру |
disabled | opacity/60 | Гаснет целиком, курсор запрета |
| focus | — | Видимого состояния нет: outline-none снимает системное кольцо, и своего взамен не поставлено — единственный интерактивный элемент набора без фокус-кольца |
Свойства
| Свойство | Назначение |
|---|---|
min / max / step | Диапазон — только у Slider |
label | Подпись слева над ползунком |
valueLabel | Текущее значение справа — отформатировано вызывающим (число + единица) |
hint | Произвольный контент под ползунком — подсказка мин/макс или текст валидации. Несёт свой отступ сверху сам |
...остальное | Все свойства Slider — min/max/step/value/onChange/disabled |
Составные
Собраны из примитивов. Здесь же лежат компоненты, знающие про домен приложения — ребёнка, Premium, типы записей, — тогда как примитивы про данные не знают ничего.
Notice «плашка-сообщение» ui-kit/Notice.tsx
Сообщение с необязательными действиями справа: ошибка формы, сбой загрузки, смена часового пояса, предложение пробного периода. Собран из четырёх копий одного паттерна — три баннера писались руками прямо на страницах, четвёртой была плашка под формой.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Текст | BodyPrimary | 14/400 |
| Заголовок | weight/semibold | только когда задан title |
| Отступы | space/16 · space/12 | 16 × 12px |
| Скругление | rounded-control | 10px |
| Обводка | --danger · --success · --accent | 1px цветом тона |
| Заливка | --danger-bg · --success-bg · --accent-light | полупрозрачная, по тону |
| Крестик | Button | variant ghost, size sm — 32×32, иконка 16px |
| Зазор до действий | space/12 | 12px |
Варианты
| Тон | Токен | Когда |
|---|---|---|
danger | --danger | Операция не удалась — ошибка формы, сбой загрузки, неудачное удаление |
success | --success | Успех или предложение — пробный период |
accent | --accent | Нейтральное уведомление — смена часового пояса |
Свойства
| Свойство | Назначение |
|---|---|
tone | danger (по умолчанию) · success · accent |
title | Заголовок над текстом. Без него сообщение — одна строка |
children | Текст сообщения |
actions | Кнопки справа. Ожидается Button с size sm, а не голая разметка |
onDismiss | Крестик закрытия. Проп, а не содержимое слота: он часть анатомии — всегда справа последним, всегда с aria-label. Рисуется Button в варианте ghost: закрыть — самое второстепенное действие в плашке |
ScreenState «экран-заглушка» ui-kit/ScreenState.tsx
Полноэкранное состояние вместо всего приложения: обрыв связи с сервером и подгрузка страниц вне мини-аппа. Внутри мини-аппа загрузку показывают скелетоны — по фидбэку пользователя спиннер «проскакивал» и прыгал на настоящие данные. Отличается от Notice ролью — тот живёт внутри страницы рядом с содержимым, этот заменяет собой страницу целиком.
Показано в уменьшенной рамке: в приложении блок занимает весь экран.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Эмодзи | — | 48px — иллюстрация, а не значок; в шкалу иконок не входит |
| Заголовок | BodyPrimary · weight/semibold | только когда задан title |
| Текст | Body · --text-secondary | 14/400 вторичным |
| Фон | --bg-page | фон страницы, не карточки |
| Высота | min-h-screen | весь экран, содержимое по центру |
| Отступ по бокам | space/24 | 24px |
Состояния
Оба состояния показаны в превью выше: у компонента их ровно два, и они и есть весь компонент — отдельного «базового» вида, от которого они отличались бы, не существует.
| Состояние | Токен | Что меняется |
|---|---|---|
загрузка | — | Эмодзи 👶, текст «Загрузка...», спиннер вместо кнопки. Suspense на витрине разработчика, юридических страницах и лендинге. В самом мини-аппе загрузку показывают скелетоны, а не этот экран |
ошибка | --accent | Эмодзи ⚠️, заголовок, текст ошибки от сервера и кнопка перезагрузки страницы |
Свойства
| Свойство | Назначение |
|---|---|
emoji | Крупная иллюстрация сверху |
title | Заголовок. Без него состояние — эмодзи и строка текста |
children | Пояснение под заголовком |
action | Кнопка или спиннер под текстом |
ChipSelect «выбрать один из N» ui-kit/ChipSelect.tsx
Сетка кнопок-пилюль — выбор одного варианта из нескольких, активная опция залита акцентом. options/value/onChange, иконка в опции необязательна.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Подпись — обычный режим | ButtonCaption | 12/500 |
| Подпись — compact | BodyPrimary | 14/500 |
| Высота — минимум, не фикс | min-h-11 · min-h-8 | 44px обычный · 32px compact — тот же тап-таргет 44px, что у остальных интерактивных элементов (см. Layout) |
| Скругление | rounded-control · rounded-control-sm | 10px обычный · 8px у пилюли внутри жёлоба |
| Зазор | gap-2 · space/2 | 8px в сетке · 2px между пилюлями compact |
| Заливка выбранного | --accent | сплошной акцент, подпись белая |
| Заливка невыбранного | --bg-input | фон поля · прозрачная в compact |
| Обводка | stroke/none | нет ни в одном состоянии |
Состояния
| Состояние | Токен | Что меняется |
|---|---|---|
default | — | Невыбранная опция — заливка жёлоба, обычная подпись |
selected | --accent | Акцентная заливка, белая подпись |
pressed | opacity/70 | Пилюля гаснет |
focus-visible | --accent-border | Кольцо 2px: с зазором в обычном режиме, внутрь — в compact, где пилюля сидит в жёлобе |
disabled | opacity/50 · opacity/40 | Вся сетка гаснет; у отдельной опции compact гаснет только подпись — бейдж «скоро» остаётся читаемым |
Свойства
| Свойство | Назначение |
|---|---|
options | { id, label, icon?, disabled?, badge? }[] |
value | Выбранный id |
onChange | Обработчик выбора |
compact | Однострочный сегмент-контрол 32px вместо сетки-пилюль 44px |
columns | Число колонок сетки — по умолчанию по числу опций |
disabled | Блокирует всю сетку |
QuickActionButton ui-kit/QuickActionButton.tsx
Плитка быстрой записи на Главной: крупная эмодзи над подписью, вся плитка — одна кнопка. Здесь сразу в работе: сон уже идёт, поэтому плитка активна и подпись сменилась на «Проснулся», а «Прикорм» ещё не реализован — выключен с бейджем. Два «этажа»: primary — фиксированная пара «Кормление/Сон», secondary — настраиваемые. Не вариант Button: у того содержимое всегда в строку, здесь вертикальная плитка фиксированной высоты.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Подпись | BodyPrimary · ButtonCaption | 14/500 у primary · 12/500 у secondary |
| Эмодзи | font-size/24 · font-size/16 | 24px у primary, 16px у secondary |
| Высота | space/96 · space/80 | 96px primary · 80px secondary |
| Обводка | stroke/bold · stroke/thin | 2px акцентная · 1px нейтральная |
| Скругление | rounded-control | 10px |
| Зазор эмодзи ↔ подпись | gap-2 · gap-1-5 | 8px primary · 6px secondary |
| Заливка | --bg-card · --success-bg | фон карточки; success при active |
| Паддинг — primary | инлайн | 16 × 8px — не классами, вне шкалы Tailwind |
| Паддинг — secondary | инлайн | 10 × 4px — не классами, вне шкалы Tailwind |
Состояния
Показаны на компактной плитке — у крупной состояния те же, меняются только размеры и толщина рамки. В приложении, впрочем, активной бывает только крупная, а выключенной только компактная: см. таблицу.
| Состояние | Токен | Что меняется |
|---|---|---|
default | — | Базовый вид |
active | --success | Запись уже идёт — рамка и заливка на success. Только у primary: таймер кормления и активный сон |
pressed | opacity/70 | Плитка гаснет |
focus-visible | --accent-border | Кольцо 2px с зазором |
disabled | opacity/40 | Гаснут эмодзи и подпись, бейдж «скоро» остаётся читаемым. Только у secondary: тип записи ещё не реализован |
Свойства
| Свойство | Назначение |
|---|---|
icon | Эмодзи над подписью — 24px у primary, 16px у secondary |
label | Подпись под иконкой |
variant | primary — 96px, обводка 2px акцентная; secondary — 80px, 1px нейтральная (по умолчанию) |
active | Запись уже идёт — обводка и фон на success |
disabled | Гасит иконку и подпись до 40% |
badge | Бейдж в правом верхнем углу — «скоро». Не гаснет вместе с плиткой |
ListRow ui-kit/ListRow.tsx
Кликабельная строка со слотами — основной способ показать список в приложении: так собраны настройки, все шторки, модалки, Дашборд и Premium.
В интерфейсе
Правый хвост
Слот trailing — то, чем строки на самом деле отличаются друг от друга; ради него компонент и появился. Ниже каждый элемент показан один раз; сочетания из них перечислены в таблице, чтобы не повторять одно и то же по три раза.
| Элемент | Размер | Где |
|---|---|---|
| Значок | icon/sm | Шеврон и галочка — хвост по умолчанию у кликабельной и выбранной строки. Правка в Истории и «Настройках», внешняя ссылка в Premium |
| Значок «плюс» | icon/md | Добавить событие в шторке выбора. Единственный хвост крупнее остальных — расхождение, а не решение |
| Время | 11px, вторичным | История, последние кормления и сны |
| Бейдж «скоро» | badge/sm | Нереализованный пункт |
| Переключатель | 44×24 | Семь мест: фильтр истории, настройки уведомлений, шторки настройки карточек и графиков |
| Кнопка | button/sm | «Удалить» у соопекуна. В коде собрана вручную, а не из Button — расхождение |
| Пилюля-действие | badge/sm | «Оставить» в модалке выбора ребёнка. Тоже вручную: сплошной акцент с белой подписью, тона под это в Badge нет |
| Скелетон | 36×20 | На месте переключателя, пока грузятся настройки |
| Ручка перетаскивания | 44×44 · icon/md | Не часть слота, а свой проп onDragStart: всегда последняя, тянуть можно только за неё |
| Сочетания | — | Встречаются четыре: время + значок (История), время + бейдж (последние кормления и сны), бейдж + переключатель (нереализованный пункт с тумблером), переключатель + ручка (перетаскиваемая строка) |
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Метрика строки | space/16 · space/12 · min-h/52 | 16px по бокам, 12px сверху и снизу, минимум 52px высоты — задаёт сам компонент |
| Иконка категории | text-xl w-6 | эмодзи 20px в колонке 24px — 13 вызовов из 20 |
Аватар (leading) | rounded-full · фон --accent-light | 36px, круг — 2 вызова, оба собраны вручную мимо Avatar |
| Заголовок | BodyPrimary | 14px / 500 |
| Заголовок, деструктивный | --danger | 14px / 500 |
| Подпись и заметка | BodySecondary · space/2 | 12px, отступ сверху 2px |
| Зазор иконка ↔ текст | gap-3 | 12px; 8px только в перетаскиваемой строке — там место занимает ручка |
| Зазор внутри хвоста | gap-2 | 8px |
| Скругление | rounded-control 10px + overflow-hidden | нет — несёт контейнер группы |
| Разделитель | --row-divider | 1px, рисует карточка через divide-y — сама строка о нём не знает |
| Высота строки | min-h/52 | минимум 52px, дальше растёт по содержимому — около 62px с подписью |
Состояния
| Состояние | Токен | Что меняется |
|---|---|---|
default | — | Базовый вид |
pressed | opacity/60 · opacity/70 | Вся строка гаснет до 60% (active:opacity-60; в Истории переопределено на 70%) |
focus-visible | --accent-border | Кольцо 2px внутрь границы, цвет --accent-border. Именно inset: группа строк — overflow-hidden, кольцо с зазором обрезалось бы по краям карточки |
active | --accent | Строка выбрана — в хвосте по умолчанию галочка вместо шеврона. Пришло из SettingsRow, когда тот был отдельным компонентом |
danger | --danger | Заголовок краснеет — необратимое действие |
dimmed | opacity/40 | Иконка и текстовый блок до 40% — пункт «скоро». Хвост остаётся в полную силу, чтобы бейдж и Switch читались |
disabled | — | cursor-not-allowed; работает только вместе с onClick |
некликабельная | — | Без onClick рендерится <div>, а не <button> — нет ни tap-фидбэка, ни фокуса |
Свойства
| Свойство | Назначение |
|---|---|
icon | Эмодзи в колонке 24px, без подложки — иконка категории |
leading | Произвольный узел вместо колонки-иконки — аватар. Приоритетнее icon. Avatar умеет photoUrl, но фото в строке не применяется ни разу |
label | Заголовок, text-sm/medium, обрезается многоточием |
subtitle | Вторая строка под заголовком — 6 вызовов из 20 |
footnote | Третья строка и максимум для блока — заметка к записи в Истории, единственный вызов. Надзаголовка у компонента нет |
trailing | Правый хвост — см. раздел ниже: значок, время, бейдж, переключатель, кнопка, пилюля, скелетон |
onDragStart | Строку можно перетаскивать — в самом конце хвоста появляется ручка 44×44. Обычный колбэк: ListRow про framer-motion ничего не знает |
dimmed | Гасит иконку и подписи до 40% — нереализованный пункт «скоро». Хвост не гасится |
danger | Красный заголовок — деструктивное действие |
onClick | Делает строку кнопкой: tap-фидбэк и фокус-кольцо ring-inset |
disabled | Только вместе с onClick |
Карточка и заголовок секции
Карточка держит края, заливку, обводку по правилу и разделитель между детьми — всё, что относится к поверхности. Вёрстки в ней нет: ни ширины, ни выравнивания, ни внешнего отступа — их задаёт страница.
Варианты
| Вариант | Токен | Когда |
|---|---|---|
bordered | --bg-card · --border-card | Группа лежит прямо на фоне страницы — по общему правилу «блок на фоне страницы с обводкой» |
| по умолчанию | --bg-card2 | Группа внутри страницы ребёнка, где обводки не было ни у одной секции |
Состав карточки
| Элемент | Токен | Значение |
|---|---|---|
| Заголовок секции | SectionLabel | 11/600, межбуквенный 5% — не настраивается |
| Отступы заголовка | space/20 · space/8 | 20px сверху, 8px снизу |
| Скругление карточки | rounded-control | 10px, с обрезкой содержимого |
| Заливка карточки | --bg-card2 · --bg-card | card2 внутри страницы, card с обводкой на фоне страницы |
| Разделитель строк | --row-divider | 1px между строками, рисует группа, а не строка |
| Внешний отступ | — | Своего нет: расстояние до следующего блока задаёт страница. Раньше в компоненте стоял 8px, и 17 вызовов из 23 его перебивали — 12 в ноль |
Экспорты
| Свойство | Назначение |
|---|---|
ListHeader | Заголовок секции. Слот action — элемент справа. В приложении применён один раз: бейдж «Premium» в разделе «Внешний вид» |
List | Карточка со строками. Проп bordered переключает заливку и обводку |
ListRow | Строка. Отдельного «SettingsRow» больше нет — прослойка с шестью применениями против одиннадцати прямых вызовов основы себя не окупала |
| хвост по умолчанию | Галочка у выбранной строки, шеврон у кликабельной, ничего у некликабельной — иконки 16px, младший размер шкалы. Задаётся trailing, если нужен свой |
BottomNav «нижняя навигация» components/BottomNav.tsx
Плавающая пилюля поверх контента, четыре фиксированных таба. Не уезжает со скроллом (position: fixed), поэтому отступ снизу отсчитывается от safe area. Свойств нет вовсе: табы зашиты константой tabs в самом файле. Ни disabled, ни loading — разделы доступны всегда.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Поверхность | --nav-bg · --nav-border · stroke/thin | Фон, обводка 1px, backdrop-filter: blur(20px) |
| Скругление | rounded-panel | 24px |
| Тень | shadow/nav | 0 4px 24px rgba(0,0,0,.25) |
| Ширина | — | calc(100% − 40px), потолок 408px — инсет 20px с каждой стороны |
| Отступ снизу | space/10 | env(safe-area-inset-bottom) + 10px |
| Паддинг таба | space/10 · space/8 | 10px сверху и снизу, 8px по бокам |
| Зазор иконка ↔ подпись | space/4 | 4px |
| Иконка | w-5-5 · h-5-5 | 22px, lucide |
| Подпись | TabLabel | 11px / 500, без интерлиньяжа |
| Слой | — | z-index 50 — выше контента, ниже шторок и модалок |
Состояния
| Состояние | Токен | Что меняется |
|---|---|---|
default | --text-secondary | Иконка и подпись вторичным цветом, штрих 2 |
active | --accent | Текущий раздел: цвет акцента и штрих 2.25 — единственное место в приложении, где состояние показано толщиной штриха, а не только цветом |
pressed | opacity/60 | Таб гаснет до 60% |
Что содержит
| Таб | Маршрут | Иконка |
|---|---|---|
Главная | /dashboard | House |
История | /history | ClipboardList |
Статистика | /stats | ChartColumn |
Настройки | /settings | Settings |
StatCard «карточка-показатель» ui-kit/StatCard.tsx
Подпись сверху → крупное число с единицей → вторичная строка → отбитый разделителем футер. Два слота под значением намеренно разные: secondary идёт без разделителя, footer — за ним. Сама карточка не интерактивна, она только показывает число.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Подпись | BodySecondary · space/8 | 12px, отступ снизу 8px |
| Значение | Number | 24/700, интерлиньяж 1 |
| Единица | text-base font-normal · --text-secondary (один сайт, не заведён как стиль) | 16/400 |
| Иконка вместо значения | text-2xl opacity-40 | 24px, 40% |
| Вторичная строка | BodySecondary · space/8 | 12px, отступ сверху 8px |
| Футер | mt-2 pt-2 · разделитель --row-divider | 12px, отступ и поля 8px |
| Бейдж | absolute top-2 right-2 | 8px от верха и правого края |
| Отступы | p-4 | 16px по кругу |
| Скругление и фон | rounded-control · --bg-card | 10px, без обводки |
Состояния
| Состояние | Токен | Что меняется |
|---|---|---|
default | — | Подпись, значение с единицей, опционально вторичная строка и футер |
нет данных | opacity-40 у иконки | Вместо значения приглушённая иконка, вторичная строка — «Нет записей». Подпись яркая: метрика живая |
dimmed («скоро») | opacity-40 у подписи и иконки | Гаснет и подпись — метрика не реализована. Бейдж в углу остаётся в полную силу |
Свойства
| Свойство | Назначение |
|---|---|
label | Подпись сверху — «Кормление», «Кормлений/день» |
value | Крупное число. Не задано — вместо него рисуется icon |
unit | Единица рядом с числом — «раз», «мл», «ч». Мельче и не жирная: это не часть числа |
icon | Эмодзи вместо значения — «нет данных» или «не реализовано». Всегда приглушена |
secondary | Строка под значением, без разделителя: «0 мл всего», дельта к прошлому периоду |
footer | Строка под разделителем: когда событие было в последний раз |
badge | Бейдж в правом верхнем углу — «скоро». Не гаснет вместе с карточкой |
dimmed | Гасит подпись до 40% — метрика не реализована. У «нет записей» не ставится |
SelectField и TimeSelect ui-kit/SelectField.tsx · ui-kit/TimeSelect.tsx
Выпадающий список — обёртка над shadcn Select, владеющая его оформлением. TimeSelect — время чч:мм двумя списками; нативный <input type="time"> не используется, потому что в Telegram WebView он рендерится по-разному на разных платформах.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Высота | h-11 | 44px — как у Input |
| Текст | BodyPrimary | 14/400 |
| Отступ по бокам | space/12 | 12px |
| Обводка | stroke/thin · border/card | 1px |
| Скругление | rounded-control | 10px |
| Заливка триггера | --bg-input · --bg-card | фон поля · фон карточки при tone="card" |
| Выпадающий список | --bg-card · shadow/popover | фон карточки, обводка 1px, тень |
| Пункт списка | --accent-light | 14/400, подсветка выбранного акцентом |
Состояния
| Состояние | Токен | Что меняется |
|---|---|---|
default | — | Базовый вид |
open | — | Список раскрыт, шеврон повёрнут |
focus-visible | --accent-border | Кольцо 2px без зазора — как у Input |
disabled | opacity/50 | Гаснет, курсор запрета |
active | --accent | Обводка 2px акцентом. Не проп, а className по месту: так помечен селектор сравнения в Статистике — единственный контрол, который включён всегда |
Свойства
| Свойство | Назначение |
|---|---|
options | { value, label }[] |
label | Подпись над списком — та же, что у Input |
icon | Иконка внутри триггера перед значением |
tone | input — фон как у полей (по умолчанию); card — сплошная заливка для списка, стоящего прямо на странице |
placeholder, disabled | Как у обычного поля |
Sheet ui-kit/Sheet.tsx
Шторка снизу — основная форма всех модальных экранов: запись кормления, сна, подгузника, замеры, редактирование. Собрана из слотов Header, Body, Section и Footer. В проекте есть и ui/drawer.tsx поверх той же vaul, но его не импортирует ни один файл.
Состав
| Элемент | Токен | Значение |
|---|---|---|
| Скругление верха | rounded-t-sheet | 20px |
| Заливка | --bg-card | фон карточки |
| Затемнение под шторкой | opacity/50 | чёрный 50% |
| Ручка-индикатор | h-1-5 · space/12 · space/16 | 48 × 6px, отступы 12 сверху и 16 снизу |
| Цвет ручки | --border-card | обводка карточки |
| Body — отступы по бокам/снизу | px-5 pb-6 | 20px / 24px |
| Footer — граница сверху, отступы | --border-card · px-5 pt-3 pb-5 | 1px, 20px/12px/20px |
| Строка-переключатель — высота | min-h-13 | 52px |
Состояния
Превью здесь нет намеренно: оба состояния шторки — движение, а не вид. «Открыта» и «закрыта» отличаются положением по вертикали и затемнением фона, свайп — это жест. Статичная картинка показала бы только конечное положение, которое и так видно в превью выше.
| Состояние | Токен | Что меняется |
|---|---|---|
open / closed | opacity/50 | Выезд снизу и затемнение фона |
swipe | — | Тянется пальцем за ручку и закрывается свайпом вниз |
Свойства
| Слот | Свойство | Назначение |
|---|---|---|
Sheet | open, onClose | Состояние шторки |
Sheet | maxHeightRatio | Потолок высоты как доля вьюпорта, по умолчанию 0.8. Формы в несколько шагов просят больше |
Sheet | container | Портал-контейнер — только для изолированного показа в галерее компонентов |
Header | children | Текст заголовка |
Header | subtitle | Строка под заголовком — переключает нижний отступ (mb-1 вместо mb-5) |
Header | right | Бейджи/дата справа от заголовка |
Body | children | Единственный обязательный слот — всё, что не Footer |
Section | label | Необязательна — таймер/слайдер иногда сами несут подпись внутри |
Footer | children | Кнопки — закреплены вне скролла, с верхней границей |
Иконки
В приложении два набора, и это разные вещи. Векторные иконки — lucide-react, ими нарисован интерфейс: навигация, действия, стрелки, состояния. Эмодзи иконками только выглядят: они приходят из данных — тип события, аватар ребёнка — и живут по правилам текста, а не SVG.
lucide-react 25 иконок
Единственный источник векторных иконок, своих SVG в проекте нет. Иконки ниже нарисованы теми же path, что и в приложении — вытащены из node_modules/lucide-react, а не перерисованы.
Навигация
HouseТаб «Главная»ClipboardListТаб «История»ChartColumnТаб «Статистика»SettingsТаб «Настройки», иконка приложения в AppРаскрытие и перелистывание
ChevronDownРаскрыть попап, выпадающий список, фильтрChevronUpСвернуть раскрытый списокChevronLeftПредыдущая страница ИсторииChevronRightСледующая страница, хвост-шеврон строкиДействия
PencilИзменить запись — Главная и СтатистикаEditИзменить — История и Настройки. Другая иконка, чем Pencil, при том же действииPlusДобавить событиеXЗакрыть модалкуRefreshCwСбросить настройки ребёнкаDownloadВыгрузить данные СтатистикиShare2Поделиться — приглашение второго родителяExternalLinkВнешняя ссылка — Premium и НастройкиListFilterФильтр ИсторииGitCompareСравнение периодов в СтатистикеGripVerticalРучка перетаскивания строкиСостояние
CheckВыбранный пункт, галка в спискеLoader2Спиннер внутри кнопкиДанные и тема
BarChart3Переключатель вида графика — столбцы. Тот же глиф, что ChartColumn: BarChart3 — устаревший алиасLineChartПереключатель вида графика — линияSunСветлая темаMoonТёмная темаДве находки при сверке с кодом. BarChart3 и ChartColumn — один и тот же глиф: первое имя в lucide устаревшее, но в проекте импортируются оба, в разных файлах. И наоборот: действие «Изменить» нарисовано двумя разными иконками — Pencil на Главной и в Статистике, Edit (это square-pen, карандаш в рамке) в Истории и Настройках. Ни то, ни другое не решение — просто так сложилось.
Размеры
Три размера — 16, 20, 24. Все кратны --space-unit (4px), той же базе, на которой стоят отступы и скругления: иконки не исключение из общей шкалы. Раньше значений было девять и размер задавал каждый вызывающий код — те, что создавали разнобой (11, 14, 18, 22, 26), ровно те, что не кратны четырём.
| Образец | Значение | Реализация | Где используется |
|---|---|---|---|
| 16px | size={16} | Инлайн рядом с текстом — галка выбора, «Изменить», внешняя ссылка. Шеврон списка, мелкое действие, эмодзи в ChipSelect, иконка в кнопке sm. 18 мест | |
| 20px | size={20} | Значок в строке списка, действие в шапке и шторке, все четыре таба BottomNav, переключатель темы, иконка в кнопке md. 5 мест | |
| 24px | ICON_PX · fontSize | Эмодзи плитки QuickActionButton в варианте primary и иконка в кнопке lg. Размер выводится из размера компонента, а не задаётся вызовом |
Единственное исключение — эмодзи 48px на заставке поворота экрана: это иллюстрация во весь экран, а не значок, и в шкалу иконок её не загоняли.
Штрих и цвет
Толщина штриха везде 2 — дефолт lucide, никто его не переопределяет. Единственное исключение — активный таб навигации: strokeWidth 2.25, чтобы состояние читалось не только цветом. Цвет иконка не задаёт вовсе: lucide рендерит stroke="currentColor", поэтому иконка всегда того же цвета, что текст рядом — --text-secondary в спокойном состоянии, --accent в активном.
Эмодзи 11 размеров
Это не иконки и не часть набора: эмодзи приходят из данных (тип события, аватар ребёнка) или стоят заглушкой в пустом состоянии. Отсюда и разнобой размеров — у каждого свой, потому что это текст внутри своего компонента, а не элемент шкалы. Переиспользуется как «стандартная иконка строки списка» только 20px; остальные размеры под одно-два места и токенами не являются.
Размер задан теми же классами, что и текст: 12–16px — text-xs/text-sm/text-base, 18–48px — text-lg…text-5xl, кроме 22px и 26px, у которых инлайновый style={{ fontSize: N }}. То есть эмодзи размечены как типографика, а не как иконки — что и объясняет, почему их размеры не сходятся со шкалой lucide.
Figma ↔ Код
Что вошло в выборку. Весь mamkin-mini-app/src, кроме библиотечных components/ui/* (shadcn/Radix, их не трогали) и служебной галереи pages/dev/*. Публичный лендинг LandingPage.tsx тоже вне шкал: он вне Telegram Mini App и размеры там свои, некратные (1.75rem, 1.8rem, 3rem). Отдельные значения из библиотечных файлов в таблицах всё же есть — они помечены в колонке «Где используется».
Всё, что нужно для переноса в Figma Variables, в одном месте — код-токен, примитив/переменная Figma, значение. Без «см. выше»: цвета, типографика, spacing, opacity и border — полностью, без сокращений.
Словарь колонок один на все справочные таблицы страницы: Образец — как токен выглядит на самом деле, Primitive — имя примитива или ссылка на него, Semantic — именованная переменная Figma, ссылающаяся на примитив, CSS-переменная — что от этого есть в коде (бывает только у семантического тира), Значение — разрешённое значение, Реализация — механизм в коде там, где он не сводится к примитиву, Где используется — места в интерфейсе. В таблицах gap/height/width/margin/padding имя переменной приведено к единому виду для Figma и не всегда 1:1 с классом в коде: дробные значения через дефис (h-1-5 вместо h-1.5 — точка не годится для имени переменной), max-h/max-w без приставки (h-44/w-48), margin/padding без направления (m-N/p-N вместо mb-N/px-N). Настоящий класс из кода в таких случаях — в колонке «Где используется». Таблицы разбора компонентов ниже устроены иначе: там строка — это элемент, состояние или свойство, а не токен.
Типографика — Text Styles
Типографическая настройка в Figma — это Text Style: именованный набор Family + Size + Weight + Line height + Letter spacing, применяется одним кликом. Только стили, которые реально повторяются в коде, с готовым line-height под свой размер.
В Figma Text Style к переменной привязаны family (всегда Typography/font-family/base), weight и size — все трое через примитив. Не биндятся вообще (ограничение Figma, не наше решение) — line-height и letter-spacing: всегда сырое число внутри стиля, в процентах. В коде у типографики, в отличие от цвета, вообще нет CSS-переменной — ни одной --font-* в index.css. Реализация — сырые Tailwind-классы прямо в JSX; какие именно и где применяется каждый стиль — в разделе Типографика.
| Semantic | Размер / вес | Line-height | Letter-spacing |
|---|---|---|---|
Number | font-size/24 · weight/bold | 100% | 0% |
PageTitle | font-size/20 · weight/bold | 140% | 0% |
ModalTitle | font-size/18 · weight/bold | 156% (125% у имени ребёнка — не отдельный стиль) | 0% |
SheetHeader | font-size/16 · weight/semibold | 150% | 0% |
TileValue | font-size/14 · weight/semibold | 125% | 0% |
BodyPrimary | font-size/14 · weight/medium | 143% | 0% |
Body | font-size/14 · weight/regular | 143% | 0% |
AccentCaption | font-size/12 · weight/semibold | 133% | 0% |
ButtonCaption | font-size/12 · weight/medium | 133% | 0% |
BodySecondary | font-size/12 · weight/regular | 133% | 0% |
SectionLabel | font-size/11 · weight/semibold | Auto | 5% |
TabLabel | font-size/11 · weight/medium | 100% | 0% |
SmallBadge | font-size/10 · weight/medium | Auto | 0% |
MicroBadge | font-size/8 · weight/medium | 100% | 0% |
Line-height «Auto» — код не переопределяет line-height (наследуется от шрифта), в Figma это отдельный режим поля, не число. 5% letter-spacing у SectionLabel — то же самое, что 0.05em в коде (Figma-проценты у трекинга считаются от font-size, как и em).
Цвет — Primitives
Neutrals — тёмная и светлая шкала отдельно (не совпадают 1:1, поэтому разные примитивы, не тема одного).
Neutrals — Dark
| Образец | Primitive | Значение |
|---|---|---|
color/white | #ffffff | |
color/white 8% | #FFFFFF 8% | |
color/black | #000000 | |
color/dark/500 | #8a8a8e | |
color/dark/400 | #555555 | |
color/dark/600 | #333333 | |
color/dark/700 | #2e2e2e | |
color/dark/800 | #252525 | |
color/dark/750 | #1e1e1e | |
color/dark/900 | #111111 |
Neutrals — Light
| Образец | Primitive | Значение |
|---|---|---|
color/white | #ffffff | |
color/black | #000000 | |
color/black 5% | #000000 5% | |
color/gray/50 | #f9f9f9 | |
color/gray/100 | #f2f2f7 | |
color/gray/150 | #f0f0f0 | |
color/gray/200 | #e5e7eb | |
color/gray/400 | #9ca3af | |
color/gray/500 | #6b7280 | |
color/gray/900 | #111111 |
Цвета — по оттенку
| Образец | Primitive | Значение |
|---|---|---|
color/green/400 | #30D158 | |
color/green/400 15% | #30D158 15% | |
color/green/500 | #34C759 | |
color/green/500 12% | #34C759 12% | |
color/green/600 | #1D9E75 | |
color/green/600 15% | #1D9E75 15% | |
color/green/600 40% | #1D9E75 40% | |
color/blue/300 | #64D2FF | |
color/blue/500 | #0A84FF | |
color/blue/500 15% | #0A84FF 15% | |
color/blue/500 40% | #0A84FF 40% | |
color/blue/600 | #007AFF | |
color/blue/600 12% | #007AFF 12% | |
color/amber/300 | #FFB340 | |
color/amber/500 | #FF9F0A | |
color/amber/500 15% | #FF9F0A 15% | |
color/amber/600 | #FF9500 | |
color/amber/600 12% | #FF9500 12% | |
color/amber/600 15% | #FF9500 15% | |
color/amber/600 40% | #FF9500 40% | |
color/red/500 | #FF453A | |
color/red/500 15% | #FF453A 15% | |
color/red/600 | #FF3B30 | |
color/red/600 12% | #FF3B30 12% | |
color/purple/300 | #A78BFA | |
color/purple/500 | #7B72E9 | |
color/purple/500 15% | #7B72E9 15% | |
color/purple/500 40% | #7B72E9 40% | |
color/rose/300 | #FF6B8A | |
color/rose/500 | #FF375F | |
color/rose/500 15% | #FF375F 15% | |
color/rose/500 40% | #FF375F 40% | |
color/teal/300 | #5AC8FA | |
color/teal/500 | #32ADE6 | |
color/teal/500 15% | #32ADE6 15% | |
color/teal/500 40% | #32ADE6 40% | |
color/neutral-accent/300 | #AEAEB2 | |
color/neutral-accent/500 | #8E8E93 | |
color/neutral-accent/500 15% | #8E8E93 15% | |
color/neutral-accent/500 40% | #8E8E93 40% |
Строки с процентом (напр. color/green/400 15%) — отдельные primitive с прозрачностью, запечённой в HEX/rgba, не наложение opacity поверх основного цвета: Figma не даёт применить Fill Opacity к уже вставленной ссылке на переменную, только к слою целиком.
Цвет — Semantic
CSS-переменная в коде · Semantic (Figma) · Primitive, на который ссылается · где используется. Разрешённое значение цвета — у самого примитива в таблице выше, здесь не повторяется.
Light mode
| CSS-переменная | Semantic | Primitive | Где используется |
|---|---|---|---|
--bg-page | bg/page | color/gray/100 | Фон страницы |
--bg-card | bg/card | color/white | Фон карточки/шита |
--bg-card2 | bg/card2 | color/gray/50 | Фон вложенной группы (settings) |
--bg-input | bg/input | color/gray/100 | Фон полей ввода, неактивные кнопки |
--border-card | border/card | color/gray/200 | Обводка карточек/полей |
--text-primary | text/primary | color/gray/900 | Основной текст |
--text-secondary | text/secondary | color/gray/500 | Вторичный текст |
--text-placeholder | text/placeholder | color/gray/400 | Плейсхолдеры |
--row-divider | row/divider | color/gray/150 | Разделители строк списков |
--nav-bg | nav/bg | color/white | Фон нижней навигации |
--nav-border | nav/border | color/gray/200 | Обводка нижней навигации |
--icon-btn-bg | icon-btn/bg | color/black 5% | Фон Button variant=subtle |
Dark mode
| CSS-переменная | Semantic | Primitive | Где используется |
|---|---|---|---|
--bg-page | bg/page | color/dark/900 | Фон страницы |
--bg-card | bg/card | color/dark/800 | Фон карточки/шита |
--bg-card2 | bg/card2 | color/dark/750 | Фон вложенной группы (settings) |
--bg-input | bg/input | color/dark/700 | Фон полей ввода, неактивные кнопки |
--border-card | border/card | color/dark/600 | Обводка карточек/полей |
--text-primary | text/primary | color/white | Основной текст |
--text-secondary | text/secondary | color/dark/500 | Вторичный текст |
--text-placeholder | text/placeholder | color/dark/400 | Плейсхолдеры |
--row-divider | row/divider | color/dark/600 | Разделители строк списков |
--nav-bg | nav/bg | color/dark/750 | Фон нижней навигации |
--nav-border | nav/border | color/dark/600 | Обводка нижней навигации |
--icon-btn-bg | icon-btn/bg | color/white 8% | Фон Button variant=subtle |
Status & CTA
Один токен — одна строка. accent/accent-light сюда не входят — дубль варианта default из Accent ребёнка ниже. -bg — тот же primitive, что у основного токена, с прозрачностью.
Dark
| CSS-переменная | Semantic | Primitive | Где используется |
|---|---|---|---|
--success | success | color/green/400 | Подтверждение, идёт/активно |
--success-bg | success-bg | color/green/400 15% | Подтверждение, идёт/активно (фон) |
--warning | warning | color/amber/500 | Предупреждение, error-баннер Dashboard |
--warning-bg | warning-bg | color/amber/500 15% | Предупреждение, error-баннер Dashboard (фон) |
--danger | danger | color/red/500 | Удаление, ошибки ввода |
--danger-bg | danger-bg | color/red/500 15% | Удаление, ошибки ввода (фон) |
--info | info | color/blue/500 | Кормление в EventRow/history |
--info-bg | info-bg | color/blue/500 15% | Кормление в EventRow/history (фон) |
Light
| CSS-переменная | Semantic | Primitive | Где используется |
|---|---|---|---|
--success | success | color/green/500 | Подтверждение, идёт/активно |
--success-bg | success-bg | color/green/500 12% | Подтверждение, идёт/активно (фон) |
--warning | warning | color/amber/600 | Предупреждение, error-баннер Dashboard |
--warning-bg | warning-bg | color/amber/600 12% | Предупреждение, error-баннер Dashboard (фон) |
--danger | danger | color/red/600 | Удаление, ошибки ввода |
--danger-bg | danger-bg | color/red/600 12% | Удаление, ошибки ввода (фон) |
--info | info | color/blue/600 | Кормление в EventRow/history |
--info-bg | info-bg | color/blue/600 12% | Кормление в EventRow/history (фон) |
Accent ребёнка (7 вариантов × 4 роли)
Modes на бесплатном тарифе Figma нет — вместо одной переменной с 7 режимами здесь 28 отдельных Semantic-переменных (по 4 на цвет: сам акцент / подложка 15% / текст на подложке / обводка 40%). Смена акцента у ребёнка — это выбор готового набора, не один клик по переменной.
| CSS-переменная | Semantic | Primitive | Где используется |
|---|---|---|---|
--accent | accent/default | color/neutral-accent/500 | Цвет CTA/аватара, пока ребёнку не выбрали свой |
--accent-light | accent-light/default | color/neutral-accent/500 15% | Подложка под accent (default) |
--accent-text | accent-text/default | color/neutral-accent/300 | Текст поверх accent-light (default) |
--accent-border | accent-border/default | color/neutral-accent/500 40% | Обводка инфо-блока/плашки (default) |
--accent | accent/purple | color/purple/500 | Цвет CTA/аватара — вариант «Фиолетовый» |
--accent-light | accent-light/purple | color/purple/500 15% | Подложка под accent (Фиолетовый) |
--accent-text | accent-text/purple | color/purple/300 | Текст поверх accent-light (Фиолетовый) |
--accent-border | accent-border/purple | color/purple/500 40% | Обводка инфо-блока (Фиолетовый) |
--accent | accent/blue | color/blue/500 | Цвет CTA/аватара — вариант «Синий» |
--accent-light | accent-light/blue | color/blue/500 15% | Подложка под accent (Синий) |
--accent-text | accent-text/blue | color/blue/300 | Текст поверх accent-light (Синий) |
--accent-border | accent-border/blue | color/blue/500 40% | Обводка инфо-блока (Синий) |
--accent | accent/green | color/green/600 | Цвет CTA/аватара — вариант «Зелёный» |
--accent-light | accent-light/green | color/green/600 15% | Подложка под accent (Зелёный) |
--accent-text | accent-text/green | color/green/400 | Текст поверх accent-light (Зелёный) |
--accent-border | accent-border/green | color/green/600 40% | Обводка инфо-блока (Зелёный) |
--accent | accent/pink | color/rose/500 | Цвет CTA/аватара — вариант «Розовый» |
--accent-light | accent-light/pink | color/rose/500 15% | Подложка под accent (Розовый) |
--accent-text | accent-text/pink | color/rose/300 | Текст поверх accent-light (Розовый) |
--accent-border | accent-border/pink | color/rose/500 40% | Обводка инфо-блока (Розовый) |
--accent | accent/orange | color/amber/600 | Цвет CTA/аватара — вариант «Оранжевый» |
--accent-light | accent-light/orange | color/amber/600 15% | Подложка под accent (Оранжевый) |
--accent-text | accent-text/orange | color/amber/300 | Текст поверх accent-light (Оранжевый) |
--accent-border | accent-border/orange | color/amber/600 40% | Обводка инфо-блока (Оранжевый) |
--accent | accent/teal | color/teal/500 | Цвет CTA/аватара — вариант «Бирюзовый» |
--accent-light | accent-light/teal | color/teal/500 15% | Подложка под accent (Бирюзовый) |
--accent-text | accent-text/teal | color/teal/300 | Текст поверх accent-light (Бирюзовый) |
--accent-border | accent-border/teal | color/teal/500 40% | Обводка инфо-блока (Бирюзовый) |
--accent-gradient (7 вариантов, 135°, opacity/30 → opacity/15 поверх того же primitive) — Opacity-приёмом не сделать, нужен отдельный Gradient-стиль в Figma, не Number+Color.
Пространство, прозрачность, обводка
Один примитив (space/N), но каждое из семи свойств — свои именованные переменные Figma, каждая со своей ссылкой на примитив (как rounded-control ниже ссылается на space/10). Токен из кода = имя переменной Figma 1:1.
Про колонку «CSS-переменная». Она стоит только у семантического тира — как и у цвета; в таблице примитивов ниже её нет, потому что примитив в коде вообще не существует отдельной переменной. В отличие от цвета, где на каждый семантический токен есть своя --*, здесь настоящая CSS-переменная существует только у скруглений (--radius-*) и у прозрачностей, запечённых в цветовые переменные (--accent-light, --success-bg и т.п.). У gap / height / width / margin / padding и у толщины обводки переменных нет вообще, и прочерк в колонке — не пропуск в документации, а как есть: Tailwind 3 генерирует эти классы статически на этапе сборки, значение подставляется прямо в CSS-правило и во время исполнения ничего не читается. Единая база --space-unit: 0.25rem в index.css объявлена, но реально связывает только шкалу --radius-*, где каждое значение — её множитель; со шкалой Tailwind она совпадает численно, но не связана.
| Primitive | Значение |
|---|---|
space/0 | 0px |
space/2 | 2px |
space/4 | 4px |
space/6 | 6px |
space/8 | 8px |
space/10 | 10px |
space/12 | 12px |
space/14 | 14px |
space/16 | 16px |
space/20 | 20px |
space/22 | 22px |
space/24 | 24px |
space/28 | 28px |
space/32 | 32px |
space/36 | 36px |
space/40 | 40px |
space/44 | 44px |
space/48 | 48px |
space/52 | 52px |
space/56 | 56px |
space/64 | 64px |
space/68 | 68px |
space/72 | 72px |
space/80 | 80px |
space/96 | 96px |
space/112 | 112px |
space/128 | 128px |
space/144 | 144px |
space/160 | 160px |
space/176 | 176px |
space/192 | 192px |
space/224 | 224px |
space/448 | 448px |
corner radius 8 значений
| CSS-переменная | Semantic | Primitive | Где используется |
|---|---|---|---|
| — | rounded-none | space/0 | Только в библиотечном компоненте, в интерфейсе приложения не встречается |
--radius-xs | rounded-xs | space/4 | Мелкие декоративные скругления |
--radius-control-sm | rounded-control-sm | space/8 | Вложенные элементы внутри карточек — пункты списка при hover/active |
--radius-control | rounded-control | space/10 | Базовый радиус — карточки, кнопки, инпуты, попапы по всему приложению |
--radius-sheet | rounded-sheet | space/20 | Верх bottom-sheet. В коде применяется направленным вариантом rounded-t-sheet — скруглены только верхние углы, нижние уходят за край экрана |
--radius-panel | rounded-panel | space/24 | Крупные панели — нижняя навигация, шторка выбора ребёнка |
--radius-plate | rounded-plate | space/28 | Акцентная плашка-шапка — Главная, История, Статистика |
| — | rounded-full | — | Аватары, точки-индикаторы, круглые кнопки, переключатели — не шаг сетки, keyword-значение |
item spacing (gap) 8 значений
| CSS-переменная | Semantic | Primitive | Где используется |
|---|---|---|---|
| — | gap-0-5 | space/2 | Зазор между пилюлями в сегмент-контроле ChipSelect compact — единственное место |
| — | gap-1 | space/4 | Минимальный отступ между мелкими элементами — иконка и текст рядом |
| — | gap-1-5 | space/6 | Отступ между элементами строки настроек |
| — | gap-2 | space/8 | Стандартный отступ внутри блока — самое частое значение |
| — | gap-2-5 | space/10 | Отступ между строками карточки |
| — | gap-3 | space/12 | Отступ между карточками |
| — | gap-4 | space/16 | Отступ между секциями страницы |
| — | gap-5 | space/20 | Одно место — промо-страница Premium, и там это space-y-5: отступ между детьми, а не gap флексбокса |
height 26 значений
| CSS-переменная | Semantic | Primitive | Где используется |
|---|---|---|---|
| — | h-1-5 | space/6 | Ручка-индикатор шторки (единообразно во всех шторках), трек слайдеров в настройках ребёнка |
| — | h-2 | space/8 | Точки-индикаторы статуса, высота полоски в skeleton |
| — | h-3 | space/12 | Skeleton текстовых плейсхолдеров |
| — | h-3-5 | space/14 | Skeleton текстовых плейсхолдеров (вариант длиннее строки) |
| — | h-4 | space/16 | Разделительная вертикальная линия между элементами |
| — | h-5 | space/20 | Skeleton тогла в настройках уведомлений, иконка Premium-фичи |
| — | h-5-5 | space/22 | Skeleton-иконка нижней навигации (парно с width — тот же элемент) |
| — | h-6 | space/24 | Skeleton-заголовки, компактные круглые кнопки/аватары (size=sm) |
| — | h-7 | space/28 | Skeleton-заголовок; бегунок переключателя |
| — | h-8 | space/32 | Компактные кнопки/бейджи; трек переключателя |
| — | h-9 | space/36 | Круглые аватары/иконки среднего размера (карточки настроек, приглашения) |
| — | h-10 | space/40 | Аватары среднего размера, инпуты |
| — | h-11 | space/44 | Минимальная высота тап-таргета (Apple HIG) — самое частое значение: инпуты, кнопки, строки попапов |
| — | h-13 | space/52 | Строки настроек с крупной иконкой, крупная кнопка |
| — | h-14 | space/56 | Крупные круглые аватары (карточка ребёнка), skeleton-плашка блока статистики |
| — | h-16 | space/64 | Строки настроек с превью-иконкой карточки |
| — | h-17 | space/68 | Skeleton статус-строк «Следующее кормление/Следующий сон» в акцентной плашке-шапке |
| — | h-18 | space/72 | Запас снизу под sticky-бар «Сохранить/Отменить» |
| — | h-20 | space/80 | Переиспользуемый размер skeleton-карточек — блоки быстрой записи/статистики |
| — | h-24 | space/96 | Переиспользуемый размер skeleton-карточек по всему приложению |
| — | h-36 | space/144 | Нейтральная заглушка при переключении вкладок (не форма конкретного экрана) |
| — | h-40 | space/160 | Placeholder-блок графика в Истории |
| — | h-44 | space/176 | Ограничение высоты списка в попапе (Статистика) |
| — | h-48 | space/192 | Skeleton графика (Статистика) |
| — | h-full | — | На всю высоту родителя (изображения аватаров) |
| — | h-screen | — | Корневой контейнер приложения; потолок высоты шторок и модалок |
width 23 значения
| CSS-переменная | Semantic | Primitive | Где используется |
|---|---|---|---|
| — | w-2 | space/8 | Точки-индикаторы статуса (парно с height) |
| — | w-5 | space/20 | Skeleton тогла в настройках уведомлений (парно с height, тот же элемент), иконка Premium-фичи |
| — | w-5-5 | space/22 | Skeleton-иконка нижней навигации — совпадает с реальным размером иконок. Не на 4px-сетке — совпадение, не шаг ритма |
| — | w-6 | space/24 | Контейнер эмодзи-иконки в строке настроек (единообразно во всех шторках), компактные круглые кнопки |
| — | w-8 | space/32 | Компактные круглые аватары/иконки (size=xs/sm) |
| — | w-9 | space/36 | Круглые аватары/skeleton тогла среднего размера |
| — | w-10 | space/40 | Аватары среднего размера |
| — | w-11 | space/44 | Крупные квадратные иконки/drag-хендлы настроек |
| — | w-12 | space/48 | Ручка-индикатор шторки (парно с height, единообразно во всех шторках) |
| — | w-13 | space/52 | Трек переключателя |
| — | w-14 | space/56 | Крупные круглые аватары |
| — | w-16 | space/64 | Skeleton текстовых плейсхолдеров |
| — | w-20 | space/80 | Skeleton текстовых плейсхолдеров |
| — | w-24 | space/96 | Skeleton текстовых плейсхолдеров |
| — | w-28 | space/112 | Skeleton текстового плейсхолдера (доступ к профилю) |
| — | w-32 | space/128 | Skeleton заголовков/строк |
| — | w-40 | space/160 | Skeleton заголовков |
| — | w-44 | space/176 | Skeleton заголовков (вариант длиннее) |
| — | w-48 | space/192 | Ограничение ширины второй skeleton-строки (уведомления) — потолок, не жёсткая ширина |
| — | w-56 | space/224 | Ширина всплывающей панели — переключатель ребёнка, сравнение периодов в Статистике |
| — | w-md | space/448 | Максимальная ширина мобильного контейнера приложения |
| — | w-full | — | На всю ширину родителя — самое частое значение |
| — | w-auto | — | Автоматическая ширина (кнопка сравнения периодов в Статистике) |
margin 12 значений
| CSS-переменная | Semantic | Primitive | Где используется |
|---|---|---|---|
| — | m-0 | space/0 | Сброс отступа у последнего элемента списка |
| — | m-0-5 | space/2 | Минимальный зазор, подпись под заголовком строки |
| — | m-1 | space/4 | Отступ между тесно связанными строками (лейбл/значение) |
| — | m-1-5 | space/6 | Подпись поля перед инпутом |
| — | m-2 | space/8 | Пояснительный текст под группой контролов |
| — | m-2-5 | space/10 | Отступ под сеткой быстрых кнопок |
| — | m-3 | space/12 | Отступ между карточками, второе по частоте после 16px |
| — | m-4 | space/16 | Отступ между блоками, самое частое margin-значение в коде |
| — | m-5 | space/20 | Отступ между секциями страницы, см. rhythm/section-gap ниже |
| — | m-6 | space/24 | Отступ перед списком опций в шторке; отступ под заголовком страницы |
| — | m-8 | space/32 | Отступ в самом низу страницы Настроек, перед версией приложения |
| — | m-24 | space/96 | Только в библиотечном компоненте (шторка снизу), не в интерфейсе приложения |
padding 14 значений
| CSS-переменная | Semantic | Primitive | Где используется |
|---|---|---|---|
| — | p-0 | space/0 | Явный сброс паддинга у переиспользуемого компонента |
| — | p-0-5 | space/2 | Микро-паддинг вокруг мелких бейджей/иконок нижней навигации |
| — | p-1 | space/4 | Паддинг компактных иконочных кнопок-«карандашей» |
| — | p-1-5 | space/6 | Паддинг маленьких бейджей/чипов |
| — | p-2 | space/8 | Паддинг компактных кнопок/строк-табов нижней навигации |
| — | p-2-5 | space/10 | Паддинг средних кнопок/строк табов нижней навигации |
| — | p-3 | space/12 | Паддинг строки настроек — самое частое значение после 16px |
| — | p-3-5 | space/14 | Паддинг чуть крупнее стандартной строки |
| — | p-4 | space/16 | Самый частый вариант — то же самое, что layout/margin ниже (поля страницы) |
| — | p-5 | space/20 | Паддинг шторок (bottom sheet) по бокам |
| — | p-6 | space/24 | Паддинг крупных модалок |
| — | p-8 | space/32 | Запас снизу под safe-area/sticky-бар в шторках (единообразно во многих модалках) |
| — | p-10 | space/40 | Отступ под иконку слева; запас снизу в отдельных модалках |
| — | p-16 | space/64 | Пустое состояние (empty state) по центру страницы История/Статистика |
Opacity и толщина обводки — по строению это примитивы (сырое число, ни на что не ссылается), но в Figma-файле их пока нет вообще: вбиваются вручную в каждом использовании. Семантического тира над ними тоже нет — поэтому колонки «Semantic» здесь не бывает, а «Реализация» показывает механизм в коде. rounded-full уже показан выше, в таблице corner radius — отдельной таблицы под него не заводим. Колонка «CSS-переменная» у opacity — не переменная прозрачности как таковой: своей --opacity-* в проекте нет, процент запечён внутрь цветовой переменной (--accent-light — это акцент, уже смешанный с 15%). Где прочерк — значение живёт классом Tailwind или инлайн-стилем, в CSS-переменную не вынесено.
Opacity
| CSS-переменная | Primitive | Значение | Реализация | Где используется |
|---|---|---|---|---|
| — | opacity/0 | 0 | rgba(...,0), inline | Второй (конечный) стоп градиента-маски плашки-шапки (AccentPlate.tsx) |
--icon-btn-bg | opacity/5 | 5 | запечено в переменной, светлая тема | Фон Button variant=subtle, светлая тема |
--icon-btn-bg | opacity/8 | 8 | запечено в переменной (тёмная) + отдельный inline rgba | Фон Button variant=subtle тёмная тема; отдельно — hover-курсор графиков в Статистике (совпадение процента, не общий токен) |
| — | opacity/10 | 10 | — | Не используется в реальном интерфейсе — только пример в dev/Showcase; в проде объединено в --danger-bg (0.12/0.15) |
--success-bg | opacity/12 | 12 | запечено в переменной, светлая тема | Фон статус-плашек, светлая тема — по одной переменной на статус: --success-bg, --warning-bg, --danger-bg, --info-bg |
--success-bg · --accent-light | opacity/15 | 15 | запечено в переменных | Фон статус-плашек тёмная тема (те же 4 переменные); фон --accent-light — все 7 акцентных цветов |
| — | opacity/18 | 18 | rgba(255,255,255,.18), inline | Первый стоп градиента плашки-шапки; полупрозрачные подложки на ней (Dashboard.tsx) |
| — | opacity/20 | 20 | rgba(255,255,255,.2), inline ×6 | Overlay-подложка под skeleton загрузки — не общая переменная, значение вручную скопировано в 6 файлах |
| — | opacity/25 | 25 | rgba, inline | Тень нижней навигации (BottomNav); отдельно — подложка аватара на акцентном фоне (Avatar onAccent) |
| — | opacity/30 | 30 | rgba(255,255,255,.3), inline | Обводка hero-карточки на Главной (Dashboard); бегунок Switch сюда не относится — у него Tailwind shadow-lg, другой механизм |
--accent-border | opacity/40 | 40 | смешанно: класс opacity-40, inline-style и запечено в переменной | Иконки нереализованных функций и disabled-пагинация Истории (класс), disabled-опции в шторках (inline), кольцо фокуса и обводки на акценте (--accent-border, все 7 цветов) |
| — | opacity/50 | 50 | disabled:opacity-50 | Disabled-состояние кнопок — вторая по частоте; у primary/danger кнопок гашение иначе (--bg-input, без opacity) |
| — | opacity/60 | 60 | active:/disabled:opacity-60, отдельно inline rgba | Active/tap и disabled-состояния строк настроек (класс); тёмный скрим модалок — отдельный inline rgba(0,0,0,.6) |
| — | opacity/70 | 70 | active:opacity-70 | Active/tap-нажатие — большинство кнопок приложения, самое частое; единственное значение с одной реализацией везде |
| — | opacity/75 | 75 | JS-константа rgba | Вторичный текст на цветной hero-карточке (Dashboard, ChildSwitcherPopover) |
| — | opacity/92 | 92 | opacity-[92%] | Paywall-баннер «Ранняя история» — единственное место, Tailwind arbitrary value |
| — | opacity/100 | 100 | inline style, ветка «иначе» | Явная отмена унаследованной прозрачности — всегда «иначе» в тернарнике рядом с другим opacity-значением, не отдельный класс |
stroke — толщина обводки
| CSS-переменная | Primitive | Значение | Реализация | Где используется |
|---|---|---|---|---|
| — | stroke/none | 0px | border-0 / border-transparent | Два разных паттерна: last:border-0 — сброс разделителя у последнего пункта списка; border-transparent — ширина остаётся, гасится только цвет |
| — | stroke/thin | 1px | класс border / inline | Базовая обводка карточек, полей, разделителей — доминирующее значение; вперемешку Tailwind-класс и inline 1px solid var(...). Переменная в inline-записи — это цвет (--border-card), не толщина |
| — | stroke/bold | 2px | inline, не border-2 | Активный таймер кормления/сна (QuickActionButton), выбранный тариф Premium — оба места инлайн-стилем, класс border-2 в коде есть, но не здесь |
Тень, градиент, ритм и сетка
Как у opacity и обводки, семантического тира здесь нет: значения одноразовые, ссылаться вниз не на что — поэтому колонки «Semantic» не бывает, а сами токены по строению примитивы. «Реализация» показывается там, где механизм в коде не очевиден из значения.
shadow (elevation) 4 значения
Тень — только у элементов, плавающих поверх контента (навигация, попапы). У обычных карточек сигнал глубины — обводка stroke/thin (значения — в разделе Figma ↔ Код), не тень.
| Primitive | Значение | Где используется |
|---|---|---|
shadow/nav | 0 4px 24px rgba(0,0,0,.25) | Плавающая нижняя навигация (BottomNav.tsx) и её skeleton-заглушка на старте (App.tsx) — дословный дубль значения в обоих местах, не общая константа |
shadow/popover | 0 8px 24px rgba(0,0,0,.18) | Поверхность выпадающих попапов — переключатель ребёнка, выбор метрики графика. Больше не дубль: вынесена в ui-kit/PopoverPanel.tsx |
shadow-lg | Tailwind default | Бегунок Switch (ui/switch.tsx) — единственное место, где тень взята стоковым Tailwind-классом, а не собственным значением |
focus-ring | 0 0 0 2px transparent, 0 0 0 4px var(--accent-border) | Кольцо фокуса Button (см. Components) — тоже box-shadow (ring-2 + ring-offset-2), не обводка; единственная тень с зазором (offset) в приложении |
Библиотечный shadow-md в базовых классах ui/popover.tsx — фактически мёртвый: оба реальных потребителя (shadow/popover выше) переопределяют его инлайн-стилем. Тень DesktopFrame (0 24px 80px rgba(0,0,0,.5)) — вне основной шкалы: рендерится только вне мобильного Telegram (браузер, Telegram Desktop), не часть мобильного интерфейса.
gradient 2 значения
Только 2 реальных градиента, оба акцентные — завязаны на var(--accent) текущего ребёнка, не статичная палитра.
| Primitive | Значение | Где используется |
|---|---|---|
ACCENT_PLATE_BG | 135deg, rgba(255,255,255,.18)→0 60%, поверх var(--accent) | Акцентная плашка-шапка (rounded-plate, см. corner radius) — Главная/История/Статистика, переключатель ребёнка. Единственный градиент, вынесенный в общий JS-экспорт: живёт в ui-kit/AccentPlate.tsx, фон задаёт сам AccentPlate |
--accent-gradient | accent-gradient/* — color/{hue}/500 · 135°, stop1 opacity/30 → stop2 opacity/15 | Промо-карточка Premium (Settings.tsx) — CSS-переменная, 7 версий (по одной на каждый акцентный цвет ребёнка, см. Colors) |
section header & rhythm 3 значения
Вертикальный ритм между секциями страницы — 20px практически везде. За это отвечает не отдельный «межсекционный» класс, а штатное схлопывание CSS-margin (margin collapse) между нижним отступом предыдущего блока и встроенным mt-5 компонента ListHeader: браузер берёт максимум из двух соседних margin, не их сумму, поэтому 8/12px нижнего отступа карточки плюс 20px верхнего отступа заголовка дают ровно 20px, а не 28/32px. Эмуляция экрана с этим ритмом — в разделе layout & grid выше.
| Primitive | Значение | Реализация | Где используется |
|---|---|---|---|
heading/page | 20px / 700 | text-xl font-bold | Заголовок страницы — 16px до контента |
heading/section | 11px / 600 | text-[11px] font-semibold | Uppercase-лейбл секции — единообразно по всему приложению |
rhythm/section-gap | 20px | mt-5, схлопывается с нижним отступом предыдущего блока | Интервал перед новой секцией |
Margin collapse работает только между block-уровневыми соседями — исключение здесь: компонент с базовым классом inline-flex, а inline-flex элементы в collapse не участвуют, поэтому интервал перед такой секцией держится на встроенном mt-5 заголовка, а не на схлопывании отступов.
layout 2 значения
Ширина мобильного контейнера и горизонтальные поля страницы. Разбор внутренних отступов компонентов, с картинками, остался в разделе Layout & grid.
| Primitive | Значение | Реализация | Где используется |
|---|---|---|---|
layout/container | 448px | max-w-md | Максимальная ширина мобильного контейнера, центрируется на широких экранах — то же значение, что и w-md в шкале width выше |
layout/margin | 16px | px-4 | Горизонтальные поля страницы на каждой вкладке |