Contribution Guide

Руководство по устройству репо и по тому, как вносить изменения: от нового компонента с нуля до миграции из старой дизайн-системы.

Как устроен репозиторий

Это pnpm monorepo. Пакеты дизайн-системы публикуются из packages/*, приложения лежат в apps/*.

design-system/
├── packages/                      # Публикуемые npm-пакеты @ds/*
│   └── <pkg>/
│       ├── src/<Name>/            # nested-раскладка по компоненту
│       ├── stories/<Name>/        # Playground + VisualMatrix
│       ├── demos/                 # <Name>Demo.tsx для docs
│       ├── docs/                  # index.mdx + props.json
│       └── __test__/<Name>/       # Playwright specs + __snapshots__/
├── apps/
│   ├── storybook/                 # Storybook 10
│   └── docs/                      # Astro + Starlight
├── playwright/                    # Корневые fixtures, constants, utils
├── playwright.config.ts           # Сканирует packages/**/__test__/**/*.spec.ts
├── tests/docs/                    # Docs-only Playwright suite
├── tsconfig.base.json             # Единый источник общих compilerOptions
├── tsconfig.json                  # Typecheck-профиль (noEmit)
├── packages/tsconfig.esm.json     # Оркестратор ESM-сборки (project references)
├── packages/tsconfig.cjs.json     # Оркестратор CJS-сборки
├── scripts/
│   ├── add-package.mts            # Создаёт новый пакет и подключает его к репо
│   ├── add-package/               # scaffold.mts, wire.mts
│   ├── gen-props.mts              # docs/props.json из типов
│   └── gen-readme.mts             # README.md из MDX
└── .claude/                       # Rules / Skills / Commands для Claude Code

Чем собирается и связывается репо:

  • pnpm workspaces — линкует пакеты между собой и с приложениями.
  • Корневые скрипты (package.json) — сборка пакетов: tspc (ESM + CJS) → SCSS/CSS → постобработка CJS для CSS Modules.
  • lerna — только для версий и публикации в npm (independent).

Быстрый старт

pnpm deps
npx playwright install               # один раз, для E2E

pnpm dev:storybook                   # localhost:6006
pnpm dev:docs                        # localhost:4321
pnpm dev                             # оба параллельно

Анатомия пакета

Реальная раскладка пакета — nested по компоненту, даже если компонент один. Подробности — в .claude/rules/package-src-structure.md и .claude/rules/reference-package-anatomy.md.

packages/<pkg>/
├── src/
│   ├── <Name>/
│   │   ├── <Name>.tsx
│   │   ├── constants.ts
│   │   ├── types.ts
│   │   ├── styles.module.scss
│   │   └── index.ts               # export * from './<Name>'
│   └── index.ts                   # export * по секциям
├── stories/
│   └── <Name>/
│       ├── <Name>.Playground.stories.tsx     # обязателен
│       ├── <Name>.VisualMatrix.stories.tsx   # обязателен
│       ├── testIds.ts                        # если id повторяются в 2+ местах
│       ├── examples/                         # появляется только если в ней есть story
│       │   └── <Name>.<Scenario>.stories.tsx # сценарии, копируемые потребителем
│       └── tests/                            # появляется только если в ней есть story
│           └── <Name>.<Scenario>.stories.tsx # InteractionTest и т.п. — только тест-обвязка
├── __test__/
│   └── <ParentComponent>/                    # одна папка на parent; сабкомпоненты — через args
│       ├── helpers.ts
│       ├── rendering.spec.ts                 # всегда
│       ├── visual.spec.ts                    # всегда
│       ├── interaction.spec.ts               # только при applicable browser-specific сценариях
│       ├── keyboard.spec.ts                  # только при applicable kbd-сценариях
│       ├── polymorphism.spec.ts              # если есть `as`
│       └── __snapshots__/                    # baseline PNG (chrome-only)
├── demos/
│   ├── <Name>Demo.tsx
│   └── examples/<Example>.tsx                # для MDX через ?raw
├── docs/
│   ├── index.mdx
│   └── props.json                            # автоген: pnpm gen:props
├── package.json
├── tsconfig.json
├── tsconfig.esm.json
├── tsconfig.cjs.json
└── README.md                                 # автоген: pnpm gen:readme

Зачем нужны demos/

Папка demos/ — это источник живых примеров для документационного портала, а не дубль stories.

  • demos/<Name>Demo.tsx — Canvas-плейграунд для секции ## Демо в MDX (только для props-driven компонентов без центральных колбеков; см. .claude/rules/docs-structure.md).
  • demos/examples/<Example>.tsx — самостоятельные React-компоненты для блоков <Example> в MDX. Каждый файл импортируется и как живой компонент (через client:visible), и как исходник через ?raw — читатель видит работающий пример и код, который можно скопировать целиком, включая import-строки.

Stories vs demos: stories живут в Storybook (визуальная регрессия + play-тесты), demos живут в docs-портале (объяснение API на живых сценариях, ориентированных на потребителя). Один и тот же компонент в обеих ролях не переиспользуется — у них разные требования к state и к отображению кода.

Multi-component пакеты (@ds/button: Button + ButtonGroup) повторяют эту раскладку для каждого публичного компонента: src/Button/, src/ButtonGroup/, stories/Button/, stories/ButtonGroup/, __test__/Button/, __test__/ButtonGroup/.

Зависимости пакета

  • Только строгие версии ("classnames": "2.5.1", не ^2.5.1).
  • Повторяющиеся внешние зависимости (react, react-dom, @types/react*, classnames, sass, typescript, merge-refs, tabbable) подключаются через catalog: — версия живёт в pnpm-workspace.yaml::catalog.
  • В пакете нет react / react-dom / @types/react* — ни в одном блоке. Корень workspace’а поставляет React для сборки и тестов.
  • Внутренние зависимости — workspace:*.

Правило: .claude/rules/packages-deps.md.

package.json — exports

{
  "name": "@ds/button",
  "exports": {
    ".": {
      "source": "./src/index.ts",
      "types": "./dist/esm/index.d.ts",
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js"
    }
  }
}

source читают Storybook и docs в dev-режиме. CSS живёт как side-effect в самих JS-бандлах (SCSS-модули) — потребителю отдельным импортом подключать ничего не нужно.

Как работают стили

styles.module.scss + токены @ds/figma-variables:

@use '@ds/figma-variables/build/scss/styles/styles.module' as base;

.button {
  background: base.$sn-theme-color-primary-accent;
}

CSS Modules — camelCaseOnly (styles.primaryButton, не styles['primary-button']). SCSS компилируется рядом с JS в dist/esm и dist/cjs.

Storybook (версия 10)

Storybook автоматически подхватывает packages/*/stories/**/*.stories.tsx — ручной регистрации нет.

pnpm dev:storybook

CSF3 — формат stories

Все stories пишем в CSF3 (Component Story Format 3) — текущий формат Storybook 7+. Каждая story — объект StoryObj<typeof Component> с полями args, play, tags. Старый CSF2 (функциональные stories () => <Button /> с присваиванием Story.args = {}) не используется — он не даёт строгой типизации args и хуже работает с Test Runner.

Документация:

Структура stories — Playground + VisualMatrix

Полные правила: .claude/rules/stories-standard.md. Каждый компонент получает минимум два файла.

// packages/<pkg>/stories/<Name>/<Name>.Playground.stories.tsx
import { Meta, StoryObj } from '@storybook/react';
import { expect, within } from 'storybook/test';
import { APPEARANCE, Button, SIZE } from '@ds/button';

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  parameters: { layout: 'centered' },
  args: {
    label: 'Button',
    appearance: APPEARANCE.Primary,
    size: SIZE.M,
    'data-test-id': 'button',
  },
  argTypes: {
    appearance: { control: 'radio', options: Object.values(APPEARANCE) },
    size: { control: 'select', options: Object.values(SIZE) },
  },
};
export default meta;
type Story = StoryObj<typeof Button>;

export const Playground: Story = {
  tags: ['dev', 'test'],
  play: async ({ canvasElement }) => {
    await expect(within(canvasElement).getByTestId('button')).toBeVisible();
  },
};
// packages/<pkg>/stories/<Name>/<Name>.VisualMatrix.stories.tsx
import { StoryTable } from '#storybook/components';
// ...

Ключевые требования:

  • CSF3, Meta<typeof Component> + StoryObj<typeof Component>.
  • Импорты play-утилит — из storybook/test (subpath ядра storybook@10), не из @storybook/test.
  • Каждая story имеет data-test-id в args (kebab-case от имени компонента).
  • В play — только getByTestId. getByRole/getByText/getByLabelText запрещены.
  • VisualMatrix строится на StoryTable из #storybook/components.
  • Запрещены файлы: «на одну ось» (Sizes, Appearances, Views, Variants, LoadingState, DisabledState, EmptyState, WithIcon/IconOnly/WithCounter если это просто включение слота), ClickTest/KeyboardTest (объединяются в InteractionTest).
  • Запрещены: тег autodocs, тег fixture, parameters.docs.description.*, дубль одной story между examples/ и tests/, висящий /Tests или /Examples в title без имени сценария.
  • Повторяющиеся data-test-id выносятся в stories/<Name>/testIds.ts (single-component) или stories/testIds.ts (multi-component) единым объектом TEST_IDS со вложенной структурой по компонентам/слотам. Россыпь отдельных <NAME>_TEST_ID const’ов не заводится. Если компонент сам ставит data-test-id на свои внутренние слоты — эти строки публикуются через TEST_IDS в src/constants.ts (часть публичного API пакета).

Подпапки examples/ и tests/

Доп. story всегда живёт в подпапке stories/<Name>/examples/ либо stories/<Name>/tests/. Корень stories/<Name>/ содержит только Playground/VisualMatrix. Куда положить — решается механическим критерием копируемости:

  • examples/<Name>.<Scenario>.stories.tsx — если фрагмент копируется потребителем в продакшн-код как самостоятельный, работающий снаружи теста (composition, slot-пресет, polymorphism с as={Link}, controlled-режим, реальный react state). Title — Components/<…>/<Name>/Examples/<Scenario>.
  • tests/<Name>.<Scenario>.stories.tsx — если фрагмент содержит fn()-моки, контролируемые stub-state, edge-state или последовательность действий, важную только для assertion’а; вне теста смысла не имеет. Title — Components/<…>/<Name>/Tests/<Scenario>. Один интеракционный сценарий — один экспорт InteractionTest (клик + клавиатура + фокус через step()).

Каждая дополнительная story обязана проходить «Критерий обоснованности артефакта» из .claude/rules/complexity-tiers.md (3 условия: проверяет что-то новое из публичной поверхности, не выражается через args/StoryTable/play, не дублирует другой слой). При переезде story между корнем и подпапкой обязательно обновить story IDs в packages/<pkg>/__test__/<Name>/helpers.ts — иначе e2e получит 404.

Trigger-based компоненты

Modal, drawer, popover, dropdown, tooltip, toaster и любые dialog-like / portal-компоненты следуют отдельному правилу .claude/rules/trigger-based-stories.md: open не выводится в args (живёт в локальном useState render-компонента), триггер — Button из @ds/button с data-test-id={TEST_IDS.triggerOpen}, parameters.layout: 'fullscreen', конфликты значений между осями разрешаются runtime + <DemoWarning>, а не через if:.

Документационный портал

Портал (apps/docs/) собирается на Astro и автоподтягивает MDX из двух источников:

ИсточникURL
packages/*/docs/*.mdx/components/<pkg>/<filename>
apps/docs/src/content/patterns/*.mdx/patterns/<filename>

Доменная группировка пакетов

Главная страница и сайдбар группируют пакеты по префиксу имени. Конфиг — apps/docs/src/config/domains.ts:

Префикс пакетаДомен в портале и Storybook
uikit-product-*Uikit Product
ai-*AI
admin-*Admin
(остальное)Snack

Резолвер resolveDomain(pkg) идёт по списку DOMAINS сверху вниз, первое попадание по prefix выигрывает. Порядок в массиве задаёт порядок секций на главной и групп в сайдбаре. Чтобы завести новый домен — добавить блок { id, label, storybookLabel, description, prefix } и подобрать правильный префикс пакета. description показывается абзацем под заголовком секции на главной и внутри QuestionTooltip рядом с заголовком группы в сайдбаре.

Категории внутри домена

Внутри домена пакеты разбиты на категории — под-группы сайдбара доки и Storybook, секции главной и разделы /llms.txt: домен → категория → пакет. Категория задаётся вручную в apps/docs/src/config/categories.ts (CATEGORIES_BY_DOMAIN); домен без записи рендерит пакеты плоско.

Добавляя пакет в домен с категориями (Snack, Uikit Product, AI), впишите его имя (без @ds/-скоупа) в packages подходящей категории. Если категории нет — заведите новую запись (id / label / description / packages). Не отнесённый к категории пакет попадает в «Other» (build-warn). Порядок категорий в массиве = порядок в сайдбаре и на главной.

Canvas, PropsTable, StorybookEmbed, FigmaEmbed

import { ButtonDemo } from '../demos/ButtonDemo';
import { PropsTable } from '#docs/components/PropsTable';
import { StorybookEmbed } from '#docs/components/StorybookEmbed';
import { FigmaEmbed } from '#docs/components/FigmaEmbed';
import { figmaNode } from '#docs/lib/figma';
import buttonDoc from './props.json';

<ButtonDemo client:visible />
<StorybookEmbed storyId='components-button-button--playground' />
<FigmaEmbed node={figmaNode('button')} />
<PropsTable data={buttonDoc.Button} />

Canvas строит контролы из props.json (componentDoc). FigmaEmbed получает FigmaNodeRef через figmaNode(pkg, sub?) из apps/docs/src/lib/figma.ts — все узлы компонентов централизованы в FIGMA_NODES map’е по имени пакета.

Полный шаблон MDX-страницы — .claude/rules/docs-structure.md.

Как добавить новый компонент

Быстрый путь: pnpm add-package

pnpm add-package

Скрипт (scripts/add-package.mtsscripts/add-package/scaffold.mts + wire.mts):

  1. создаёт packages/<name>/ со всеми обязательными файлами;
  2. регистрирует ссылки в packages/tsconfig.esm.json и packages/tsconfig.cjs.json;
  3. добавляет alias в apps/storybook/.storybook/main.ts (между маркерами <add-package:aliases>); для apps/docs алиасы @ds/* собираются из packages/ автоматически (astro.config.mjs);
  4. добавляет "@ds/<name>": "workspace:*" в apps/storybook/package.json.

Дальше:

pnpm deps
pnpm gen                 # props.json + README.md
pnpm dev:storybook
pnpm dev:docs

Флаг --dry-run сообщит что будет создано без настоящей генерации файлов.

Тестирование

Тесты живут в трёх слоях (.claude/rules/e2e-testing-standard.md). Каждый слой решает свою задачу и не дублирует другие.

СлойЧто проверяетГде живёт
1. Storybook playBehavioral: click, keyboard, focus, controlled-state, callback assertions, ARIA-state-after-actionstories/<Name>/tests/<Name>.InteractionTest.stories.tsx::play — валидируется командой pnpm test:stories
2. Playwright rendering.spec.tsSmoke render + props propagation в data-* для ключевых значений осейpackages/<pkg>/__test__/<ParentComponent>/rendering.spec.ts
3. Playwright interaction.spec.ts / keyboard.spec.ts / polymorphism.spec.ts / visual.spec.tsТолько то, что Storybook play не может: browser-specific API, focus-trap, скриншотытам же, отдельные spec-файлы

Каждый дополнительный артефакт обязан проходить «Критерий обоснованности артефакта» из .claude/rules/complexity-tiers.md: (1) проверяет что-то новое из публичной поверхности, (2) не выражается через args/StoryTable/play, (3) не дублирует другой слой.

1. Play-функции в stories

pnpm test:stories          # Storybook Test Runner — CI-gate для play-функций
pnpm test:stories:ci

Behavioral assertion’ы пишутся только здесь — в Playwright не дублируются. Test Runner запускает все play-функции в headless Chrome через Playwright; падение assertion’а делает CI красным.

2. Playwright E2E против Storybook iframe

Полные правила: .claude/rules/e2e-testing-standard.md. Тесты живут внутри пакетаpackages/<pkg>/__test__/<Name>/*.spec.ts. Fixtures импортируются из корневого playwright/.

// packages/button/__test__/Button/rendering.spec.ts
import { expect, test } from '#playwright-tooling/fixtures';

import { BUTTON_STORIES, BUTTON_TEST_ID, buildStoryOptions } from './helpers';

test('renders', async ({ gotoStory, getByTestId }) => {
  await gotoStory(buildStoryOptions({ appearance: 'critical', size: 'l' }, BUTTON_STORIES.playground));
  await expect(getByTestId(BUTTON_TEST_ID)).toBeVisible();
});

Импорт fixtures и utils — только через TS-алиас #playwright-tooling/*, относительные пути ('../../../../playwright/...') запрещены. Канонический вызов gotoStory в spec’ах — только через buildStoryOptions(args?, storyRef?); id-строковая форма устарела.

Локаторы — только getByTestId. getByRole / getByText / getByLabelText запрещены (они привязаны к локализации и структуре DOM, перестают работать при первых же изменениях).

Разные комбинации пропсов проверяются через gotoStory(buildStoryOptions(args)) — отдельную story «под каждую комбинацию» заводить не нужно.

Набор spec-файлов на parent-компонент:

  • rendering.spec.ts — всегда. Smoke render + props propagation для ключевых значений осей (1–3 на ось, не цикл по всем enum-values).
  • interaction.spec.tsтолько если применим хотя бы один пункт закрытого списка browser-specific сценариев (file upload, real DnD, viewport resize, body scroll lock, focus-trap, click outside portal, theme switching внутри портала, rel=noopener инжекция, native disabled vs aria-disabled и т.п. — полный список в .claude/rules/e2e-testing-standard.md §«interaction.spec.ts»).
  • keyboard.spec.tsтолько если применим хотя бы один пункт закрытого списка kbd-сценариев (roving tabindex, focus-trap, Escape closes layered portals, multi-step focus management — там же §«keyboard.spec.ts»). Tab/Enter/Space на одном focusable — это play, а не keyboard.spec.
  • polymorphism.spec.tsтолько если в API есть as prop.
  • visual.spec.ts — всегда. Покрытие — по .claude/rules/visual-regression-standard.md.

Behavioral assertion’ы (click, keyboard, focus, callback) не живут в Playwright — они в Storybook play (stories/<Name>/tests/<Name>.InteractionTest.stories.tsx::play) и валидируются командой pnpm test:stories. Дублировать их в interaction.spec.ts/keyboard.spec.ts запрещено. Ориентиры по числу тестов и tier-таблица — в .claude/rules/complexity-tiers.md.

pnpm test:e2e                            # все проекты — финальная сверка перед PR
pnpm test:e2e:chrome                     # только chrome (включая visual)
pnpm test:e2e:chrome packages/<pkg>      # один пакет (дефолт во время разработки)
pnpm test:e2e:ui
pnpm test:e2e:update-snapshots           # регенерация baselines (chrome-only)
pnpm test:e2e:update-snapshots packages/<pkg>  # baselines одного пакета
pnpm test:e2e:docs                       # отдельный suite для apps/docs

3. Визуальная регрессия

Baseline’ы лежат рядом со спекомpackages/<pkg>/__test__/<Name>/__snapshots__/<arg>-<projectName>.png. Снимки только на chrome, имена без префикса пакета (visual-matrix.png, не button-visual-matrix.png). Правила — .claude/rules/visual-regression-standard.md.

Генерация артефактов

pnpm gen:props        # docs/props.json через react-docgen-typescript
pnpm gen:readme       # README.md из docs/*.mdx
pnpm gen              # оба

README.md руками не трогаем — он генерируется.

Публикация

Публикуем только packages/* (приложения private: true).

pnpm build:packages                # ESM + CJS + CSS (полная сборка всех пакетов)
pnpm build:pkg <pkg>               # инкрементальная сборка одного пакета — для итераций
pnpm version:packages              # lerna version, independent
pnpm release                       # lerna publish from-package

Локальная проверка правки в приложении

Типовая ситуация: в приложении обнаружился баг компонента, правку нужно проверить на живом сценарии — но ждать CI, выпускать preview-версию и потом её отзывать долго. Для этого есть ds-link: пакеты собираются локально и доставляются приложению под теми же именами, что в реестре (@ds/button@cloud-ru/ds-button), поэтому в его коде ничего менять не нужно. Подходит любому потребителю пакетов — микрофронту, монолитному сервису, песочнице.

Рабочий цикл — две команды: подключить один раз и оставить watch запущенным на всё время работы.

# 1. один раз за сессию
pnpm ds:link ~/path/to/app modal    # собрать, доставить, подключить (+ pnpm install)

# 2. в отдельном терминале — правки в src уезжают сами
pnpm ds:watch

# 3. по завершении
pnpm ds:unlink ~/path/to/app        # вернуть версии из реестра

Без запущенного ds:watch правки к приложению не поедут. ds:link доставляет пакет один раз, в момент подключения; дальше нужна либо непрерывная доставка (ds:watch), либо разовая (ds:push) после каждой правки. Обе команды без аргументов берут ровно те пакеты, что уже подключены, — перечислять их заново не нужно.

КомандаЧто делает
pnpm ds:watch [<pkg>,...]Основной режим. Следит за packages/<pkg>/src, на каждое сохранение пересобирает и доставляет. Без аргументов — все подключённые пакеты.
pnpm ds:link <путь> [<pkg>,...]Собирает названные пакеты, кладёт в <приложение>/.ds-link/, прописывает pnpm.overrides, дополняет .gitignore, запускает pnpm install. Без списка пакетов — переподключает уже закреплённое.
pnpm ds:push [<pkg>,...]Разовая пересборка и доставка — когда watch держать не хочется.
pnpm ds:statusЧто и в какие приложения подключено.
pnpm ds:unlink <путь>Снимает overrides и .ds-link, возвращает версии из реестра.

Флаги: --with-deps (подменить и workspace-зависимости названных пакетов), --skip-build, --skip-install, --keep-scope (работать под @ds/*), --scope= / --prefix=.

Что стоит знать:

  • Подменяются только названные пакеты. Их зависимости остаются теми, что стоят у потребителя: в staging-копии диапазоны заменены на *, и pnpm переиспользует уже установленную версию — одна копия в дереве и никаких незапрошенных обновлений соседних пакетов. Если правка задела несколько пакетов, перечисли их в команде либо возьми --with-deps.
  • Ключ override содержит версию ("@cloud-ru/ds-modal@3.0.0": "file:.ds-link/…"), поэтому подменяется только та ветка графа, которая просит именно её. Если в дереве живёт вторая версия того же пакета и компонент приходит через неё, правка не будет видна — убери версию из ключа, тогда override применится ко всем веткам.
  • Локальная сборка компилировалась против версий монорепы. Всё, что осталось версий потребителя, ds:link печатает таблицей: расхождение может дать ошибку в рантайме, если правка задевает изменившийся API.
  • Новая зависимость внутри пакета устанавливается автоматически. Частая проблема при локальном подключении: зависимость добавлена в пакет, у потребителя её в node_modules нет, и подключённый пакет перестаёт работать без явной ошибки. Здесь diff зависимостей отслеживается, и pnpm install у потребителя запускается автоматически.
  • Overrides подменяют зависимость, но не создают её. Если пакет в приложении ещё не используется, сначала pnpm add @cloud-ru/ds-<pkg>.
  • Применит ли правку работающий dev-сервер — зависит от сборщика. Next 16 на Turbopack пересобирает подключённый пакет сам, без перезапуска. Webpack по умолчанию не следит за node_modules, а Vite выполняет pre-bundling зависимостей — там нужен optimizeDeps.exclude либо перезапуск.
  • ds:link / ds:unlink запускают pnpm install у потребителя — он приводит node_modules к lockfile, поэтому версии, установленные до этого вручную, вернутся к объявленным. Мешает — --skip-install.
  • Lock-файл потребителя коммитить не нужно. Если локальный pnpm новее того, которым собирали lockfile, install допишет служебные поля; git checkout pnpm-lock.yaml после отключения.

Механизм живёт в scripts/ds-link/, внешних зависимостей не имеет. Прежний путь (pnpm transform:scope + npm pack + overrides на tarball’ы) больше не нужен: он деструктивно правил package.json всех пакетов рабочего дерева и не работал в watch-режиме.

Сценарии разработки

Три типовых сценария, в которых разработчик заходит в этот репо. Каждый — короткий рецепт: где брать дизайн, какой командой генерировать план, какой префикс пакета использовать, что проверить руками, куда нести PR. Общий Claude Code workflow описан в ## Миграция компонентов из старой дизайн-системы и ## Claude Code инструментарий — здесь только различия по сценариям.

Базовая установка для миграций: публичное API нового пакета @ds/* стараемся оставлять совместимым со старым (@snack-uikit/<pkg> или @cloud-ru/uikit-product-<pkg>). Все осознанные расхождения фиксируем в .claude/plan/<pkg>.md и в описании PR.

Ручные проверки, общие для всех сценариев (то, что автотесты не ловят):

  • hover / focus-visible / pressed эффекты в браузере (Storybook + dev:docs);
  • согласованность пропсов с legacy-источником и, если был, с мобильной версией;
  • визуальное соответствие Figma (через VisualMatrix baseline + ручной diff).

PR и поддержка — общие для всех сценариев:

  • PR на ревью — открывайте Pull Request в репозитории на GitHub.
  • Вопросы по ходу разработки — через Issues репозитория.

Сценарий 1. Миграция из @snack-uikit/*

  • Источник дизайна: Snack Ui Kit variables (fileKey: aNPU3MHwRJiEwbk5F82zux).
  • Префикс пакета: имя оригинального компонента без скоупа — button, accordion, tabs, tooltip и т.п.
  • Шаги:
    1. Изучить макет в Figma и оригинальную реализацию в legacy-репо storybook/ либо в npm @snack-uikit/<pkg>. Если у компонента была отдельная мобильная версия (@snack-uikit/<pkg>-mobile или mobile-обёртка) — её тоже на стол.
    2. pnpm add-package — создать пакет. Делается до генерации плана, чтобы план опирался на реальную раскладку.
    3. /migrate-to-v2 <pkg> <figma-url> [--ref <legacy-pkg>] [--mobile <mobile-pkg>] [--note "..."] — план в .claude/plan/<pkg>.md. В --note обязательно указать ссылку на Figma, на legacy-источник и на мобильную реализацию (если была).
    4. /add-stories <pkg>, дальше снять baselines: pnpm dev:storybook + pnpm test:e2e:update-snapshots packages/<pkg>.
    5. /add-tests <pkg>, /add-docs <pkg>, pnpm gen:props && pnpm gen:readme.
    6. Прогнать ручные проверки из общего списка выше.
    7. /make-commit → PR в front-review.

Сценарий 2. Миграция из @cloud-ru/uikit-product-*

  • Источник дизайна: Product UI Kit variables.
  • Префикс пакета: обязательно uikit-product-* (например, uikit-product-table, uikit-product-filter-bar). Это разделяет пакеты Product-кита и Snack-кита, чтобы их имена не пересекались.
  • API-совместимость: держим совместимость с @cloud-ru/uikit-product-<name>.
  • Шаги: идентичны Сценарию 1 — pnpm add-package/migrate-to-v2 <pkg> <figma-url> [--ref @cloud-ru/uikit-product-<pkg>] [--note "..."]/add-stories|tests|docspnpm gen → ручные проверки → /make-commit → PR.

Сценарий 3. Новый компонент / пакет с нуля

  • Когда: legacy-источника нет, есть только Figma-узел (или серия узлов).
  • Префикс пакета: uikit-product-*.
  • Шаги:
    1. pnpm add-package — создать пакет.
    2. /create-from-figma <pkg> <figma-url> [<figma-url> ...] [--note "..."] — команда строит черновик публичного API (constants.ts / types.ts / слоты / коллбэки) из variant-осей Figma и обязательно показывает API на подтверждение до финализации плана.
    3. Дальше — /add-stories, /add-tests, /add-docs, pnpm gen:props && pnpm gen:readme.
    4. Ручные проверки → /make-commit → PR.

Миграция компонентов из старой дизайн-системы

Пакеты портируются из соседнего репо storybook/ (@design-system/*@ds/*) и из legacy-скоупов @snack-uikit/* / @cloud-ru/*. Конкретные рецепты под каждый источник — выше, в ## Сценарии разработки. Ниже — общий Claude Code workflow, на который опираются оба миграционных сценария.

Флоу через Claude Code:

  1. Получить план миграции — слэш-команда /migrate-to-v2:

    /migrate-to-v2 <pkg> <figma-url> [--ref <pkg>] [--note "..."]

    План сохраняется в .claude/plan/<pkg>.md. Команда учитывает Figma-узел, текущие правила DS и эталон (--ref button).

    Если legacy-источника нет (компонент проектируется с нуля по Figma) — используй /create-from-figma:

    /create-from-figma <pkg> <figma-url> [<figma-url> ...] [--note "..."]

    Команда строит черновик публичного API (constants/types/slots/коллбэки) из variant-осей Figma и обязательно показывает его на подтверждение пользователю до финализации плана.

  2. Создание пакета — скилл new-component-package (pnpm add-package + tier-специфичные артефакты).

  3. Доработка — скиллы figma-component-import, component-story-set, component-e2e-tests, component-docs.

  4. Обновление токенов старой DS — команда /up-cloud-deps обновляет зависимости @snack-uikit/* / @cloud-ru/* до актуальных версий.

  5. Валидация — скилл component-validation-loop (сквозной цикл до зелёного билда).

  6. Коммит — команда /make-commit.

Figma-интеграция

Источник дизайна — Snack Ui Kit variables:


Открыть файл в Figma

  • fileKey: aNPU3MHwRJiEwbk5F82zux
  • fileName: Snack-Ui-Kit-variables

Узлы компонентов централизованы в apps/docs/src/lib/figma.ts в map’е FIGMA_NODES по имени пакета:

export const FIGMA_NODES = {
  button: { ...SNACK, nodeId: '2507-25203' },
  toggles: {
    _: { ...SNACK, nodeId: '2815-30903' }, // корневой узел пакета
    checkbox: { ...SNACK, nodeId: '2834-25233' }, // субкомпонент
    radio: { ...SNACK, nodeId: '7587-163964' },
  },
} as const satisfies Record<string, NodeOrSub>;

Использование в MDX:

<FigmaEmbed node={figmaNode('button')} />
<FigmaEmbed node={figmaNode('toggles', 'checkbox')} />

Новый пакет → новый ключ в FIGMA_NODES. Sub-ключ — kebab-case имени публичного субкомпонента (совпадает с сегментом story title); _ — узел пакета по умолчанию.

Плагин для просмотра пропсов

В Figma удобно смотреть API компонентов через плагин:


Figma community plugin

Плагин показывает variant axes выбранного компонента в виде таблицы пропсов — это быстрый способ сверить Figma-оси с constants.ts и с argTypes Playground’а.

Figma MCP

Как подключить

  1. Скачайте и авторизуйтесь в приложении Figma Desktop.
  2. Запустите приложение, откройте нужный файл и в панели справа найдите раздел “MCP Server”. Раздел MCP Server в Figma Desktop
  3. Включите локальный MCP-сервер для фигмы, нажав кнопку “Enable desktop MCP Server”.

В figma-remote-mcp доступны (при настроенном MCP):

  • get_metadata — структура Frame/Component/Variant (работает по nodeId, без выделения в Figma Desktop).
  • get_design_context — React+Tailwind референс + токены (требует выделения в Figma Desktop).
  • get_variable_defs — имена токенов.
  • get_screenshot — PNG узла.

Правила работы с Figma — .claude/rules/figma-integration.md и .claude/rules/figma-to-code.md.

Claude Code инструментарий

В репо подключён набор правил, скиллов и слэш-команд — это основной способ сопровождения компонентов.

Несмотря на имя .claude/, Cursor тоже читает эту папку: правила из .claude/rules/*.md подхватываются как контекст, а слэш-команды из .claude/commands/*.md доступны через палитру Cursor. Отдельной конфигурации под Cursor дублировать не нужно.

Rules vs Skills vs Commands — в чём разница

Три механизма Claude Code решают разные задачи. Главное отличие — кто и когда их активирует.

МеханизмГде лежитКогда применяетсяЧто внутри
Rules.claude/rules/*.mdВсегда — как фоновый контекст ко всем действиям агента в репоСтандарты («как делать»): структура пакета, формат stories, правила импортов, запреты
Skills.claude/skills/*.mdАгент сам решает активировать, когда задача соответствует описаниюПошаговые workflows со своим контекстом и подзадачами
Commands.claude/commands/*.mdТолько когда пользователь вызвал /<имя>Прямой триггер конкретного действия с аргументами

Правило большого пальца:

  • Хочешь зафиксировать стандарт («все stories должны быть в CSF3») — это rule.
  • Хочешь связать несколько шагов под понятную задачу («покрой компонент e2e-тестами») — это skill.
  • Хочешь дать пользователю кнопку («сделай мне коммит из staged diff») — это command.

Rules — правила уровня репо (.claude/rules/)

ФайлО чём
packages-deps.mdСтрогие версии, без react/react-dom в пакетах
package-src-structure.mdFlat vs nested раскладка src/
react-types.mdТипы из 'react', без React.*
imports-exports.mdБез import type / export type, export *
stories-standard.mdPlayground + VisualMatrix, data-test-id, StoryTable
reference-package-anatomy.mdАнатомия эталонного пакета (@ds/button)
complexity-tiers.mdTier XS/S/M/L/XL — набор артефактов
component-api-surface.mdconstants.ts + types.ts + JSDoc
e2e-testing-standard.mdPlaywright specs по tier’у
visual-regression-standard.mdСнимки, стабилизация, baselines
docs-structure.mdШаблон MDX, обязательные секции
figma-integration.mdКарта variants ↔ props, FIGMA_NODES[pkg]
figma-to-code.mdПеренос Figma-слоёв в DOM/SCSS
dont-do-that.mdОбщий свод запретов

Skills — пошаговые workflows (.claude/skills/)

SkillКогда вызывать
new-component-package«добавить компонент», «новый пакет», «портировать из storybook/»
component-story-set«написать stories», «покрыть состояния», «обновить baselines»
component-e2e-tests«написать e2e», «playwright»
component-docs«написать docs», «страница пакета», «Storybook embed»
figma-component-importПользователь дал Figma URL / nodeId
figma-to-codeПеренос Figma-слоёв в React + SCSS Modules
scss-styles-audit«проверь SCSS», аудит хардкода и axis-копипаста в стилях
component-tier-audit«проверь эталонность», «аудит пакета»
component-validation-loop«проверь готовность компонента» (сквозной цикл)
mr-commentsРабота с комментариями GitLab MR через scripts/mr-comments/*
figma-selected-blockПолучить SCSS стили выделенного слоя из Figma-ноды

Commands — слэш-команды (.claude/commands/)

КомандаЧто делает
/make-commitConventional-commit из staged diff
/up-cloud-depsОбновить @snack-uikit/* / @cloud-ru/* до актуальных версий
/migrate-to-v2План миграции компонента (есть legacy-источник --ref) в .claude/plan/<pkg>.md
/create-from-figmaПлан нового пакета из одной только Figma-ноды (API проектируется по variant-осям) в .claude/plan/<pkg>.md
/add-stories <pkg>Playground + VisualMatrix (+ оправданные доп. stories) в packages/<pkg>/stories/<Name>/
/add-tests <pkg>Набор Playwright E2E specs в packages/<pkg>/__test__/<Name>/ по tier’у
/add-docs <pkg>docs/index.mdx + demos для packages/<pkg> (role-based структура)

Все три команды-обёртки /add-* принимают имя пакета или путь:

# имя пакета
/add-stories button
/add-tests   button
/add-docs    button

# или путь
/add-stories packages/button
/add-tests   packages/button
/add-docs    packages/button

Без аргумента команда остановится и попросит указать пакет. Если пакета нет — предложит pnpm add-package. Все три не трогают src/ и ничего не коммитят.

После /add-stories — снять visual baselines:

pnpm dev:storybook              # в отдельном терминале
pnpm test:e2e:update-snapshots  # chrome-only, PNG в packages/<pkg>/__test__/<Name>/__snapshots__/

После /add-docs — обновить генерируемые артефакты и посмотреть страницу:

pnpm gen:props && pnpm gen:readme
pnpm dev:docs                   # открыть /components/<pkg>

После /add-tests — прогнать локально:

pnpm test:e2e:chrome packages/<pkg>                                       # только этот пакет (быстрее всего)
pnpm test:e2e:chrome packages/<pkg>/__test__/<Component>/rendering.spec.ts # один spec
pnpm test:e2e:chrome -g "props propagation"                               # по grep'у имени теста

Селективные команды для итераций (lint/build:pkg/typecheck/тесты по одному пакету) — см. .claude/rules/fast-build-commands.md.

Типовой workflow: новый компонент из Figma

  1. /migrate-to-v2 <pkg> <figma-url> — план в .claude/plan/<pkg>.md.
  2. Skill new-component-package — создание пакета через pnpm add-package.
  3. Skill figma-component-import / figma-to-code — разметка + токены.
  4. /add-stories <pkg> — Playground + VisualMatrix (+ baselines вручную после).
  5. /add-tests <pkg> — spec-файлы по tier’у.
  6. /add-docs <pkg>docs/index.mdx, demos/, FIGMA_NODES[pkg].
  7. Skill component-validation-loop — пройти цикл до зелёного.
  8. /make-commit.

Пример вызова end-to-end из Claude Code:

/migrate-to-v2 accordion https://www.figma.com/design/aNPU3MHwRJiEwbk5F82zux/...?node-id=2507-25203
pnpm add-package                # создание пакета по плану
/add-stories accordion
pnpm dev:storybook              # в отдельном терминале
pnpm test:e2e:update-snapshots  # baselines
/add-tests accordion
pnpm test:e2e:chrome
/add-docs accordion
pnpm gen:props && pnpm gen:readme
/make-commit

Дизайн-токены

Визуальные константы-переменные — пакет @ds/figma-variables (установлен в монорепо).

В SCSS компонентов:

@use '@ds/figma-variables/build/scss/styles/styles.module' as base;

.button {
  color: base.$sn-theme-color-primary-accent;
  border: base.$sn-primitive-strokeWeight-strokeRegular solid base.$sn-theme-color-neutral-decor;
}

В точке входа приложения — CSS-переменные:

@import '@ds/figma-variables/build/css/tokens.css';

Связанные ресурсы

РесурсURL
Репозиторий (GitHub)https://github.com/cloud-ru-tech/snack-v2
Snack Ui Kit — Figmahttps://www.figma.com/design/aNPU3MHwRJiEwbk5F82zux/Snack-Ui-Kit-variables
Product UI Kit — Figmahttps://www.figma.com/design/VWNiBRIUmVXIWYlLzMxcs6/Product-UI-Kit—variables-
Пакет токенов (@ds/figma-variables)https://github.com/cloud-ru-tech/snack-v2/tree/master/packages/figma-variables