feat: add breadcrumbs

This commit is contained in:
2026-08-14 14:30:20 +03:00
parent e5517cceae
commit 113ad5e9a2
6 changed files with 186 additions and 0 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-14
@@ -0,0 +1,57 @@
## Context
См. `proposal.md` для мотивации и `specs/app-breadcrumb-navigation/spec.md` для контрактов поведения. `AppShell` является общим layout защищённых маршрутов и уже располагает содержимое над `<Outlet />`, но находится в shared-слое. Название Brokerage Account доступно через существующий query списка счетов и может отсутствовать при прямом переходе на маршрут счёта.
## Goals / Non-Goals
**Goals:**
- Показывать единый маршрутный trail над защищённым page content без изменения API.
- Использовать настоящее название Brokerage Account, когда оно доступно, и безопасный fallback в противном случае.
- Сохранить понятную навигацию на узких экранах.
**Non-Goals:**
- Не добавлять крошки на public и not-found маршруты.
- Не создавать endpoint отдельного Brokerage Account и не менять cache policy списка счетов.
- Не заменять существующую боковую или нижнюю навигацию.
## Decisions
### Route-aware компонент остаётся в app-слое
Добавить app-level компонент, который строит trail из текущего маршрута, его параметра `accountId` и локализованных строк. `AppRoutes` передаёт его в shared `AppShell` как content slot перед `<Outlet />`.
Это сохраняет FSD-направление зависимостей: shared layout не импортирует entity или feature. Альтернатива с определением маршрутов и загрузкой счета внутри `AppShell` смешала бы shared-слой с доменными данными.
### Название счёта берётся из существующего query списка
Подкомпонент для маршрутов `/accounts/:accountId` и `/accounts/:accountId/operations` использует существующий query Brokerage Account и находит запись по `accountId`. Пока query не вернул соответствующее название, trail показывает «Счёт».
Это поддерживает прямые переходы и повторно использует query cache списка. Альтернатива с передачей имени из каждой страницы дублировала бы route-логику и не покрывала бы loading/error state до отображения page content. Новый API endpoint не нужен: список уже содержит стабильный id и непустое name.
### Trail строится из фиксированной карты защищённых маршрутов
Карта определяет `Брокерские счета`, `Настройки`, Portfolio счёта и Account Ledger, а также destination URL для предшествующих элементов. Текущий элемент рендерится текстом, остальные - router links. Неизвестные и public routes возвращают отсутствие trail.
Фиксированная карта соответствует текущему небольшому набору маршрутов и исключает разбор URL по сегментам. Автоматическое построение из pathname было бы хрупким для динамического `accountId` и не предоставляло бы локализованного названия счёта.
### Крошки заменяют локальный возврат из Account Ledger
Удалить действие «К счёту» из заголовка Account Ledger: trail уже предоставляет ссылку на Portfolio текущего Brokerage Account.
Это устраняет дублирующий navigation control. Боковая и нижняя навигация остаются без изменений, поскольку ведут к другим разделам приложения.
### Адаптация сохраняет последние два элемента
На узком экране компонент передаёт в presentation только ближайшего родителя и текущий элемент; на широком экране показывает полный trail. Стили layout отвечают за перенос и отступ от page content.
Это прямо реализует согласованное поведение для трёхуровневой цепочки. Горизонтальная прокрутка или отдельное меню скрытых элементов добавили бы управление без новых маршрутов и не нужны для текущей глубины.
## Risks / Trade-offs
- [Прямой переход временно показывает обобщённое имя] → отображать «Счёт» до результата существующего query и не раскрывать `accountId`.
- [Запрос списка счётов ради названия] → монтировать account-aware подкомпонент только на маршрутах счёта; React Query повторно использует его 60-секундный cache.
- [Рост route map при новых страницах] → добавлять маршрут и его expected trail в той же app-level карте и соответствующем тесте.
## Migration Plan
Изменение не требует миграции данных или API. При rollback удалить slot и app-level компонент; существующие маршруты и page content продолжат работать без крошек.
@@ -0,0 +1,25 @@
## Why
Пользователь теряет быстрый путь к списку Brokerage Account и к Portfolio при переходе к Account Ledger. Нужна единая навигационная цепочка в защищённой части приложения.
## What Changes
- Добавить хлебные крошки над содержимым каждого защищённого маршрута.
- Отразить иерархию `Брокерские счета → <название счёта> → Операции` и отдельный маршрут «Настройки».
- Сделать только предшествующие крошки ссылками; текущую страницу показывать текстом.
- Сохранять текущую страницу и ближайшего родителя на узких экранах; при недоступном названии Brokerage Account показывать «Счёт».
- Удалить из Account Ledger избыточное локальное действие возврата к счёту.
## Capabilities
### New Capabilities
- `app-breadcrumb-navigation`: единая и адаптивная навигация по защищённым маршрутам приложения.
### Modified Capabilities
_Нет._
## Impact
- Затрагивает shared layout и локализованные UI-строки frontend.
- Не изменяет backend API, данные Brokerage Account или зависимости.
@@ -0,0 +1,42 @@
## Purpose
Позволяет User быстро переходить по иерархии защищённых инвестиционных маршрутов через хлебные крошки над содержимым страницы.
## ADDED Requirements
### Requirement: Хлебные крошки доступны на защищённых маршрутах
Клиент SHALL показывать хлебные крошки над содержимым на защищённых маршрутах `/accounts`, `/accounts/:accountId`, `/accounts/:accountId/operations` и `/settings`. Клиент MUST NOT показывать хлебные крошки на public-маршрутах и странице несуществующего маршрута.
#### Scenario: User открывает список Brokerage Account
- **WHEN** авторизованный User открывает `/accounts`
- **THEN** клиент показывает единственную текущую крошку «Брокерские счета» над содержимым списка
#### Scenario: User открывает Settings
- **WHEN** авторизованный User открывает `/settings`
- **THEN** клиент показывает единственную текущую крошку «Настройки» над содержимым страницы
### Requirement: Цепочка крошек отражает иерархию Brokerage Account
На `/accounts/:accountId` клиент SHALL показывать «Брокерские счета → <название счёта>». На `/accounts/:accountId/operations` клиент SHALL показывать «Брокерские счета → <название счёта> → Операции». Если название Brokerage Account недоступно, клиент MUST показывать «Счёт» вместо технического идентификатора.
#### Scenario: User открывает Portfolio счёта с доступным названием
- **WHEN** авторизованный User открывает `/accounts/:accountId` и название Brokerage Account доступно
- **THEN** клиент показывает его название как текущую крошку после ссылки «Брокерские счета»
#### Scenario: User открывает Account Ledger
- **WHEN** авторизованный User открывает `/accounts/:accountId/operations` и название Brokerage Account доступно
- **THEN** клиент показывает ссылку на список, ссылку на Portfolio этого счёта и текущую крошку «Операции» в указанном порядке без отдельного локального действия возврата к счёту
#### Scenario: Название Brokerage Account недоступно
- **WHEN** клиент не получил название Brokerage Account для защищённого маршрута счёта
- **THEN** клиент показывает «Счёт» и не показывает `accountId`
### Requirement: Крошки предоставляют доступную адаптивную навигацию
Клиент SHALL делать ссылками только крошки, предшествующие текущей странице; текущая крошка MUST быть обычным текстом. На узком экране клиент MUST скрывать промежуточные крошки, сохраняя ближайшую родительскую крошку и текущую страницу.
#### Scenario: User выбирает предшествующую крошку
- **WHEN** User выбирает крошку, предшествующую текущей странице
- **THEN** клиент переходит по маршруту этой крошки
#### Scenario: Крошки показаны на узком экране
- **WHEN** цепочка содержит более двух элементов и User открывает её на узком экране
- **THEN** клиент сохраняет видимыми ближайшую родительскую крошку и текущую страницу, скрывая более дальние промежуточные крошки
@@ -0,0 +1,16 @@
## 1. Навигация по маршрутам
- [x] 1.1 Добавить app-level компонент хлебных крошек, зависящий от маршрута: он строит локализованную цепочку для каждого protected route и не отображается на public или неизвестных маршрутах.
- [x] 1.2 Получать название Brokerage Account через существующий query списка счетов на маршрутах счёта, используя локализованный fallback `Счёт`.
- [x] 1.3 Добавить content slot в `AppShell` и отрисовать компонент крошек над outlet защищённого маршрута, не добавляя entity-зависимости в shared.
- [x] 1.4 Удалить из Account Ledger локальное действие «К счету», которое дублирует ссылку крошек на Portfolio.
## 2. Отображение и тесты
- [x] 2.1 Добавить локализованные строки крошек и адаптивные стили, сохраняющие ближайшую родительскую и текущую крошку на узком экране.
- [x] 2.2 Добавить component или route-тесты для цепочек protected route, назначений ссылок, текста текущей крошки, fallback названия счёта и отсутствия крошек на public и неизвестных маршрутах.
## 3. Проверка
- [x] 3.1 Выполнить `npm run verify` в `frontend`.
- [x] 3.2 Выполнить `openspec validate add-app-breadcrumbs --type change --store wealth-ledger-specs` в `specs`.
@@ -0,0 +1,44 @@
# app-breadcrumb-navigation Specification
## Purpose
Позволяет User быстро переходить по иерархии защищённых инвестиционных маршрутов через хлебные крошки над содержимым страницы.
## Requirements
### Requirement: Хлебные крошки доступны на защищённых маршрутах
Клиент SHALL показывать хлебные крошки над содержимым на защищённых маршрутах `/accounts`, `/accounts/:accountId`, `/accounts/:accountId/operations` и `/settings`. Клиент MUST NOT показывать хлебные крошки на public-маршрутах и странице несуществующего маршрута.
#### Scenario: User открывает список Brokerage Account
- **WHEN** авторизованный User открывает `/accounts`
- **THEN** клиент показывает единственную текущую крошку «Брокерские счета» над содержимым списка
#### Scenario: User открывает Settings
- **WHEN** авторизованный User открывает `/settings`
- **THEN** клиент показывает единственную текущую крошку «Настройки» над содержимым страницы
### Requirement: Цепочка крошек отражает иерархию Brokerage Account
На `/accounts/:accountId` клиент SHALL показывать «Брокерские счета → <название счёта>». На `/accounts/:accountId/operations` клиент SHALL показывать «Брокерские счета → <название счёта> → Операции». Если название Brokerage Account недоступно, клиент MUST показывать «Счёт» вместо технического идентификатора.
#### Scenario: User открывает Portfolio счёта с доступным названием
- **WHEN** авторизованный User открывает `/accounts/:accountId` и название Brokerage Account доступно
- **THEN** клиент показывает его название как текущую крошку после ссылки «Брокерские счета»
#### Scenario: User открывает Account Ledger
- **WHEN** авторизованный User открывает `/accounts/:accountId/operations` и название Brokerage Account доступно
- **THEN** клиент показывает ссылку на список, ссылку на Portfolio этого счёта и текущую крошку «Операции» в указанном порядке без отдельного локального действия возврата к счёту
#### Scenario: Название Brokerage Account недоступно
- **WHEN** клиент не получил название Brokerage Account для защищённого маршрута счёта
- **THEN** клиент показывает «Счёт» и не показывает `accountId`
### Requirement: Крошки предоставляют доступную адаптивную навигацию
Клиент SHALL делать ссылками только крошки, предшествующие текущей странице; текущая крошка MUST быть обычным текстом. На узком экране клиент MUST скрывать промежуточные крошки, сохраняя ближайшую родительскую крошку и текущую страницу.
#### Scenario: User выбирает предшествующую крошку
- **WHEN** User выбирает крошку, предшествующую текущей странице
- **THEN** клиент переходит по маршруту этой крошки
#### Scenario: Крошки показаны на узком экране
- **WHEN** цепочка содержит более двух элементов и User открывает её на узком экране
- **THEN** клиент сохраняет видимыми ближайшую родительскую крошку и текущую страницу, скрывая более дальние промежуточные крошки