7.6 KiB
Context
В frontend UI почти полностью построен на @mantine/core (v9.5.1). Единственное место с собственной ручной реализацией хлебных крошек — src/app/ui/AppBreadcrumbs.tsx: внутренний BreadcrumbTrail вручную рендерит <nav aria-label> → <span class="breadcrumbs-*"> → сепаратор «→», плюс кастомные классы breadcrumbs, breadcrumbs-entry, breadcrumbs-separator-N, breadcrumbs-item-N и медиа-правило data-depth="3" в src/app/styles.css. Тест src/app/ui/AppBreadcrumbs.test.tsx проверяет data-depth, aria-current, aria-label, navigation role и ссылки.
Mantine Breadcrumbs (v9.5.1) — штатный аналог: рендерит children с сепаратором между ними, поддерживает separator, separatorMargin и произвольные children (элементы или текст). Рендерится как <Box> (<div> по умолчанию), принимает component и data-*/aria-атрибуты. Нативно НЕ скрывает промежуточные крошки по глубине и НЕ выставляет aria-current — адаптивность и текущую крошку нужно сохранить на уровне представления/CSS.
Мобильная навигация AppShell (bottom-nav) выходит из scope по решению Change Owner — остаётся кастомной.
Goals / Non-Goals
Goals:
- Заменить ручную разметку
BreadcrumbTrailна штатныйMantine Breadcrumbs. - Сохранить идентичное пользовательское и доступное поведение: те же цепочки, ссылки только у предшествующих крошек, текущая страница — текст,
aria-label, navigation role,data-depth, адаптивное скрытие промежуточных крошек на узком экране.
Non-Goals:
- Не менять маршруты, иерархию, тексты, API и данные.
- Не трогать другую кодовую базу (уже на Mantine) и мобильную навигацию
AppShell.
Decisions
D1. Использовать Mantine Breadcrumbs с component="nav" и aria-label
Ручной <nav> заменяется на <Breadcrumbs component="nav" aria-label={t("breadcrumbs.label")} data-depth={items.length} separator="→">. data-depth и aria-label передаются в root через BoxProps/aria-атрибуты — сохраняет тесты navigation role, label и data-depth.
Rationale: Breadcrumbs — это и есть штатная замена собственной разметки сепаратора/навигации. Альтернатива (оставить кастом) отвергнута — цель change удалить дублирующий кастом.
D2. Ссылки и текущая крошка рендерятся как children
Каждая крошка — элемент в children: предшествующие — <Link to={...}>{label}</Link> (получает Mantine breadcrumb class), текущая — <span aria-current="page">{label}</span>. Сепаратор «→» задаётся через separator="→" вместо ручных breadcrumbs-separator-N spans.
Rationale: сохраняет поведение «ссылки только предшествующие, текущая — текст с aria-current». Mantine автоматически ставит сепаратор между children.
D3. Убрать теперь неиспользуемые кастомные CSS-классы, оставить только адаптивное правило
Удаляются breadcrumbs, breadcrumbs-entry, breadcrumbs-separator-N, breadcrumbs-item-N, а также селекторы и правило data-depth="3" из src/app/styles.css. Стиль оставшихся крошек даёт Mantine (accent-цвет ссылки можно сохранить через минимальную настройку Breadcrumbs/.mantine-класс или переиспользовать существующий --ft-color-accent).
Адаптивное скрытие промежуточных крошек на узком экране НЕ переносится на компонент: Mantine Breadcrumbs не умеет прятать по глубине. Сохраняется как кастомное CSS-правило по data-depth на root. Рассматривалась альтернатива — рендерить сокращённую цепочку на mobile через props — отвергнута как избыточная переделка поведения ради минимизации CSS.
Delivery Map
| Area | Status | Architectural outcome or N/A reason |
|---|---|---|
| frontend | affected |
Замена AppBreadcrumbs.tsx на Mantine Breadcrumbs, обновление AppBreadcrumbs.test.tsx, чистка кастомных breadcrumbs-* стилей в src/app/styles.css. |
| backend | N/A |
Нет изменений backend API, данных или контрактов. |
| specs | affected |
Change объявляет skip_specs: true — чистый рефакторинг, observable behavior и требования app-breadcrumb-navigation не меняются. |
| docs | N/A |
Ни один as-built документ в docs/ не описывает эти компоненты; правки не требуются. |
Contract Gate Decision
| Gate | Status | Reason |
|---|---|---|
| runtime-openapi | N/A |
API contract не меняется ни с одной стороны; рефакторинг UI-представления только во frontend. |
Risks / Trade-offs
- Разметка сепаратора/классов меняется — Mantine оборачивает children и рендерит сепаратор иначе. Митигируется тем, что тест проверяет поведение (roles,
aria-current,data-depth, ссылки), а не внутренние классы. - Адаптивное скрытие остаётся кастомным CSS — не «чистая» замена, но это осознанный trade-off: Mantine не предоставляет скрытия по глубине, а переписывание поведения на props добавило бы риск регрессий.
aria-currentи navigation role — должны быть явно построены, т.к. Mantine не добавляет их сам. Покрыто тестом.
Migration Plan
Один атомарный шаг во frontend: заменить реализацию BreadcrumbTrail на Mantine Breadcrumbs (с сохранением логики AccountBreadcrumbs/AppBreadcrumbs нетронутой), обновить тест, почистить кастомные breadcrumbs-* стили, прогнать npm run verify.
Rollback Contract
N/A — безопасный обратимый рефакторинг UI-представления без auth/security, data migration, публичного контракта или труднообратимых решений. Откат — возврат прежней реализации BreadcrumbTrail и стилей.