67 lines
7.6 KiB
Markdown
67 lines
7.6 KiB
Markdown
## 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` и стилей.
|