From 9b3196363c829418e537da9b0ca40b568acb9e2b Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 20 Sep 2026 11:49:44 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=B0=D0=B4=D0=B0=D0=BF=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D1=88=D0=B0=D0=BF=D0=BA?= =?UTF-8?q?=D1=83=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B=20?= =?UTF-8?q?=D0=BA=20=D1=83=D0=B7=D0=BA=D0=B8=D0=BC=20=D1=8D=D0=BA=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/page-header.tsx | 28 ++++++++++--------- docs/ux-ui-progress.md | 11 +++++++- docs/ux-ui-stage-6.md | 44 ++++++++++++++++++++++++++++++ tests/e2e/page-header.spec.ts | 51 +++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 14 deletions(-) create mode 100644 docs/ux-ui-stage-6.md create mode 100644 tests/e2e/page-header.spec.ts diff --git a/components/page-header.tsx b/components/page-header.tsx index b2a2a7db..56e2dd05 100644 --- a/components/page-header.tsx +++ b/components/page-header.tsx @@ -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 ( -
+
- - + + {crumbs.map((crumb, i) => ( - - {i > 0 && } - + + {i > 0 && } + {i < crumbs.length - 1 && crumb.href ? ( {crumb.label} ) : i < crumbs.length - 1 ? ( {crumb.label} ) : ( - {crumb.label} + {crumb.label} )} - + ))} -
- {actions ?
{actions}
: null} - - +
+ {actions ?
{actions}
: null} +
+ + +
) diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index ced7a6f0..f9ecda17 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -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` + diff --git a/docs/ux-ui-stage-6.md b/docs/ux-ui-stage-6.md new file mode 100644 index 00000000..ee801ce6 --- /dev/null +++ b/docs/ux-ui-stage-6.md @@ -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 не менять. diff --git a/tests/e2e/page-header.spec.ts b/tests/e2e/page-header.spec.ts new file mode 100644 index 00000000..da7b2d56 --- /dev/null +++ b/tests/e2e/page-header.spec.ts @@ -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([]) + }) +}