Перейти к содержанию

Изолированная мастерская компонентов

В проекте «Набор» соберём небольшую библиотеку интерфейсных компонентов и мастерскую Storybook для её изучения. Начнём с одной кнопки. Нужны основы HTML, CSS, TypeScript и понимание React-компонента как функции, описывающей интерфейс. Цель первого урока — отделить компонент от страницы приложения и получить явное описание его начального состояния. Среда при написании не устанавливается и не запускается.

Компонент обычно появляется внутри большого экрана, где его поведение зависит от данных, маршрутов и состояния формы. Это полезно для пользователя, но неудобно при разработке самого элемента. Чтобы увидеть отключённую кнопку, приходится воспроизводить нужную ошибку или этап загрузки. Storybook позволяет описать такой вариант отдельно и рассматривать его в изолированной области. История является конкретным примером использования компонента, а не пересказом его исходного кода.

Первый контракт кнопки

В src/Button.tsx кнопка принимает содержимое и признак disabled. Возвращается настоящий HTML-элемент button. Мы не заменяем его div с обработчиком клика: нативный элемент уже участвует в фокусе, поддерживает клавиатурное действие и выражает назначение для браузера. Атрибут type="button" предотвращает неожиданную отправку родительской формы. Позднее публичный интерфейс позволит явно выбирать submit, когда именно это требуется.

Полный файл src/Button.tsx для этого состояния:

export function Button({ children, disabled = false }: { children: import('react').ReactNode; disabled?: boolean }) {
  return <button type="button" disabled={disabled} className="nabor-button nabor-button--primary">{children}</button>;
}

Полный файл src/Button.stories.tsx для этого состояния:

import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button';
const meta = { title: 'Nabor/Button', component: Button, args: { children: 'Сохранить' } } satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};

Полный файл .storybook/main.ts для этого состояния:

import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
  stories: ['../src/**/*.stories.@(ts|tsx)'],
  addons: ['@storybook/addon-docs'],
  framework: { name: '@storybook/react-vite', options: {} },
};
export default config;

Полный файл .storybook/preview.tsx для этого состояния:

import type { Preview } from '@storybook/react-vite';
import '../src/styles.css';
const preview: Preview = {
  decorators: [(Story) => <div className="nabor-theme" style={{ padding: '1rem' }}><Story /></div>],
};
export default preview;

Полный файл package.json для этого состояния:

{
  "name": "@kontur/nabor",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "storybook": "storybook dev -p 6006 --host 127.0.0.1",
    "build-storybook": "storybook build"
  },
  "dependencies": {
    "react": "19.1.0",
    "react-dom": "19.1.0"
  },
  "devDependencies": {
    "storybook": "10.2.9",
    "@storybook/react-vite": "10.2.9",
    "@storybook/addon-docs": "10.2.9",
    "@storybook/addon-a11y": "10.2.9",
    "vite": "7.1.7",
    "typescript": "5.9.2",
    "@types/react": "19.1.13",
    "@types/react-dom": "19.1.9"
  }
}

Содержимое имеет тип ReactNode, потому что кнопка может показывать текст или небольшой составной фрагмент. Однако возможность передать сложную разметку не означает, что любую структуру следует помещать внутрь. Вложенный интерактивный элемент нарушил бы смысл кнопки. Контракт должен сопровождаться понятными примерами допустимого использования. Сейчас мы передаём только слово «Сохранить», чтобы не смешивать начальное описание с иконками, состоянием операции и локализацией.

CSS подключается через файл preview, который действует в области историй. Класс кнопки относится к библиотеке и обозначает её элемент. В первых состояниях уже имеются семантические переменные оформления; следующий урок отдельно объяснит их устройство. Важно пока увидеть направление зависимости: компонент выдаёт разметку и классы, стили задают внешний вид, история передаёт входные значения. Ни один из этих слоёв не должен незаметно загружать данные приложения.

История как именованный пример

Файл Button.stories.tsx использует Component Story Format. По умолчанию экспортируется объект meta с названием, компонентом и общими аргументами. Именованный экспорт Default является одной историей. В TypeScript конструкция satisfies Meta<typeof Button> сверяет описание с типом компонента, сохраняя достаточно точную информацию для последующих определений. Тип StoryObj<typeof meta> обозначает объектный формат отдельной истории.

Свойство args представляет входные параметры экземпляра. Оно не является глобальной настройкой приложения и не меняет исходники кнопки. Storybook связывает историю с компонентом и передаёт ей значения. Если позже появится история Disabled с disabled: true, она будет описывать тот же компонент в другом состоянии. Это важное отличие от копирования второй кнопки с отдельной разметкой: варианты остаются примерами одного общего контракта.

Название Nabor/Button задаёт читаемое место в боковой структуре мастерской. Название экспортированной истории задаёт отдельный вариант. Не привязывайте библиотечный компонент к конкретному маршруту сайта вроде Account/Edit/GreenSaveButton: это смешает область применения с сущностью. Составные экраны появятся в своей группе примеров, а кнопка сохранит самостоятельное место. Такая организация облегчает дальнейшее обсуждение общего интерфейса.

Профиль среды и граница запуска

В .storybook/main.ts перечислены шаблон путей к историям, React Vite framework и addon документации. Файл имеет расширение .ts и экспортирует обычный объект конфигурации. Preview использует JSX, поэтому его расширение .tsx. Эта небольшая деталь важна при ручной подготовке: файл с JSX не следует сохранять как обычный .ts, ожидая, что инструмент угадает синтаксис. Истории также используют .tsx, поскольку возвращают разметку.

Прямые зависимости закреплены: Storybook 10.2.9, React и React DOM 19.1.0, Vite 7.1.7, TypeScript 5.9.2. Node.js 22.14.0 соответствует выбранному учебному профилю. Это конкретный набор, а не заявление о последних версиях на любой будущий момент. Зависимости автором не устанавливались, разрешённый lockfile отсутствует. После самостоятельной установки читатель должен отдельно зафиксировать результат разрешения зависимостей и подтвердить совместимость своей платформы.

Скрипт storybook задаёт порт 6006 и loopback-хост. Он предназначен для самостоятельной локальной мастерской. Сам факт наличия скрипта в package.json не означает, что сервер уже работает. Архив содержит исходники, а не установленные пакеты или готовый storybook-static. Также нет автоматического запуска историй как тестов: в рамках этой работы пользователь попросил не выполнять проверки. Мы описываем ожидаемую структуру интерфейса без фиктивных результатов.

Что видно в первом результате

При отдельном будущем запуске слева ожидается группа Nabor с компонентом Button, а в области примера — кнопка «Сохранить». Сама история не сохраняет данные: обработчика операции пока нет. Это нормально для изолированного представления. Нажатие показывает только доступность нативного действия; настоящая операция принадлежит приложению-потребителю. Позднее мы добавим локальную демонстрацию ожидания, сохраняя это разделение.

Если пример не появляется, сначала сопоставьте путь файла и шаблон stories в main. История вне src или с другим суффиксом не попадёт в выбранный набор. Если появится пустая кнопка, проверьте args.children, а не начинайте менять CSS. Такие простые причинные связи учат искать ошибку на нужном уровне. Следующий урок займётся токенами оформления: общими именами цветов, размеров и интервалов, от которых сможет зависеть вся библиотека.

Переход между окружениями разработки

История не должна получать настоящие пользовательские данные только потому, что приложение уже умеет их загружать. Для первого примера достаточно фиксированной подписи. Это делает исходник понятным и не создаёт зависимость от доступности внешнего сервиса. Если будущая история моделирует сложный набор данных, используйте явно учебные значения с известным происхождением. Мастерская тогда остаётся пригодной для чтения без доступа к рабочему аккаунту.

Также разделяйте установку и использование исходников. Архив не обещает готовый сервер, потому что зависимостное дерево не разрешалось. Читатель выбирает целую папку урока, подтверждает среду и устанавливает зависимости самостоятельно, когда разрешён запуск. Копирование только одного story-файла в случайный проект может изменить профиль TypeScript и CSS. Полное состояние показывает, какие файлы образуют именно этот учебный контракт.

Оглавление серии, общая подборка и полный архив исходников. Для этого урока выберите lesson-01.

Первичные источники: Storybook CSF, React Vite integration, Аргументы историй, Темы и globals, Декораторы, Accessibility addon, Автоматическая документация, React useId, HTML dialog.

Редакционный статус: локальный черновик. Учебные команды, приложение и проверки не запускались; описанные результаты ожидаемые. Профиль версий закреплён, разрешённый lockfile не создавался.