refactor(header): unified menu navigation for all devices #11

Merged
slaid098 merged 1 commit from refactor/header-unified-menu into main 2026-08-07 16:08:48 +03:00
Owner

Что сделано

Реализован унифицированный header для issue #8 — меню ☰ одинаковое на всех устройствах.

Изменённые файлы

  • src/components/site-header.tsx — переписан: flex [бренд] ... [язык] [☰] вместо grid-cols-3 с условным swap. Бренд всегда слева (чинит «прыжок» шапки). Меню ☰ на чистом React (useState/useRef/useEffect, БЕЗ radix) с a11y.
  • src/app/[locale]/[slug]/page.tsx — добавлена стрелка «← Утилиты» над <h1> (переехала из шапки). Padding pt-10→pt-4 (стрелка даёт отступ).
  • src/lib/i18n.ts — ключи header.menu (aria-label ☰), header.subscriptions (пункт меню) для ru/en.

Новые файлы

  • src/__tests__/site-header.test.tsx — 9 тестов (рендер brand, open/close по Esc/click-outside, пункты меню ru/en, отсутствие backToUtilities в шапке).
  • src/__tests__/slug-back-link.test.tsx — 2 теста (стрелка «← Утилиты» над h1 в ru/en).

Меню ☰

  • Кнопка с aria-label, aria-expanded, aria-controls="nav-menu".
  • Закрытие: Esc (keydown), click-outside (mousedown + ref), смена pathname (useEffect на pathname).
  • Фокус-менеджмент: при открытии — фокус на первую ссылку, при закрытии — возврат на кнопку.
  • Пункты: «Утилиты» → /[loc]/apps, «Подписки» → /[loc]/subscriptions (масштабируется: элемент + i18n ключ).
  • Чистый React + Tailwind v4, БЕЗ новой зависимости (radix не нужен для 2 пунктов).

Почему

Шапка «прыгала» при переходе на карточку (лого слева → центр), стрелка «← Утилиты» ложно показывалась на /contacts и /subscriptions, навигация не масштабировалась (подписки, будущие статьи — тесно на мобильных без бургер-меню). Унифицированное меню решает все три проблемы.

Watch out

  • biome useExhaustiveDependencies: эффект закрытия меню по pathname требует ignore (dep — только pathname, но reset state при навигации). biome-ignore на useEffect с пояснением. Альтернатива (deps массив с pathname + setOpen) ломает lint иначе.
  • Шапка клиентская ("use client") — layout серверный, но header рендерится в него как клиентский island. SSR/hydration OK (статика не ломается, /[slug] остался SSG с ISR 1h).
  • /contacts и /subscriptions: шапка унифицирована — стрелки на них НЕТ. Контент страниц не менялся.
  • Стрелка в [slug]/page.tsx: next/link со scroll={false} (симметрично с /apps карточками, ADR-008 scroll restoration).

Pending

  • CI на PR не запустится (issue #10 — ci.yml в .github/workflows/ не триггерится на Forgejo pull_request). Мерж по review APPROVE + локальным проверкам.
  • EN-переводы длинных подписей в панель — проверено визуально (помещаются).

Closes #8

## Что сделано Реализован унифицированный header для issue #8 — меню ☰ одинаковое на всех устройствах. ### Изменённые файлы - `src/components/site-header.tsx` — переписан: flex `[бренд] ... [язык] [☰]` вместо grid-cols-3 с условным swap. Бренд всегда слева (чинит «прыжок» шапки). Меню ☰ на чистом React (useState/useRef/useEffect, БЕЗ radix) с a11y. - `src/app/[locale]/[slug]/page.tsx` — добавлена стрелка «← Утилиты» над `<h1>` (переехала из шапки). Padding `pt-10→pt-4` (стрелка даёт отступ). - `src/lib/i18n.ts` — ключи `header.menu` (aria-label ☰), `header.subscriptions` (пункт меню) для ru/en. ### Новые файлы - `src/__tests__/site-header.test.tsx` — 9 тестов (рендер brand, open/close по Esc/click-outside, пункты меню ru/en, отсутствие backToUtilities в шапке). - `src/__tests__/slug-back-link.test.tsx` — 2 теста (стрелка «← Утилиты» над h1 в ru/en). ### Меню ☰ - Кнопка с `aria-label`, `aria-expanded`, `aria-controls="nav-menu"`. - Закрытие: Esc (keydown), click-outside (mousedown + ref), смена pathname (useEffect на pathname). - Фокус-менеджмент: при открытии — фокус на первую ссылку, при закрытии — возврат на кнопку. - Пункты: «Утилиты» → `/[loc]/apps`, «Подписки» → `/[loc]/subscriptions` (масштабируется: элемент + i18n ключ). - Чистый React + Tailwind v4, БЕЗ новой зависимости (radix не нужен для 2 пунктов). ## Почему Шапка «прыгала» при переходе на карточку (лого слева → центр), стрелка «← Утилиты» ложно показывалась на /contacts и /subscriptions, навигация не масштабировалась (подписки, будущие статьи — тесно на мобильных без бургер-меню). Унифицированное меню решает все три проблемы. ## Watch out - **`biome useExhaustiveDependencies`**: эффект закрытия меню по pathname требует ignore (dep — только pathname, но reset state при навигации). `biome-ignore` на useEffect с пояснением. Альтернатива (deps массив с pathname + setOpen) ломает lint иначе. - **Шапка клиентская** (`"use client"`) — layout серверный, но header рендерится в него как клиентский island. SSR/hydration OK (статика не ломается, `/[slug]` остался SSG с ISR 1h). - **`/contacts` и `/subscriptions`**: шапка унифицирована — стрелки на них НЕТ. Контент страниц не менялся. - **Стрелка в `[slug]/page.tsx`**: `next/link` со `scroll={false}` (симметрично с `/apps` карточками, ADR-008 scroll restoration). ## Pending - CI на PR не запустится (issue #10 — ci.yml в `.github/workflows/` не триггерится на Forgejo pull_request). Мерж по review APPROVE + локальным проверкам. - EN-переводы длинных подписей в панель — проверено визуально (помещаются). Closes #8
- шапка: flex [бренд] ... [язык] [☰], бренд всегда слева (убран swap в центр)
- меню ☰ (чистый React state, без radix): Esc, click-outside, pathname-change close, фокус-менеджмент, aria-expanded/controls
- пункты: «Утилиты», «Подписки» (масштабируется добавлением элемента + i18n ключа)
- стрелка «← Утилиты» переехала из шапки в [slug]/page.tsx (над h1)
- на /contacts и /subscriptions стрелки больше нет (isAppPage удалён)
- i18n: ключи header.menu, header.subscriptions (ru/en)
- тесты: 9 site-header + 2 slug-back-link

Closes #8
Author
Owner

Code Review Summary

Рефакторинг шапки выполнен чисто: isAppPage полностью удалён, шапка теперь идентична на всех страницах ([бренд] ... [язык] [☰]), ☰-меню реализовано с полным a11y-контрактом. Все контракты issue #8 выполнены, тесты покрывают ключевые сценарии.

Positives

  • A11y контракт реализован полностью: кнопка имеет type="button", aria-label, aria-expanded, aria-controls="nav-menu"; панель имеет совпадающий id="nav-menu"; фокус-менеджмент работает — фокус на первый пункт при открытии (site-header.tsx:21-25), возврат на кнопку при Esc (site-header.tsx:32)
  • Три механизма закрытия меню: Esc (site-header.tsx:27-37), click-outside (site-header.tsx:39-48), смена pathname (site-header.tsx:51-53) — все с корректным cleanup в useEffect
  • usePathname используется правильно — только для закрытия меню при навигации, НЕ для условного рендера brand. Статика не ломается (client hook, не dynamic API)
  • Стрелка «← Утилиты» изолирована — только в [slug]/page.tsx:82-88, отсутствует в contacts/apps/subscriptions и в шапке. Тест site-header.test.tsx:70-73 явно проверяет отсутствие
  • Мёртвый код удалёнisAppPage не осталось ни в одном файле (grep подтверждает), все импорты используются
  • i18n консистентенmenu и subscriptions добавлены в тип Dict + ruDict + enDict, backToUtilities сохранён (перемещён consumer из шапки в [slug])
  • Тесты качественные — 9 тестов в site-header (brand ru/en, aria-атрибуты, open, Esc, click-outside, пункты меню, отсутствие back-link) + 2 теста в slug-back-link (ru/en). Имена тестов описательные
  • Biome стиль соблюдён — 2 пробела, двойные кавычки, semicolons, trailing commas, 100 ширина, no any

Suggestions (info, not blocking)

  • site-header.tsx:42 [a11y] При закрытии через click-outside фокус не возвращается на кнопку (только при Esc). Это допустимо — пользователь кликнул куда-то и фокус ушёл туда, но для полноты можно добавить buttonRef.current?.focus() в onMouseDown handler после setOpen(false)
  • site-header.tsx:51-53 [style] biome-ignore для useExhaustiveDependencies оправдан (нужен только pathname, setOpen стабилен), комментарий корректен

CI не запустится на PR #11 — известная проблема issue #10 (ci.yml не триггерится), не связана с этим PR.

Verdict: APPROVE

## Code Review Summary Рефакторинг шапки выполнен чисто: `isAppPage` полностью удалён, шапка теперь идентична на всех страницах (`[бренд] ... [язык] [☰]`), ☰-меню реализовано с полным a11y-контрактом. Все контракты issue #8 выполнены, тесты покрывают ключевые сценарии. ### Positives - **A11y контракт реализован полностью**: кнопка имеет `type="button"`, `aria-label`, `aria-expanded`, `aria-controls="nav-menu"`; панель имеет совпадающий `id="nav-menu"`; фокус-менеджмент работает — фокус на первый пункт при открытии (`site-header.tsx:21-25`), возврат на кнопку при Esc (`site-header.tsx:32`) - **Три механизма закрытия меню**: Esc (`site-header.tsx:27-37`), click-outside (`site-header.tsx:39-48`), смена pathname (`site-header.tsx:51-53`) — все с корректным cleanup в useEffect - **`usePathname` используется правильно** — только для закрытия меню при навигации, НЕ для условного рендера brand. Статика не ломается (client hook, не dynamic API) - **Стрелка «← Утилиты» изолирована** — только в `[slug]/page.tsx:82-88`, отсутствует в contacts/apps/subscriptions и в шапке. Тест `site-header.test.tsx:70-73` явно проверяет отсутствие - **Мёртвый код удалён** — `isAppPage` не осталось ни в одном файле (grep подтверждает), все импорты используются - **i18n консистентен** — `menu` и `subscriptions` добавлены в тип Dict + ruDict + enDict, `backToUtilities` сохранён (перемещён consumer из шапки в [slug]) - **Тесты качественные** — 9 тестов в site-header (brand ru/en, aria-атрибуты, open, Esc, click-outside, пункты меню, отсутствие back-link) + 2 теста в slug-back-link (ru/en). Имена тестов описательные - **Biome стиль соблюдён** — 2 пробела, двойные кавычки, semicolons, trailing commas, 100 ширина, no `any` ### Suggestions (info, not blocking) - **site-header.tsx:42** [a11y] При закрытии через click-outside фокус не возвращается на кнопку (только при Esc). Это допустимо — пользователь кликнул куда-то и фокус ушёл туда, но для полноты можно добавить `buttonRef.current?.focus()` в `onMouseDown` handler после `setOpen(false)` - **site-header.tsx:51-53** [style] `biome-ignore` для `useExhaustiveDependencies` оправдан (нужен только `pathname`, `setOpen` стабилен), комментарий корректен CI не запустится на PR #11 — известная проблема issue #10 (ci.yml не триггерится), не связана с этим PR. ### Verdict: APPROVE
slaid098 deleted branch refactor/header-unified-menu 2026-08-07 16:08:48 +03:00
Sign in to join this conversation.
No reviewers
No labels
bug
performance
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
slaid098/slaid098-dev!11
No description provided.