Storybook
Своего Storybook у библиотеки нет и не будет. Как подключить компоненты в ваш — конфиг, тема, оверлеи и главная ловушка.
Своего Storybook у Granularity нет, и он не планируется. За вопросом «а Storybook у вас есть?» почти всегда стоят два других: есть ли у компании единая витрина компонентов и встраивается ли библиотека в её процессы визуального тестирования. Ответ на оба — да, и эта страница про то, как.
Почему не свой
Портал уже делает то, ради чего Storybook заводят: живые демо на странице каждого компонента, таблицу API прямо из кода, playground с изменяемыми пропами, проверку доступности и визуальную регрессию. Второй такой инструмент рядом означал бы второй источник правды и дублирование сотен демо — при одном мейнтейнере это прямая дорога к расхождению, а расходятся такие пары молча.
То же относится к Chromatic и Histoire: совместимость документируется, своё не строится.
Свой Storybook у вас при этом остаётся осмысленным — но по другой причине. В нём живут ваши компоненты, собранные из наших, и витрина нужна именно им.
Что подключить
Granularity ничем не отличается от любой другой Vue-библиотеки, кроме одного: её CSS не импортируется файлом, а собирается UnoCSS из granular-провайдера (установка). Поэтому вся настройка — про то, чтобы UnoCSS отработал внутри Storybook.
Сборка
Storybook на Vite (@storybook/vue3-vite) читает ваш vite.config.ts. Если
плагин UnoCSS уже стоит там, добавлять в .storybook/main.ts нечего:
import { defineConfig } from 'vite'
import Vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite'
export default defineConfig({ plugins: [Vue(), UnoCSS()] })Если конфиг у Storybook свой, плагин добавляется через viteFinal:
import UnoCSS from 'unocss/vite'
export default {
framework: '@storybook/vue3-vite',
stories: ['../src/**/*.stories.@(ts|tsx)'],
viteFinal: async config => ({
...config,
plugins: [...(config.plugins ?? []), UnoCSS()],
}),
}Стили
Те же три импорта, что в точке входа приложения, — в preview.ts:
import '@unocss/reset/tailwind-compat.css'
import 'virtual:uno:granular.css'
import 'virtual:uno.css'Второй импорт есть только при layer: 'granular' в конфиге. Без слоя всё
приезжает одним virtual:uno.css, и лишний импорт сломает сборку. Забыть же
virtual:uno.css — получить компоненты с токенами, но без раскладки: цвета
есть, вёрстки нет.
Главная ловушка: extractor не видит dist
Это первое, что ломается, и ломается одинаково у всех: истории отрисовались, а компоненты приехали без стилей.
Причина не в Storybook. Утилитарные классы, которыми нарисованы шаблоны
компонентов, лежат в собранном dist пакета, а extractor UnoCSS по умолчанию
туда не заглядывает — он сканирует исходники вашего проекта. Ему нужно сказать:
import { defineConfig, presetMini } from 'unocss'
import { granularContent, presetGranularNode } from '@feugene/unocss-preset-granular/node'
import granularityProvider from '@feugene/granularity/granular-provider/node'
// Один объект на оба вызова: разойдутся — extractor сканирует не то,
// что генерирует пресет.
const granular = {
providers: [granularityProvider],
themes: { names: ['light', 'dark'] },
layer: 'granular' as const,
}
export default defineConfig({
content: granularContent(granular),
presets: [presetMini(), presetGranularNode(granular)],
})content читается только из верхнего уровня конфига, не из пресета. Это то
же требование, что и в приложении, — Storybook просто делает его заметнее,
потому что в нём чаще собирают отдельным конфигом.
Перечислять components в конфиге Storybook обычно не нужно: витрина по смыслу
показывает всё, а вес CSS в ней никого не волнует. Сужение списка — приём
боевого приложения, и там оно остаётся.
Обёртка историй
Дефолты поддерева и цель для оверлеев задаёт GrConfigProvider. В Storybook он
живёт в декораторе:
import { GrConfigProvider } from '@feugene/granularity/components/GrConfigProvider'
import { h } from 'vue'
export const decorators = [
(story: () => unknown) => ({
setup: () => () => h(GrConfigProvider, { size: 'md' }, () => h(story() as never)),
}),
]Провайдер рендерится прозрачно (display: contents) и раскладку истории не
меняет, поэтому его можно ставить на все истории разом.
Оверлеи
Модалки, дропдауны и тултипы телепортируются в общий портал — #gr-portal в
body, который пакет создаёт сам при первом открытии. В Storybook это работает
без настройки: body у превью свой, и портал появится в нём.
Настройка нужна, только если вы указываете portalTarget своим контейнером.
Тогда требование к нему то же, что в приложении, и оно жёсткое: никаких
transform, filter, contain, perspective и will-change — они создают
containing block для position: fixed, и панели начнут считать позицию от
контейнера, а не от вьюпорта. Декораторы Storybook такие обёртки создают охотно.
Переключение темы
Тема — атрибут на корне документа, и переключается она так же, как в приложении:
export const globalTypes = {
theme: {
toolbar: {
items: [
{ value: 'light', title: 'Light' },
{ value: 'dark', title: 'Dark' },
],
},
},
}
export const decorators = [
(story: () => unknown, context: { globals: { theme?: string } }) => {
document.documentElement.dataset.theme = context.globals.theme ?? 'light'
return story()
},
]Обе темы обязаны попасть в CSS — за это отвечает themes.names в конфиге выше.
Указав там только light, вы получите переключатель, который меняет атрибут и
не меняет ничего больше.
Импорты в историях
Резолвер авто-импорта (@feugene/unplugin-granularity) работает в шаблонах SFC.
История на TypeScript — не шаблон, и компонент в ней импортируется подпутём:
import { GrButton } from '@feugene/granularity/components/GrButton'
export default { component: GrButton }
export const Primary = { args: { variant: 'primary', tone: 'primary' } }Подпуть, а не корневой импорт: он и есть весь смысл гранулярности — в бандл истории попадает один компонент, а не пакет.
Что взять с портала, а не переписывать
- Аргументы историй — таблица API на странице компонента порождается из
кода, включая типы и значения по умолчанию. Копировать её в
argTypesруками незачем: Storybook выведет большую часть сам из типов SFC. - Проверка доступности — подпуть
@feugene/granularity/testingдаёт окружение для монтирования и уборку между тестами, а axe и Playwright подключаются вашими. Подробности — на странице тестирования.@feugene/granularity-test-kitдля этого не нужен: он про контрактные тесты пакетов семейства, а не приложения. - Клавиатурные контракты — описаны в репозитории библиотеки по компонентам, и история их не заменяет: они про поведение, а не про вид.