From b3712af0c0a5a7cb91da123da7417ba7a88f9ffa Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 6 Nov 2026 21:52:09 +0700 Subject: [PATCH 1/7] =?UTF-8?q?docs(ui):=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BF=D0=BB=D0=B0=D0=BD=20=D0=BC=D0=B8?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BD=D0=B0=20=D0=B4?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D0=B9=D0=BD=20VPS-Tracker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/ui-design-contract.md | 13 ++++-- docs/ui-migration-plan.md | 88 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 docs/ui-migration-plan.md diff --git a/docs/ui-design-contract.md b/docs/ui-design-contract.md index 87ecd18..c57743e 100644 --- a/docs/ui-design-contract.md +++ b/docs/ui-design-contract.md @@ -15,7 +15,7 @@ Project lock: **`surface: frame`**. Ops / list / dashboard / detail / settings export const UI_SURFACE = 'frame' as const ``` -Settings: секции через Frame + `gap` (без hairline `Separator` под PageHeader); `SettingRow` без `FieldSeparator` по умолчанию (`separated` opt-in). Preview: [settings-3](https://reui.io/preview/base/settings-3) · [settings-16](https://reui.io/preview/base/settings-16). +Settings: **line-tabs сверху** (Общие / Синхронизация / Уведомления / Интеграции) вместо боковой навигации; секции через Frame + `gap` (без hairline `Separator` под PageHeader); `SettingRow` без `FieldSeparator` по умолчанию (`separated` opt-in). Preview: [settings-3](https://reui.io/preview/base/settings-3) · [settings-16](https://reui.io/preview/base/settings-16). ## Canonical PRO references @@ -31,6 +31,10 @@ Settings: секции через Frame + `gap` (без hairline `Separator` п | Empty | `empty-state-14` (prefer) / `empty-state-12` | https://reui.io/preview/base/empty-state-14 · https://reui.io/preview/base/empty-state-12 | | Forms | `form-7` → Sheet/Drawer | https://reui.io/preview/base/form-7 | +## Theme + +Палитра — нейтрали hue 264 (макет «VPS-Tracker»), шрифты **Geist / Geist Mono**. Оверрайд токенов — `apps/web/src/styles/theme.css` поверх `@cfdm/ui/globals.css` (globals — только вывод CLI). В светлой теме сайдбар остаётся тёмным. План миграции: [`ui-migration-plan.md`](ui-migration-plan.md). + ## Kit API (`reui-kit/`) | Component | Role | @@ -51,7 +55,7 @@ Settings: секции через Frame + `gap` (без hairline `Separator` п | App | Section order | |-----|---------------| | EvoBGP / CFDM / EvoFirewall | KPI → **QuickActionGrid** → charts / rest | -| vps-tracker | banner → KPI → charts → attention → **QuickActionGrid** → CSV | +| vps-tracker | banner → KPI → line-tabs «Проблемы» (QuickActionGrid → charts → attention) · «Последние VPS» · «Аккаунты» → CSV | | auth-portal | portal-specific; Quick Actions при наличии dashboard | Gating: DB `show_quick_actions` / `showQuickActions` / `ui_show_quick_actions` (default `true`). @@ -67,8 +71,9 @@ Gating: DB `show_quick_actions` / `showQuickActions` / `ui_show_quick_actions` ( | Sidebar / hover colors | theme `--sidebar` / `--sidebar-accent` — **без** AppShell `color-mix` override | | Header | `h-12`, `sticky`, `border-b`, `px-4 md:px-6` | | Header left | `SidebarTrigger` + `Separator` + Breadcrumb | -| Header right | **AppsMenu** → **SystemMonitorPopover** (тема — в NavUser) | -| Sidebar | AppSwitcher → groups → icons `size-4` → **NavUser** в `SidebarFooter` | +| Header right | **SystemMonitorPopover** (пилюля «Внимание»/«Норма»; приложения — в AppSwitcher; тема — в NavUser) | +| Sidebar | AppSwitcher (лого + подзаголовок + список приложений) → SpaceSwitcher → **сворачиваемые** группы (UPPERCASE label + шеврон, `collapsible`) → icons `size-4` → **NavUser** (muted-карточка) в `SidebarFooter` | +| Проблемы в меню | точка `destructive`/`warning` у пункта (не числовой Badge) | | `main` | `gap-4 md:gap-6`, `px-4 py-4 md:px-6 md:py-5` | | Search | hotkey ⌘K / Ctrl+K only (не кнопка в header) | diff --git a/docs/ui-migration-plan.md b/docs/ui-migration-plan.md new file mode 100644 index 0000000..1e54246 --- /dev/null +++ b/docs/ui-migration-plan.md @@ -0,0 +1,88 @@ +# План миграции UI vps-tracker на дизайн «VPS-Tracker» + +Источник: артефакт-канвас [VPS-Tracker](https://claude.ai/artifact/7kPVbQNXa4AWgFkSekZJiN#page-dcb81aee8d83), доски `Vps*.dc.html` + шаблон `TplVps.dc.html` + токены `reui-auth.css`. +Канвас — макет, имитирующий ReUI; его CSS **не переносим**. Используем как спецификацию регионов и токенов, а реализуем на ReUI PRO + shadcn primitives (контракт: [`ui-design-contract.md`](ui-design-contract.md)). + +Лицензия ReUI PRO: `REUI_LICENSE_KEY` есть, премиум-реестр отвечает 200 (проверено на `app-shell-12`). + +## Что не является страницами + +- Блоки `states` («Справочник состояний · не часть страницы») — только спецификация empty/loading/error, на страницах не рендерим. +- Доски `VpsCollapsed`, `VpsLight`, `VpsMobile`, `VpsPalette` — состояния одного экрана; используем для проверки (свёрнутый сайдбар, светлая тема, ширина телефона, ⌘K). +- Инструменты канваса: полосы `wedit`, «звёздочки»/закрепы в меню, атрибуты density/contrast — пропускаем. + +## Сохраняем то, чего нет в макете + +`SpaceSwitcher` (пространства — реальная функция), `GlobalSearch` (⌘K), фильтры ReUI `Filters` в списках, CSV-экспорт, row actions. Макет их не отменяет. + +## Сопоставление блоков макета → ReUI + +| Блок макета | Реализация | ReUI / shadcn | +|---|---|---| +| sidebar `asbtn` (лого + «Учёт серверов и платежей») + поповер приложений | `AppSwitcher` (+ пункты бывшего `AppsMenu`) | shadcn `sidebar`, `dropdown-menu`; эталон [app-shell-12](https://reui.io/preview/base/app-shell-12) `brand.tsx` | +| `grph` — сворачиваемые группы (UPPERCASE + шеврон) | `NavGroup` в `app-shell.tsx` | shadcn `sidebar` + `collapsible` (паттерн Collapsible SidebarGroup) | +| `pdot` — точка проблем у пункта меню | `SidebarMenuBadge` / точка `bg-destructive` | shadcn `sidebar` | +| `user` — NavUser карточкой | `NavUser` | [app-shell-1](https://reui.io/preview/base/app-shell-1) user menu | +| header: trigger + crumbs + `statb` | `AppShell` header, без `AppsMenu` | shadcn `breadcrumb`, `popover` | +| `statb` «Внимание · ●1 ●2» → монитор | триггер `SystemMonitorPopover` | `@reui/badge` (`warning-light`/`destructive-light`) | +| h1 + sub + actions | `PageHeader` | — | +| `banner` / `note` | `Alert` | `@reui/alert` | +| `kpi` (иконка слева, label, value, chip) | `KpiStatGrid` | `@reui/icon-tile`, `@reui/badge`, Frame ([stats-12](https://reui.io/preview/base/stats-12)) | +| `ptabs` — вкладки страницы со счётчиками | `CountedLineTabs` | shadcn `tabs` variant `line` (c-tabs-2) | +| `seg` — сегменты вида/снимка | `ToggleGroup` | shadcn `toggle-group` | +| `grid` — Frame: заголовок, поиск, колонки, виды, bulk, футер | `ResourcePage` / `FrameDataGrid` | `@reui/data-grid`, `@reui/filters`, `@reui/frame` ([data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2)) | +| `cards` (быстрые действия) | `QuickActionGrid` | Frame tiles | +| `chart` | Recharts через shadcn `chart` | — | +| `kv` | `DetailPanel` | `@reui/frame` | +| `timeline` | `Timeline` | `@reui/timeline` | +| `form` (`frow`) | `SettingRow` | [settings-16](https://reui.io/preview/base/settings-16) | + +## Тема и шрифты + +- Палитра макета: нейтрали с оттенком **hue 264** (`--background: oklch(0.133 0.011 264.376)` и т.д.), `info` — фиолетовый, `chart-1..4` из макета; в светлой теме **сайдбар остаётся тёмным**. +- Где живёт: `packages/ui/src/styles/globals.css` — только вывод `shadcn` CLI, руками не трогаем. Оверрайд — слой приложения **`apps/web/src/styles/theme.css`** (импорт после `@cfdm/ui/globals.css` в `main.tsx`): переопределяет переменные `:root`/`.dark` и `--sidebar*` для светлой темы. Компоненты ReUI не рестайлим — меняются только токены. +- Шрифты: **Geist** / **Geist Mono** через `@fontsource-variable/geist` и `@fontsource-variable/geist-mono`; `--font-sans` / `--font-mono` в `theme.css`. + +## Изменения по экранам (`apps/web/src/routes/_auth/*`) + +| Маршрут | Доска | Изменения | +|---|---|---| +| `dashboard` | VpsDashboard | баннер → KPI (6) → **вкладки** «Проблемы» (быстрые действия, графики Платежи/Расходы, «Здоровье инвентаря») · «Последние VPS» · «Аккаунты» (под риском) | +| `vps` | VpsList | заголовок «VPS / Виртуальные серверы», Alert несвязанных доменов, Frame-грид «Инвентарь VPS» с подзаголовком | +| `vps.$vpsId` | VpsDetail | крошки VPS / IP, действия «К списку VPS» + «Редактировать», **вкладки** Обзор / Финансы / Заметки / Синк | +| `accounts` | VpsAccounts | KPI (4) + грид, действия «Синхронизировать все» + «Добавить» | +| `providers` | VpsProviders | грид «Хостеры» | +| `projects`, `projects.$projectId` | VpsProjects / VpsProjectDetail | KPI (4) + грид | +| `tariffs` | VpsTariffs | Alert расхождений + грид | +| `payments` | VpsPayments | KPI (3) + грид | +| `balance` | VpsBalance | KPI (3) + грид, футер с итогами | +| `reports`, `resources` | VpsReports / VpsResources | сегмент периода, KPI, графики в Frame | +| `renewals` | VpsRenewals | KPI (3) + грид | +| `blocking`, `geo` | VpsBlocking / VpsGeo | KPI (4) + сегменты + вкладки Текущие / История | +| `topology` | VpsTopology | без изменений структуры (холст) | +| `spaces` | VpsSpaces | секции Frame | +| `sync-journal` | VpsSync | грид «Запуски синхронизации» | +| `audit` | VpsAudit | вкладки по действию + переключатель Лента / Таблица | +| `settings/*` | VpsSettings | **вкладки сверху** Общие / Синхронизация / Уведомления / Интеграции вместо боковой навигации `SettingsShell` | + +## Общие компоненты + +- `layout/app-shell.tsx` — сворачиваемые группы, точки проблем, header без `AppsMenu`. +- `app-switcher.tsx` — подзаголовок, приложения из `AppsMenu`; `layout/apps-menu.tsx` удаляется из header. +- `layout/nav-user.tsx` — карточка (muted фон), меню Настройки / Тема / Выйти (без изменений логики). +- `layout/system-monitor-popover.tsx` — триггер-пилюля «Внимание» со счётчиками ошибок/предупреждений, футер «Журнал синка». +- `reui-kit/settings-shell.tsx` — line-tabs сверху. +- `counted-line-tabs.tsx` — счётчик моноширинным текстом как в макете. + +## Порядок + +1. План + контракт (этот документ, [`ui-design-contract.md`](ui-design-contract.md)). +2. Тема и шрифты (`theme.css`, fontsource). +3. Shell: AppSwitcher, группы, NavUser, header, монитор. +4. Kit: `CountedLineTabs`, `SettingsShell`. +5. Экраны: dashboard → VPS detail → settings → audit → blocking/geo → прочие (тексты, колонки). +6. Проверка: dev-сервер, 1440×1000 + свёрнутый сайдбар + светлая тема + телефон; `pnpm --filter web build`, `pnpm --filter web test`; `validate_usage`, `get_audit_checklist`. + +## Установка + +Новые ReUI-элементы не требуются: всё нужное уже установлено (`frame`, `badge`, `alert`, `data-grid`, `filters`, `timeline`, `icon-tile`) и shadcn (`sidebar`, `collapsible`, `tabs`, `toggle-group`). Добавляются только пакеты шрифтов. Если при реализации понадобится новый элемент — `cd apps/web && pnpm dlx shadcn@latest add @reui/`. From 10b20b9d55e4c02f1a5dca5d104b1c233ccca8d6 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 6 Nov 2026 21:53:04 +0700 Subject: [PATCH 2/7] =?UTF-8?q?feat(web):=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B2?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B8=20=D1=82=D0=B5=D0=BC=D1=83=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=B0=D0=BB=D0=B8=D1=82=D1=80=D1=83=20=D0=B8=20?= =?UTF-8?q?=D1=88=D1=80=D0=B8=D1=84=D1=82=D1=8B=20Geist=20=D0=B8=D0=B7=20?= =?UTF-8?q?=D0=B4=D0=B8=D0=B7=D0=B0=D0=B9=D0=BD=D0=B0=20VPS-Tracker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- apps/web/package.json | 2 + apps/web/src/main.tsx | 1 + apps/web/src/styles/theme.css | 112 ++++++++++++++++++++++++++++++++++ pnpm-lock.yaml | 16 +++++ 4 files changed, 131 insertions(+) create mode 100644 apps/web/src/styles/theme.css diff --git a/apps/web/package.json b/apps/web/package.json index 3576dc8..6d61bff 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -18,6 +18,8 @@ "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@fontsource-variable/geist": "^5.3.0", + "@fontsource-variable/geist-mono": "^5.3.0", "@hookform/resolvers": "^3.10.0", "@tanstack/react-query": "^5.90.2", "@tanstack/react-query-devtools": "^5.90.2", diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 1f93d91..461d7e4 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -5,6 +5,7 @@ import { RouterProvider } from '@tanstack/react-router' import { Toaster } from '@cfdm/ui/components/sonner' import '@cfdm/ui/globals.css' +import '@/styles/theme.css' import { queryClient } from '@/lib/queryClient' import { createRouter } from '@/lib/router' diff --git a/apps/web/src/styles/theme.css b/apps/web/src/styles/theme.css new file mode 100644 index 0000000..0e50890 --- /dev/null +++ b/apps/web/src/styles/theme.css @@ -0,0 +1,112 @@ +/* + * VPS-Tracker theme layer (design canvas «VPS-Tracker», see docs/ui-migration-plan.md). + * Overrides tokens only — @cfdm/ui/globals.css stays shadcn CLI output, ReUI components are not restyled. + */ +@import '@fontsource-variable/geist'; +@import '@fontsource-variable/geist-mono'; + +:root { + --font-sans: 'Geist Variable', ui-sans-serif, system-ui, sans-serif; + --font-mono: 'Geist Mono Variable', ui-monospace, SFMono-Regular, monospace; + + --background: oklch(0.973 0.004 247.858); + --foreground: oklch(0.141 0.006 264); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.006 264); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.006 264); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.958 0.005 247.858); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.958 0.005 247.858); + --muted-foreground: oklch(0.52 0.01 264); + --accent: oklch(0.942 0.016 264.376); + --accent-foreground: oklch(0.205 0 0); + --border: oklch(0.904 0.006 264); + --input: oklch(0.904 0.006 264); + --ring: oklch(0.488 0.243 264.376); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.444 0.177 26.899); + --success: oklch(0.696 0.17 162.48); + --success-foreground: oklch(0.378 0.077 168.94); + --warning: oklch(0.795 0.184 86.047); + --warning-foreground: oklch(0.421 0.095 57.708); + --info: oklch(0.606 0.25 292.717); + --info-foreground: oklch(0.283 0.141 291.089); + --invert: oklch(0.21 0.006 285.885); + --invert-foreground: oklch(0.985 0 0); + --chart-1: oklch(0.546 0.215 264.376); + --chart-2: oklch(0.527 0.154 150.069); + --chart-3: oklch(0.625 0.162 41.5); + --chart-4: oklch(0.591 0.224 296.4); + + /* Sidebar stays dark in the light theme. */ + --sidebar: oklch(0.152 0.01 264.376); + --sidebar-foreground: oklch(0.92 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.24 0.012 264.376); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 8%); + --sidebar-ring: oklch(0.488 0.243 264.376); +} + +/* Dark-sidebar scope in light theme: inner text/border/muted tokens follow the sidebar. */ +:root:not(.dark) [data-slot='sidebar-inner'] { + --foreground: oklch(0.92 0 0); + --muted-foreground: oklch(0.648 0.008 264.376); + --border: oklch(1 0 0 / 8%); + --muted: oklch(0.24 0.012 264.376); + --background: oklch(0.133 0.011 264.376); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --card: oklch(0.191 0.011 264.376); + --input: oklch(1 0 0 / 15%); + --accent: oklch(0.24 0.012 264.376); + --accent-foreground: oklch(0.985 0 0); + color-scheme: dark; +} + +.dark { + --background: oklch(0.133 0.011 264.376); + --foreground: oklch(0.985 0 0); + --card: oklch(0.191 0.011 264.376); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.191 0.011 264.376); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.252 0.01 264.376); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.252 0.01 264.376); + --muted-foreground: oklch(0.648 0.008 264.376); + --accent: oklch(0.285 0.022 264.376); + --accent-foreground: oklch(0.985 0 0); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.488 0.243 264.376); + --destructive: oklch(0.704 0.191 22.216); + --destructive-foreground: oklch(0.577 0.245 27.325); + --success: oklch(0.696 0.17 162.48); + --success-foreground: oklch(0.596 0.145 163.225); + --warning: oklch(0.795 0.184 86.047); + --warning-foreground: oklch(0.681 0.162 75.834); + --info: oklch(0.606 0.25 292.717); + --info-foreground: oklch(0.541 0.281 293.009); + --invert: oklch(0.37 0.013 285.805); + --invert-foreground: oklch(0.985 0 0); + --chart-1: oklch(0.637 0.222 264.376); + --chart-2: oklch(0.696 0.17 151.328); + --chart-3: oklch(0.713 0.176 47.6); + --chart-4: oklch(0.714 0.173 303.9); + + --sidebar: oklch(0.191 0.011 264.376); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.262 0.014 264.376); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.488 0.243 264.376); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5f68d0..ec88a9a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,6 +114,12 @@ importers: '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@19.2.7) + '@fontsource-variable/geist': + specifier: ^5.3.0 + version: 5.3.0 + '@fontsource-variable/geist-mono': + specifier: ^5.3.0 + version: 5.3.0 '@hookform/resolvers': specifier: ^3.10.0 version: 3.10.0(react-hook-form@7.80.0(react@19.2.7)) @@ -978,6 +984,12 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@fontsource-variable/geist-mono@5.3.0': + resolution: {integrity: sha512-vBbuwDEo9AkrqADMXOrlAR3DFcJi4/JxeuU43FoiQERnNwsfXNnvxvReZG02cQKmyk4DZkZdBZX3oTDvy2zBAw==} + + '@fontsource-variable/geist@5.3.0': + resolution: {integrity: sha512-j0m+vLQuG5XAYoHtGCVu0spvlGreR3EzpECUVzkFmI1mTVnAO38l/NEPDCFgZ177JxzYJCLSmTQibIiYPilGrA==} + '@hookform/resolvers@3.10.0': resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==} peerDependencies: @@ -4145,6 +4157,10 @@ snapshots: '@floating-ui/utils@0.2.11': {} + '@fontsource-variable/geist-mono@5.3.0': {} + + '@fontsource-variable/geist@5.3.0': {} + '@hookform/resolvers@3.10.0(react-hook-form@7.80.0(react@19.2.7))': dependencies: react-hook-form: 7.80.0(react@19.2.7) From b5300b9082d474ec4f3aefbe26f3645ec37a993e Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 6 Nov 2026 21:57:47 +0700 Subject: [PATCH 3/7] =?UTF-8?q?feat(web):=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B2?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B8=20shell=20=D0=B8=20=D0=B4=D0=B0=D1=88?= =?UTF-8?q?=D0=B1=D0=BE=D1=80=D0=B4=20=D0=BD=D0=B0=20=D0=B4=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D0=B9=D0=BD=20VPS-Tracker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сворачиваемые группы меню с точками проблем, приложения в AppSwitcher вместо AppsMenu в шапке, пилюля монитора со счётчиками, KPI со значением и бейджем в одной строке, вкладки дашборда на уровне страницы. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/app-switcher.tsx | 16 ++ apps/web/src/components/counted-line-tabs.tsx | 4 +- apps/web/src/components/layout/app-shell.tsx | 122 +++++++----- apps/web/src/components/layout/apps-menu.tsx | 105 ---------- apps/web/src/components/layout/nav-user.tsx | 10 +- .../layout/system-monitor-popover.tsx | 68 ++++--- apps/web/src/components/reui-kit/kpi-cols.ts | 7 +- .../src/components/reui-kit/kpi-stat-grid.tsx | 35 ++-- .../src/components/reui-kit/ops-dashboard.tsx | 37 +--- apps/web/src/routes/_auth/dashboard.tsx | 182 ++++++++---------- 10 files changed, 256 insertions(+), 330 deletions(-) delete mode 100644 apps/web/src/components/layout/apps-menu.tsx diff --git a/apps/web/src/components/app-switcher.tsx b/apps/web/src/components/app-switcher.tsx index 8f76bf2..dd09f2a 100644 --- a/apps/web/src/components/app-switcher.tsx +++ b/apps/web/src/components/app-switcher.tsx @@ -2,6 +2,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, } from '@cfdm/ui/components/dropdown-menu' @@ -11,8 +12,10 @@ import { SidebarMenuItem, useSidebar, } from '@cfdm/ui/components/sidebar' +import { Link } from '@tanstack/react-router' import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react' +import { authPortalUrl, isAuthEnabled } from '@/lib/auth' import { APP_SWITCHER_ICONS, CURRENT_APP_ID, @@ -88,6 +91,19 @@ export function AppSwitcher() { ) })} + + {isAuthEnabled() ? ( + } + > + Настроить на портале + + ) : ( + }> + Интеграции + + )} diff --git a/apps/web/src/components/counted-line-tabs.tsx b/apps/web/src/components/counted-line-tabs.tsx index f6cfe20..53419e7 100644 --- a/apps/web/src/components/counted-line-tabs.tsx +++ b/apps/web/src/components/counted-line-tabs.tsx @@ -17,7 +17,7 @@ interface CountedLineTabsProps { children?: ReactNode } -/** Line tabs with count pills (c-tabs-2 / data-grid-filtering-2). */ +/** Page line tabs with mono counts (design «VPS-Tracker» ptabs; c-tabs-2). */ export function CountedLineTabs({ tabs, value, @@ -37,7 +37,7 @@ export function CountedLineTabs({ > {tab.label} {tab.count !== undefined ? ( - + {tab.count} ) : null} diff --git a/apps/web/src/components/layout/app-shell.tsx b/apps/web/src/components/layout/app-shell.tsx index d04c25a..b075ce6 100644 --- a/apps/web/src/components/layout/app-shell.tsx +++ b/apps/web/src/components/layout/app-shell.tsx @@ -16,6 +16,7 @@ import { Network, ShieldAlert, Globe, + ChevronDownIcon, } from 'lucide-react' import { @@ -32,7 +33,13 @@ import { SidebarMenuItem, SidebarProvider, SidebarTrigger, + useSidebar, } from '@cfdm/ui/components/sidebar' +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@cfdm/ui/components/collapsible' import { Breadcrumb, BreadcrumbItem, @@ -41,8 +48,6 @@ import { BreadcrumbPage, BreadcrumbSeparator, } from '@cfdm/ui/components/breadcrumb' -import { Separator } from '@cfdm/ui/components/separator' -import { Badge } from '@cfdm/ui/components/badge' import { TooltipProvider } from '@cfdm/ui/components/tooltip' import { Link, useRouterState } from '@tanstack/react-router' @@ -50,7 +55,6 @@ import { useQuery } from '@tanstack/react-query' import { useState, type CSSProperties, type ReactNode } from 'react' import { SystemMonitorPopover } from '@/components/layout/system-monitor-popover' -import { AppsMenu } from '@/components/layout/apps-menu' import { SpaceSwitcher } from '@/components/layout/space-switcher' import { NavUser } from '@/components/layout/nav-user' import { AppSwitcher } from '@/components/app-switcher' @@ -62,12 +66,14 @@ import { } from '@/lib/auth' import { SpaceProvider, useSpaceId } from '@/lib/space' import { SKIP_TO_CONTENT_CLASS } from '@/lib/ui-surface' +import { cn } from '@cfdm/ui/lib/utils' interface NavItem { to: string label: string icon: typeof LayoutDashboard - badge?: number + /** Problem dot (design `pdot`): destructive / warning. */ + dot?: { tone: 'destructive' | 'warning'; label: string } } interface NavGroup { @@ -162,9 +168,21 @@ export function AppShell({ children }: { children: ReactNode }) { const perm = permissionForPath(item.to) return !perm || can(perm) }) - .map((item) => { + .map((item): NavItem => { if (item.to === '/dashboard' && stats?.issuesCount) { - return { ...item, badge: stats.issuesCount } + return { + ...item, + dot: { tone: 'destructive', label: `Проблем в инвентаре: ${stats.issuesCount}` }, + } + } + if (item.to === '/accounts' && stats?.lowBalanceAccountCount) { + return { + ...item, + dot: { + tone: 'warning', + label: `Низкий баланс: ${stats.lowBalanceAccountCount}`, + }, + } } return item }), @@ -190,38 +208,7 @@ export function AppShell({ children }: { children: ReactNode }) { {navGroups.map((group) => ( - - {group.label} - - - {group.items.map((item) => { - const Icon = item.icon - const isActive = - pathname === item.to || pathname.startsWith(`${item.to}/`) - return ( - - } - isActive={isActive} - tooltip={item.label} - > - - {item.label} - {item.badge ? ( - - {item.badge} - - ) : null} - - - ) - })} - - - + ))} @@ -231,10 +218,6 @@ export function AppShell({ children }: { children: ReactNode }) {
- {parentLabel && parentTo ? ( @@ -255,7 +238,6 @@ export function AppShell({ children }: { children: ReactNode }) {
-
@@ -270,6 +252,60 @@ export function AppShell({ children }: { children: ReactNode }) { ) } +/** Collapsible nav group (design `grph`): uppercase label + chevron; always open in icon rail. */ +function NavGroupSection({ group, pathname }: { group: NavGroup; pathname: string }) { + const { state, isMobile } = useSidebar() + const [open, setOpen] = useState(true) + const iconRail = state === 'collapsed' && !isMobile + + return ( + + + } + className="group/label hover:text-sidebar-foreground cursor-pointer uppercase tracking-wider transition-colors" + > + {group.label} + + + + + + {group.items.map((item) => { + const Icon = item.icon + const isActive = pathname === item.to || pathname.startsWith(`${item.to}/`) + return ( + + } + isActive={isActive} + tooltip={item.label} + > + + {item.label} + {item.dot ? ( + + ) : null} + + + ) + })} + + + + + + ) +} + /** Remount route content when space changes so snapshot queries pick new X-Space-Id. */ function SpaceScopedMain({ children }: { children: ReactNode }) { const { spaceId } = useSpaceId() diff --git a/apps/web/src/components/layout/apps-menu.tsx b/apps/web/src/components/layout/apps-menu.tsx deleted file mode 100644 index c6c9f23..0000000 --- a/apps/web/src/components/layout/apps-menu.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { Link } from '@tanstack/react-router' -import { LayoutGridIcon } from 'lucide-react' -import { Button } from '@cfdm/ui/components/button' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@cfdm/ui/components/dropdown-menu' -import { authPortalUrl, isAuthEnabled } from '@/lib/auth' -import { - APP_SWITCHER_ICONS, - CURRENT_APP_ID, -} from '@/lib/app-switcher-config' -import { useAppSwitcherConfig } from '@/hooks/use-app-switcher' - -/** Header apps grid — app-shell-12 AppsMenu, wired to auth-portal App Switcher. */ -export function AppsMenu() { - const { config, isLoading } = useAppSwitcherConfig() - const portalAppsUrl = `${authPortalUrl().replace(/\/$/, '')}/admin/apps` - - return ( - - - } - > - - - - - - {isLoading ? 'Загрузка…' : config.menuLabel} - -
- {config.apps.map((app) => { - const Icon = APP_SWITCHER_ICONS[app.icon] ?? APP_SWITCHER_ICONS.server - const isCurrent = app.id === CURRENT_APP_ID - - if (isCurrent) { - return ( - - - - {app.name} - - ) - } - - return ( - } - className="h-auto flex-col gap-1.5 py-3 text-center [&_svg]:size-5" - > - - - {app.name} - - ) - })} -
- - {isAuthEnabled() ? ( - } - className="justify-center text-sm font-medium" - > - Настроить на портале - - ) : ( - } - className="justify-center text-sm font-medium" - > - Интеграции - - )} -
-
-
- ) -} diff --git a/apps/web/src/components/layout/nav-user.tsx b/apps/web/src/components/layout/nav-user.tsx index 9548e67..7a610a0 100644 --- a/apps/web/src/components/layout/nav-user.tsx +++ b/apps/web/src/components/layout/nav-user.tsx @@ -141,12 +141,12 @@ export function NavUser() { render={ } > - - + + {fallback} @@ -166,8 +166,8 @@ export function NavUser() { > - - + + {fallback} diff --git a/apps/web/src/components/layout/system-monitor-popover.tsx b/apps/web/src/components/layout/system-monitor-popover.tsx index e5d2aa4..239d39d 100644 --- a/apps/web/src/components/layout/system-monitor-popover.tsx +++ b/apps/web/src/components/layout/system-monitor-popover.tsx @@ -3,12 +3,16 @@ import { useQuery } from '@tanstack/react-query' import { Activity, Bell, + ChevronRight, RefreshCw, Server, Wallet, } from 'lucide-react' +import { Link } from '@tanstack/react-router' + import { Badge } from '@/components/reui/badge' +import { Button } from '@cfdm/ui/components/button' import { cn } from '@cfdm/ui/lib/utils' import { Item, ItemMedia } from '@cfdm/ui/components/item' import { Popover, PopoverContent, PopoverTrigger } from '@cfdm/ui/components/popover' @@ -214,42 +218,43 @@ export function SystemMonitorPopover() { ], ) - const spiking = metrics.some((m) => m.alert) || !apiOk + const errorCount = metrics.filter((m) => m.alert && m.tone === 'destructive').length + (apiOk ? 0 : 1) + const warningCount = metrics.filter((m) => m.alert && m.tone === 'warning').length + const spiking = errorCount + warningCount > 0 + const statusLabel = spiking ? 'Внимание' : 'Норма' return ( } > - - - {spiking ? ( - - ) : null} - - Система - - {spiking ? 'Внимание' : 'Норма'} - + 0 ? 'text-destructive' : warningCount > 0 ? 'text-warning' : 'text-muted-foreground', + )} + /> + {statusLabel} + {errorCount > 0 ? ( + + {errorCount} + + ) : null} + {warningCount > 0 ? ( + + {warningCount} + + ) : null} + {!spiking ? ( + + OK + + ) : null} @@ -290,6 +295,13 @@ export function SystemMonitorPopover() { ) : null} + + Журнал синка + + ) diff --git a/apps/web/src/components/reui-kit/kpi-cols.ts b/apps/web/src/components/reui-kit/kpi-cols.ts index cd8054a..16f4555 100644 --- a/apps/web/src/components/reui-kit/kpi-cols.ts +++ b/apps/web/src/components/reui-kit/kpi-cols.ts @@ -3,8 +3,7 @@ export function kpiCols(count: number): string { if (count <= 1) return 'grid-cols-1' if (count === 2) return 'grid-cols-1 @xl:grid-cols-2' if (count === 3) return 'grid-cols-1 @3xl:grid-cols-3' - if (count === 4) return 'grid-cols-1 @3xl:grid-cols-2 @6xl:grid-cols-4' - if (count === 5) return 'grid-cols-2 @3xl:grid-cols-3 xl:grid-cols-5' - if (count === 6) return 'grid-cols-2 sm:grid-cols-3 xl:grid-cols-6' - return 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4' + if (count === 4) return 'grid-cols-1 @3xl:grid-cols-2 @5xl:grid-cols-4' + // Design «VPS-Tracker»: at most 4 KPI columns, wrapping to the next row. + return 'grid-cols-1 @xl:grid-cols-2 @5xl:grid-cols-4' } diff --git a/apps/web/src/components/reui-kit/kpi-stat-grid.tsx b/apps/web/src/components/reui-kit/kpi-stat-grid.tsx index e75db71..56e83fb 100644 --- a/apps/web/src/components/reui-kit/kpi-stat-grid.tsx +++ b/apps/web/src/components/reui-kit/kpi-stat-grid.tsx @@ -63,11 +63,17 @@ function isSelected(item: KpiStatItem): boolean { return Boolean(item.selected ?? item.active) } +const HINT_BADGE_VARIANT = { + default: 'outline', + warning: 'warning-light', + destructive: 'destructive-light', +} as const + function resolveFooter(item: KpiStatItem): ReactNode { if (item.footer) return item.footer if (typeof item.hint === 'string') { return ( - + {item.hint} ) @@ -81,7 +87,7 @@ function KpiStatCardBody({ item }: { item: KpiStatItem }) { const valueVariant = item.variant ?? 'default' return ( -
+
{item.icon ? ( ) : null} -
-
-
{item.label}
- {footer ?
{footer}
: null} -
-
- {item.value} + {/* Design «VPS-Tracker» KPI: label, then value + hint Badge on one baseline. */} +
+
{item.label}
+
+ + {item.value} + + {footer ? {footer} : null}
diff --git a/apps/web/src/components/reui-kit/ops-dashboard.tsx b/apps/web/src/components/reui-kit/ops-dashboard.tsx index 26e2362..ae02659 100644 --- a/apps/web/src/components/reui-kit/ops-dashboard.tsx +++ b/apps/web/src/components/reui-kit/ops-dashboard.tsx @@ -1,11 +1,4 @@ import type { ReactNode } from 'react' -import { - Frame, - FrameDescription, - FrameHeader, - FramePanel, - FrameTitle, -} from '@/components/reui/frame' import { Skeleton } from '@cfdm/ui/components/skeleton' import { PageHeader } from '@/components/page-header' import { KpiStatGrid, type KpiStatCard } from './kpi-stat-grid' @@ -16,13 +9,15 @@ interface OpsDashboardProps { title?: string description?: string kpiCards: OpsKpiCard[] - charts: ReactNode - queue: ReactNode + /** Alert between the page header and KPI (design order: title → banner → KPI → tabs). */ + banner?: ReactNode + /** Page body after KPI — design «VPS-Tracker»: line tabs (Проблемы / Последние VPS / Аккаунты). */ + children: ReactNode isLoading?: boolean } const rootClassName = - 'text-foreground @container flex w-full flex-col gap-2 md:gap-3' + 'text-foreground @container flex w-full flex-col gap-4 md:gap-6' function OpsDashboardSkeleton() { return ( @@ -45,8 +40,8 @@ export function OpsDashboard({ title = 'Панель управления', description = 'Обзор VPS, платежей, тарифов и синхронизации', kpiCards, - charts, - queue, + banner, + children, isLoading = false, }: OpsDashboardProps) { if (isLoading) { @@ -57,25 +52,13 @@ export function OpsDashboard({
+ {banner} +
-
- {charts} -
- -
- - - Требуют внимания - - Проблемы оплаты, истекающие VPS и ошибки синхронизации - - - {queue} - -
+ {children}
) } diff --git a/apps/web/src/routes/_auth/dashboard.tsx b/apps/web/src/routes/_auth/dashboard.tsx index 44bbdec..a18dc97 100644 --- a/apps/web/src/routes/_auth/dashboard.tsx +++ b/apps/web/src/routes/_auth/dashboard.tsx @@ -29,9 +29,9 @@ import { dataGridCellStack } from '@/components/data-grid-cells' import { KpiStatGridSkeleton, TableSkeleton } from '@/components/skeletons' import { Button } from '@cfdm/ui/components/button' import { Badge } from '@/components/reui/badge' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs' +import { TabsContent } from '@cfdm/ui/components/tabs' +import { CountedLineTabs } from '@/components/counted-line-tabs' import { StatusBadge } from '@/components/status-badge' -import { cn } from '@cfdm/ui/lib/utils' import { computeInventoryHealth } from '@/lib/inventory-health' import { buildAtRiskAccounts, type AtRiskAccount } from '@/lib/account-health' @@ -47,9 +47,6 @@ import { DashboardInventoryAlert } from '@/components/domain/dashboard-inventory import type { Vps } from '@/types/entities' -const DASHBOARD_TAB_TRIGGER_CLASS = - 'flex-none rounded-none border-0 border-b-2 border-transparent px-3 pb-2.5 pt-2 shadow-none after:hidden data-active:border-foreground data-active:bg-transparent data-active:shadow-none dark:data-active:border-foreground dark:data-active:bg-transparent' - const DASHBOARD_QUICK_ACTIONS: QuickActionItem[] = [ { id: 'vps', @@ -371,111 +368,92 @@ function DashboardPage() { return (
- - + } title="Дашборд" description="Сводка по VPS, балансам и здоровью инвентаря" kpiCards={statsLoading ? [] : kpiCards} isLoading={statsLoading} - charts={ - - - - - } - queue={ -
- - - - Проблемы - {issues.length > 0 ? ( - - {issues.length} - - ) : null} - - - Последние VPS - - - Аккаунты - {atRisk.length > 0 ? ( - - {atRisk.length} - - ) : null} - - - - i.key} - emptyTitle="Проблем не найдено" - emptyDescription="Критичных проблем в инвентаре не обнаружено" - pagination={false} + > +
+ + + {snap.settings?.[0]?.showQuickActions !== false ? ( + - - - }> - Все VPS - - } - columns={columnDefFromDataGrid(vpsColumns)} - data={activeVps.slice(0, 8)} - getRowId={(v) => v.id} - pagination={false} - onRowClick={(v) => navigate({ to: '/vps', search: { edit: v.id } })} + ) : null} + + - - - r.id} - emptyTitle="Рисков нет" - emptyDescription="Балансы и синхронизация в норме" - pagination={false} + - - -
- } - /> - - {snap.settings?.[0]?.showQuickActions !== false ? ( - - ) : null} + + i.key} + emptyTitle="Проблем не найдено" + emptyDescription="Критичных проблем в инвентаре не обнаружено" + pagination={false} + /> +
+ + }> + Все VPS + + } + columns={columnDefFromDataGrid(vpsColumns)} + data={activeVps.slice(0, 8)} + getRowId={(v) => v.id} + pagination={false} + onRowClick={(v) => navigate({ to: '/vps', search: { edit: v.id } })} + /> + + + r.id} + emptyTitle="Рисков нет" + emptyDescription="Балансы и синхронизация в норме" + pagination={false} + /> + + +
+
- ) : undefined + {vps ? ( + + ) : null} + } /> - - {(row) => ( - - - Обзор - Финансы - Заметки - Синк + + + {[ + ['overview', 'Обзор'], + ['finance', 'Финансы'], + ['notes', 'Заметки'], + ['sync', 'Синк'], + ].map(([value, label]) => ( + + {label} + + ))} From 9483e1122db5d0b1884d5683e26b35bd76c7be19 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 6 Nov 2026 22:03:14 +0700 Subject: [PATCH 5/7] =?UTF-8?q?fix(web):=20=D1=83=D1=87=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D1=8C=20reduced=20motion=20=D1=83=20=D1=88=D0=B5=D0=B2=D1=80?= =?UTF-8?q?=D0=BE=D0=BD=D0=B0=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF=20=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/layout/app-shell.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/layout/app-shell.tsx b/apps/web/src/components/layout/app-shell.tsx index b075ce6..bef1cfa 100644 --- a/apps/web/src/components/layout/app-shell.tsx +++ b/apps/web/src/components/layout/app-shell.tsx @@ -266,7 +266,7 @@ function NavGroupSection({ group, pathname }: { group: NavGroup; pathname: strin className="group/label hover:text-sidebar-foreground cursor-pointer uppercase tracking-wider transition-colors" > {group.label} - + From 166abb03b1a30bec0d23617e11ab409c2685e22e Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 6 Nov 2026 22:06:39 +0700 Subject: [PATCH 6/7] =?UTF-8?q?fix(web):=20=D0=BA=D1=80=D0=BE=D1=88=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=B5=D1=82=D0=B0=D0=BB=D0=B5=D0=B9,=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=82=D1=80=D0=B0=D1=81=D1=82=20warning=20=D0=B8?= =?UTF-8?q?=20=D1=82=D1=91=D0=BC=D0=BD=D1=8B=D0=B9=20=D1=81=D0=B0=D0=B9?= =?UTF-8?q?=D0=B4=D0=B1=D0=B0=D1=80=20=D0=BD=D0=B0=20=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D0=B5=D1=84=D0=BE=D0=BD=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Детальные VPS и проекта показывают «раздел / запись» в крошках, warning-значения KPI читаемы в светлой теме, мобильный сайдбар в светлой теме остаётся тёмным, заголовки групп не попадают в Tab-порядок свёрнутого меню. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/layout/app-shell.tsx | 24 ++++++++++++++----- .../src/components/reui-kit/kpi-stat-grid.tsx | 3 ++- apps/web/src/lib/page-crumb.ts | 13 ++++++++++ .../src/routes/_auth/projects_.$projectId.tsx | 2 ++ apps/web/src/routes/_auth/vps_.$vpsId.tsx | 2 ++ apps/web/src/styles/theme.css | 2 +- 6 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/lib/page-crumb.ts diff --git a/apps/web/src/components/layout/app-shell.tsx b/apps/web/src/components/layout/app-shell.tsx index bef1cfa..369eed2 100644 --- a/apps/web/src/components/layout/app-shell.tsx +++ b/apps/web/src/components/layout/app-shell.tsx @@ -65,6 +65,7 @@ import { permissionForPath, } from '@/lib/auth' import { SpaceProvider, useSpaceId } from '@/lib/space' +import { PageCrumbContext } from '@/lib/page-crumb' import { SKIP_TO_CONTENT_CLASS } from '@/lib/ui-surface' import { cn } from '@cfdm/ui/lib/utils' @@ -156,7 +157,10 @@ export function AppShell({ children }: { children: ReactNode }) { const activeItem = ALL_NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`)) ?? ALL_NAV_ITEMS[0] - const parentTo = PARENT_ROUTE[activeItem.to] + const [pageCrumb, setPageCrumb] = useState(null) + // Detail routes (design: «VPS / 203.0.113.21»): section links back, page shows the record title. + const isDetail = pageCrumb != null && pathname !== activeItem.to + const parentTo = isDetail ? activeItem.to : PARENT_ROUTE[activeItem.to] const parentLabel = parentTo ? ROUTE_LABELS[parentTo] : null const { data: stats } = useQuery(dashboardStatsQueryOptions()) @@ -232,7 +236,7 @@ export function AppShell({ children }: { children: ReactNode }) { ) : null} - {ROUTE_LABELS[activeItem.to] ?? ''} + {isDetail ? pageCrumb : (ROUTE_LABELS[activeItem.to] ?? '')} @@ -242,7 +246,9 @@ export function AppShell({ children }: { children: ReactNode }) {
- {children} + + {children} +
@@ -262,11 +268,17 @@ function NavGroupSection({ group, pathname }: { group: NavGroup; pathname: strin } - className="group/label hover:text-sidebar-foreground cursor-pointer uppercase tracking-wider transition-colors" + render={} + className="hover:text-sidebar-foreground cursor-pointer uppercase tracking-wider transition-colors" > {group.label} - + diff --git a/apps/web/src/components/reui-kit/kpi-stat-grid.tsx b/apps/web/src/components/reui-kit/kpi-stat-grid.tsx index 56e83fb..5b62100 100644 --- a/apps/web/src/components/reui-kit/kpi-stat-grid.tsx +++ b/apps/web/src/components/reui-kit/kpi-stat-grid.tsx @@ -44,7 +44,8 @@ const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current' const VALUE_VARIANT_CLASS: Record = { default: 'text-foreground', - warning: 'text-warning', + // text-warning on white fails contrast; readable *-foreground in light theme. + warning: 'text-warning-foreground dark:text-warning', destructive: 'text-destructive', } diff --git a/apps/web/src/lib/page-crumb.ts b/apps/web/src/lib/page-crumb.ts new file mode 100644 index 0000000..a732ced --- /dev/null +++ b/apps/web/src/lib/page-crumb.ts @@ -0,0 +1,13 @@ +import { createContext, useContext, useEffect } from 'react' + +/** Setter for the current page breadcrumb (detail routes: «VPS / 203.0.113.21»). */ +export const PageCrumbContext = createContext<(title: string | null) => void>(() => {}) + +/** Detail pages call this; AppShell shows `section / title` while the page is mounted. */ +export function usePageCrumb(title: string | null | undefined) { + const setTitle = useContext(PageCrumbContext) + useEffect(() => { + setTitle(title || null) + return () => setTitle(null) + }, [setTitle, title]) +} diff --git a/apps/web/src/routes/_auth/projects_.$projectId.tsx b/apps/web/src/routes/_auth/projects_.$projectId.tsx index 3a28854..4e783bf 100644 --- a/apps/web/src/routes/_auth/projects_.$projectId.tsx +++ b/apps/web/src/routes/_auth/projects_.$projectId.tsx @@ -1,4 +1,5 @@ import { createFileRoute, Link, useNavigate } from '@tanstack/react-router' +import { usePageCrumb } from '@/lib/page-crumb' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { @@ -73,6 +74,7 @@ function ProjectDetailPage() { const [editOpen, setEditOpen] = useState(false) const project = snapshot ? findProject(snapshot, projectId) : undefined + usePageCrumb(project?.name) const projectVps = useMemo( () => (snapshot ? projectVpsList(snapshot, projectId) : []), [snapshot, projectId], diff --git a/apps/web/src/routes/_auth/vps_.$vpsId.tsx b/apps/web/src/routes/_auth/vps_.$vpsId.tsx index 50de0e4..701ee8f 100644 --- a/apps/web/src/routes/_auth/vps_.$vpsId.tsx +++ b/apps/web/src/routes/_auth/vps_.$vpsId.tsx @@ -1,4 +1,5 @@ import { createFileRoute, Link } from '@tanstack/react-router' +import { usePageCrumb } from '@/lib/page-crumb' import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { @@ -95,6 +96,7 @@ function VpsDetailPage() { const ratesData = normalizeRatesPayload(rawRates) ?? rawRates ?? null const vps = snapshot?.vps.find((v) => v.id === vpsId) + usePageCrumb(vps ? vps.ip || vps.dns || 'VPS' : null) const providerById = snapshot ? providerByIdMap(snapshot.providers) : new Map() const account = snapshot?.providerAccounts.find((a) => a.id === vps?.providerAccountId) const provider = vps ? providerById.get(vps.providerId) : undefined diff --git a/apps/web/src/styles/theme.css b/apps/web/src/styles/theme.css index 0e50890..c140b63 100644 --- a/apps/web/src/styles/theme.css +++ b/apps/web/src/styles/theme.css @@ -53,7 +53,7 @@ } /* Dark-sidebar scope in light theme: inner text/border/muted tokens follow the sidebar. */ -:root:not(.dark) [data-slot='sidebar-inner'] { +:root:not(.dark) :is([data-slot='sidebar-inner'], [data-slot='sidebar'][data-mobile='true']) { --foreground: oklch(0.92 0 0); --muted-foreground: oklch(0.648 0.008 264.376); --border: oklch(1 0 0 / 8%); From a70955f90d401b5df3a0b84e5f4c1c9bcca177ea Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 6 Nov 2026 22:07:49 +0700 Subject: [PATCH 7/7] =?UTF-8?q?docs(ui):=20=D0=BE=D1=82=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=20?= =?UTF-8?q?=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=B2=20=D0=BF?= =?UTF-8?q?=D0=BB=D0=B0=D0=BD=D0=B5=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=B8=20=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE?= =?UTF-8?q?=D0=B2=D0=BE=D0=BA=20=D0=B3=D1=80=D0=B8=D0=B4=D0=B0=20VPS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- apps/web/src/routes/_auth/vps.tsx | 5 +++- docs/ui-migration-plan.md | 43 +++++++++++++++++-------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/apps/web/src/routes/_auth/vps.tsx b/apps/web/src/routes/_auth/vps.tsx index 1ef1f66..b7f9857 100644 --- a/apps/web/src/routes/_auth/vps.tsx +++ b/apps/web/src/routes/_auth/vps.tsx @@ -683,7 +683,10 @@ function VpsPage() { {tableSections.map((section) => ( v.id} diff --git a/docs/ui-migration-plan.md b/docs/ui-migration-plan.md index 1e54246..99fe722 100644 --- a/docs/ui-migration-plan.md +++ b/docs/ui-migration-plan.md @@ -45,25 +45,25 @@ ## Изменения по экранам (`apps/web/src/routes/_auth/*`) -| Маршрут | Доска | Изменения | -|---|---|---| -| `dashboard` | VpsDashboard | баннер → KPI (6) → **вкладки** «Проблемы» (быстрые действия, графики Платежи/Расходы, «Здоровье инвентаря») · «Последние VPS» · «Аккаунты» (под риском) | -| `vps` | VpsList | заголовок «VPS / Виртуальные серверы», Alert несвязанных доменов, Frame-грид «Инвентарь VPS» с подзаголовком | -| `vps.$vpsId` | VpsDetail | крошки VPS / IP, действия «К списку VPS» + «Редактировать», **вкладки** Обзор / Финансы / Заметки / Синк | -| `accounts` | VpsAccounts | KPI (4) + грид, действия «Синхронизировать все» + «Добавить» | -| `providers` | VpsProviders | грид «Хостеры» | -| `projects`, `projects.$projectId` | VpsProjects / VpsProjectDetail | KPI (4) + грид | -| `tariffs` | VpsTariffs | Alert расхождений + грид | -| `payments` | VpsPayments | KPI (3) + грид | -| `balance` | VpsBalance | KPI (3) + грид, футер с итогами | -| `reports`, `resources` | VpsReports / VpsResources | сегмент периода, KPI, графики в Frame | -| `renewals` | VpsRenewals | KPI (3) + грид | -| `blocking`, `geo` | VpsBlocking / VpsGeo | KPI (4) + сегменты + вкладки Текущие / История | -| `topology` | VpsTopology | без изменений структуры (холст) | -| `spaces` | VpsSpaces | секции Frame | -| `sync-journal` | VpsSync | грид «Запуски синхронизации» | -| `audit` | VpsAudit | вкладки по действию + переключатель Лента / Таблица | -| `settings/*` | VpsSettings | **вкладки сверху** Общие / Синхронизация / Уведомления / Интеграции вместо боковой навигации `SettingsShell` | +| Маршрут | Доска | Изменения | Статус | +|---|---|---|---| +| `dashboard` | VpsDashboard | баннер → KPI (6) → **вкладки** «Проблемы» (быстрые действия, графики Платежи/Расходы, «Здоровье инвентаря») · «Последние VPS» · «Аккаунты» (под риском) | сделано | +| `vps` | VpsList | заголовок «VPS / Виртуальные серверы», Alert несвязанных доменов, Frame-грид «Инвентарь VPS» с подзаголовком | сделано (заголовок грида); Alert доменов и фильтры — уже были | +| `vps.$vpsId` | VpsDetail | крошки VPS / IP, действия «К списку VPS» + «Редактировать», **вкладки** Обзор / Финансы / Заметки / Синк | сделано (line-вкладки, действия, крошки «VPS / IP»); маршрут вынесен в `vps_.$vpsId` | +| `accounts` | VpsAccounts | KPI (4) + грид, действия «Синхронизировать все» + «Добавить» | уже соответствует («Синхронизировать все» — при наличии API-аккаунтов) | +| `providers` | VpsProviders | грид «Хостеры» | уже соответствует | +| `projects`, `projects.$projectId` | VpsProjects / VpsProjectDetail | KPI (4) + грид | уже соответствует; крошки «Проекты / имя»; маршрут вынесен в `projects_.$projectId` | +| `tariffs` | VpsTariffs | Alert расхождений + грид | уже соответствует («Загрузить тарифы» — при наличии API) | +| `payments` | VpsPayments | KPI (3) + грид | уже соответствует | +| `balance` | VpsBalance | KPI (3) + грид, футер с итогами | уже соответствует | +| `reports`, `resources` | VpsReports / VpsResources | сегмент периода, KPI, графики в Frame | уже соответствует | +| `renewals` | VpsRenewals | KPI (3) + грид | уже соответствует | +| `blocking`, `geo` | VpsBlocking / VpsGeo | KPI (4) + сегменты + вкладки Текущие / История | уже соответствует | +| `topology` | VpsTopology | без изменений структуры (холст) | без изменений | +| `spaces` | VpsSpaces | секции Frame | уже соответствует | +| `sync-journal` | VpsSync | грид «Запуски синхронизации» | уже соответствует | +| `audit` | VpsAudit | вкладки по действию + переключатель Лента / Таблица | сделано (общие CountedLineTabs) | +| `settings/*` | VpsSettings | **вкладки сверху** Общие / Синхронизация / Уведомления / Интеграции вместо боковой навигации `SettingsShell` | сделано | ## Общие компоненты @@ -86,3 +86,8 @@ ## Установка Новые ReUI-элементы не требуются: всё нужное уже установлено (`frame`, `badge`, `alert`, `data-grid`, `filters`, `timeline`, `icon-tile`) и shadcn (`sidebar`, `collapsible`, `tabs`, `toggle-group`). Добавляются только пакеты шрифтов. Если при реализации понадобится новый элемент — `cd apps/web && pnpm dlx shadcn@latest add @reui/`. + +## Итог проверки + +- Сверка: артефакт в браузере не рендерится без входа в claude.ai, поэтому соответствие проверено по структуре досок `.dc.html` (заголовки, подзаголовки, действия, KPI, вкладки, блоки) и скриншотам dev-сервера 1440×1000, свёрнутого меню, светлой темы и ширины телефона. +- «Уже соответствует» — экран сверен по тексту (`h1`, подзаголовок, действия) с доской; визуальные отличия закрыты общими изменениями темы, shell, KPI и шапки страницы.