import type { Messages } from './en'; /** * Три формы русского числительного: 1 компонент, 2–4 компонента, 5–20 * компонентов. * * Ровно то, что шаблонная строка выразить не может — потому такие значения и * являются функциями. Живёт здесь, а не внутри объекта, потому что счётчиков * в каталоге несколько, а правило у них одно; `Messages` описывает только * форму самого объекта, так что модульная функция ей не мешает. */ function plural(n: number, one: string, few: string, many: string): string { const tens = n % 100; const ones = n % 10; if (tens >= 11 && tens <= 14) return `${n} ${many}`; if (ones === 1) return `${n} ${one}`; if (ones >= 2 && ones <= 4) return `${n} ${few}`; return `${n} ${many}`; } /** Русский. Форма берётся из `en.tsx` — недостающий ключ падает на `tsc`. */ export const ru: Messages = { common: { language: 'Язык', copy: 'Копировать', copied: 'Скопировано', close: 'Закрыть', loading: 'Загрузка…', skipToContent: 'Перейти к содержимому', }, error: { title: 'Этот пример не удалось отрисовать.', titleFor: (label: string) => `${label} не удалось отрисовать.`, cell: 'Ошибка', }, nav: { brandCaption: 'Дизайн-система', searchPlaceholder: 'Поиск компонентов…', searchShortcut: 'Нажмите слэш для поиска', matchCount: (n: number) => plural(n, 'компонент', 'компонента', 'компонентов'), overview: 'Обзор', introduction: 'Введение', tokens: 'Токены дизайна', icons: 'Иконки', buttons: 'Кнопки', alerts: 'Уведомления', avatars: 'Аватары', badges: 'Бейджи', toasts: 'Тосты', parity: 'Соответствие Figma', noMatch: 'Ничего не найдено.', open: 'Открыть навигацию', hide: 'Скрыть навигацию', show: 'Показать навигацию', rail: 'Навигация по дизайн-системе', railDescription: 'Основы и все компоненты набора.', }, header: { breadcrumb: 'Хлебные крошки', root: 'Дизайн-система', components: 'Компоненты', foundations: 'Основы', notFound: 'Не найдено', }, notFound: { title: (path: string) => `По адресу «${path}» ничего нет`, body: 'Выберите компонент в боковом меню.', }, toolbar: { figma: 'Figma', dark: 'Тёмная', light: 'Светлая', bridge: { live: 'Мост работает, окно плагина Figma открыто.', noPlugin: 'Мост работает, но его никто не опрашивает. В Figma: Plugins → Development → «CBAR Figma bridge».', off: 'На порту нет моста — все панели Figma читают зафиксированный снимок.', idle: 'Живые запросы к Figma отключены для этой сессии.', }, status: { live: 'Онлайн', noPlugin: 'Нет плагина', off: 'Нет моста', idle: 'Выкл.', probing: 'Проверяем мост…', }, file: 'Файл', a11y: 'Axe', a11yHint: 'Запускает axe-core по области main этой страницы при каждом переходе. Только для разработки — здесь проверяется и `color-contrast`, чего набор тестов на jsdom не умеет.', }, a11y: { title: 'Доступность', running: 'Проверяем…', rerun: 'Запустить снова', none: 'В области main этой страницы нарушений нет.', clean: 'Чисто', count: (n: number) => `${n} ${n % 10 === 1 && n % 100 !== 11 ? 'элемент' : 'элем.'}`, reference: 'Про правило', scope: 'Проверяется только
. Оболочка сайта не входит.', impact: { critical: 'Критично', serious: 'Серьёзно', moderate: 'Умеренно', minor: 'Незначительно', }, }, table: { props: 'Свойства', prop: 'Свойство', type: 'Тип', default: 'По умолчанию', description: 'Описание', figmaSpec: 'Размеры из Figma', required: 'обязательное', }, matrix: { copyAll: (total: number) => `Копировать все варианты (${total})`, copyAllTitle: 'Каждая ячейка этой сетки — одним блоком импорта', }, playground: { title: 'Песочница', snippetTitle: 'Строка импорта и JSX именно для этого состояния', controls: 'Свойства', reset: 'Сбросить', resetTitle: 'Вернуть свойства этого компонента к значениям по умолчанию', }, share: { share: 'Поделиться', copyLink: 'Скопировать ссылку на это состояние', copyPreview: 'Скопировать ссылку на превью', copyCode: 'Скопировать компонент и импорт', openPreview: 'Открыть превью в новой вкладке', linkCopied: 'Ссылка скопирована', }, embed: { openInShowcase: 'Открыть в showcase', notFound: 'Такого компонента нет.', }, tour: { reopen: 'Вводный тур', title: 'Знакомство с UI-китом', step: (index: number, total: number) => `Шаг ${index} из ${total}`, back: 'Назад', next: 'Далее', done: 'Начать', skip: 'Пропустить', progress: 'Ход тура', slides: { welcome: { title: 'Добро пожаловать в дизайн-систему', body: 'Этот сайт документирует UI-кит, который публикуется как пакет. Шесть слайдов о том, что внутри и что умеет сам сайт — примерно минута.', components: (n: number) => plural(n, 'компонент', 'компонента', 'компонентов'), icons: (n: number) => plural(n, 'иконка', 'иконки', 'иконок'), subpaths: (n: number) => plural(n, 'путь импорта', 'пути импорта', 'путей импорта'), }, components: { title: 'Компоненты и их варианты', body: 'Контролы собираются из двух независимых осей: способа заливки и цветовой палитры. Их пересечение — причина того, что несколькими пропсами покрывается весь холст CBAR, и страница каждого компонента рисует это пересечение сеткой.', caption: 'Каждый контрол принимает ещё и размер — от xs до xl.', }, tokens: { title: 'Токены и один переключатель, который это доказывает', body: 'Ни один компонент не называет цвет. Они называют роли — background, ctl-solid, muted-foreground, — а файл токенов сопоставляет эти роли с рампами CBAR. Именно из-за такой косвенности ребрендинг умещается в один файл.', tryIt: 'Нажмите — сайт за этим диалогом переключится вместе с ним.', switchLabel: 'Тема', dark: 'Тёмная', light: 'Светлая', }, icons: { title: 'Весь набор иконок', body: 'Библиотека CBAR приезжает вместе с китом, каждая иконка нарисована в outline и solid. Они тришейкятся поштучно, поэтому импорт одной иконки стоит меньше килобайта.', familyLabel: 'Семейство иконок', outline: 'Outline', solid: 'Solid', }, site: { title: 'Что умеет этот сайт', body: 'Каждая страница собрана из одних и тех же четырёх частей:', playground: 'Песочница с отдельным контролом на каждый пропс — её состояние живёт в URL.', matrix: 'Сетки вариантов в той раскладке, которую использует холст Figma.', copy: 'Копируется всё: ячейка, целая сетка, пример. Копируется именно тот элемент, что на экране, вместе со строками импорта.', parity: 'Отчёт о соответствии того, что нарисовано в Figma, тому, что реализовано в ките.', search: '— поиск по сайдбару откуда угодно', }, start: { title: 'С чего начать', body: 'Установите, один раз импортируйте стили, дальше берите компоненты по одному.', copyInstall: 'Скопировать команду установки', links: 'Или почитайте дальше:', }, }, }, introduction: { title: 'UI Kit', lead: 'Это не приложение, а библиотека компонентов, которая публикуется. Каждый компонент здесь приезжает к коллегам собственным путём импорта, с дизайн-системой CBAR, зашитой в его токены.', install: { title: 'Установка', body: 'Пакет несёт собственный скомпилированный файл стилей, поэтому потребляющему приложению не нужна вообще никакая настройка Tailwind.', copyInstall: 'Скопировать команду установки', copyUsage: 'Скопировать строки импорта', }, structure: { title: 'Как это устроено', folders: { term: 'Одна папка на компонент', def: 'Реализация, тест и index.ts живут вместе, и именно этот index.ts создаёт подпуть — scripts/gen-exports.mjs читает структуру папок и пишет из неё package.json#exports. Нет рукописного списка, который можно забыть обновить.', }, tokens: { term: 'Два слоя токенов', def: 'tokens.css хранит сырые значения и принадлежит машине; theme.css отображает их на роли и отдаёт роли в Tailwind. Компонентам можно называть только роль, поэтому ребрендинг — это один файл или один прогон figma-sync.', }, styles: { term: 'Стили приезжают скомпилированными', def: 'Публикуемый пакет несёт styles.css, собранный через Tailwind CLI. Приложения, которые уже на Tailwind v4, могут вместо этого импортировать слои токенов и собрать утилиты своей сборкой.', }, deps: { term: 'Ноль рантайм-зависимостей', def: 'Установка кита не добавляет в ваш lock-файл ничего. Radix по-прежнему обеспечивает контракт доступности, а tailwind-merge — победу вашего className, но первый скомпилирован в dist, а второй перенесён в src, так что это забота кита, а не ваша. Иконки, конструктор вариантов, композер классов, командная палитра, тосты и арифметика дат и так были кодом кита.', }, }, usingSite: { title: 'Как пользоваться этим сайтом', overview: (count: number) => `все ${count} компонентов в одной прокрутке, отрисованных вживую.`, tokens: 'каждый примитив и роль, прочитанные из работающего документа, рядом — собственный hex CBAR.', icons: 'весь набор иконок с поиском и четырьмя форматами копирования для каждой.', parity: 'что рисует файл CBAR в Figma против того, что реализовано в ките.', toolbar: 'Переключатели темы и языка живут в URL, поэтому любой вид этого сайта — ссылка, которую можно кому-то отправить.', copy: 'Копируется почти всё: текущее состояние песочницы, отдельная ячейка матрицы, целая сетка, пример. Копируется именно тот элемент, что на экране, вместе со строками импорта.', }, adding: { title: 'Добавить компонент', body: 'В Claude Code одна команда создаёт папку, тест и экспорт:', copySkill: 'Скопировать команду', byHand: 'Вручную: npx shadcn@latest add , перенесите созданный файл в собственную папку, добавьте index.ts, начинающийся с директивы "use client", затем выполните pnpm exports:gen.', }, }, overview: { lead: (count: number) => ( <> {count} компонент{count % 10 === 1 && count % 100 !== 11 ? '' : 'ов'}, отрисованных вживую из{' '} src/ — не из сборки. Каждая карточка ведёт на страницу с полной сеткой вариантов из Figma. ), }, component: { copyCode: 'Копировать код', copyCodeTitle: 'Импорты и JSX для этого примера', compositions: 'Композиции', }, tokens: { title: 'Токены дизайна', intro: ( <> tokens.css хранит примитивы и принадлежит машине — навык figma-sync переписывает его целиком.{' '} theme.css связывает их с семантическими ролями ниже, и компонентам доступны только эти роли. ), hexNote: ( <> В каждом образце показано заданное значение, а под ним — собственный hex CBAR: он прочитан из комментария, который генератор оставил рядом с объявлением, а не пересчитан из oklch, то есть это именно то число, что печатает Figma. Роль также называет примитив, к которому она сводится, — браузер это скрывает. Ведущий ≈ помечает немногие значения без соответствия в Figma — интерполированный шаг или роль с альфой, — восстановленные из заданного значения. ), semanticRoles: 'Семантические роли', radius: 'Радиус', radiusNote: 'Каждый радиус выводится из --ui-radius, поэтому одна правка скругляет весь кит заново. Компоненты называют алиас Tailwind (rounded-md), но никогда не примитив.', elevation: 'Тени', elevationNote: 'Двухслойные тени — плотная контактная плюс мягкая рассеянная. Одна размытая тень читается как серое пятно, а не как глубина.', motion: 'Анимация', motionNote: 'Единственная группа, которую компонентам можно называть напрямую, вместе со шкалой z-index: длительность и easing не переопределяются под тему, поэтому семантического слоя над ними нет. Вся анимация выключается при prefers-reduced-motion глобальным правилом в globals.css, так что компонентам не нужно это обрабатывать.', tokenColumn: 'Токен', valueColumn: 'Значение', syncTitle: 'Синхронизация из Figma', syncNote: 'tokens.css принадлежит машине. При запущенном мосте Figma навык figma-sync читает Variables, показывает разницу и переписывает примитивы. theme.css — отображение примитивов на роли — принадлежит человеку и переживает синхронизацию.', copyCss: 'Копировать CSS', copyCssTitle: (section: string) => `Копировать все токены «${section}» как CSS, hex-значения`, copyHexTitle: (hex: string) => `Копировать ${hex}`, derivedNote: 'Приблизительно — для этого значения нет hex из Figma, поэтому оно восстановлено из заданного. Либо шаг, интерполированный за пределы шкалы CBAR, либо значение с альфой, которого нет ни на одном шаге шкалы.', }, icons: { title: 'Иконки', lead: (count: number, categories: number, pkg: string) => ( <> Весь набор CBAR — {count} иконок в {categories} категориях, у каждой контурная и залитая отрисовка за одним пропом variant. Поставляются из {pkg}/icons и намеренно отсутствуют в корневом барреле, чтобы имена вроде CheckIcon не конфликтовали с набором приложения. Нажмите на любую, чтобы скопировать. ), searchPlaceholder: 'Поиск иконок…', allCategories: 'Все категории', variant: 'Вариант', family: 'Семейство', size: 'Размер', detailMeta: (category: string, shared: boolean, size: number) => ( <> CBAR {category} ·{' '} {shared ? 'одна отрисовка на оба семейства' : 'контур + заливка'} · {size}px ), sharedNote: ( <> CBAR рисует эту иконку один раз — variant принимается для единообразия, но ничего не меняет. ), copyUsage: 'Копировать импорт + JSX', copySvg: 'Копировать SVG', copySvgTitle: 'Отрисованная разметка, классы кита убраны', copyName: 'Копировать имя', copyComponent: 'Копировать компонент', copyComponentTitle: 'Самостоятельный React-компонент — без зависимости от этого кита', empty: (query: string, category: string | null) => `По запросу «${query}» ничего не найдено${category ? ` в категории ${category}` : ''}.`, }, buttons: { title: 'Кнопки', lead: (pkg: string) => ( <> Девять вариантов, семь палитр и девять размеров из одного компонента, поставляется из{' '} {pkg}/button. Оформление и цвет — отдельные пропсы, как это устроено у CBAR: пять вариантов сочетаются с любой палитрой, четыре закрепляют собственный цвет и игнорируют её. В каждой ячейке иконки с обеих сторон, как их по умолчанию рисует набор CBAR; переключатель{' '} Иконки их убирает. Наведите на ячейку, чтобы скопировать её как JSX со строками импорта — по одной ячейке за раз, это единственная единица, которую вставляют. Фильтры живут в URL, поэтому то, что вы видите, — это и есть ссылка, которую вы отправите. ), searchPlaceholder: 'Поиск вариантов, палитр, размеров…', palette: 'Палитра', size: 'Размер', icons: 'Иконки', disabled: 'Отключено', all: 'Все', empty: (query: string) => ( <> По запросу «{query}» ничего не найдено. Попробуйте вариант (outline), палитру ( green) или размер (icon-lg). ), composableTitle: 'Комбинируемые варианты', composableLead: ( <> Пять оформлений на семь палитр в пяти высотах. Всё это стоит двенадцати записей cva, потому что вариант читает роли --ctl-*, которые задаёт класс палитры, а не называет цвет — поэтому новая палитра ничего не добавляет к каждому варианту. ), composableGrid: (disabled: boolean) => `size × colorPalette${disabled ? ', disabled' : ''}`, pinnedTitle: 'Варианты с закреплённым цветом', pinnedDescription: 'Эти четыре берут цвет из семантических ролей в theme.css, поэтому несут собственный цвет и игнорируют colorPalette — фильтр палитры выше до них не доходит. У варианта link вообще нет рамки, поэтому его строка читается как текст, а не как элемент управления.', iconOnlyTitle: 'Размеры «только иконка»', iconOnlyDescription: (palette: string) => `Квадрат за счёт размера, а не отдельного компонента: 32 / 36 / 40 / 44px, здесь отрисовано с colorPalette="${palette}". Иконка — дочерний элемент, поэтому скопированная ячейка приносит две строки импорта. Каждой из них нужен aria-label — читать здесь нечего.`, tabs: { label: 'Вид', gallery: 'Галерея', figma: 'Раскладка Figma', }, figma: { lead: ( <> Те же кнопки в той форме, в которой их рисует холст CBAR: по полосе на палитру, по столбцу на приём заливки и по строке на состояние. Набор Button ( 2446:6959) раскладывает свои четыре размера блоками бок о бок, поэтому переключатель Размер ниже — это то, на какой блок вы смотрите. Поставьте{' '} Все, и получите сам холст. ), stateNote: ( <> Пропом здесь является только disabled. hover и{' '} active — это CSS-состояния, поэтому две строки принудительно отрисованы тем самым классом, который controlVariants ставит за :hover: строка показывает то, что кит действительно рисует, а не приближение. Они совпадают, потому что CBAR рисует их одинаково на всех пяти приёмах, а собственного :active у кита нет. ), copyNote: ( <> Копируется кнопка без этого принудительного класса: hover — состояние, а не вариант, поэтому в сниппете сам контрол, а остальное делает указатель. ), swapNote: ( <> Набор Button в CBAR красит эту полосу наоборот — его colorPalette=primary{' '} привязан к surface-colored-secondary. Figma-переменные, IconButton и ProgressCircle согласны с тем, что нарисовано здесь, поэтому набор считается неверно связанным. Разбор целиком — в docs/FigmaParity.mdx. ), sizeNote: ( <> Ступени sm для Button в CBAR нет — он прыгает с xs на{' '} md. Кит добавляет её на 36px, и она есть на вкладке «Галерея». ), state: 'Состояние', allSizes: 'Все', grid: (palette: string, size: string) => `colorPalette="${palette}", size="${size}"`, }, }, alerts: { title: 'Уведомления', lead: (pkg: string) => ( <> Четыре варианта и семь палитр из одного компонента, поставляется из{' '} {pkg}/alert. В отличие от кнопки, ни один из них не несёт hover-состояния — уведомление не интерактивно. Наведите на ячейку, чтобы скопировать её как JSX со строкой импорта — по одной ячейке за раз, это единственная единица, которую вставляют. Фильтры живут в URL, поэтому то, что вы видите, — это и есть ссылка, которую вы отправите. ), searchPlaceholder: 'Поиск вариантов, палитр, размеров…', palette: 'Палитра', size: 'Размер', all: 'Все', empty: (query: string) => ( <> По запросу «{query}» ничего не найдено. Попробуйте вариант (surface), палитру (tertiary) или размер (lg). ), grid: 'size × colorPalette', cellTitle: 'Внимание', cellDescription: 'Каждый вариант на холсте несёт его.', tabs: { label: 'Вид', gallery: 'Галерея', figma: 'Раскладка Figma', }, figma: { lead: ( <> Те же уведомления в той форме, в которой их рисует холст CBAR: по полосе на статус, по строке на приём заливки и по столбцу на размер. Набор Alert ( 2446:316) вообще не рисует состояний взаимодействия — каждый пропс здесь настоящий. Каждая ячейка шириной 421px и несёт иконку, заголовок и описание, потому что так устроены все 72 варианта на холсте и ни одно из трёх не спрятано за свойством. ), bandNote: ( <> CBAR отделяет приём заливки от смысла так же, как и кит: status — это то, что кит выражает через colorPalette. Соответствие читается по заливкам, а не по названиям: info — это бирюзовый secondary ( #4BC7B5), а info-secondary — тёмно-синий primary{' '} (#004976), и таблица переменных говорит то же самое. tertiary{' '} — единственная палитра кита без полосы здесь; она всё ещё на вкладке «Галерея», которая проходит по каждой палитре кита. ), sizeNote: ( <> Самую маленькую ступень CBAR называет default; кит называет её{' '} sm, чтобы лестница читалась как у любого другого компонента. Описание меняется вместе с ней — 12 / 16 / 18px — так же, как в наборе. ), outlineNote: ( <> Две вещи здесь намеренно не скопированы из файла: его вариант{' '} outline несёт лишний тёмно-синий оттенок на warning,{' '} success и info-secondary, но не на остальных трёх, и он рисует одну и ту же иконку статуса на всех шести полосах. См. находку 7 на вкладке{' '} /parity?tab=findings. ), status: 'Статус', grid: (status: string, palette: string) => `status="${status}" (colorPalette="${palette}")`, }, }, avatars: { title: 'Аватары', lead: (pkg: string) => ( <> Три трактовки, семь палитр, шесть ступеней и две формы из одного компонента — поставляется из {pkg}/avatar. shape — третья ось, которую CBAR рисует явно, поэтому сетка здесь закреплена за одной формой, а не пытается читаться как три измерения. Наведите на ячейку, чтобы скопировать её как JSX вместе со строкой импорта — по одной ячейке, ведь вставляют именно её. Фильтры живут в URL, так что вид, который вы смотрите, — это ссылка, которую вы отправляете. ), searchPlaceholder: 'Поиск вариантов, палитр, размеров, форм…', palette: 'Палитра', size: 'Размер', shape: 'Форма', all: 'Все', empty: (query: string) => ( <> Ничего не найдено по «{query}». Попробуйте вариант (outline), палитру ( tertiary), размер (2xs) или форму (square). ), grid: 'size × colorPalette', tabs: { label: 'Вид', gallery: 'Галерея', figma: 'Раскладка Figma', }, figma: { lead: ( <> Те же аватары в том виде, в каком их рисует холст CBAR: полоса на палитру, блок на трактовку, форма вниз и размер вбок. Набор avatar (2446:757) не рисует состояний взаимодействия, поэтому каждый показанный здесь проп — настоящий проп. Блоки стоят рядом и прокручиваются, а не переносятся, потому что уменьшающаяся слева направо лестница — это то, что показывает холст. ), bandNote: ( <> Две из четырёх полос названы в файле неверно. colorPalette=primary у CBAR привязан к surface/colored/secondary (#4BC7B5, бирюзовый), а{' '} secondary — к surface/colored/primary (#004976, тёмно-синий), то есть перепутаны — ровно так же, как в наборе Button, тогда как таблица переменных говорит brand/primary/500 = #004976. Каждый заголовок называет ту палитру кита, которая действительно красит полосу. third и{' '} neutral — просто переименования и совпадают; у red,{' '} green и yellow полосы здесь нет, они остаются на вкладке «Галерея». ), sizeNote: ( <> Измерено через мост: лестница — 24 / 32 / 36 / 40 / 48 / 64px, текст — 10 / 12 / 14 / 16 / 20 / 24px; каждая ступень совпадает с тем, что рисует кит. Квадратный угол на всех шести размерах — radius/md, а не масштабируемый, поэтому аватар 24px сохраняет те же 8px, что и аватар 64px. ), swapNote: ( <> Набор avatar у CBAR красит эту полосу противоположной ролью — та же перепутанность, что и у Button. Кит согласуется с таблицей переменных и ради этого не меняется; заголовок называет то, что красит полосу на самом деле. Полный разбор — находка 1 на вкладке{' '} /parity?tab=findings. ), imageNote: ( <> Пятая ось набора, avatar-type, нарисована всего шесть раз — при{' '} solid / square / third, по одному на ступень. Кит выражает её дочерним компонентом, а не пропом (AvatarImage вместо{' '} AvatarFallback), поэтому отчёт о соответствии закрепляет её за{' '} initials. Картинка ниже — локальная заглушка: холст заполняет их фотографией, распространять которую мы не вправе. ), palette: 'Палитра', grid: 'shape × size', imageGrid: 'size', }, }, badges: { title: 'Бейджи', lead: (pkg: string) => ( <> Пять трактовок против семи палитр плюс пять сокращений, которые закрепляют собственный цвет, — всё из {pkg}/badge. Бейдж не интерактивен, поэтому ни у чего здесь нет состояния наведения, пока asChild не сделает его ссылкой. Наведи на ячейку, чтобы скопировать её как JSX вместе со строкой импорта — по одной ячейке, потому что вставляют именно её. Фильтры живут в URL, так что вид, на который ты смотришь, — это ссылка, которую ты отправишь. ), searchPlaceholder: 'Искать варианты, палитры, размеры…', palette: 'Палитра', size: 'Размер', icons: 'Иконки', all: 'Все', empty: (query: string) => ( <> Ничего не найдено по «{query}». Попробуй вариант (surface), палитру ( tertiary) или размер (xs). ), grid: 'size × colorPalette', composableTitle: 'Компонуемые трактовки', composableLead: ( <> Эти пять красятся из ролей --ctl-*, поэтому принимают{' '} colorPalette, и ячейка есть для каждой пары. ), shorthandTitle: 'Сокращения', shorthandLead: ( <> А эти пять закрепляют цвет из семантических ролей, поэтому просто игнорируют{' '} colorPalette, а не соревнуются с ним в специфичности — именно поэтому здесь они скрещены с size, а не с осью палитры. Учти:{' '} variant="secondary" — это нейтральная серая роль, а не бирюзовая рампа colorPalette="secondary"; два имени сталкиваются, и это цена сохранения старых сокращений. ), shorthandGrid: 'colorPalette на эти пять не влияет', tabs: { label: 'Вид', gallery: 'Галерея', figma: 'Раскладка Figma', }, figma: { lead: ( <> Те же бейджи в том виде, в каком их рисует холст CBAR: полоса на цвет, строка на размер, колонка на трактовку. Набор Badge (2456:5146) объявляет 32 варианта, и все 32 ниже — четыре полосы по восемь. Состояний взаимодействия набор не рисует, поэтому каждый показанный здесь проп — настоящий проп. ), bandNote: ( <> CBAR отделяет трактовку от смысла так же, как и кит: color — это то, что кит выражает через colorPalette. Соответствие считано с заливок, а не с имён: имена называют роль (removed, default), а не рампу, так что сопоставлять по имени нечего. Каждый заголовок называет палитру кита, которая действительно красит полосу. ), defaultNote: ( <> color="default" — это бирюзовый secondary, а не тёмно-синий primary. Измерено через мост: #4BC7B5 solid /{' '} #CFF3EC surface / #A6E8DA граница — ровно{' '} secondary-500 / -100 / -200, те же три значения, что набор Alert использует для полосы info. Значит, у{' '} primary, tertiary и yellow здесь полосы нет; все три на вкладке «Галерея», которая проходит по всем палитрам кита. ), variantNote: ( <> Холст рисует две трактовки. surface у CBAR — это surface кита: все 16 таких вариантов несут{' '} box-shadow: 0 0 1px 0 <цвет> inset — границу в 1px, нарисованную внутренней тенью, то есть ровно «оттенок плюс обводка», что кит и понимает под этим именем. У subtle, outline и ghost здесь колонки нет, они на вкладке «Галерея». ), iconNote: ( <> .iconStart? и .iconEnd? — булевы свойства набора, и оба по умолчанию true, поэтому каждый вариант на холсте несёт по глифу с обеих сторон — Outline/Interface/Plus и Outline/Interface/Settings, собственные значения подстановки набора. Кит принимает иконки как детей, а не как пропы, поэтому расположение — это порядок детей, и на странице соответствия сравнивать нечего; здесь же это переключатель, начинающий там, где начинает Figma. Измерено: коробка, отступы, зазор, текст и глиф совпадают с китом на всех четырёх ступенях. ), color: 'Цвет', grid: (color: string, palette: string) => `color="${color}" (colorPalette="${palette}")`, }, }, toasts: { title: 'Тосты', lead: (pkg: string) => ( <> Пять статусов из одного компонента, из {pkg}/toast. Тост на этой полке — исключение: его статус не проп. Тост не рендерят — вызывают{' '} toast.success(…), и стор передаёт его единственному Toaster, смонтированному у корня приложения. Поэтому галерея ниже показывает и то, как выглядит каждый вид, и вызовы, которые их порождают. Фильтры живут в URL, так что вид, на который ты смотришь, — это ссылка, которую ты отправишь. ), searchPlaceholder: 'Искать виды…', closeButton: 'Кнопка закрытия', icons: 'Иконки', all: 'Все', dismissAll: 'Закрыть все', empty: (query: string) => ( <> Ничего не найдено по «{query}». Попробуй вид (warning, loading). ), kindsTitle: 'Все виды', kindsLead: ( <> Шесть видов против пяти состояний CBAR: у loading нет пары на холсте, а то, что дизайн-файл зовёт neutral, кит зовёт default. Цвет целиком задаётся видом — он выбирает палитру, а поверхность и ведущая кромка читают из неё{' '} --ctl-subtle и --ctl-border. ), kindsGrid: 'kind — кнопка копирования даёт вызов, а не разметку', liveTitle: 'Как на самом деле', liveLead: ( <> Сетка выше нарисована статически, чтобы все шесть можно было сравнить разом. А вот так тост поднимают в жизни. Здесь ничего не закреплено, поэтому каждый исчезает сам через четыре секунды — закреплённый тост поехал бы за тобой на следующую страницу, ведь очередь это модульный синглтон, общий для всего приложения. ), position: { title: 'Где он появляется', lead: ( <> bottom-right — это всего лишь значение по умолчанию. Угол — единственное, что не выбирается в момент вызова: он принадлежит Toaster, поэтому его делят все тосты приложения. Выбери угол ниже, и собственный Toaster витрины переедет — кнопка поднимает настоящий тост, а не его изображение. ), legend: 'Положение Toaster', demo: 'Поднять здесь', /* По-английски — как и все значения внутри вызовов, которые копируют. */ demoBody: 'The Toaster owns the corner; the call does not.', note: ( <> Шесть значений, и порядок стопки следует за ними: новый ближе к краю, так что верхний угол заполняется вниз, а нижний вверх. Монтируй ровно один Toaster — второй не разделит тосты между углами, а нарисует всю очередь дважды. ), }, tabs: { label: 'Вид', gallery: 'Галерея', figma: 'Раскладка Figma', }, figma: { lead: ( <> Те же тосты в том виде, в каком их рисует холст CBAR: одна колонка, строка на состояние, сверху вниз. Набор Toast (2414:10758) объявляет пять вариантов, и все пять ниже. Каждый шириной 306px, отступы 12px 16px, радиус 6px и граница 3px только по ведущей кромке — не обводка. Состояний взаимодействия набор не рисует, поэтому каждый показанный проп — настоящий. ), bandNote: ( <> На этот раз в сопоставлении нет догадок: figma_css прямо отдаёт имена привязанных переменных — --surface-subtle-secondary /{' '} --border-colored-secondary для info,{' '} --border-colored-black для neutral — и они уже совпадают с{' '} kindPalettes в компоненте. Значит info у CBAR — это бирюзовый{' '} secondary, третий набор, говорящий это, после полосы info у Alert и цвета default у Badge. ), neutralNote: ( <> Четыре из пяти совпадают побайтово. neutral — нет, и причина в дизайн-файле: CBAR держит две переменные для одной роли — --surface-subtle-neutral ( #E5E5E5), которую берёт Alert, и --surface-subtle-gray ( #F9F9F9), которую берут Toast и Badge. У кита одна{' '} palette-black, поэтому он согласен с Alert и здесь на ступень светлее. Перенаправить её значило бы починить два набора, сломав тот, что верен. ), iconNote: ( <> iconLeft? и iconRight? — булевы свойства набора, оба по умолчанию true. В левый слот холст кладёт один и тот же глиф на всех пяти состояниях — Solid/Status/Info-circle — и лишь перекрашивает его; крестик закрытия рядом берёт тот же цвет. Кит был изменён под это, поэтому вкладка ничего не навязывает: перед тобой сам компонент — так тост выглядит в любом приложении с китом. ), inkNote: ( <> Чернила собственные, CBAR’овские, снятые с векторов иконок, и каждое из них — уже именованная ступень рампы: green-600 #16A34A,{' '} red-600 #F00000, yellow-ink #865F28,{' '} secondary-500 #4BC7B5, neutral-800{' '} #222222. Именно это позволило киту принять их как токен{' '} --ctl-icon, а не как пять литералов. Стоит знать цену: относительно собственных подложек они дают 3.00:1 на success, 3.65:1 на{' '} error и 1.74:1 на info, где бирюзовый глиф лежит на бирюзовой поверхности. Заголовок и описание рядом остаются на{' '} --ctl-fg (6.18 / 6.86 / 8.34), то есть текст не затронут — глиф лишь повторяет то, что уже сказал цвет. Смотри вкладку /parity?tab=findings. ), closeNote: ( <> Правый слот холста — кнопка закрытия, и она включена по умолчанию, поэтому эта вкладка рисует её тоже. У кита Toaster closeButton по умолчанию{' '} выключен, и это показывает вкладка «Галерея». Переключить умолчание кита значило бы добавить кнопку каждому тосту в каждом уже использующем его приложении, поэтому это остаётся выбором, а не становится им. ), copyNote: ( <> Копирование ячейки даёт вызов toast.*, а не разметку — поверхность на экране рисует внутренний компонент, который пакет не публикует, так что вставлять нечего. Это единственное место на странице, где скопированное не буквально то, что видно, и ослаблять правило честно именно в эту сторону. ), state: 'Состояние', gridTitle: 'state', grid: 'state — порядок холста, сверху вниз', }, }, parity: { title: 'Соответствие Figma', tabs: { report: 'Отчёт', findings: 'Находки', }, findingsEnglishOnly: 'Эта страница оставлена на английском: это протокол аудита из id узлов, hex-значений и путей переменных, адресованный тому, у кого открыт файл Figma.', lead: ( <> Каждый набор компонентов на холсте CBAR, сопоставленный с осями, которые реализует этот кит. Снято локальным плагином-мостом — figma_sets по всему документу — и сохранено в{' '} showcase/src/registry/figma-spec.json . Пересними командой pnpm figma:spec, когда файл дизайна изменится. ), stateNote: ( <> Расхождение — ещё не ошибка. CBAR рисует ось state почти у всех наборов, а кит отрисовывает эти состояния в CSS, а не пропсами — такие строки помечены и в подсчёт не входят. ), slotsNote: ( <> Строку slots стоит читать внимательно. Это свойства, не являющиеся осями вариантов — iconLeft?, .iconEnd?, .label? — их не упоминает ни одно имя варианта. У Button есть переключатель левой и правой иконки, оба включены по умолчанию, поэтому его набор рисует четыре расстановки иконок. Кит принимает иконки как дочерние элементы, так что сравнивать нечего; эта строка — список того, что должно показывать демо компонента. ✓ / ✗ — значение boolean по умолчанию в CBAR, ⇄ отмечает подменяемый инстанс, ✎ — текстовый слот. ), stats: { sets: 'Наборов в Figma', linked: 'Связано с компонентом', gaps: 'С расхождением по осям', kitOnly: 'Только в ките', }, badge: { match: 'совпадает', diff: 'различается', state: 'ось state', noProp: 'нет пропа', extra: 'только в ките', }, values: { both: 'в обоих', drawnInCss: 'отрисовано в CSS', onlyFigma: 'только в Figma', onlyKit: 'только в ките', takenAsChildren: 'здесь принимается как дочерние элементы', }, slots: 'слоты', linkedSets: 'Связанные наборы', pageMeta: (page: string, variants: number) => `страница «${page}» · вариантов: ${variants}`, setMeta: (page: string, variants: number) => `${page} · вариантов: ${variants}`, unlinkedTitle: (count: number) => `Наборы Figma без компонента (${count})`, unlinkedNote: 'Либо действительно отсутствуют в ките, либо нарисованы в Figma как часть другого компонента — поле поиска внутри Select, одна ячейка страницы в полосе Pagination.', kitOnlyTitle: (count: number) => `Компоненты кита без набора в Figma (${count})`, kitOnlyNote: 'В основном это виджеты с большими данными и оверлеи, которые CBAR никогда не рисовал. Они следуют собственным соглашениям кита, так что сравнивать нечего — но именно их не поймает дизайн-ревью.', }, figma: { title: 'Figma', badge: { match: 'совпадает', diff: 'различается', state: 'отрисовано в CSS', noProp: 'нет пропа', extra: 'только в ките', slot: 'слот', }, columns: { figmaProperty: 'Свойство Figma', type: 'Тип', values: 'Значения', figmaDefault: 'По умолчанию в Figma', kitProp: 'Проп кита', status: 'Статус', property: 'Свойство', figma: 'Figma', kit: 'Кит', }, valueKinds: { boolean: 'вкл. / выкл.', text: 'произвольный текст', instance: 'подменяемый инстанс', }, children: 'дочерние элементы', missingHere: (values: string) => `здесь нет: ${values}`, kitAdds: (values: string) => `кит добавляет: ${values}`, status: { live: (file: string) => `вживую · ${file}`, noPlugin: 'мост поднят · окно плагина закрыто', notRunning: 'снимок · мост не запущен', snapshot: 'снимок', }, onPage: (page: string) => `Figma на странице «${page}»`, setMissing: (set: string) => ( <> Эта запись ссылается на набор компонентов {set}, которого нет в{' '} figma-spec.json. Выполните{' '} pnpm figma:spec заново после изменения файла дизайна. ), setSummary: (set: string, page: string, variants: number, props: number) => ( <> Набор компонентов {set} на странице «{page}» — вариантов: {variants}, свойств: {props}. Снято локальным плагином-мостом; для живой половины ниже он должен быть запущен. ), matchingVariant: (label: string) => ( <> Подходящий вариант{' '} {label} ), opening: 'Открывается…', openInFigma: 'Открыть в Figma', unmatched: (values: string) => ( <> CBAR не рисует {values} — кит предлагает ступень, которой нет в наборе компонентов. Ничего из того, что ниже, не было бы сравнением на равных, поэтому оно опущено. ), guessed: (values: string) => ( <> Ни один проп не задаёт {values} — использовано значение Figma по умолчанию. Добавьте{' '} pin или valueMap в связку figma этой записи, чтобы выбрать осознанно. ), noVariant: 'В наборе нет варианта с такими значениями. CBAR рисует не все комбинации — посмотрите в таблице свойств выше, какие оси расходятся.', svgTooBig: (kb: number) => `Этот вариант экспортируется в ${kb} кБ SVG — слишком много для вставки. Откройте его в Figma.`, loading: 'Загрузка…', computedStyle: 'Вычисленный стиль', computedStyleNote: ( <> getCSSAsync() из Figma против getComputedStyle элемента слева. Пустая ячейка означает, что свойство указывает только одна сторона — Figma опускает всё, что фрейм подгоняет по содержимому. ), }, groups: { Actions: 'Действия', 'Data display': 'Отображение данных', Feedback: 'Обратная связь', Forms: 'Формы', Navigation: 'Навигация', Disclosure: 'Раскрывающиеся блоки', Overlays: 'Оверлеи', Data: 'Данные', Layout: 'Разметка', }, };