From c40eff8a01c4529df8dace6f017f6faa4d701378 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Tue, 21 Jul 2026 00:23:04 +0700 Subject: [PATCH] =?UTF-8?q?feat(web):=20NavUser=20chrome=20=D0=B8=20=D1=83?= =?UTF-8?q?=D0=BD=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D1=8F=20ReUI?= =?UTF-8?q?=20PRO=20kit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Тема в NavUser вместо ModeToggle в header; KpiStatGrid/QuickActionGrid по эталону EvoBGP; обновлены rules и contract. Co-authored-by: Cursor --- .cursor/rules/frontend-shadcn.mdc | 99 ++++--- .cursor/rules/frontend-ui-patterns.mdc | 198 +++++++++++-- .cursor/rules/reui-mcp.mdc | 30 +- .cursor/rules/reui.mdc | 3 +- .cursor/rules/shadcn-mcp.mdc | 52 ++-- .cursor/rules/shadcn-ui-production.mdc | 22 +- AGENTS.md | 4 +- apps/web/src/components/app-sidebar.tsx | 6 +- apps/web/src/components/layout/nav-user.tsx | 212 ++++++++++++++ .../web/src/components/layout/site-header.tsx | 4 +- apps/web/src/components/mode-toggle.tsx | 29 -- apps/web/src/components/reui-kit/index.ts | 7 +- apps/web/src/components/reui-kit/kpi-cols.ts | 10 + .../src/components/reui-kit/kpi-stat-grid.tsx | 272 +++++++++++++----- .../components/reui-kit/quick-action-grid.tsx | 13 +- docs/ui-design-contract.md | 92 ++++-- 16 files changed, 797 insertions(+), 256 deletions(-) create mode 100644 apps/web/src/components/layout/nav-user.tsx delete mode 100644 apps/web/src/components/mode-toggle.tsx create mode 100644 apps/web/src/components/reui-kit/kpi-cols.ts diff --git a/.cursor/rules/frontend-shadcn.mdc b/.cursor/rules/frontend-shadcn.mdc index 686fb56..7890977 100644 --- a/.cursor/rules/frontend-shadcn.mdc +++ b/.cursor/rules/frontend-shadcn.mdc @@ -1,44 +1,71 @@ --- -description: Frontend — shadcn/ui + ReUI PRO workflow для EvoFirewall -globs: - - apps/web/** - - packages/ui/** +description: Frontend — shadcn primitives + ReUI PRO blocks; CLI-first; kit patterns +globs: apps/web/**/*,packages/ui/**/* alwaysApply: false --- # Frontend — shadcn/ui + ReUI PRO -**Источники:** MCP **`user-reui`** + **`plugin-shadcn-shadcn`** + docs. Не выдумывать UI. +**Источник истины:** MCP **`user-reui`** (pages/blocks/KPI/settings) + MCP `plugin-shadcn-shadcn` (primitives) + docs. Не выдумывать UI. + +Monorepo — [`frontend-monorepo.mdc`](frontend-monorepo.mdc). ReUI — [`reui-mcp.mdc`](reui-mcp.mdc). Patterns — [`frontend-ui-patterns.mdc`](frontend-ui-patterns.mdc). | Документ | URL | |----------|-----| -| shadcn Components | https://ui.shadcn.com/docs/components | -| ReUI Blocks | https://reui.io/blocks | -| ReUI llms.txt | https://reui.io/llms.txt | -| License | https://reui.io/docs/license-setup | -| Design contract | [`docs/ui-design-contract.md`](../../docs/ui-design-contract.md) | +| **shadcn Components** | https://ui.shadcn.com/docs/components | +| **ReUI Blocks** | https://reui.io/blocks | +| **ReUI llms.txt** | https://reui.io/llms.txt | +| **ReUI Settings** | https://reui.io/blocks/application/settings | +| **Installation** | https://ui.shadcn.com/docs/installation | +| **Monorepo** | https://ui.shadcn.com/docs/monorepo | +| **Forms (RHF)** | https://ui.shadcn.com/docs/forms/react-hook-form | -## Шаг 0 +## Шаг 0 — перед любым UI-кодом -0. Поиск `reui-kit/*` / shared -1. Pages / KPI / settings / enterprise → `user-reui` (`search` → `get_block` / `compose_page`) + cite `previewUrl` -2. Primitives → `plugin-shadcn-shadcn` + `pnpm dlx shadcn@latest docs ` +0. Найти существующие `reui-kit/*` / shared +1. **Pages / KPI / settings / enterprise** → MCP `user-reui` (`search` → `get_block` / `compose_page`) + cite `previewUrl` +2. **Primitives** → MCP `plugin-shadcn-shadcn` + `pnpm dlx shadcn@latest docs ` 3. CLI add из `apps/web` → adapt -## Shared / kit (цель) +**Новая страница** → сначала ReUI PRO `compose_page` / blocks ([reui.io/blocks](https://reui.io/blocks)), не ui.shadcn.com/blocks как primary. -| Component | Путь | +## Shared / kit (обязательно) + +| Component | Файл | |-----------|------| +| `PageShell` | `page-shell.tsx` | | `ResourcePage` | `reui-kit/resource-page.tsx` | -| `KpiStatGrid` | `reui-kit/kpi-stat-grid.tsx` | -| `OpsDashboard` | `reui-kit/ops-dashboard.tsx` | -| `SettingsShell` | `reui-kit/settings-shell.tsx` | +| `OpsDashboard` / `KpiStatGrid` / `QuickActionGrid` | `reui-kit/ops-dashboard.tsx`, `kpi-stat-grid.tsx`, `quick-action-grid.tsx` | +| `KanbanBoard` | `reui-kit/kanban-board.tsx` | | `DetailPanel` | `reui-kit/detail-panel.tsx` | +| `SettingsShell` | `reui-kit/settings-shell.tsx` | +| `EmptyState` | `empty-state.tsx` | +| `QueryState` | `query-state.tsx` | +| `ConfirmDialog` | `confirm-dialog.tsx` | +| `StatusBadge` | `status-badge.tsx` | +| `FormSheet` | `form-sheet.tsx` | +| `FormField` | `form-field.tsx` | +| `LoadingButton` | `loading-button.tsx` | +| `SettingRow` | `setting-row.tsx` | + +**Не использовать как эталон:** устаревшие `PageHeader` / `SectionCards` / `DataGridCard` / `TableCard` (если удалены или мёртвы). + +**Overlay:** Sheet — forms; AlertDialog — destructive confirm. + +## Шаг 1 — CLI + +```bash +cd apps/web +pnpm dlx shadcn@latest add button field input ... +pnpm dlx shadcn@latest add @reui/frame @reui/data-grid @reui/filters +pnpm dlx shadcn@latest add @reui/stats-12 @reui/card-35 @reui/settings-16 @reui/auth-13 +pnpm dlx shadcn@latest apply b2fA --only theme -y +``` ## Приоритет композиции -1. `@evofw/ui/components/*` -2. ReUI PRO block → adapt +1. `@evofw/ui/components/*` (primitives) +2. ReUI PRO block → adapt (`blocks/` reference + kit) 3. `reui-kit/*` + shared 4. Domain-обёртка @@ -46,22 +73,30 @@ alwaysApply: false | ❌ | ✅ | |----|-----| -| raw `` | DataGrid / `ResourcePage` | +| `
`, raw select | DataGrid / `Select` | | `bg-emerald-*` | semantic / ReUI `variant` | | Card как ops-shell | ReUI `Frame` | | Hand-roll KPI | `KpiStatGrid` / [stats-12](https://reui.io/preview/base/stats-12) | | `space-y-*` | `flex` + `gap-*` | | `@/components/ui/*` | `@evofw/ui/components/*` | -## Эталоны +## Эталоны проекта -| Зона | Preview | -|------|---------| -| Shell | https://reui.io/preview/base/app-shell-12 | -| Login | https://reui.io/preview/base/auth-13 | -| Dashboard / KPI | https://reui.io/preview/base/stats-12 | -| Lists | https://reui.io/preview/base/data-grid-filtering-2 | -| Settings | https://reui.io/preview/base/settings-16 | -| Empty | https://reui.io/preview/base/empty-state-12 | +| Зона | Файл | Preview | +|------|------|---------| +| Shell | `layout/app-shell.tsx` | [app-shell-12](https://reui.io/preview/base/app-shell-12) | +| Login | `routes/login.tsx` | [auth-13](https://reui.io/preview/base/auth-13) | +| Dashboard | `routes/_auth/index.tsx` | [stats-12](https://reui.io/preview/base/stats-12) / dashboard-1 | +| Lists | `ResourcePage` | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) | +| Settings | `settings/integrations.tsx` | [settings-16](https://reui.io/preview/base/settings-16) | -См. [`reui-mcp.mdc`](reui-mcp.mdc), [`frontend-ui-patterns.mdc`](frontend-ui-patterns.mdc). +## Чеклист + +- [ ] `user-reui` + preview/docs для зоны +- [ ] Frame surface +- [ ] CLI add при новых items +- [ ] `pnpm --filter @evofw/web build` + +## Язык + +Ответы — русский. Commits — [`commit-messages-ru.mdc`](commit-messages-ru.mdc). diff --git a/.cursor/rules/frontend-ui-patterns.mdc b/.cursor/rules/frontend-ui-patterns.mdc index 38330fb..423357e 100644 --- a/.cursor/rules/frontend-ui-patterns.mdc +++ b/.cursor/rules/frontend-ui-patterns.mdc @@ -1,52 +1,194 @@ --- -description: Frontend UI patterns — Frame surface, kit, spacing, states -globs: - - apps/web/** - - packages/ui/** +description: Единые UI-паттерны web — shared components, docs workflow, матрица стандартизации +globs: apps/web/**/* alwaysApply: false --- # Frontend UI Patterns -См. [`frontend-shadcn.mdc`](frontend-shadcn.mdc), [`reui-mcp.mdc`](reui-mcp.mdc), [`docs/ui-design-contract.md`](../../docs/ui-design-contract.md), [llms.txt](https://reui.io/llms.txt). +См. также: [`frontend-shadcn.mdc`](frontend-shadcn.mdc), [`shadcn-mcp.mdc`](shadcn-mcp.mdc), [`reui-mcp.mdc`](reui-mcp.mdc), [Components](https://ui.shadcn.com/docs/components), [ReUI llms.txt](https://reui.io/llms.txt). + +## Docs workflow (обязательно) + +0. Codegraph / поиск существующих shared/domain / `reui-kit` +1. Skill ReUI + shadcn — component selection +2. **Primary:** MCP `user-reui` — search → `get_block` / `compose_page` (`surface: "frame"`) — **всегда cite `previewUrl` + `docsUrl`** +3. Primitives: MCP `plugin-shadcn-shadcn` + `pnpm dlx shadcn@latest docs ` +4. CLI: `cd apps/web && pnpm dlx shadcn@latest add @reui/...` → сверить API +5. Код по examples + docs (только после MCP ↔ docs) +6. Context7 — **только** TanStack Router/Query, Recharts +7. MCP `validate_usage` / `get_audit_checklist` — перед merge ## Surface -**`surface: frame`**. Ops / list / dashboard / detail / settings — ReUI Frame, не Card shell. +Проект использует **ReUI Frame** (`surface: frame`), не shadcn Card как оболочку list/ops-экранов. Эталон списка: [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2). -## Docs workflow +## Обязательные референсы по зонам -0. Поиск `reui-kit/*` -1. MCP **`user-reui`** — search → `get_block` / `compose_page` (`surface: "frame"`) — cite `previewUrl` + `docsUrl` -2. Primitives: `plugin-shadcn-shadcn` + CLI docs -3. CLI: `cd apps/web && pnpm dlx shadcn@latest add @reui/...` -4. Adapt в kit — не hand-roll -5. Context7 — только TanStack / Recharts -6. `validate_usage` / `get_audit_checklist` +| Зона | Preview | +|------|---------| +| KPI | [stats-12](https://reui.io/preview/base/stats-12) — EvoBGP hybrid SoT | +| Quick Actions | [stats-12](https://reui.io/preview/base/stats-12) · [card-12](https://reui.io/preview/base/card-12) → `QuickActionGrid` | +| List | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) | +| Settings | [settings-16](https://reui.io/preview/base/settings-16), [Application Settings](https://reui.io/blocks/application/settings) | +| Settings rows / Health-check | [settings-2](https://reui.io/preview/base/settings-2), [settings-3](https://reui.io/preview/base/settings-3) | +| Auth | [auth-13](https://reui.io/preview/base/auth-13) | +| Empty | [empty-state-12](https://reui.io/preview/base/empty-state-12) | +| Forms | [form-7](https://reui.io/preview/base/form-7) | +| Shell | [app-shell-12](https://reui.io/preview/base/app-shell-12) | -## Матрица +**SettingRow:** `FieldSeparator` opt-in (`separated`); не между toggle-row и nested fields (Health-check). Settings-секции — отдельные Frame + `gap`, без hairline под PageHeader. -| Элемент | Shared | Preview | -|---------|--------|---------| +## Иерархия компонентов + +``` +@evofw/ui/components/* ← shadcn CLI (packages/ui) +@/components/reui/* ← ReUI CLI @reui/* (apps/web) +apps/web/src/components/ ← shared + domain + layout + page-shell.tsx + reui-kit/ + resource-page.tsx ← list: Frame + line Tabs + Filters + DataGrid + kanban-board.tsx ← kanban + KanbanBoardSkeleton + detail-panel.tsx ← detail: Frame header/metrics + settings-shell.tsx + ops-dashboard.tsx ← KPI stats-12 + charts + catalog-board-toggle.tsx + empty-state.tsx + query-state.tsx + confirm-dialog.tsx + status-badge.tsx + form-sheet.tsx + form-field.tsx + loading-button.tsx + layout/ ← app-shell, site-header, app-sidebar + domain-* ← бизнес-компоненты +``` + +## Матрица стандартизации + +| Элемент | Shared | Primitive | +|---------|--------|-----------| | Page wrapper | `PageShell` | — | -| KPI | `reui-kit/KpiStatGrid` | [stats-12](https://reui.io/preview/base/stats-12) | -| Data list | `reui-kit/ResourcePage` | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) | -| Dashboard | `reui-kit/OpsDashboard` | [dashboard-1](https://reui.io/preview/base/dashboard-1) | -| Settings | `reui-kit/SettingsShell` | [settings-16](https://reui.io/preview/base/settings-16) | -| Empty | `EmptyState` | [empty-state-12](https://reui.io/preview/base/empty-state-12) | -| Status | `StatusBadge` | `@reui/badge` | -| Nav | `AppShell` | [app-shell-12](https://reui.io/preview/base/app-shell-12) | +| List page | `ResourcePage` | ReUI `Frame` + `data-grid` + `filters` + shadcn `Tabs` `variant="line"` | +| Catalog / Board | `CatalogBoardToggle` + `ResourcePage` / `KanbanBoard` | `?view=board` на `/groups`, `/services` | +| Kanban | `KanbanBoard` / `KanbanBoardSkeleton` | ReUI `kanban` + `Frame` | +| Detail | `DetailPanel` | ReUI `Frame` | +| Settings | `SettingsShell` | — | +| Dashboard KPI | `OpsDashboard` / `KpiStatGrid` | ReUI Frame [stats-12](https://reui.io/preview/base/stats-12) hybrid | +| Quick Actions | `QuickActionGrid` | Frame tiles + Badge «Перейти» | +| Empty | `EmptyState` | `Empty` | +| Loading / Error | `QueryState` / kit skeletons | `Skeleton`, `Alert` | +| Status | `StatusBadge` | ReUI `Badge` (`success`/`info`/`warning`) | +| Create/Edit | `FormSheet` + `*-edit-sheet.tsx` | `Sheet`, `Field` | +| Form field | `FormField` | `Field`, `Input`, `Select` | +| Submit button | `LoadingButton` | `Button`, `Spinner` | +| Delete confirm | `ConfirmDialog` | `AlertDialog` | +| Nav | `AppSidebar` (`isActive` обязателен) | `Sidebar` | +| Breadcrumbs | `SiteHeader` | `Breadcrumb` | +| Dates | `lib/format.ts` | — | + +## Header actions + +Независимые CTA в header → `flex shrink-0 flex-wrap items-center justify-end gap-2`. + +**Не** оборачивать в `ButtonGroup` (он склеивает кнопки). `ButtonGroup` — только для связанных контролок (filter chips и т.п.). + +Max 1 primary (`default`) на экран; остальные `outline` / `ghost`. + +Toggle «Доска» / «К каталогу» — всегда `outline` в `primaryAction` / `KanbanBoard.toolbarActions` (не отдельный Frame-shell). + +## Line tabs (project standard) + +Эталон: [c-tabs-2](https://reui.io/preview/base/components/c-tabs-2) + counted [filtering-2](https://reui.io/preview/base/data-grid-filtering-2). + +```tsx + + + Label + {count} + + +``` + +- Примитив: [`packages/ui/.../tabs.tsx`](../../packages/ui/src/components/tabs.tsx) — line = `h-auto`, без `flex-1`, без `dark:data-active:bg-input/30`, underline `after:bottom-0` +- Active = яркий текст + **foreground underline**, без фона +- Count pill всегда `bg-muted` +- Active state — Base UI `data-active`, не Radix `data-[state=active]` +- Не трогать internals `reui/date-selector` + +## Catalog / Board (`/groups`, `/services`) + +| Режим | UI | Search | +|-------|-----|--------| +| Catalog (default) | `ResourcePage` + primary create | omit / `view=catalog` | +| Board | `KanbanBoard` DnD | `?view=board` | + +- Groups tabs: Все / С доменами / Пустые +- Services tabs: Все / Включены / Выключены / Без группы +- DnD только на board; kanban hooks/cards не удалять + +## Dashboard KPI + +Эталон: [stats-12](https://reui.io/preview/base/stats-12) — icon tile + value + label + optional badge/footer; клик через `to` (`Link`) / `onSelect`. Compact strip: [card-35](https://reui.io/preview/base/card-35). + +## Sidebar + +- Каждый `SidebarMenuButton` получает `isActive` от pathname (`useRouterState`) +- `--sidebar-accent` в `AppShell` — заметный mix primary (~14%), не 5% + +## Overlay selection + +| Сценарий | Компонент | +|----------|-----------| +| Create/edit форма | `Sheet` | +| Destructive confirm | `AlertDialog` via `ConfirmDialog` | +| Modal preview | `Dialog` | + +## Block registry + +| Зона | Block / эталон | +|------|----------------| +| Shell | [app-shell-12](https://reui.io/preview/base/app-shell-12) | +| List + tabs + filters | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) | +| Line tabs | [c-tabs-2](https://reui.io/preview/base/components/c-tabs-2) | +| KPI | [stats-12](https://reui.io/preview/base/stats-12) — на dashboard, certificates, domains, groups, services | +| Login | [auth-13](https://reui.io/preview/base/auth-13) | +| Settings | [settings-16](https://reui.io/preview/base/settings-16) / [settings-6](https://reui.io/preview/base/settings-6) | +| Empty | [empty-state-12](https://reui.io/preview/base/empty-state-12) | ## Spacing ``` AppShell main: gap-4 md:gap-6, px-4 md:px-6, py-4 md:py-5 + (--sidebar-width: 240px; header h-12; AppSwitcher + AppsMenu — Shared App Shell chrome) PageShell: gap-4 md:gap-6 -Toolbar: gap-2 +Title/desc: gap-px +Card/Frame grid: gap-4 (dashboard denser: gap-2 md:gap-3) +FieldGroup: gap-4 +Item list: gap-2 +Toolbar / header actions: gap-2 +Tabs list (line): gap-5 ``` -**Запрещено:** `space-y-*`, raw colors, Card как ops-shell, page-level Spinner без Skeleton. +Shared chrome (vps-tracker / CFDM / EvoBGP): см. [`docs/ui-design-contract.md`](../../docs/ui-design-contract.md) — секция **Shared App Shell chrome**. Preview: [app-shell-12](https://reui.io/preview/base/app-shell-12). -## Button hierarchy +**Запрещено:** `space-y-*`, raw colors (`bg-emerald-*`), custom empty divs, page-level Spinner / plain «Загрузка…» без Skeleton. -Max 1 primary (`default`) на экран; остальные `outline` / `ghost`. +## UX/UI (состояния данных) + +Каждый блок: **default, hover, focus, disabled, empty, loading, error**. + +- **Loading** — `Skeleton` / `ResourcePage` skeleton / `KanbanBoardSkeleton` / `OpsDashboard` skeleton, не Spinner на странице +- **Empty** — `EmptyState` с CTA +- **Zero-results** — message внутри DataGrid (+ «Сбросить») +- **Error** — `QueryState` / `Alert` + `onRetry` +- **Overflow** — `truncate`, `max-w-*`, `Tooltip`; `tabular-nums` для чисел +- **Density** — operational (`dense` Frame/DataGrid); max 1 primary CTA +- **A11y** — `aria-invalid`, `aria-label`/`sr-only` на icon-only, `aria-current="page"` на active nav/settings + +## Button hierarchy (max 1 primary per screen) + +1. `default` — главный CTA +2. `outline` — вторичные действия +3. `ghost` / `link` — навигация, cancel +4. `destructive` — только с `ConfirmDialog` diff --git a/.cursor/rules/reui-mcp.mdc b/.cursor/rules/reui-mcp.mdc index 622fc47..bd4355b 100644 --- a/.cursor/rules/reui-mcp.mdc +++ b/.cursor/rules/reui-mcp.mdc @@ -7,9 +7,7 @@ alwaysApply: true Проект: **Base UI** (`style: base-nova`), surface lock **`frame`**. -Связанные: [`shadcn-mcp.mdc`](shadcn-mcp.mdc), [`frontend-monorepo.mdc`](frontend-monorepo.mdc), [`frontend-shadcn.mdc`](frontend-shadcn.mdc), [`frontend-ui-patterns.mdc`](frontend-ui-patterns.mdc), [`docs/ui-design-contract.md`](../../docs/ui-design-contract.md). - -Skill: [`.agents/skills/reui/SKILL.md`](../../.agents/skills/reui/SKILL.md) (версия из `get_agent_skill`; обновление: `curl -fsSL https://mcp.reui.io/install | node -`). +Связанные: [`shadcn-mcp.mdc`](shadcn-mcp.mdc), [`frontend-monorepo.mdc`](frontend-monorepo.mdc), [`frontend-shadcn.mdc`](frontend-shadcn.mdc), [`frontend-ui-patterns.mdc`](frontend-ui-patterns.mdc). | Документ | URL | |----------|-----| @@ -20,7 +18,6 @@ Skill: [`.agents/skills/reui/SKILL.md`](../../.agents/skills/reui/SKILL.md) (в | **Blocks** | https://reui.io/blocks | | **Settings blocks** | https://reui.io/blocks/application/settings | | **License** | https://reui.io/docs/license-setup | -| **Agent skills** | https://reui.io/docs/agent-skills | | **Base UI components** | https://reui.io/docs/components/base/ | ## Primary MCP @@ -39,15 +36,17 @@ Skill: [`.agents/skills/reui/SKILL.md`](../../.agents/skills/reui/SKILL.md) (в | Data Grid | `@reui` | `@/components/reui/data-grid/*` → `ResourcePage` | | Filters | `@reui` | `@/components/reui/filters` | | Frame surface | `@reui` | `@/components/reui/frame` | -| KPI | block [stats-12](https://reui.io/preview/base/stats-12) | `reui-kit/KpiStatGrid` | +| KPI | block [stats-12](https://reui.io/preview/base/stats-12) | `reui-kit/KpiStatGrid` (EvoBGP hybrid SoT) | +| Quick Actions | Frame tiles sibling KPI | `reui-kit/QuickActionGrid` | | Semantic badge / alert | `@reui` | `@/components/reui/badge`, `@/components/reui/alert` | | Number / date / autocomplete / color / kanban | `@reui` | `@/components/reui/*` | -**Сложные списки** — `ResourcePage` (Frame + data-grid + filters), не raw `
`. +**Сложные списки** — `ResourcePage` (Frame + data-grid + filters), не raw `
`, не DataGridCard. +**Quick Actions** — только `QuickActionGrid` (не Card / Button grid). ## MCP workflow -0. Поиск существующих `reui-kit/*`, `@/components/reui/*` +0. Codegraph / поиск существующих `reui-kit/*`, `@/components/reui/*` 1. `user-reui` `search` (`surface: "frame"`, `category` при известном) 2. Страница целиком → `compose_page`; секция → `get_block` 3. `get_component` для API primitives из `componentsUsed` @@ -65,10 +64,18 @@ Skill: [`.agents/skills/reui/SKILL.md`](../../.agents/skills/reui/SKILL.md) (в | PRO blocks (reference) | `apps/web/src/components/blocks/` | adapt into kit, не копипаст в routes | | Kit | `apps/web/src/components/reui-kit/` | `@/components/reui-kit/*` | +## Установленные ReUI (apps/web) + +**Components:** `frame`, `data-grid/*`, `filters`, `kanban`, `badge`, `alert`, `autocomplete`, `number-field`, `date-selector`, `color-picker`, `timeline`, `rating`, `phone-input`, `icon-stack` + +**Kit:** `ResourcePage`, `KpiStatGrid`, `QuickActionGrid`, `OpsDashboard`, `KanbanBoard`, `DetailPanel`, `SettingsShell` + +**Blocks (reference):** `stats-12`, `card-35`, `auth-13`, `app-shell-12`, `settings-16`, `settings-8`, `empty-state-12`, `form-7`, `data-grid-filtering-2`, `dashboard-1`, … + ## License ```env -# apps/web/.env.local (gitignored) +# .env.local (gitignored) REUI_LICENSE_KEY= ``` @@ -78,7 +85,7 @@ REUI_LICENSE_KEY= | Зона | Preview | |------|---------| -| KPI | https://reui.io/preview/base/stats-12 | +| KPI / Quick Actions | https://reui.io/preview/base/stats-12 · https://reui.io/preview/base/card-12 | | List | https://reui.io/preview/base/data-grid-filtering-2 | | Settings | https://reui.io/preview/base/settings-16 | | Auth | https://reui.io/preview/base/auth-13 | @@ -91,8 +98,9 @@ REUI_LICENSE_KEY= - ReUI в `packages/ui` / импорт как `@evofw/ui` - Radix-варианты docs — только Base UI - Raw `bg-emerald-*` вместо ReUI `variant` -- Hand-roll data-grid/filters/KPI/settings rows при наличии `@reui` / kit +- Hand-roll data-grid/filters/KPI/Quick Actions/settings rows при наличии `@reui` / kit - Смешивать Card и Frame на ops-экране +- Ставить shadcn/ui blocks выше ReUI PRO ## Чеклист @@ -100,4 +108,4 @@ REUI_LICENSE_KEY= - [ ] `surface: frame` - [ ] CLI add из `apps/web` при новом item - [ ] Kit / `@/components/reui` / `@evofw/ui` — правильный слой -- [ ] `pnpm --filter web build` (после init Vite) +- [ ] `pnpm --filter @evofw/web build` diff --git a/.cursor/rules/reui.mdc b/.cursor/rules/reui.mdc index 758cc82..2c881c6 100644 --- a/.cursor/rules/reui.mdc +++ b/.cursor/rules/reui.mdc @@ -3,6 +3,7 @@ description: Use the ReUI registry (blocks, primitives, icons) correctly globs: ["**/*.tsx","**/*.ts"] alwaysApply: false --- + --- name: reui description: Use the ReUI registry from your AI agent - find, install, and correctly use ReUI components (the 17 free building blocks like data-grid, kanban, filters), their free examples, premium blocks, and Motion Icons. Applies in any project using ReUI, the @reui registry, REUI_LICENSE_KEY, or any shadcn project where the user asks for premium blocks, data grids, kanban boards, dashboards, or full pages. Pairs with the free ReUI MCP server for live, scored registry search and inline component APIs. @@ -42,7 +43,7 @@ Run ReUI as explicit slash commands (via the ReUI MCP) **or** just ask in plain | Command | Invoke | Does | | ----------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------ | -| **build** | `/mcp__reui__build ` | Compose a page/section/feature from ReUI: plan в†’ install в†’ read API в†’ adapt в†’ craft в†’ audit. | +| **build** | `/mcp__reui__build ` | Compose a page/section/feature from ReUI: plan → install → read API → adapt → craft → audit. | | **add** | `/mcp__reui__add ` | Find & install one component/example/block/icon and wire it in. | | **fix** | `/mcp__reui__fix [target]` | Diagnose & fix ReUI usage: wrong/undocumented props, base/radix mismatch, missing states, a11y/scroll. | | **improve** | `/mcp__reui__improve [target]` | Refine + extend existing ReUI UI to a production-exceptional bar (hierarchy, density, states, responsive, motion). | diff --git a/.cursor/rules/shadcn-mcp.mdc b/.cursor/rules/shadcn-mcp.mdc index 7db6f17..b313b34 100644 --- a/.cursor/rules/shadcn-mcp.mdc +++ b/.cursor/rules/shadcn-mcp.mdc @@ -1,50 +1,44 @@ --- -description: ВСЕГДА использовать MCP-плагин shadcn UI перед любым UI-кодом +description: shadcn MCP — primitives secondary; pages/blocks → user-reui first alwaysApply: true --- -# shadcn MCP — обязательно +# shadcn MCP — primitives (secondary) -Перед **любой** задачей с UI (новый экран, компонент, стили, рефакторинг внешнего вида) — **сначала MCP** `plugin-shadcn-shadcn`, не память и не веб-поиск. +Перед UI-задачей: **сначала** MCP `user-reui` для pages / KPI / lists / settings / Frame ([`reui-mcp.mdc`](reui-mcp.mdc)). +MCP `plugin-shadcn-shadcn` — для **primitives** `@shadcn` и registry sync, не вместо PRO blocks. CLI и docs — **после** MCP, по команде из `get_add_command_for_items`. -Enterprise / PRO (Data Grid, Filters, Frame, blocks) — дополнительно [`reui-mcp.mdc`](reui-mcp.mdc) и MCP **`user-reui`**. - -## Порядок (строго) +## Порядок (primitives) 0. Поиск существующих shared / `reui-kit` -1. **`get_project_registries`** — какие registry доступны (`@shadcn`, `@reui`) -2. **`search_items_in_registries`** — компонент, block, example -3. **`get_item_examples_from_registries`** — полный код примера перед написанием JSX -4. **`get_add_command_for_items`** — точная CLI-команда `pnpm dlx shadcn@latest add ...` -5. Выполнить add из `apps/web` (см. [`frontend-monorepo.mdc`](frontend-monorepo.mdc)) -6. **CLI docs:** `cd apps/web && pnpm dlx shadcn@latest docs ` — сверить API с [ui.shadcn.com/docs/components](https://ui.shadcn.com/docs/components) -7. Сверить examples из MCP с API из docs CLI — реализовать только после совпадения -8. Адаптировать пример под TanStack Router / Query → `apps/web/src/` -9. **Context7** — только TanStack / Recharts / не-shadcn -10. **`get_audit_checklist`** — перед merge PR +1. **`get_project_registries`** +2. **`search_items_in_registries`** — primitive / example +3. **`get_item_examples_from_registries`** +4. **`get_add_command_for_items`** → CLI из `apps/web` +5. `pnpm dlx shadcn@latest docs ` — сверить API +6. Адаптировать под TanStack → `apps/web/src/` +7. Context7 — только TanStack / Recharts / не-shadcn +8. **`get_audit_checklist`** — перед merge -## Когда вызывать MCP +## Когда | Задача | MCP | |--------|-----| -| Новая страница / layout | `user-reui` compose/block **или** shadcn `search` → block | +| Новая страница / KPI / list / settings | **`user-reui`** ([`reui-mcp.mdc`](reui-mcp.mdc)) | | Нет примитива в `@evofw/ui` | `plugin-shadcn-shadcn` → add | -| Data Grid, Filters, Frame, KPI | `user-reui` ([`reui-mcp.mdc`](reui-mcp.mdc)) | -| Сомнение в API/props | examples + `get_component` | -| Ревью UI перед сдачей | `get_audit_checklist` / ReUI `validate_usage` | +| Сомнение в props примитива | examples + docs CLI | ## Запрещено -- Писать UI по памяти, не проверив MCP -- Копипаст с ui.shadcn.com / reui.io без CLI -- Самописные примитивы, если есть item в registry -- Пропускать MCP «потому что компонент простой» +- Писать UI по памяти +- Ставить ui.shadcn.com/blocks выше ReUI PRO +- Самописные примитивы при наличии registry item -## Сервер и инструменты +## Сервер -- **MCP:** `plugin-shadcn-shadcn` + `user-reui` -- Перед вызовом — schema инструмента +- **MCP:** `plugin-shadcn-shadcn` (+ `user-reui` primary) +- Schema: `mcps/plugin-shadcn-shadcn/tools/` -Связанные: [`shadcn-ui-production.mdc`](shadcn-ui-production.mdc), [`reui-mcp.mdc`](reui-mcp.mdc), skill `.agents/skills/shadcn`. +Связанные: [`shadcn-ui-production.mdc`](shadcn-ui-production.mdc), [`reui-mcp.mdc`](reui-mcp.mdc), [`frontend-shadcn.mdc`](frontend-shadcn.mdc). diff --git a/.cursor/rules/shadcn-ui-production.mdc b/.cursor/rules/shadcn-ui-production.mdc index ecc5dd2..dc75298 100644 --- a/.cursor/rules/shadcn-ui-production.mdc +++ b/.cursor/rules/shadcn-ui-production.mdc @@ -1,21 +1,23 @@ --- -description: shadcn/ui + ReUI PRO — глобальные UI-принципы EvoFirewall +description: shadcn/ui + ReUI PRO — глобальные UI-принципы; ReUI PRO выше shadcn alwaysApply: true --- -# shadcn/ui — правила проекта +# shadcn/ui + ReUI PRO — правила проекта -UI строится по [shadcn/ui](https://ui.shadcn.com/docs/installation) + **ReUI** (`@reui`): [Components](https://ui.shadcn.com/docs/components), [Blocks](https://ui.shadcn.com/blocks), [ReUI Get Started](https://reui.io/docs/get-started), [llms.txt](https://reui.io/llms.txt). +UI строится по [shadcn/ui](https://ui.shadcn.com/docs/installation) + **ReUI PRO** (`@reui`): [Components](https://ui.shadcn.com/docs/components), [Blocks](https://ui.shadcn.com/blocks), [ReUI Get Started](https://reui.io/docs/get-started), [llms.txt](https://reui.io/llms.txt). + +**Иерархия:** ReUI PRO (`user-reui`) **выше** базового shadcn. Pages / KPI / lists / settings / shell → ReUI; shadcn — primitives. **Первый шаг UI-задачи:** -1. MCP **`user-reui`** для pages / KPI / data-grid / settings / Frame (`surface: frame`) +1. MCP **`user-reui`** для pages / KPI / data-grid / settings / Frame / Quick Actions (`surface: frame`) 2. MCP **`plugin-shadcn-shadcn`** для primitives `@shadcn` -См. [`shadcn-mcp.mdc`](shadcn-mcp.mdc), [`reui-mcp.mdc`](reui-mcp.mdc). +См. [`shadcn-mcp.mdc`](shadcn-mcp.mdc), [`reui-mcp.mdc`](reui-mcp.mdc). Contract: [`docs/ui-design-contract.md`](../../docs/ui-design-contract.md). ## Frontend -Детали: [`frontend-shadcn.mdc`](frontend-shadcn.mdc), [`frontend-ui-patterns.mdc`](frontend-ui-patterns.mdc), [`frontend-monorepo.mdc`](frontend-monorepo.mdc), [`docs/ui-design-contract.md`](../../docs/ui-design-contract.md). +Детали: [`frontend-shadcn.mdc`](frontend-shadcn.mdc), [`frontend-ui-patterns.mdc`](frontend-ui-patterns.mdc), [`frontend-monorepo.mdc`](frontend-monorepo.mdc). Кратко: MCP → CLI из `apps/web` → Block → kit → `pnpm --filter @evofw/web build`. Кастомный CSS и самописные примитивы **запрещены**. @@ -23,10 +25,14 @@ UI строится по [shadcn/ui](https://ui.shadcn.com/docs/installation) + - Monorepo: `apps/web` + `packages/ui` (`@evofw/ui`), pnpm workspaces - Vite + TanStack Router/Query + shadcn **base-nova** + ReUI **@reui** -- Конфиг: [`apps/web/components.json`](../../apps/web/components.json), [`packages/ui/components.json`](../../packages/ui/components.json) +- Конфиг: [`apps/web/components.json`](apps/web/components.json), [`packages/ui/components.json`](packages/ui/components.json) - Тема: `pnpm dlx shadcn@latest apply b2fA --only theme -y`; ReUI tokens — [Styling](https://reui.io/docs/styling) - License: `REUI_LICENSE_KEY` в `apps/web/.env.local` +## Backend → UI + +При правках API с экранами: [`docs/ui-design-contract.md`](docs/ui-design-contract.md). Backend: `apps/api` (Fastify + Drizzle). + ## Язык -Русский. +Русский. Commits: [`commit-messages-ru.mdc`](commit-messages-ru.mdc). diff --git a/AGENTS.md b/AGENTS.md index 4151e60..11775db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,9 @@ pnpm --filter @evofw/web build ## ReUI PRO -Surface: **frame**. Contract: `docs/ui-design-contract.md`. MCP `user-reui` + CLI из `apps/web`. +Surface: **frame**. Hierarchy: **ReUI PRO > shadcn**. Contract: `docs/ui-design-contract.md`. +MCP `user-reui` primary + `plugin-shadcn-shadcn` primitives. CLI из `apps/web`. +KPI/QuickActions: EvoBGP hybrid SoT (`KpiStatGrid`, `QuickActionGrid`). ## Docs diff --git a/apps/web/src/components/app-sidebar.tsx b/apps/web/src/components/app-sidebar.tsx index 89fa1f5..e2f4590 100644 --- a/apps/web/src/components/app-sidebar.tsx +++ b/apps/web/src/components/app-sidebar.tsx @@ -8,6 +8,7 @@ import { SettingsIcon, } from 'lucide-react' import { AppSwitcher } from '@/components/app-switcher' +import { NavUser } from '@/components/layout/nav-user' import { Sidebar, SidebarContent, @@ -93,7 +94,8 @@ export function AppSidebar() { - - + + + ) } diff --git a/apps/web/src/components/layout/nav-user.tsx b/apps/web/src/components/layout/nav-user.tsx new file mode 100644 index 0000000..120f175 --- /dev/null +++ b/apps/web/src/components/layout/nav-user.tsx @@ -0,0 +1,212 @@ +import { Link } from '@tanstack/react-router' +import { useEffect, useState } from 'react' +import { useTheme } from 'next-themes' +import { + ChevronsUpDownIcon, + LogOutIcon, + MonitorIcon, + MoonIcon, + PaletteIcon, + SettingsIcon, + SunIcon, +} from 'lucide-react' + +import { cn } from '@evofw/ui/lib/utils' +import { Avatar, AvatarFallback } from '@evofw/ui/components/avatar' +import { Button } from '@evofw/ui/components/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@evofw/ui/components/dropdown-menu' +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from '@evofw/ui/components/sidebar' + +import { + authPortalUrl, + getClaims, + isAuthEnabled, + logout, +} from '@/lib/auth' + +/** Sidebar footer NavUser — theme in footer, not header. @see https://reui.io/preview/base/app-shell-1 */ + +const THEMES = [ + { + value: 'light', + label: 'Светлая', + icon: , + }, + { + value: 'dark', + label: 'Тёмная', + icon: , + }, + { + value: 'system', + label: 'Системная', + icon: , + }, +] as const + +function ThemeSegmentedToggle() { + const { theme, setTheme } = useTheme() + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + const currentTheme = mounted ? (theme ?? 'system') : 'system' + + return ( +
e.stopPropagation()} + onPointerDown={(e) => e.stopPropagation()} + > + {THEMES.map(({ value, label, icon }) => { + const isActive = currentTheme === value + return ( + + ) + })} +
+ ) +} + +function initials(source: string): string { + const base = source.trim() + if (!base) return '?' + const parts = base.split(/\s+/).filter(Boolean) + if (parts.length >= 2) { + return `${parts[0]![0] ?? ''}${parts[1]![0] ?? ''}`.toUpperCase() + } + return base.slice(0, 2).toUpperCase() +} + +export function NavUser() { + const { isMobile } = useSidebar() + const authOn = isAuthEnabled() + const claims = getClaims() + + const name = claims?.name?.trim() || (authOn ? 'Пользователь' : 'Гость') + const email = claims?.email?.trim() || (authOn ? '' : 'auth выключен') + const fallback = initials(name || email) + + return ( + + + + + } + > + + {fallback} + +
+ {name} + + {email || '—'} + +
+ +
+ + + + + + {fallback} + + +
+ {name} + + {email || '—'} + +
+
+
+ + + + + } + > + + Настройки + + {authOn ? ( + } + > + Auth Portal + + ) : null} + + + Тема +
+ +
+
+
+ + {authOn ? ( + <> + + + logout()}> + + Выйти + + + + ) : null} +
+
+
+
+ ) +} diff --git a/apps/web/src/components/layout/site-header.tsx b/apps/web/src/components/layout/site-header.tsx index 93ec5bd..549c968 100644 --- a/apps/web/src/components/layout/site-header.tsx +++ b/apps/web/src/components/layout/site-header.tsx @@ -11,7 +11,6 @@ import { import { Separator } from '@evofw/ui/components/separator' import { SystemMonitorPopover } from '@/components/layout/system-monitor-popover' import { AppsMenu } from '@/components/layout/apps-menu' -import { ModeToggle } from '@/components/mode-toggle' import { SidebarTrigger } from '@evofw/ui/components/sidebar' export interface RouteBreadcrumbLoaderData { @@ -71,7 +70,7 @@ function useDynamicBreadcrumbLabels() { }, [matches]) } -/** Header chrome — AppsMenu + SystemMonitor + ModeToggle (EvoBGP etalon). */ +/** Header chrome — AppsMenu + SystemMonitor; theme in NavUser. */ export function SiteHeader() { const pathname = useRouterState({ select: (s) => s.location.pathname }) const dynamicLabels = useDynamicBreadcrumbLabels() @@ -114,7 +113,6 @@ export function SiteHeader() {
-
) diff --git a/apps/web/src/components/mode-toggle.tsx b/apps/web/src/components/mode-toggle.tsx deleted file mode 100644 index 019ad42..0000000 --- a/apps/web/src/components/mode-toggle.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Moon, Sun } from 'lucide-react' -import { useTheme } from 'next-themes' - -import { Button } from '@evofw/ui/components/button' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@evofw/ui/components/dropdown-menu' - -export function ModeToggle() { - const { setTheme } = useTheme() - - return ( - - }> - - - Сменить тему - - - setTheme('light')}>Светлая - setTheme('dark')}>Тёмная - setTheme('system')}>Системная - - - ) -} diff --git a/apps/web/src/components/reui-kit/index.ts b/apps/web/src/components/reui-kit/index.ts index 762569f..48c5431 100644 --- a/apps/web/src/components/reui-kit/index.ts +++ b/apps/web/src/components/reui-kit/index.ts @@ -10,7 +10,12 @@ export { } from './resource-page' export { KpiStatGrid, - type KpiStatCard, + KpiStatCard, + KpiStatCardTile, + kpiStatItemKey, + type KpiStatItem, + type KpiStatCardData, + type KpiStatCard as KpiStatCardType, type KpiStatVariant, type OpsKpiCard, } from './kpi-stat-grid' diff --git a/apps/web/src/components/reui-kit/kpi-cols.ts b/apps/web/src/components/reui-kit/kpi-cols.ts new file mode 100644 index 0000000..cd8054a --- /dev/null +++ b/apps/web/src/components/reui-kit/kpi-cols.ts @@ -0,0 +1,10 @@ +/** Shared grid column classes for hybrid KPI / Quick Actions tiles. */ +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' +} 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 8a14c74..aaa699f 100644 --- a/apps/web/src/components/reui-kit/kpi-stat-grid.tsx +++ b/apps/web/src/components/reui-kit/kpi-stat-grid.tsx @@ -1,40 +1,44 @@ -import type { ReactNode } from 'react' +import type { KeyboardEvent, ReactNode } from 'react' import { Link } from '@tanstack/react-router' + import { Frame, FramePanel } from '@/components/reui/frame' import { Badge } from '@/components/reui/badge' -import { Item, ItemMedia } from '@evofw/ui/components/item' import { cn } from '@evofw/ui/lib/utils' +import { kpiCols } from './kpi-cols' +import { Item, ItemMedia } from '@evofw/ui/components/item' import { Skeleton } from '@evofw/ui/components/skeleton' export type KpiStatVariant = 'default' | 'warning' | 'destructive' -export interface KpiStatCard { - id: string - label: string - value: string | number - hint?: string +/** + * KPI tile data — horizontal compact hybrid (icon left + label/Badge + value). + * @see https://reui.io/preview/base/stats-12 + */ +export type KpiStatItem = { + id?: string + label: ReactNode + value: ReactNode + hint?: ReactNode to?: string search?: Record onSelect?: () => void + onClick?: () => void selected?: boolean + active?: boolean icon?: ReactNode iconClassName?: string - /** Semantic color for the primary value */ variant?: KpiStatVariant footer?: ReactNode } -/** @deprecated Use KpiStatCard */ -export type OpsKpiCard = KpiStatCard +/** CFDM-compatible card shape (id required). */ +export type KpiStatCardData = KpiStatItem & { id: string } -interface KpiStatGridProps { - cards: KpiStatCard[] - isLoading?: boolean - emptyMessage?: ReactNode - emptyIcon?: ReactNode - className?: string - skeletonCount?: number -} +/** @deprecated Use KpiStatCardData */ +export type OpsKpiCard = KpiStatCardData + +/** @deprecated Use KpiStatCardData — type alias for CFDM kit parity */ +export type KpiStatCard = KpiStatCardData const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current' @@ -44,107 +48,146 @@ const VALUE_VARIANT_CLASS: Record = { destructive: 'text-destructive', } -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' +function handleCardKeyDown(onActivate: () => void, event: KeyboardEvent) { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + onActivate() + } } -function resolveFooter(card: KpiStatCard): ReactNode { - if (card.footer) return card.footer - if (card.hint) { +function resolveActivate(item: KpiStatItem): (() => void) | undefined { + return item.onClick ?? item.onSelect +} + +function isSelected(item: KpiStatItem): boolean { + return Boolean(item.selected ?? item.active) +} + +function resolveFooter(item: KpiStatItem): ReactNode { + if (item.footer) return item.footer + if (typeof item.hint === 'string') { return ( - {card.hint} + {item.hint} ) } + if (item.hint) return item.hint return null } -function KpiStatCardBody({ card }: { card: KpiStatCard }) { - const footer = resolveFooter(card) - const valueVariant = card.variant ?? 'default' +function KpiStatCardBody({ item }: { item: KpiStatItem }) { + const footer = resolveFooter(item) + const valueVariant = item.variant ?? 'default' return (
- {card.icon ? ( + {item.icon ? ( - {card.icon} + {item.icon} ) : null}
- {card.label} +
{item.label}
{footer ?
{footer}
: null}
- - {card.value} - + {item.value} +
) } -function KpiStatCardItem({ card }: { card: KpiStatCard }) { - const interactive = Boolean(card.to || card.onSelect) - const panelClass = cn( - 'relative isolate flex h-full flex-col', - card.selected && 'ring-primary/30 bg-muted/30 ring-1', - interactive && - 'hover:bg-muted/40 focus-within:ring-ring cursor-pointer transition-colors focus-within:ring-2', - ) +function panelClassName(item: KpiStatItem, className?: string) { + const onActivate = resolveActivate(item) + const clickable = Boolean(item.to || onActivate) + const selected = isSelected(item) - if (card.to) { - return ( + return cn( + 'relative isolate flex h-full flex-col', + clickable && + 'hover:bg-muted/40 focus-within:ring-ring cursor-pointer transition-colors focus-within:ring-2', + selected && 'ring-primary/30 bg-muted/30 ring-1', + className, + ) +} + +/** Single KPI tile — used for embedded / standalone contexts. */ +export function KpiStatCardTile({ + item, + embedded = false, + className, +}: { + item: KpiStatItem + embedded?: boolean + className?: string +}) { + const onActivate = resolveActivate(item) + const panelClass = panelClassName(item, className) + + let panel: ReactNode + + if (item.to) { + panel = ( - - + + ) - } - - if (card.onSelect) { - return ( + } else if (onActivate) { + panel = ( + handleCardKeyDown(onActivate, e)} + > + + + ) + } else { + panel = ( - + ) } - return ( - - - - ) + if (embedded) { + return {panel} + } + + return {panel} +} + +/** @deprecated Prefer KpiStatCardTile — kept for existing imports */ +export function KpiStatCard({ + item, + embedded = false, + className, +}: { + item: KpiStatItem + embedded?: boolean + className?: string +}) { + return } function KpiStatGridSkeleton({ count }: { count: number }) { @@ -168,23 +211,78 @@ function KpiStatGridSkeleton({ count }: { count: number }) { ) } +interface KpiStatGridProps { + /** EvoBGP primary API */ + items?: KpiStatItem[] + /** CFDM-compatible API */ + cards?: KpiStatCardData[] + isLoading?: boolean + emptyMessage?: ReactNode + emptyIcon?: ReactNode + className?: string + skeletonCount?: number + /** Wrap each tile in its own Frame (analytics panels). */ + embedded?: boolean + 'aria-label'?: string +} + +function KpiStatCardItem({ item }: { item: KpiStatItem }) { + const onActivate = resolveActivate(item) + const panelClass = panelClassName(item) + + if (item.to) { + return ( + + + + + + ) + } + + if (onActivate) { + return ( + handleCardKeyDown(onActivate, e)} + > + + + ) + } + + return ( + + + + ) +} + /** - * Hybrid KPI — EvoBGP visual (colored icon + Badge) + horizontal compact layout. + * Hybrid KPI — EvoBGP visual + horizontal compact layout (icon left). * Preview: https://reui.io/preview/base/stats-12 */ export function KpiStatGrid({ + items, cards, isLoading = false, emptyMessage, emptyIcon, className, skeletonCount = 4, + embedded = false, + 'aria-label': ariaLabel, }: KpiStatGridProps) { if (isLoading) { return } - if (cards.length === 0) { + const list = items ?? cards ?? [] + + if (list.length === 0 && (emptyMessage || emptyIcon)) { return ( @@ -197,13 +295,31 @@ export function KpiStatGrid({ ) } + if (embedded) { + return ( +
+
+ {list.map((item, index) => ( + + ))} +
+
+ ) + } + return ( - -
- {cards.map((card) => ( - + +
+ {list.map((item, index) => ( + ))}
) } + +export function kpiStatItemKey(item: KpiStatItem, index: number): string { + if (item.id) return item.id + if (typeof item.label === 'string') return item.label + return `kpi-${index}` +} diff --git a/apps/web/src/components/reui-kit/quick-action-grid.tsx b/apps/web/src/components/reui-kit/quick-action-grid.tsx index bb77a82..2485a06 100644 --- a/apps/web/src/components/reui-kit/quick-action-grid.tsx +++ b/apps/web/src/components/reui-kit/quick-action-grid.tsx @@ -10,6 +10,7 @@ import { import { Badge } from '@/components/reui/badge' import { Item, ItemMedia } from '@evofw/ui/components/item' import { cn } from '@evofw/ui/lib/utils' +import { kpiCols } from './kpi-cols' export interface QuickActionItem { id: string @@ -30,16 +31,6 @@ interface QuickActionGridProps { const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current' -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' -} - function QuickActionBody({ action }: { action: QuickActionItem }) { return (
@@ -99,7 +90,7 @@ export function QuickActionGrid({ > diff --git a/docs/ui-design-contract.md b/docs/ui-design-contract.md index 9f818f4..753e641 100644 --- a/docs/ui-design-contract.md +++ b/docs/ui-design-contract.md @@ -1,71 +1,119 @@ -# UI Design Contract (EvoFirewall) +# UI Design Contract (ops apps) -Единый контракт ops-приложений семейства (vps-tracker / CFDM / EvoBGP / **EvoFirewall**). Surface: **ReUI Frame**. Kit: `apps/web/src/components/reui-kit/`. +Единый контракт для **CFDM · vps-tracker · EvoBGP · EvoFirewall · auth-portal**. +Surface: **ReUI Frame**. Kit: `apps/web/src/components/reui-kit/`. +Иерархия: **ReUI PRO > shadcn primitives**. -Карта: [llms.txt](https://reui.io/llms.txt) · [Styling](https://reui.io/docs/styling) · [License](https://reui.io/docs/license-setup) · [Blocks](https://reui.io/blocks) · [MCP](https://reui.io/docs/mcp) · [Agent skills](https://reui.io/docs/agent-skills) +Карта: [llms.txt](https://reui.io/llms.txt) · [Styling](https://reui.io/docs/styling) · [License](https://reui.io/docs/license-setup) · [Blocks](https://reui.io/blocks) · [MCP](https://reui.io/docs/mcp) ## Surface Project lock: **`surface: frame`**. Ops / list / dashboard / detail / settings — только **Frame**, не shadcn Card как shell. Не смешивать Card и Frame на одном ops-экране. +```ts +// apps/web/src/lib/ui-surface.ts (где есть) +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). + ## Canonical PRO references | Зона | Block | Preview | |------|-------|---------| -| Shell | `app-shell-12` | https://reui.io/preview/base/app-shell-12 | -| KPI | `stats-12` | https://reui.io/preview/base/stats-12 | +| Shell | `app-shell-12` (+ cmdk/monitor где нужно) | https://reui.io/preview/base/app-shell-12 · https://reui.io/preview/base/app-shell-7 | +| KPI | horizontal compact hybrid (EvoBGP SoT: icon left + label/Badge + value ± variant) | https://reui.io/preview/base/stats-12 | +| Quick Actions | Frame tiles (sibling KPI) + Badge «Перейти» | https://reui.io/preview/base/stats-12 · https://reui.io/preview/base/card-12 | | Dashboard | `dashboard-1` | https://reui.io/preview/base/dashboard-1 | | Lists | `data-grid-filtering-2` | https://reui.io/preview/base/data-grid-filtering-2 | -| Settings | `settings-16` | https://reui.io/preview/base/settings-16 | +| Settings | `settings-16` + SettingRow | https://reui.io/preview/base/settings-16 | | Auth | `auth-13` | https://reui.io/preview/base/auth-13 | | Empty | `empty-state-12` | https://reui.io/preview/base/empty-state-12 | -| Forms | `form-7` → Sheet | https://reui.io/preview/base/form-7 | +| Forms | `form-7` → Sheet/Drawer | https://reui.io/preview/base/form-7 | -## Kit API (`reui-kit/`) — целевой +## Kit API (`reui-kit/`) | Component | Role | |-----------|------| | `ResourcePage` | Frame + line tabs + Filters + DataGrid | -| `KpiStatGrid` | KPI tiles | -| `OpsDashboard` | KPI + charts + attention | +| `KpiStatGrid` | EvoBGP hybrid KPI tiles (`items`/`cards`, `variant`, Badge) | +| `QuickActionGrid` | KPI-like quick action tiles (gated by `showQuickActions`) | +| `OpsDashboard` | KPI + optional `afterKpi` + charts + attention queue | | `SettingsShell` | settings nav + Outlet | | `DetailPanel` | detail Frame sections | +| `filter-utils` | apply/clear ReUI Filters | + +`KpiStatGrid` / `QuickActionGrid` markup — SoT **EvoBGP**; в остальных apps diff только `@scope/ui` imports. + +## Dashboard layout + +| App | Section order | +|-----|---------------| +| EvoBGP / CFDM / EvoFirewall | KPI → **QuickActionGrid** → charts / rest | +| vps-tracker | banner → KPI → charts → attention → **QuickActionGrid** → CSV | +| auth-portal | portal-specific; Quick Actions при наличии dashboard | + +Gating: DB `show_quick_actions` / `showQuickActions` / `ui_show_quick_actions` (default `true`). ## Shared App Shell chrome -Эталон: EvoBGP / CFDM + [app-shell-12](https://reui.io/preview/base/app-shell-12). +Эталон разметки: production apps + ReUI [app-shell-12](https://reui.io/preview/base/app-shell-12). +При переключении между apps меняются **только** sidebar nav labels/hrefs и `main` content. | Токен / зона | Значение | |--------------|----------| -| `--sidebar-width` | `240px` | -| Header | `h-12`, sticky, `border-b` | +| `SIDEBAR_WIDTH` / `--sidebar-width` | `240px` | +| 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` | | `main` | `gap-4 md:gap-6`, `px-4 py-4 md:px-6 md:py-5` | +| Search | hotkey ⌘K / Ctrl+K only (не кнопка в header) | -App Switcher: auth-portal `GET /api/v1/app-switcher`. Планируемый app id: **`fw`**. +Запрещено в chrome: `SidebarRail`, sync-row footer, Search pill в header, issues Badge в header, `ModeToggle` в header (тема только в NavUser), Provider `color-mix` для `--sidebar*`. + +NavUser (footer): avatar + name/email; dropdown — Настройки / Тема (segmented) / Выйти. Preview: [app-shell-1](https://reui.io/preview/base/app-shell-1). + +App Switcher: auth-portal `GET /api/v1/app-switcher`. Ids: `cfdm` · `vps` · `bgp` · `fw`. Admin: portal `/admin/apps`. + +QuickActionGrid / KPI icons: только semantic **text** (`text-info` / `text-primary` / …) на kit `bg-muted` — без solid fills. + +## System monitor + +`SystemMonitorPopover` in header after AppsMenu. Preview: https://reui.io/preview/base/app-shell-12 · https://reui.io/preview/base/app-shell-7 ## MCP workflow -1. MCP `user-reui` — `search` / `get_block` / `get_component` with `surface: "frame"` +1. MCP `user-reui` — `search` / `get_block` / `compose_page` / `get_component` with `surface: "frame"` 2. Cite `previewUrl` + `docsUrl` 3. CLI from `apps/web`: `pnpm dlx shadcn@latest add @reui/...` -4. Adapt into kit — do not hand-roll KPI/grid/settings +4. Adapt into kit — do not hand-roll KPI / Quick Actions / grid / settings rows 5. `validate_usage` / `get_audit_checklist` -Primitives: MCP `plugin-shadcn-shadcn` + `@evofw/ui`. +Primitives: MCP `plugin-shadcn-shadcn` + project `@scope/ui` (`@cfdm/ui` / `@evobgp/ui` / `@evofw/ui` / `@authportal/ui`). ## License ```env -# apps/web/.env.local +# apps/web/.env.local (gitignored) REUI_LICENSE_KEY= ``` -`apps/web/components.json` → `Authorization: Bearer ${REUI_LICENSE_KEY}`. +`apps/web/components.json` → `@reui` с `Authorization: Bearer ${REUI_LICENSE_KEY}`. + +## Spacing + +- AppShell main / PageShell: `gap-4 md:gap-6`, `px-4 py-4 md:px-6 md:py-5` +- No `space-y-*` / `space-x-*` — use `flex` + `gap-*` +- Max 1 primary CTA per screen +- Semantic tokens only — no raw `bg-emerald-*` ## Forbidden - Card as ops list/dashboard shell -- Hand-rolled tables when ReUI DataGrid exists +- Hand-rolled data tables when ReUI DataGrid / `ResourcePage` exists - Hand-rolled KPI when `KpiStatGrid` exists -- Mixing Card and Frame on one ops screen -- Raw `bg-emerald-*` / `space-y-*` +- Hand-rolled Quick Actions when `QuickActionGrid` exists +- SectionCards / DataGridCard as design эталон +- Mixing Card and Frame surfaces on one ops screen