Основные принципы
Компоненты Lemu настраиваются общим набором визуальных пропсов. Словарь
объявлен в одном файле — src/types/visual.ts; каждый компонент библиотеки
берёт типы оттуда.
| Проп | Значения | Что задаёт |
|---|---|---|
size | xs · sm · md · lg · xl | Масштаб: габариты, отступы, кегль |
radius | none · sm · lg · circle | Форму углов |
variant | default · primary · secondary · success · warning · danger | Смысл — и цвет через него |
appearance | solid · outline · dashed · ghost | Вес подачи: насколько громко компонент заявляет о себе |
color | любое CSS-значение | Цвет, который приходит из данных |
Зачем это нужно
- Знание переносится. Освоив один компонент, вы знаете API остальных: имена пропов и наборы значений совпадают, автодополнение подсказывает одно и то же.
- Плотность задаётся одним решением. Плотный блок —
size="sm"на всех его частях, просторный —lg. Достаточно выбрать ступень; подбирать каждому компоненту свою величину не нужно. - Композиция без сюрпризов. Компоненты вкладываются друг в друга —
аватар в ячейку, бейдж в карточку, кнопка в тулбар — и ступень передаётся
вглубь: ячейка
size="md"сама укрупняет вложенный аватар. - Код читается как текст.
variant="danger" appearance="ghost"описывает намерение; ревью видит его без похода в исходники. - Библиотека растёт, API остаётся прежним. Новый компонент берёт
готовый словарь вместо собственных
tone,kind,typeиlevel— его API известен ещё до того, как компонент написан.
Один узор для разных компонентов
Кнопка интерактивна, бейдж статичен; у них разная разметка, разные роли и разные части внутри. Настраиваются они одинаково, буква в букву:
Каждый компонент трактует словарь по-своему
Пропы называются и выбираются одинаково, но воплощает их каждый компонент
в своей природе. Проп описывает свойство; во что оно превратится в рендере,
решает компонент: lg всегда крупнее md, а за счёт чего именно —
зависит от роли компонента.
sizeуAvatar— диаметр; уCell— высота строки, паддинги, кегль заголовка и размер вложенного аватара; уCheckbox— сторона квадрата и кегль подписи.variantуButtonкрасит саму кнопку; уCell— всю строку списка; уCheckbox— отмеченное состояние, неотмеченный чекбокс всегда нейтральный.radius="circle"уButtonдаёт «таблетку»; уButtonIcon— круглую кнопку; уAvatar— круглый аватар.colorуBadgeкрасит весь бейдж; уAvatar.Fallback— подложку с инициалами; уCard— фон карточки.
Умолчания у каждого компонента тоже свои — они выбраны по типичному
сценарию использования. Кнопка по умолчанию primary, потому что чаще
всего зовёт к действию; бейдж — default, потому что чаще всего лишь
помечает; ячейка списка без variant и appearance остаётся вовсе
неокрашенной, как обычная строка.
size — шкала ступеней
size задаёт ступень плотности: xs — плотные списки и таблицы, sm —
базовый размер интерфейса, md — заголовки и формы, lg и xl —
акцентные и посадочные экраны. Величину ступени каждый компонент выбирает
по своей роли: sm-кнопка выше sm-бейджа — метке ни к чему занимать
столько же места, сколько действию.
Шкала гарантирует порядок величин: md крупнее sm у любого компонента,
поэтому одна ступень на весь блок даёт согласованную плотность. Внутри
компонента размер меняет всё сразу — отступы, кегль, иконки и вложенные
компоненты масштабируются вместе.
Ступень наследуется
size — единственный каскадирующий проп: ступень, заданная контейнеру,
доходит до всех вложенных компонентов ДС. Ячейка size="lg" сама
укрупняет вложенные аватар, бейдж и кнопку; свой size им не нужен.
Явный size на вложенном компоненте перебивает каскад.
Класс ступени ставит наследуемые CSS-флаги --visual-size-*, а величину
компонент выбирает по ним уже в своём модуле. Отсюда два следствия:
- Дефолт
sizeживёт в CSS-фолбэке шкалы. JS-значения по умолчанию у пропаsizeнет: собственный класс перебивал бы ступень контейнера. - Оверлеи каскад не пробивает. Диалоги, меню, тосты и выпадающие
списки рендерятся порталом в
body; их плотность собственная и от триггера не зависит.
variant, appearance, radius и color не каскадируют: цвет и подача —
решение про конкретный компонент.
radius — форма углов
От прямых углов none до circle. Крайнее значение читается по форме
компонента: вытянутый превращается в «таблетку», квадратный — в круг.
variant — смысл действия
Вариант выбирают по смыслу, цвет приходит из темы: удаление — danger,
подтверждение — success, второстепенное действие — secondary,
нейтральный элемент — default. При смене палитры темы интерфейс
перекрашивается целиком и сохраняет смысловые акценты.
appearance — вес подачи
Один и тот же смысл можно подать с разной громкостью: solid — сплошная
заливка, outline — обводка, dashed — та же обводка пунктиром (обычно
для черновых и «добавить» состояний), ghost — приглушённая подложка
цветом варианта.
variant и appearance — две независимые оси. Первая отвечает на вопрос
«что это значит», вторая — «насколько это важно здесь». На экране обычно
одно solid-действие, всё остальное — outline и ghost.
color — цвет из данных
Метка задачи, тег, проект, событие календаря, аватар пользователя: их цвет
хранится в данных и в шесть вариантов не укладывается. Для таких случаев
есть color — он принимает любое CSS-значение и перекрывает цвет
варианта. Контрастный цвет текста компонент подбирает сам.
Стабильный цвет
Чтобы цвет сущности совпадал во всех списках и сессиях, берите его
у хелпера getStableColor: одна и та же
строка всегда даёт один и тот же цвет палитры темы.
Свои компоненты на visual()
Словарь — публичный API пакета: приложения строят собственные компоненты
в том же языке. Типы забираются выборкой Visual<...> из общего
интерфейса, классы и inline-стиль отдаёт хелпер visual():
import { visual, type Visual } from "@tulls/ui";
import clsx from "clsx";
import styles from "./chip.module.css";
interface ChipProps extends Visual<"size" | "variant" | "appearance"> {
children: React.ReactNode;
}
export function Chip({
variant = "default", // смысловые дефолты — в JS
appearance = "ghost",
size, // дефолт size — в CSS-фолбэке, иначе сломается каскад
children,
}: ChipProps) {
const vis = visual({ variant, appearance, size });
return (
<span className={clsx(styles.Root, vis.className)} style={vis.style}>
{children}
</span>
);
}Слой пишет только переменные контракта — где их применить, решает CSS
компонента. Читать var(--visual-*) можно только на элементе с классами
visual() (переменные не наследуются); частям компонента значения
раздаются через приватные --_*.
| Классы слоя | Переменные |
|---|---|
Size--* | флаги ступени --visual-size-{xs,sm,md,lg,xl} (наследуются — это и есть каскад) |
Variant--*, Colored | --visual-base, --visual-text |
Appearance--* | --visual-bg, -fg, -border, -border-style, -bg-hover, -border-hover, -bg-active, -border-active |
Radius--* | --visual-radius |
Свою шкалу компонент объявляет guard-цепочкой: переменная
var(--visual-size-md) <величина> валидна только на активной ступени,
а цепочка фолбэков выбирает первую валидную; последний фолбэк — дефолтная
ступень компонента. Имена ступеней — --_step-<роль>-<ступень>, дефолт
цепочки — --_<роль>-default, свёрнутый итог — описательное имя без step
(--_avatar-size, --_cell-padding):
.Root {
--_step-height-sm: var(--visual-size-sm) 24px;
--_step-height-md: var(--visual-size-md) 32px;
height: var(--_step-height-sm, var(--_step-height-md, 24px));
background: var(--visual-bg, transparent);
color: var(--visual-fg, inherit);
border-radius: var(--visual-radius, var(--radius-1x));
}Селекторы слоя обёрнуты в :where() — их специфичность равна нулю, и
компонент переопределяет трактовку любого значения обычным одиночным
классом в своём модуле, не завися от порядка модулей в бандле.
Шпаргалка
variant— смысл,appearance— вес,sizeиradius— плотность и форма,color— данные.- Для состояния используйте
variant, для идентичности данных —color: тема должна уметь перекрасить смысл. - Кастомный цвет задавайте только через
color— компонент сам подберёт контрастный текст и приглушённые состояния. Цвет, заданный мимо словаря, черезstyleилиclassName, слой не увидит.