
Привет, Хабр! Меня зовут Вячеслав Таранников, я ведущий Android-разработчик в RuStore, и сегодня расскажу о нашей дизайн-системе, разобрав две ключевые темы: токены и компоненты.
Эта статья основана на моем совместном докладе с Дмитрием Смирновым, руководителем команды разработки, — «Как мы создали дизайн-систему для мобильных устройств и ТV на Jetpack Compose». Мы представили его на митапе «Coffee&Code ✕ RuStore | TechBrew» и теперь делимся основными идеями с вами.
Небольшой дисклеймер: наша цель — показать концепцию дизайн-системы, поэтому в статье мы сознательно опустили некоторые технические нюансы, не влияющие на суть. Например, мы не затрагиваем оптимизацию рекомпозиций, а в примерах компонентов вы не найдете Modifier. Используйте представленный код как отправную точку для собственных решений, а не как финальную реализацию.
Дизайн-система
Что такое дизайн-система? Система — это сложная структура, состоящая из разных элементов, работающих по определенным правилам и механизмам. Слово «Дизайн» подсказывает нам, в каком контексте используется и приносит пользу эта система.
Для того, чтобы выстраивать дизайн-систему, мы определили три важных свойства.
Ключевые свойства
Единая концепция. У дизайн-системы должна быть идея, от которой выстраивается интерфейс. Это обеспечивает понятную и консистентную коммуникацию с пользователями.
Метаязык. Важно придерживаться общего языка для общения между платформами и дизайном. Например, слово «ячейка» понимается одинаково дизайнером, разработчиком и другими участниками процесса.
Библиотечность. Дизайн-система должна иметь понятную структуру, документацию и гайдлайны по применению с лучшими практиками. Также в приоритете — хорошее и понятное API, которое поддерживает обратную совместимость.
Почему не взять готовое?
Перед тем как разработчик начинает делать что-то сложное, он поищет готовые решения на рынке. Мы тоже пошли по этому пути и в первой итерации использовали дизайн-систему Material. Она позволила создать консистентный UI, сфокусироваться на бизнес-логике и запустить проект всего за три месяца.
Но у Material есть минусы:
Сам себя ограничивает. Расширить палитру и компоненты сложно. Например, не получится расширить палитру Material так, чтобы это расширение использовалось и в компонентах Material, поскольку сами компоненты не будут знать о ваших расширениях.
Слишком «умный». Material часто принимает решения за разработчиков. Обнаружить это разработчики могут лишь тогда, когда откроют код Material. А дизайнеры вообще не смогут узнать об этом. Например, однажды нам попался такой скриншот, где темно-розовый текст сливается с черным фоном. Это произошло из-за того, что компонент Text был обернут в Surface от Material, который переопределил цвет компонента Text по умолчанию.
// Определение цвета Text в Material3 val textColor = color.takeOrElse { style.color.takeOrElse { LocalContentColor.current } }
// Замена LocalContentColor в Material3 Surface fun Surface( // <...> color: Color = MaterialTheme.colorScheme.surface, contentColor: Color = contentColorFor(color), content: @Composable () -> Unit ) { CompositionLocalProvider( LocalContentColor provides contentColor, ) { // ... content() } }

О дизайн-системе RuStore
Рассмотрим несколько особенностей нашей дизайн-системы.
Имя
Мы присвоили дизайн-системе имя, чтобы подчеркнуть её значимость и наделить характером. Это не просто модуль в коде или платформа, а связка дизайна и платформы.
Наша дизайн-система носит имя известного художника Louis William Wain.

Архитектура
Общая схема архитектуры представлена ниже. Центральный модуль rustore-core содержит общие компоненты, которые используются во всех проектах RuStore и не зависят от платформы. Это палитры, иконки и иллюстрации. Модули web и client зависят от rustore-core. Клиентские модули делятся на mobile и tv, а web — на B2B и B2C.

Рассмотрим подробнее архитектуру мобильной имплементации. В центре находится core, который содержит общие для mobile и tv сущности – рутовую тему, палитру, общие компоненты и т.д. От core зависят mobile и tv модули, которые содержат элементы, необходимые для конкретной платформы – имплементации темы и типографики, платформенные компоненты (например, SwipeRefresh).

Все три модуля (сore, mobile и tv) содержат модули theme и components. Mobile и tv также содержат quarantine. Зависимости между модулями на схеме транзитивные: mobile-components имеет доступ и к mobile-theme, и к core-theme.
theme — включает базовые функцию темы и интерфейсы токенов;
components — переиспользуемые элементы интерфейса;
quarantine — содержит упрощенные компоненты, добавленные фича-командами для их задач и требующие стабилизации перед переводом в components.
Демо-приложение

Важной частью дизайн-системы является демо-приложение. Его главное назначение – это реализация подхода «код как документация», при котором разработчик на примерах может посмотреть варианты применения компонента. Также демо-приложение выполняет роль каталога и позволяет посмотреть приложения так, как они будут выглядеть на проде, исключая возможные отличия из-за разных рендерных движков у Figma и Android.
Токены
Токены — это абстракция над сущностями дизайн-системы, которая позволяет иметь разные реализации с общей сигнатурой. Можно представить их как интерфейсы: вы пишете свои реализации токенов, но обращаетесь по общему интерфейсу.
Как токены упрощают разработку?
Все мы сталкивались с самой сложной задачей программиста — придумать хорошее название для переменной: оно должно быть однозначным, а если еще и будет помещаться на экран, то вообще здорово. Решить эту задачу помогает семантический подход к названиям. Например, при указании цвета вместо «чёрный» и «синий» используем «основной» и «акцентный».
// ❌ Don't Colors: pink700, green300 // ✅ Do Colors: accent, backgroundPositive
Токены в цветовой палитре
Самая распространённая фича, требующая токены – это тёмная тема. Экраны одни и те же, но цвета целиком зависят от того, какой цветовой мод пользователь выбрал в настройках. В минимальном варианте у нас две палитры — тёмная и светлая. А можно пойти дальше и поддержать несколько цветовых схем, у каждой из которых есть свой светлый и тёмный варианты. Сущности «синий цвет», «белый цвет фона», «чёрный цвет текста с прозрачностью» здесь уже не помогут, нужны семантические сущности: «позитивный цвет», «акцентный цвет», «основной цвет фона».

Типографика
Когда мы абстрагируемся от понятиий вроде «шрифт Roboto, кегль 22, курсив» и поразмышляем над смыслом различных стилей текста, приходим к выводу, что существует некий набор шрифтов, каждый из которых несёт определенный смысл. Семантика в виде токенов позволяет нам перейти к понятиям «заголовок», «основной текст», «подпись» и оперировать ими. На смартфоне и телевизоре заголовки будут иметь разные кегли, и об этом не нужно задумываться при вёрстке.
Другие варианты токенов
Прочие токены могут оказаться полезными, когда у нас есть несколько проектов на одной дизайн-системе. Или когда мы имеем гибкую мультитему и хотим иметь возможность менять их на лету.
Набор форм одного приложения может быть острым и грубым, а в другом обрести мягкость за счет скруглений — и все это в рамках одной дизайн-системы.
Токен «Иконка для поиска» может иметь различные значения: классический вариант лупы или что-то более экстравагантное. Главное, что это будет консистентно на всех экранах продукта, а значит привычно и понятно пользователю.

А как в коде?
О локальной композиции
Перед тем как обсуждать токены, давайте разберёмся с механизмом LocalComposition. Compose использует декларативный подход, при котором разработчик прямо в коде строит дерево UI-элементов, вызывая соответствующие функции, и передаёт в них нужные для отрисовки параметры.

Данные в таком дереве явно передаются сверху вниз через аргументы функций. Это неудобно для часто используемых общих данных (например, для палитры).
// Передаём палитру в аргументах @Composable fun SomeScreen(palette: Palette, state: ScreenState) { Text( text = "Hello, habr", color = palette.text.primary, ) SomeWidget(palette, state.content) } @Composable fun SomeWidget(palette: Palette, content: Content) { // ... }
А теперь представьте, что, помимо палитры, нам также нужно передать типографику, иконки, отступы и ещё десяток параметров.
Локальная композиция решает эту проблему, передавая данные неявно (без аргументов).

Давайте рассмотрим на примере диаграммы с котиками, как работает локальная композиция. Родительский Composable предоставляет бело-синего котика, который доступен во всех наследниках. Один из наследников предоставляет рыжего котика, который доступен только уже в его насл��дниках, но недоступен для родителя или брата.
// Передаём палитру через локальную композицию (Material) @Composable fun MaterialTheme(colorScheme: ColorScheme) { CompositionLocalProvider(LocalColorScheme provides colorScheme) } object MaterialTheme { val colorScheme: @Composable get() = LocalColorScheme.current } // Обращаемся к локальной композиции в коде @Composable fun Icon(...) { val iconContentColor = MaterialTheme.colorScheme.onSurfaceVariant // ... }
Узнать подробнее о CompositionLocal можно в официальной документации.
Базовые токены на примере палитры
Первый тип токенов, который мы используем в RuStore, — это базовые токены. Они существуют на уровне темы, например, для палитры, типографики, отступов и форм. Рассмотрим пример для палитры.
Класс LouisColors содержит подгруппы цветов RuStore.
// Палитра цветов RuStore public data class LouisColors( val Background: BackgroundColors, val Text: TextColors, val Icon: IconColors, )
Подгруппы цветов — это дата-классы, в которых есть поля с нужными нам цветами.
// Подгруппа цветов public data class BackgroundColors( val primary: Color, val secondary: Color, val accent: Color, )
Создаём светлый и темный вариант палитры, которые отличаются значением цветов, но имеют одинаковый набор значений.
// Светлый вариант палитры public val LightRuStorePalette: LouisColors = LouisColors( Background = BackgroundColors( primary = Color(0xFFF2F3F5), secondary = Color(0xFFFFFFFF), accent = Color(0xFF0077FF), ), Text = TextColors( primary = Color(0xFF222222), secondary = Color(0xFF6D7885), accent = Color(0xFF0077FF), ), )
// Тёмный вариант палитры public val DarkRuStorePalette: LouisColors = LouisColors( Background = BackgroundColors( primary = Color(0xFF000000), secondary = Color(0xFF1A1C20), accent = Color(0xFF0072E5), ), Text = TextColors( primary = Color(0xFFEBF1F6), secondary = Color(0xFF96A6B1), accent = Color(0xFF2994FF), ), )
На уровне темы решаем, какой из вариантов добавить в локальную композицию.
// Добавляем токены через композицию private val LocalColors = staticCompositionLocalOf<LouisColors> { error("No LouisColors palette provided!") } @Composable public fun LouisTheme( // isSystemInDarkTheme() проверяет, какой цветовой режим установлен на устройстве пользователя isDark: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit, ) { val palette = if (isDark) DarkRuStorePalette else LightRuStorePalette CompositionLocalProvider( LocalColors provides palette, ) { content() } }
По аналогии с Material, у нас есть объект Louis для доступа к токенам композиции, к которым мы обращаемся, чтобы получить нужное значение.
//Даём доступ к токенам public object Louis { public val Colors: LouisColors @Composable @ReadOnlyComposable get() = LocalColors.current }
И, наконец, используем токен. Тема вызывается в рутовом активити. Внутри дочерних Composable мы вызываем объекты Louis и обращаемся к палитре. Это позволяет нам в точках применения не задумываться о том, какая тема используется: светлая или тёмная. Мы просто обращаемся по названию токена.
// В рутовом активити вызываем функцию темы LouisTheme { // <...> }
// В фиче-экране обращаемся к токену Text( text = "Привет, habr", color = Louis.Colors.Text.primary, )
Базовые токены на примере типографики
Рассмотрим еще один пример для типографики. У нас есть класс LouisTypography, который содержит разные текстовые стили. Хочется, чтобы один и тот же стиль по-разному выглядел на мобильных устройствах и TV.
// Текстовые стили RuStore public data class LouisTypography( val Headline: TextStyle, val Body: TextStyle, val Caption: TextStyle, )
Чтобы при написании кода не задумываться о том, какая у нас платформа, создадим реализации типографики для каждой из платформ и зададим в них разные размеры шрифта.
// Токены типографики для мобильных устройств val mobileTypography = LouisTypography( Headline = TextStyle( fontSize = 22.sp, lineHeight = 28.sp, ), // ... )
// Токены типографики для TV val tvTypography = LouisTypography( Headline = TextStyle( fontSize = 36.sp, lineHeight = 44.sp, ), // ... )
Добавляем локальную композицию для хранения типографики.
//Добавляем типографику в базовую тему @Composable public fun LouisTheme( typography: LouisTypography, isDark: Boolean, content: @Composable () -> Unit, ) { val palette = if (isDark) DarkRuStorePalette else LightRuStorePalette CompositionLocalProvider( LocalColors provides palette, LocalTypography provides typography, ) { content() } }
Чтобы учесть разделение на две платформы, создаём две отдельные темы для мобильных устройств и TV в отдельных модулях. Эти темы будут вызывать рутовую тему с платформенным имплементациями токена.
// Создаем тему для мобильных устройств @Composable public fun MobileTheme( isDark: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit, ) { LouisTheme( typography = MobileTypography, isDark = isDark, content = content, ) }
// Создаем тему для TV @Composable public fun TvTheme( isDark: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit, ) { LouisTheme( typography = TvTypography, isDark = isDark, content = content, ) }
Теперь мы будем использовать не core-тему, а платформенные темы.
// В рутовом активити для платформы вызываем соответствующую тему MobileTheme { // ... } // Или TvTheme { // ... }
//Добавляем обращение к токену типографики Text( text = "Привет, habr!", color = Louis.Colors.Text.primary, style = Louis.Typography.Headline, )
Обе платформенные темы оборачивают передаваемый контент в core-тему, поэтому у нас остаётся доступ к токенам, которые выставляются на уровне core-темы.
Платформенные токены на примере мобильной палитры
Дисклеймер: с момента презентации мы перенесли цвета баннеров из мобильной палитры в core, полностью отказавшись от отдельной мобильной палитры. Мы это сделали, потому что выделение отдельной группы цветов в палитре под компонент нарушило семантический подход, и из-за этого мы некорректно решили, что раз компонент существует только в мобильной версии, то и цвета его фона должны находиться там же. Этот пример показывает, почему важн�� использовать семантические названия для токенов. Оставляю раздел как пример создания платформенных токенов.
Если вы пользовались RuStore, то наверняка видели баннер с просьбой разрешить присылать уведомления. Этот компонент баннера существует только на мобильных устройствах, и для него мы сделали отдельную мобильную палитру, доступ к которой есть только в скоупе мобильной темы. Сделали мы это при помощи платформенных базовых токенов.
Платформенные токены существуют только в скоупе платформенной темы. Рассмотрим пример для палитры, доступной только для мобильных устройств.
Создаем новый класс палитры в модуле mobile-theme и описываем в нём две реализации для светлой и тёмной темы.
public data class LouisMobileColors( val infoBanner: Color, // ... ) val LightMobilePalette = LouisMobileColors(...) val DarkMobilePalette = LouisMobileColors(...)
// Добавляем платформенные токены в тему @Composable public fun MobileTheme(isDark: Boolean) { val extendedPalette = if (isDark) DarkMobilePalette else LightMobilePalette CompositionLocalProvider( LocalMobileColors provides extendedPalette, ) { LouisTheme(...) } }
// Используем платформенный токен в коде MobileTheme { Box( modifier = Modifier.background(LouisMobile.ExtendedColors.infoBanner) ) }
Эти токены будут доступны только в скоупе мобильной темы, и если попытаться обратиться к ним из TV-темы, то произойдёт рантайм краш из-за пустой композиции.
Компоненты UI
Компоненты дизайн-системы — это переиспользуемые UI-блоки, такие как тулбары, кнопки, поля ввода. Они делают интерфейс консистентным и упрощают работу дизайнерам и разработчикам, исключая необходимость переделывать часто используемые элементы.
Ранее я рассказывал о базовых токенах, а теперь рассмотрим токены компонентов.
Токены компонентов на примере кнопок
Токены компонентов состоят из базовых токенов, объединённых по общему смыслу, и явно определяют какую-то характеристику компонента. Через комбинацию токенов компонента мы определяем его финальный вид. Мы выделили два основных вида токенов компонентов: палитра для покраски и форма для измерения и размещения компонента на экране.
Рассмотрим создание токенов компонентов на примере кнопок. Ниже представлены разные варианты состояния кнопок.

Можно выделить следующие токены кнопки:
форма M и L;
палитры Accent и Critical, у каждой есть имплементации Primary, Secondary и Tertiary;
также есть состояния Default, Progress и Disabled, которые мы не считаем токенами.
Попробуем при помощи этих свойств определить одну из кнопок.

Эта кнопка определяется следующей комбинацией свойств:
форма M;
палитра Critical.Secondary;
состояние Default.
Токены компонента
Токены компонента — это базовый интерфейс с абстрактными значениями, а также его реализации, которые определяют какую-то характеристику компонента — размеры, цвет, состояние. Токены должны содержать все необходимые значения для создания компонента и, по возможности, использовать базовые токены, чтобы поддержать смену темы.
Наиболее популярные токены компонента — это палитра и форма.
Палитра содержит (но не ограничивается) такие токены, как фон, цвет текста, цвет иконок, рипл.
Форма содержит (но, опять же, не ограничивается) скругления, внутренние и внешние отступы, размеры компонента (абсолютные или относительные), типографику.
Токены формы кнопки
Рассмотрим на примере, как выглядят токены формы кнопки
//Cоздаём токены формы кнопки: L и M interface ButtonForm { internal val textStyle: TextStyle @Composable get internal val minWidth: Dp @Composable get internal val minHeight: Dp @Composable get public data object Large : ButtonForm { override val textStyle: TextStyle @Composable get() = Louis.Typography.Headline10 override val minWidth: Dp @Composable get() = 48.dp override val minHeight: Dp @Composable get() = 48.dp } public data object Medium : ButtonForm { override val textStyle: TextStyle @Composable get() = Louis.Typography.Body10 // ... } }
Токены палитры кнопки
// Создаём токены палитры кнопки: Accent и Critical и для каждой из них имплементации Primary, Secondary и Tertiary interface ButtonPalette { internal val background: Color @Composable get internal val text: Color @Composable get public sealed class Accent { public data object Primary : ButtonPalette { override val background: Color @Composable get() = Louis.Colors.Background.Accent override val text: Color @Composable get() = Louis.Colors.Text.ConstantLight } public data object Secondary : ButtonPalette {...} public data object Tertiary : ButtonPalette {...} } public sealed class Critical { public data object Primary : ButtonPalette {...} public data object Secondary : ButtonPalette {...} public data object Tertiary : ButtonPalette {...} } }
Создаём компонент кнопки
// Создаём упрощенный компонент кнопки @Composable public fun ButtonComponent( palette: ButtonPalette, form: ButtonForm, onClick: () -> Unit, text: String, ) { Box( modifier = Modifier .defaultMinWidth(minWidth = form.minWidth, minHeight = form.minHeight) .background(palette.background) .clickable(onClick = onClick), ) { Text( text = text, color = palette.text, style = form.textStyle, ) } }
// Используем кнопку ButtonComponent( form = ButtonForm.Large, palette = ButtonPalette.Accent.Primary, onClick = { ... }, text = "Hello, habr", ) // Или с другой комбинацией палитры и формы ButtonComponent( form = ButtonForm.Medium, palette = ButtonPalette.Critical.Primary, onClick = { ... }, text = "Hello, habr", )

Упрощаем использование
Вспомним, что одно из свойств дизайн-системы — библиотечность. Нужно стремиться сделать компоненты как можно более удобными в использовании разработчиками.
Значения по умолчанию
В большинстве случаев вы можете задать значения по умолчанию для токенов компонента, основываясь на наиболее популярных способах использования.
// Задаём значения по умолчанию для кнопки @Composable fun ButtonComponent( palette: ButtonPalette = ButtonPalette.Accent.Primary, form: ButtonForm = ButtonForm.Large, onClick: () -> Unit, text: String, ) // Используем кнопку ButtonComponent( onClick = { ... }, text = "Hello, habr", )
Slot API
Дизайнеры любят что-то точечно закастомить, например, добавить несколько иконок рядом с текстом кнопки. Чтобы упростить кастомизацию контента, мы делаем компоненты расширяемыми с помощью Slot API. Это паттерн Compose, позволяющий передать любой контент внутрь Composable-функции.

Чтобы поддержать этот подход, берем старую реализацию и меняем тип входящего параметра content со String на Composable-функцию:
// Применяем Slot API @Composable fun ButtonComponent( palette: ButtonPalette = ButtonPalette.Accent.Primary, form: ButtonForm = ButtonForm.Large, onClick: () -> Unit, content: @Composable () -> Unit, ) { Box( ) { content() } } // Используем кнопку со Slot API ButtonComponent( onClick = { ... } ) { Text(...) }
Хотя теперь в компонент можно передать что угодно, мы усложнили самый популярный вариант использования — отображение текста внутри кнопки. Чтобы вернуть его, давайте доработаем компонент. Для этого мы оставим реализацию со Slot API и добавим override функцию, которая будет принимать String вместо @Composable как параметр.
@Composable fun ButtonComponent( text: String, palette: ButtonPalette = ButtonPalette.Accent.Primary, form: ButtonForm = ButtonForm.Large, onClick: () -> Unit, ) { ButtonComponent( palette = palette, form = form, onClick = onClick, ) { Text( text = text, style = form.textStyle, // ... ) } }
Теперь текст можно задать как через Slot API, так и через override-функцию, что позволяет кастомизировать контент.
//Slot API ButtonComponent( onClick = { ... } ) { Text(...) } //Override ButtonComponent( text = "Hello, habr", onClick = { ... } )
Custom токены
Делаем компоненты расширяемыми с помощью Custom-токенов. Это бывает полезно, когда нужно поменять что-то не в контенте, который мы передаём, а в структуре компонента. Например, покрасить цвет кнопки в зелёный, которого нет в палитре компонента.

Для этого мы добавим в токены компонента класс Custom, который принимает в качестве параметров класс токена, который будем изменять, а также набор нулабельных параметров для замены. Если переданный параметр null, то мы берём значение из основного класса.
// Создаём custom-класс, который наследуется от токена компонента interface ButtonPalette { internal val background: Color @Composable get internal val text: Color @Composable get sealed class Accent { data object Primary : ButtonPalette { ... } } //... class Custom( private val base: ButtonPalette = Accent.Primary, private val customBackground: Color? = null, private val customText: Color? = null, ) : ButtonPalette { override val background: Color @Composable get() = customBackground ?: base.background override val text: Color @Composable get() = customText ?: base.text } }
Объединим кастомные токены с использованием Slot API, и получим полностью кастомизированную кнопку.
// Обычная кнопка ButtonComponent( onClick = { ... }, text = "Hello, habr", ) // Кастомизированная кнопка ButtonComponent( palette = ButtonPalette.Custom( background = Louis.Colors.Background.Positive ), onClick = { ... } ) { Row { Icon(...) Text(...) Icon(...) } }

Увеличиваем абстракцию с адаптивными компонентами
Однажды к нам обратились дизайнеры с просьбой: они заметили, что использовать диалоги в боттомшите на планшетах неудобно, и предложили заменить их на классический попап-диалог. Наивным решением было бы в каждой точке вызова проверять размер экрана и создавать соответствующий компонент, но мы решили добавить новый уровень абстракции — адаптивные компоненты, которые имеют одинаковую структуру, но их внешний вид зависит от платформы или размера экрана.

Для адаптивных компонентов мы так же выделяем токены, которые матчатся с токенами используемых компонентов. Мы на проекте решили, что адаптивные компоненты не должны иметь кастомизации, чтобы избежать попыток сматчить значения токенов двух не связанных компонентов. При желании, можно выделить какие-то общие токены: например, стили текста или отступы от края компонента до контента.
public interface AdaptiveDialogForm { data object Form1 : AdaptiveDialogForm { ... } data object Form2 : AdaptiveDialogForm { ... } }
Напишем упрощённый код компонента.
@Composable public fun AdaptiveDialogComponent( header: String, body: String, form: AdaptiveDialogForm = AdaptiveDialogForm.Form1, ) { if (isTablet()) { DialogComponent( form = form.toDialogForm(), header = header, body = body, // ... ) } else { BottomSheetComponent( form = form.toBottomSheetForm(), header = header, body = body, // ... ) } } private fun AdaptiveDialogForm.toDialogForm() = ... private fun AdaptiveDialogForm.toBottomSheetForm() = ...
Мы добавили еще один уровень абстракции в компоненты, и теперь эти компоненты могут сами принимать решение о том, как они будут выглядеть в зависимости от платформы или размера экрана. Разработчикам не нужно вмешиваться в этот процесс.
Проверка нового компонента перед созданием Pull Request
Для проверки компонентов мы используем следующий чек-лист:
Использование базовых токенов без кастома.
Все вариации можно привести к заданным токенам компонента без Custom.
Понятно поведение компонента и его стейты.
Для картинок описано правило скейлинга.
Есть размеры компонента.
Понятно поведение эджкейсов.
Прописаны требования для невизуальной доступности.
Нет фиксированной высоты у компонента с текстом.
Итоги
Описанный подход к созданию дизайн-системы дает нам следующие преимущества:
Верстка один в один: разработчик видит в Figma и коде одно и то же, без необходимости думать о деталях.
Поддержка разных цветовых тем: светлая и темная темы в синих и розовых вариантах.
Легкость добавления новых фич: например, новый набор иконок или дополнительная тема, новый стиль. Мы уверены, что новые фичи не поломают существующий код.
Если у вас есть дополнения или релевантный опыт по этой теме — обязательно делитесь в комментариях!
