UIKit на Mantine
packages/ui — независимый React-пакет @gheilt/ui. Сайт использует его реальные
компоненты; Storybook показывает их изолированно с тем же UiProvider.
Установка и API, почему используются CSS-слои.
Базовые компоненты
Button, ActionIcon, Anchor, Text, Title, Paper, Card, поля, селекты, Checkbox,
Radio, Switch, Modal, Drawer, Alert, Tabs, Accordion, Table, Loader, Skeleton,
Progress и Tooltip берутся непосредственно из Mantine. Тема задаёт общую палитру,
шрифт, скругления и defaults. Нативные props (variant, size, loading, disabled,
error) сохраняются, поэтому библиотека не вводит собственную копию их API.
Notifications подключены один раз в UiProvider, управление — через
notifications.show из @mantine/notifications.
Составные компоненты
| Компонент | Ответственность |
|---|---|
| PageContainer / Section | Ширина, заголовок, якорь и действие раздела |
| SiteHeader / SiteNavigation / SiteFooter | Слоты шапки, адаптивное меню и подвал |
| Carousel / AwardCarousel / LogoCarousel | Готовый Mantine/Embla механизм слайдов |
| HeroCarousel | Изображение и текст большого слайда |
| ArticleCard / ArticleCollection | Карточки, featured desktop/mobile и обычная сетка |
| Schedule | Дни, площадки, время и ссылки событий |
| InformationCard / RoutePanel / MapEmbed | Выбор маршрута или размещения, карта |
| ImageText / Faq | Иллюстрированный текст и раскрываемые ответы |
У UIKit нет доступа к CMS и нет импортов Next.js. Для роутера передаётся renderLink.
Пример Next: renderLink={props => <Link {...props} />}. Маршруты передают содержимое
как ReactNode; Strapi Blocks рендерит адаптер приложения. Группировка расписания
находится в apps/web/src/lib/schedule.ts, использует Europe/Moscow и сохраняет
все события. Извлечение карточек из Blocks — в apps/web/src/lib/information.ts.
Для iframe разрешён только HTTPS yandex.ru/map-widget/v1/, остальные ссылки остаются
обычными ссылками и не превращаются в embed.
Storybook
pnpm --filter @gheilt/ui storybook запускает каталог на http://localhost:6006.
pnpm --filter @gheilt/ui build-storybook собирает статический каталог.
Используются React Webpack 5 и SWC, отдельной Vite-сборки нет.
Истории включают варианты кнопок, типографику, панели, поля с ошибками, модалки,
уведомления, загрузку, таблицы, пустые карусели, одиночный слайд, расписание с пустым
днём и длинный вопрос FAQ. Controls позволяют менять параметры, Autodocs показывает API.
Проверяйте эти состояния на узком и широком экране. /ui-kit в Next — небольшой
встроенный пример; полный каталог находится в Storybook.
Учебная документация в каталоге
Раздел «Обучение» находится первым в Storybook. Шесть MDX-страниц в
packages/ui/src/docs объясняют устройство UIKit, подключение к React/Next,
тему и CSS Modules, композицию и адаптеры ссылок, CSF/Controls/Docs, а также
содержат практикум с критериями проверки. Живые примеры используют существующие
stories. На Docs-странице каждого компонента есть назначение, пример применения,
ключевые параметры и ограничения; у примитивов приведены ссылки на API Mantine.
Для новой учебной страницы добавьте .mdx в src/docs с Meta title, для
описания компонента — parameters.docs.description.component в его CSF-файл.
Пример подключения задаётся строкой в parameters.docs.usage: общий шаблон
.storybook/component-docs.tsx выводит его через штатный блок Source, отдельно
от текста и живых stories. Это обходит отсутствие fenced-кода в Markdown-описаниях
текущего Storybook 10.6. На MDX-страницах обычные fenced-блоки работают.
Нумерация заголовков и storySort в .storybook/preview.tsx задают порядок уроков.
Текст в MDX проверяется статической сборкой Storybook: TypeScript сам по себе
не проверяет кодовые блоки, приведённые как учебные примеры.
Данные и миграция
Site.programPoster — редактируемая медиаиллюстрация расписания в Strapi/Garage. Порядок FAQ хранится в поле order. Карточки маршрутов и размещения образуются из заголовков и абзацев соответствующих страниц; безопасная ссылка на карту внутри карточки используется для iframe. Меню берётся из Site.navigation.
infra/strapi/scripts/upgrade-presentation.mjs — узкое повторяемое обновление
импортированного оформления. Запускается внутри подготовленного Strapi-контейнера
с snapshot и report; --dry-run читает published/draft и проверяет конфликты без записи. В рабочем режиме
обновляет только content страниц directions/placement, FAQ.order, Site.navigation
и Site.programPoster/overviewTitle/overviewDescription/overviewImage. Новости и события, тексты FAQ и прочие настройки Site не
переписывает. До первой записи сверяются все draft/published и исходное содержимое страниц.
Любые неопубликованные изменения или редакторские правки страниц останавливают
обновление; миграция не публикует их автоматически. Миграция не имеет общей транзакции: при ошибке изучите report и повторите
после исправления причины. Не запускайте параллельно другой импорт.
Учебные упражнения
- Измените primaryColor через UiProvider: компоненты должны сохранить общий вид.
- Добавьте историю собственного состояния поля без обёртки над TextInput.
- Подключите Section и ArticleCollection к обычному React-приложению без Next/CMS.
- Добавьте площадку в расписание: UIKit не должен знать, откуда пришли данные.
- Проверьте длинные подписи и пустые списки в Storybook на ширине 390 px.
Обзор события и фильтр по дням
EventOverview показывает название, описание, квадратную фотографию и произвольное действие. Данные берутся из Site.overviewTitle, overviewDescription, overviewImage; UIKit ничего не знает о Strapi. TabbedArticleCollection принимает группы { id, label, items }, опциональный initialGroup и адаптер ссылок. На главной странице адаптер группирует подсобытия по московской дате и сохраняет ссылки /events/[slug]. Обзор фестиваля и его подсобытия — разные блоки.
Storybook на VPS
Каталог доступен на https://storybook.gheilt.mxsource.xyz. Статическая сборка
упакована в отдельный образ infra/storybook/Dockerfile. Внутри контейнера Caddy
раздаёт файлы на 8080, а основной Caddy обеспечивает публичный HTTPS и HTTP/3.
Порт контейнера не публикуется на хосте; два Compose-проекта используют существующую
сеть gheilt_default. Контейнер работает от UID 1000 с файловой системой только
для чтения, без Linux capabilities. Индексация запрещена через robots.txt и
X-Robots-Tag. Каталог публичный; секреты и реальные пользовательские данные в
stories добавлять не следует.
Отдельный workflow .github/workflows/storybook.yaml собирает образ, публикует его
в GHCR и запускает scripts/deploy/storybook.sh. Он обновляет только Storybook и
перезагружает конфигурацию основного Caddy, сохраняя работающие образы остальных
сервисов. На VPS образ закреплён digest в
/opt/gheilt/storybook/current/release.env. Публикация запускается при изменении
UIKit/статических ресурсов/конфигурации Storybook или вручную:
gh workflow run storybook.yaml --ref main
Деплой использует общий /opt/gheilt/deploy.lock, проверяет health контейнера и
доступность публичного index.json. При ошибке восстанавливает конфигурацию Caddy
и предыдущий образ каталога. На первом деплое добавляет блок поддомена в фактически
подключённый Caddyfile; в репозитории этот блок тоже сохранён, поэтому очередной
обычный деплой сайта его сохраняет. Основной Compose-проект нужно запускать первым:
он создаёт общую сеть и основной прокси.
Основание: публикация статической сборки Storybook и перезагрузка Caddy.
Организация stories
У каждого базового компонента отдельный CSF-файл в src/stories с собственной
component-метаинформацией и типом Meta<typeof Component>. Разделы Actions,
Forms, Overlays, Typography, Surfaces, Feedback, Navigation и Data группируют
компоненты по назначению; состояния находятся внутри соответствующего компонента.
Default у Modal/Drawer связывает открытие и закрытие с args через useArgs: Controls
opened отражает состояние окна. Opened использует локальное состояние закрытия и
реагирует на изменение opened в Controls; при новом открытии story окно снова
открыто. В Docs Opened размещён в отдельном iframe высотой 450 px, чтобы портал,
фокус и блокировка прокрутки оставались внутри примера. Controls в Docs управляют
основным Default, а на отдельной странице Opened — его начальными параметрами. Составные
блоки также имеют собственные метаданные; они не наследуют Controls другой карусели
или расписания. Сценарии без одного компонента (пустое состояние, очередь уведомлений)
выделены отдельно, без фиктивной component: Button.
Учебный раздел Storybook
В каталоге «Обучение» есть шесть MDX-глав: введение, подключение, тема и CSS-каскад,
композиция, создание stories/docs и практикум. Главы находятся в
packages/ui/src/docs, подключаются через stories в
.storybook/main.ts и показываются перед каталогом компонентов через storySort.
MDX объединяет обычный текст, примеры JSX и живые Canvas существующих stories.
Изменение примера в Controls меняет его args, а не исходники приложения.
Для проверки новых глав нужно собрать Storybook: одного tsc --noEmit недостаточно,
потому что TypeScript не компилирует MDX и не строит индекс stories.
pnpm --filter @gheilt/ui build-storybook
Страница 404 приложения — пример композиции готового UI kit с Mantine: разбор компонентов и серверной границы.