fix(ui): адаптировать шапку страницы к узким экранам
This commit is contained in:
1 parent
6a726b13b4
commit
9b3196363c
4 files changed
+120
-14
No files matched your search
+15
-13
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import type { ReactNode } from "react"
|
||||
import { Fragment, type ReactNode } from "react"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar"
|
||||
import {
|
||||
@@ -27,31 +27,33 @@ interface PageHeaderProps {
|
||||
/** Header chrome — AppsMenu + SystemMonitor; theme in NavUser. @see https://reui.io/preview/base/app-shell-12 */
|
||||
export function PageHeader({ crumbs, actions }: PageHeaderProps) {
|
||||
return (
|
||||
<header className="bg-background sticky top-0 z-10 flex h-12 shrink-0 items-center gap-2 border-b px-4 md:px-6">
|
||||
<header className="bg-background sticky top-0 z-10 flex min-h-12 shrink-0 flex-wrap items-center gap-2 border-b px-4 py-1.5 md:px-6">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<Breadcrumb aria-label="Путь страницы" className="min-w-0 flex-1">
|
||||
<BreadcrumbList className="flex-nowrap">
|
||||
{crumbs.map((crumb, i) => (
|
||||
<span key={i} className="flex items-center gap-1.5">
|
||||
{i > 0 && <BreadcrumbSeparator />}
|
||||
<BreadcrumbItem>
|
||||
<Fragment key={i}>
|
||||
{i > 0 && <BreadcrumbSeparator className="hidden shrink-0 sm:block" />}
|
||||
<BreadcrumbItem className={i < crumbs.length - 1 ? "hidden shrink-0 sm:inline-flex" : "min-w-0"}>
|
||||
{i < crumbs.length - 1 && crumb.href ? (
|
||||
<BreadcrumbLink href={crumb.href}>{crumb.label}</BreadcrumbLink>
|
||||
) : i < crumbs.length - 1 ? (
|
||||
<span className="text-muted-foreground text-sm">{crumb.label}</span>
|
||||
) : (
|
||||
<BreadcrumbPage>{crumb.label}</BreadcrumbPage>
|
||||
<BreadcrumbPage className="wrap-anywhere">{crumb.label}</BreadcrumbPage>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
</span>
|
||||
</Fragment>
|
||||
))}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{actions ? <div className="flex items-center gap-2">{actions}</div> : null}
|
||||
<AppsMenu />
|
||||
<SystemMonitorPopover />
|
||||
<div className={`ml-auto flex max-w-full items-center gap-2 ${actions ? "w-full xl:w-auto" : "shrink-0"}`}>
|
||||
{actions ? <div role="group" aria-label="Действия страницы" className="flex min-w-0 flex-1 flex-wrap items-center gap-2">{actions}</div> : null}
|
||||
<div className="ml-auto flex shrink-0 items-center gap-2">
|
||||
<AppsMenu />
|
||||
<SystemMonitorPopover />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
||||
+10
-1
@@ -20,7 +20,7 @@
|
||||
| 3 | Основные таблицы и справочники | Завершён 2026-09-13, границы проверок ниже |
|
||||
| 4 | Остальные таблицы, удаление старых API, общая регрессия | Завершён 2026-09-20; границы приёмки ниже |
|
||||
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Завершён 2026-09-20 для дашборда; границы проверок ниже |
|
||||
| 6 | Навигация и дашборд | Не начат |
|
||||
| 6 | Навигация и дашборд | В работе: адаптивная шапка готова, далее навигация и очередь проблем |
|
||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Не начат |
|
||||
| 8 | Трафик и статистика | Не начат |
|
||||
| 9 | VPN, Firewall и Filters | Не начат |
|
||||
@@ -381,6 +381,14 @@ Internet Path использует успешный серверный сним
|
||||
|
||||
Квота: 41% пятичасовой на старте, 73% после профильных проверок (недельная 11%). Следующий этап 6 — общая оболочка и дашборд: показать одну схему ПК/телефона по App Shell 12/Stats 12, затем улучшать навигацию, контекст и очередь проблем с переходами к объектам. Этот этап сохранить отдельным локальным коммитом без push; настройки skills/AGENTS/.codex не включать.
|
||||
|
||||
## Этап 6 — блок 1: схема и адаптивная шапка (2026-09-20)
|
||||
|
||||
Схема ПК/телефона и таблица референсов сохранены в `docs/ux-ui-stage-6.md`. PageHeader сохраняет установленные SidebarTrigger/Breadcrumb/AppsMenu/SystemMonitorPopover: текущая страница видна на телефоне, действия переносятся, фиксированная высота снята. У Breadcrumb корректные прямые li внутри ol и русское доступное имя. Примитивы ReUI, зависимости и backend не менялись.
|
||||
|
||||
Проверки: TypeScript и scoped ESLint успешны. Четыре браузерных сценария шапки (320/390/1024/1440 px) прошли: геометрия заголовка/кнопок, меню с клавиатуры, Escape и возврат фокуса. Дополнительно прошли таблица рекурсивных маршрутов и мобильный сценарий серверов. Просмотрены скриншоты 320/1440. Первый запуск теста ошибочно искал banner: header внутри main этой роли не имеет; локатор исправлен без изменения семантики приложения. ReUI validate_usage/checklist получены ранее, используются установленные API и семантические цвета.
|
||||
|
||||
Предыдущая попытка запуска была остановлена ошибкой автоматической проверки разрешений из-за исчерпанной квоты; после сброса проверка запуска прошла. На старте этого продолжения использовано 2% пятичасовой и 16% недельной квоты. Этап 6 остаётся открытым. Следующий блок — единый список Sidebar/CommandPalette и стандартное управление фокусом поиска, затем дашборд и ссылки очереди. Локальный коммит без push; файлы настройки другой работы не включать.
|
||||
|
||||
## Карта миграции (исходная инвентаризация этапа 0)
|
||||
|
||||
Исходная связка на этапе 0: @tanstack/react-table ^8.21.3, @tanstack/react-virtual ^3.14.4, base-nova. По итогам этапа 4 используется @tanstack/react-table 9.2.4 с useTable/dataGridFeatures и дерево FilterQuery вместо старого filters/onChange. Список ниже сохранён как историческая инвентаризация.
|
||||
@@ -445,3 +453,4 @@ Internet Path использует успешный серверный сним
|
||||
- `app/(main)/statistics/page.tsx`
|
||||
- `components/backups/backups-history.tsx`
|
||||
- `components/data-page-toolbar.tsx`
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Этап 6: общая оболочка и дашборд
|
||||
|
||||
Рабочая схема от 2026-09-20. Основа — ui-design-contract.md: ReUI PRO, base-nova, Frame, Sidebar 240 px. Поиск вызывается Ctrl+K/⌘K, тема остаётся в NavUser. Весь этап ещё не завершён.
|
||||
|
||||
| Область | Референс | Результат |
|
||||
| --- | --- | --- |
|
||||
| Оболочка | [App Shell 12](https://reui.io/preview/base/app-shell-12?ref=mcp) | Контекст страницы, доступные действия, меню и поиск с клавиатуры |
|
||||
| Сводка | [Stats 12](https://reui.io/preview/base/stats-12?ref=mcp) | KpiStatGrid/IconTile во Frame, предметные подписи, неизвестное значение — «—» |
|
||||
| Очередь проблем | [List 9](https://reui.io/preview/base/list-9?ref=mcp) | Проблема → объект → доступное действие |
|
||||
|
||||
## ПК
|
||||
|
||||
```text
|
||||
┌ Навигация 240 px ┬ Путь страницы · Действия · Приложения · Монитор ┐
|
||||
│ Обзор ├ Свежесть / ошибки источников / обновить │
|
||||
│ Данные ├ Сводка: серверы · туннели · BGP · проверки │
|
||||
│ Управление ├ Очередь проблем → сервер / профильный раздел │
|
||||
│ Инструменты ├ Измерения и события │
|
||||
│ Система │ Быстрые переходы │
|
||||
└─────────────────┴─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Телефон
|
||||
|
||||
```text
|
||||
┌ Меню · Текущая страница ┐
|
||||
│ Действия с переносом · Приложения · Монитор │
|
||||
├ Свежесть / ошибки источников ┤
|
||||
│ Сводка в адаптивной сетке │
|
||||
│ Очередь проблем → объект │
|
||||
│ Измерения → события → быстрые переходы │
|
||||
└────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
На страницах без действий служебные кнопки остаются рядом с заголовком. При наличии действий и ширине менее 1280 px выделяется дополнительная строка с переносом кнопок. DOM-порядок совпадает с визуальным и клавиатурным, текущий заголовок не скрывается многоточием. На ПК сохраняется компактная строка, при нехватке места шапка растёт по высоте.
|
||||
|
||||
## Блоки и приёмка
|
||||
|
||||
1. **Шапка.** Убрать фиксированную высоту и обрезку; текущая страница видна на телефоне; корректный список Breadcrumb. Проверить 320/390/1024/1440 px, клавиатуру и Escape.
|
||||
2. **Навигация.** Общий источник названий, адресов и поисковых синонимов для Sidebar и CommandPalette. Использовать установленные Command/Dialog для фокуса, поиска, клавиатуры и семантических цветов. Закрывать мобильное меню после перехода. Сверить маршруты с существующими страницами.
|
||||
3. **Дашборд.** Уточнить подписи «Флот», «Оверлей», «BGP Rx», «Здоровье». Поднять очередь проблем после сводки, дать контекст и переходы к объектам. Сохранить состояния источников этапа 5; отсутствие данных не считать отсутствием проблем. Быстрые переходы не должны вытеснять проблемы за первый экран.
|
||||
4. **Приёмка.** Просмотреть ПК/телефон, проверить переходы, пустое состояние, частичный сбой и клавиатуру; профильные тесты, TypeScript, lint и production build общей оболочки.
|
||||
|
||||
HTTP-моки проверяют интерфейс и запросы. Реальные данные, права и RouterOS требуют тестового окружения. Формы и полный URL-контекст сервера относятся к этапу 7; сетевые операции в этапе 6 не менять.
|
||||
@@ -0,0 +1,51 @@
|
||||
import { test, expect } from "@playwright/test"
|
||||
|
||||
for (const width of [320, 390, 1024, 1440]) {
|
||||
test(`шапка ${width}px: заголовок, действия и меню доступны без обрезки`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 900 })
|
||||
const errors: string[] = []
|
||||
page.on("pageerror", error => errors.push(error.message))
|
||||
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
|
||||
await page.route("**/api/**", route => {
|
||||
const path = new URL(route.request().url()).pathname
|
||||
return route.fulfill({ json: path === "/api/recursive-routes" ? { routes: [] }
|
||||
: path === "/api/recursive-routes/gateways" ? { gateways: [] }
|
||||
: path === "/api/auth/config" ? { required: false }
|
||||
: path === "/api/servers" ? [{ id: 1, name: "Роутер", host: "192.0.2.100", type: "exit-node", site: "TEST", country: "RU", asn: "64512", enabled: true, status: "online", latency: 1 }]
|
||||
: path === "/api/evobgp/settings" ? { enabled: false, baseUrl: "", secretConfigured: false } : {} })
|
||||
})
|
||||
await page.goto("/recursive-routes")
|
||||
const header = page.locator("header").filter({ has: page.getByRole("navigation", { name: "Путь страницы" }) })
|
||||
const current = header.locator('[aria-current="page"]')
|
||||
await expect(current).toHaveText("Рекурсивные маршруты")
|
||||
await expect(header.getByRole("button", { name: "Обновить", exact: true })).toBeEnabled()
|
||||
// Check the actual visible controls, not just document scrollWidth: a clipped
|
||||
// child in an overflow-hidden application shell can otherwise pass unnoticed.
|
||||
const visibleBounds = await header.evaluate(element => {
|
||||
const parent = element.getBoundingClientRect()
|
||||
return [...element.querySelectorAll('button, [aria-current="page"]')]
|
||||
.filter(child => child.getClientRects().length > 0)
|
||||
.map(child => {
|
||||
const rect = child.getBoundingClientRect()
|
||||
return { label: child.textContent, inside: rect.left >= parent.left && rect.right <= Math.min(parent.right, window.innerWidth) + 1 && rect.top >= parent.top && rect.bottom <= parent.bottom + 1 }
|
||||
})
|
||||
})
|
||||
expect(visibleBounds.filter(control => !control.inside)).toEqual([])
|
||||
expect(await header.locator("ol").evaluate(list => [...list.children].every(child => child.tagName === "LI"))).toBe(true)
|
||||
const apps = header.getByRole("button", { name: "Приложения", exact: true })
|
||||
await apps.focus()
|
||||
await page.keyboard.press("Enter")
|
||||
await expect(page.getByRole("menu")).toBeVisible()
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(apps).toBeFocused()
|
||||
if (width < 768) {
|
||||
const sidebar = header.getByRole("button", { name: "Боковое меню", exact: true })
|
||||
await sidebar.press("Enter")
|
||||
await expect(page.getByRole("dialog")).toBeVisible()
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(sidebar).toBeFocused()
|
||||
}
|
||||
await page.screenshot({ path: `test-results/page-header-${width}.png`, fullPage: true, animations: "disabled" })
|
||||
expect(errors).toEqual([])
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user