feat(web): NavUser chrome и унификация ReUI PRO kit
Тема в NavUser вместо ModeToggle в header; KpiStatGrid/QuickActionGrid по эталону EvoBGP; обновлены rules и contract. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -1,44 +1,71 @@
|
|||||||
---
|
---
|
||||||
description: Frontend — shadcn/ui + ReUI PRO workflow для EvoFirewall
|
description: Frontend — shadcn primitives + ReUI PRO blocks; CLI-first; kit patterns
|
||||||
globs:
|
globs: apps/web/**/*,packages/ui/**/*
|
||||||
- apps/web/**
|
|
||||||
- packages/ui/**
|
|
||||||
alwaysApply: false
|
alwaysApply: false
|
||||||
---
|
---
|
||||||
|
|
||||||
# Frontend — shadcn/ui + ReUI PRO
|
# 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 |
|
| Документ | URL |
|
||||||
|----------|-----|
|
|----------|-----|
|
||||||
| shadcn Components | https://ui.shadcn.com/docs/components |
|
| **shadcn Components** | https://ui.shadcn.com/docs/components |
|
||||||
| ReUI Blocks | https://reui.io/blocks |
|
| **ReUI Blocks** | https://reui.io/blocks |
|
||||||
| ReUI llms.txt | https://reui.io/llms.txt |
|
| **ReUI llms.txt** | https://reui.io/llms.txt |
|
||||||
| License | https://reui.io/docs/license-setup |
|
| **ReUI Settings** | https://reui.io/blocks/application/settings |
|
||||||
| Design contract | [`docs/ui-design-contract.md`](../../docs/ui-design-contract.md) |
|
| **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
|
0. Найти существующие `reui-kit/*` / shared
|
||||||
1. Pages / KPI / settings / enterprise → `user-reui` (`search` → `get_block` / `compose_page`) + cite `previewUrl`
|
1. **Pages / KPI / settings / enterprise** → MCP `user-reui` (`search` → `get_block` / `compose_page`) + cite `previewUrl`
|
||||||
2. Primitives → `plugin-shadcn-shadcn` + `pnpm dlx shadcn@latest docs <component>`
|
2. **Primitives** → MCP `plugin-shadcn-shadcn` + `pnpm dlx shadcn@latest docs <component>`
|
||||||
3. CLI add из `apps/web` → adapt
|
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` |
|
| `ResourcePage` | `reui-kit/resource-page.tsx` |
|
||||||
| `KpiStatGrid` | `reui-kit/kpi-stat-grid.tsx` |
|
| `OpsDashboard` / `KpiStatGrid` / `QuickActionGrid` | `reui-kit/ops-dashboard.tsx`, `kpi-stat-grid.tsx`, `quick-action-grid.tsx` |
|
||||||
| `OpsDashboard` | `reui-kit/ops-dashboard.tsx` |
|
| `KanbanBoard` | `reui-kit/kanban-board.tsx` |
|
||||||
| `SettingsShell` | `reui-kit/settings-shell.tsx` |
|
|
||||||
| `DetailPanel` | `reui-kit/detail-panel.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/*`
|
1. `@evofw/ui/components/*` (primitives)
|
||||||
2. ReUI PRO block → adapt
|
2. ReUI PRO block → adapt (`blocks/` reference + kit)
|
||||||
3. `reui-kit/*` + shared
|
3. `reui-kit/*` + shared
|
||||||
4. Domain-обёртка
|
4. Domain-обёртка
|
||||||
|
|
||||||
@@ -46,22 +73,30 @@ alwaysApply: false
|
|||||||
|
|
||||||
| ❌ | ✅ |
|
| ❌ | ✅ |
|
||||||
|----|-----|
|
|----|-----|
|
||||||
| raw `<table>` | DataGrid / `ResourcePage` |
|
| `<table>`, raw select | DataGrid / `Select` |
|
||||||
| `bg-emerald-*` | semantic / ReUI `variant` |
|
| `bg-emerald-*` | semantic / ReUI `variant` |
|
||||||
| Card как ops-shell | ReUI `Frame` |
|
| Card как ops-shell | ReUI `Frame` |
|
||||||
| Hand-roll KPI | `KpiStatGrid` / [stats-12](https://reui.io/preview/base/stats-12) |
|
| Hand-roll KPI | `KpiStatGrid` / [stats-12](https://reui.io/preview/base/stats-12) |
|
||||||
| `space-y-*` | `flex` + `gap-*` |
|
| `space-y-*` | `flex` + `gap-*` |
|
||||||
| `@/components/ui/*` | `@evofw/ui/components/*` |
|
| `@/components/ui/*` | `@evofw/ui/components/*` |
|
||||||
|
|
||||||
## Эталоны
|
## Эталоны проекта
|
||||||
|
|
||||||
| Зона | Preview |
|
| Зона | Файл | Preview |
|
||||||
|------|---------|
|
|------|------|---------|
|
||||||
| Shell | https://reui.io/preview/base/app-shell-12 |
|
| Shell | `layout/app-shell.tsx` | [app-shell-12](https://reui.io/preview/base/app-shell-12) |
|
||||||
| Login | https://reui.io/preview/base/auth-13 |
|
| Login | `routes/login.tsx` | [auth-13](https://reui.io/preview/base/auth-13) |
|
||||||
| Dashboard / KPI | https://reui.io/preview/base/stats-12 |
|
| Dashboard | `routes/_auth/index.tsx` | [stats-12](https://reui.io/preview/base/stats-12) / dashboard-1 |
|
||||||
| Lists | https://reui.io/preview/base/data-grid-filtering-2 |
|
| Lists | `ResourcePage` | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) |
|
||||||
| Settings | https://reui.io/preview/base/settings-16 |
|
| Settings | `settings/integrations.tsx` | [settings-16](https://reui.io/preview/base/settings-16) |
|
||||||
| Empty | https://reui.io/preview/base/empty-state-12 |
|
|
||||||
|
|
||||||
См. [`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).
|
||||||
|
|||||||
@@ -1,52 +1,194 @@
|
|||||||
---
|
---
|
||||||
description: Frontend UI patterns — Frame surface, kit, spacing, states
|
description: Единые UI-паттерны web — shared components, docs workflow, матрица стандартизации
|
||||||
globs:
|
globs: apps/web/**/*
|
||||||
- apps/web/**
|
|
||||||
- packages/ui/**
|
|
||||||
alwaysApply: false
|
alwaysApply: false
|
||||||
---
|
---
|
||||||
|
|
||||||
# Frontend UI Patterns
|
# 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 <component>`
|
||||||
|
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
|
||||||
|
|
||||||
**`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/*`
|
| Зона | Preview |
|
||||||
1. MCP **`user-reui`** — search → `get_block` / `compose_page` (`surface: "frame"`) — cite `previewUrl` + `docsUrl`
|
|------|---------|
|
||||||
2. Primitives: `plugin-shadcn-shadcn` + CLI docs
|
| KPI | [stats-12](https://reui.io/preview/base/stats-12) — EvoBGP hybrid SoT |
|
||||||
3. CLI: `cd apps/web && pnpm dlx shadcn@latest add @reui/...`
|
| Quick Actions | [stats-12](https://reui.io/preview/base/stats-12) · [card-12](https://reui.io/preview/base/card-12) → `QuickActionGrid` |
|
||||||
4. Adapt в kit — не hand-roll
|
| List | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) |
|
||||||
5. Context7 — только TanStack / Recharts
|
| Settings | [settings-16](https://reui.io/preview/base/settings-16), [Application Settings](https://reui.io/blocks/application/settings) |
|
||||||
6. `validate_usage` / `get_audit_checklist`
|
| 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` | — |
|
| Page wrapper | `PageShell` | — |
|
||||||
| KPI | `reui-kit/KpiStatGrid` | [stats-12](https://reui.io/preview/base/stats-12) |
|
| List page | `ResourcePage` | ReUI `Frame` + `data-grid` + `filters` + shadcn `Tabs` `variant="line"` |
|
||||||
| Data list | `reui-kit/ResourcePage` | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) |
|
| Catalog / Board | `CatalogBoardToggle` + `ResourcePage` / `KanbanBoard` | `?view=board` на `/groups`, `/services` |
|
||||||
| Dashboard | `reui-kit/OpsDashboard` | [dashboard-1](https://reui.io/preview/base/dashboard-1) |
|
| Kanban | `KanbanBoard` / `KanbanBoardSkeleton` | ReUI `kanban` + `Frame` |
|
||||||
| Settings | `reui-kit/SettingsShell` | [settings-16](https://reui.io/preview/base/settings-16) |
|
| Detail | `DetailPanel` | ReUI `Frame` |
|
||||||
| Empty | `EmptyState` | [empty-state-12](https://reui.io/preview/base/empty-state-12) |
|
| Settings | `SettingsShell` | — |
|
||||||
| Status | `StatusBadge` | `@reui/badge` |
|
| Dashboard KPI | `OpsDashboard` / `KpiStatGrid` | ReUI Frame [stats-12](https://reui.io/preview/base/stats-12) hybrid |
|
||||||
| Nav | `AppShell` | [app-shell-12](https://reui.io/preview/base/app-shell-12) |
|
| 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
|
||||||
|
<TabsList variant="line" className="gap-5">
|
||||||
|
<TabsTrigger value="…" className="text-muted-foreground hover:text-foreground h-auto gap-2 px-0 pb-3">
|
||||||
|
<span>Label</span>
|
||||||
|
<span className="bg-muted text-muted-foreground … tabular-nums rounded-md">{count}</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Примитив: [`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
|
## Spacing
|
||||||
|
|
||||||
```
|
```
|
||||||
AppShell main: gap-4 md:gap-6, px-4 md:px-6, py-4 md:py-5
|
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
|
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`
|
||||||
|
|||||||
+19
-11
@@ -7,9 +7,7 @@ alwaysApply: true
|
|||||||
|
|
||||||
Проект: **Base UI** (`style: base-nova`), surface lock **`frame`**.
|
Проект: **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).
|
Связанные: [`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).
|
||||||
|
|
||||||
Skill: [`.agents/skills/reui/SKILL.md`](../../.agents/skills/reui/SKILL.md) (версия из `get_agent_skill`; обновление: `curl -fsSL https://mcp.reui.io/install | node -`).
|
|
||||||
|
|
||||||
| Документ | URL |
|
| Документ | URL |
|
||||||
|----------|-----|
|
|----------|-----|
|
||||||
@@ -20,7 +18,6 @@ Skill: [`.agents/skills/reui/SKILL.md`](../../.agents/skills/reui/SKILL.md) (в
|
|||||||
| **Blocks** | https://reui.io/blocks |
|
| **Blocks** | https://reui.io/blocks |
|
||||||
| **Settings blocks** | https://reui.io/blocks/application/settings |
|
| **Settings blocks** | https://reui.io/blocks/application/settings |
|
||||||
| **License** | https://reui.io/docs/license-setup |
|
| **License** | https://reui.io/docs/license-setup |
|
||||||
| **Agent skills** | https://reui.io/docs/agent-skills |
|
|
||||||
| **Base UI components** | https://reui.io/docs/components/base/<name> |
|
| **Base UI components** | https://reui.io/docs/components/base/<name> |
|
||||||
|
|
||||||
## Primary MCP
|
## 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` |
|
| Data Grid | `@reui` | `@/components/reui/data-grid/*` → `ResourcePage` |
|
||||||
| Filters | `@reui` | `@/components/reui/filters` |
|
| Filters | `@reui` | `@/components/reui/filters` |
|
||||||
| Frame surface | `@reui` | `@/components/reui/frame` |
|
| 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` |
|
| Semantic badge / alert | `@reui` | `@/components/reui/badge`, `@/components/reui/alert` |
|
||||||
| Number / date / autocomplete / color / kanban | `@reui` | `@/components/reui/*` |
|
| Number / date / autocomplete / color / kanban | `@reui` | `@/components/reui/*` |
|
||||||
|
|
||||||
**Сложные списки** — `ResourcePage` (Frame + data-grid + filters), не raw `<table>`.
|
**Сложные списки** — `ResourcePage` (Frame + data-grid + filters), не raw `<table>`, не DataGridCard.
|
||||||
|
**Quick Actions** — только `QuickActionGrid` (не Card / Button grid).
|
||||||
|
|
||||||
## MCP workflow
|
## MCP workflow
|
||||||
|
|
||||||
0. Поиск существующих `reui-kit/*`, `@/components/reui/*`
|
0. Codegraph / поиск существующих `reui-kit/*`, `@/components/reui/*`
|
||||||
1. `user-reui` `search` (`surface: "frame"`, `category` при известном)
|
1. `user-reui` `search` (`surface: "frame"`, `category` при известном)
|
||||||
2. Страница целиком → `compose_page`; секция → `get_block`
|
2. Страница целиком → `compose_page`; секция → `get_block`
|
||||||
3. `get_component` для API primitives из `componentsUsed`
|
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 |
|
| PRO blocks (reference) | `apps/web/src/components/blocks/` | adapt into kit, не копипаст в routes |
|
||||||
| Kit | `apps/web/src/components/reui-kit/` | `@/components/reui-kit/*` |
|
| 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
|
## License
|
||||||
|
|
||||||
```env
|
```env
|
||||||
# apps/web/.env.local (gitignored)
|
# .env.local (gitignored)
|
||||||
REUI_LICENSE_KEY=
|
REUI_LICENSE_KEY=
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -78,7 +85,7 @@ REUI_LICENSE_KEY=
|
|||||||
|
|
||||||
| Зона | Preview |
|
| Зона | 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 |
|
| List | https://reui.io/preview/base/data-grid-filtering-2 |
|
||||||
| Settings | https://reui.io/preview/base/settings-16 |
|
| Settings | https://reui.io/preview/base/settings-16 |
|
||||||
| Auth | https://reui.io/preview/base/auth-13 |
|
| Auth | https://reui.io/preview/base/auth-13 |
|
||||||
@@ -91,8 +98,9 @@ REUI_LICENSE_KEY=
|
|||||||
- ReUI в `packages/ui` / импорт как `@evofw/ui`
|
- ReUI в `packages/ui` / импорт как `@evofw/ui`
|
||||||
- Radix-варианты docs — только Base UI
|
- Radix-варианты docs — только Base UI
|
||||||
- Raw `bg-emerald-*` вместо ReUI `variant`
|
- 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-экране
|
- Смешивать Card и Frame на ops-экране
|
||||||
|
- Ставить shadcn/ui blocks выше ReUI PRO
|
||||||
|
|
||||||
## Чеклист
|
## Чеклист
|
||||||
|
|
||||||
@@ -100,4 +108,4 @@ REUI_LICENSE_KEY=
|
|||||||
- [ ] `surface: frame`
|
- [ ] `surface: frame`
|
||||||
- [ ] CLI add из `apps/web` при новом item
|
- [ ] CLI add из `apps/web` при новом item
|
||||||
- [ ] Kit / `@/components/reui` / `@evofw/ui` — правильный слой
|
- [ ] Kit / `@/components/reui` / `@evofw/ui` — правильный слой
|
||||||
- [ ] `pnpm --filter web build` (после init Vite)
|
- [ ] `pnpm --filter @evofw/web build`
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ description: Use the ReUI registry (blocks, primitives, icons) correctly
|
|||||||
globs: ["**/*.tsx","**/*.ts"]
|
globs: ["**/*.tsx","**/*.ts"]
|
||||||
alwaysApply: false
|
alwaysApply: false
|
||||||
---
|
---
|
||||||
|
|
||||||
---
|
---
|
||||||
name: reui
|
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.
|
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 |
|
| Command | Invoke | Does |
|
||||||
| ----------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
|
| ----------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
|
||||||
| **build** | `/mcp__reui__build <what>` | Compose a page/section/feature from ReUI: plan в†’ install в†’ read API в†’ adapt в†’ craft в†’ audit. |
|
| **build** | `/mcp__reui__build <what>` | Compose a page/section/feature from ReUI: plan → install → read API → adapt → craft → audit. |
|
||||||
| **add** | `/mcp__reui__add <item>` | Find & install one component/example/block/icon and wire it in. |
|
| **add** | `/mcp__reui__add <item>` | 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. |
|
| **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). |
|
| **improve** | `/mcp__reui__improve [target]` | Refine + extend existing ReUI UI to a production-exceptional bar (hierarchy, density, states, responsive, motion). |
|
||||||
|
|||||||
@@ -1,50 +1,44 @@
|
|||||||
---
|
---
|
||||||
description: ВСЕГДА использовать MCP-плагин shadcn UI перед любым UI-кодом
|
description: shadcn MCP — primitives secondary; pages/blocks → user-reui first
|
||||||
alwaysApply: true
|
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`.
|
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`
|
0. Поиск существующих shared / `reui-kit`
|
||||||
1. **`get_project_registries`** — какие registry доступны (`@shadcn`, `@reui`)
|
1. **`get_project_registries`**
|
||||||
2. **`search_items_in_registries`** — компонент, block, example
|
2. **`search_items_in_registries`** — primitive / example
|
||||||
3. **`get_item_examples_from_registries`** — полный код примера перед написанием JSX
|
3. **`get_item_examples_from_registries`**
|
||||||
4. **`get_add_command_for_items`** — точная CLI-команда `pnpm dlx shadcn@latest add ...`
|
4. **`get_add_command_for_items`** → CLI из `apps/web`
|
||||||
5. Выполнить add из `apps/web` (см. [`frontend-monorepo.mdc`](frontend-monorepo.mdc))
|
5. `pnpm dlx shadcn@latest docs <component>` — сверить API
|
||||||
6. **CLI docs:** `cd apps/web && pnpm dlx shadcn@latest docs <component>` — сверить API с [ui.shadcn.com/docs/components](https://ui.shadcn.com/docs/components)
|
6. Адаптировать под TanStack → `apps/web/src/`
|
||||||
7. Сверить examples из MCP с API из docs CLI — реализовать только после совпадения
|
7. Context7 — только TanStack / Recharts / не-shadcn
|
||||||
8. Адаптировать пример под TanStack Router / Query → `apps/web/src/`
|
8. **`get_audit_checklist`** — перед merge
|
||||||
9. **Context7** — только TanStack / Recharts / не-shadcn
|
|
||||||
10. **`get_audit_checklist`** — перед merge PR
|
|
||||||
|
|
||||||
## Когда вызывать MCP
|
## Когда
|
||||||
|
|
||||||
| Задача | 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 |
|
| Нет примитива в `@evofw/ui` | `plugin-shadcn-shadcn` → add |
|
||||||
| Data Grid, Filters, Frame, KPI | `user-reui` ([`reui-mcp.mdc`](reui-mcp.mdc)) |
|
| Сомнение в props примитива | examples + docs CLI |
|
||||||
| Сомнение в API/props | examples + `get_component` |
|
|
||||||
| Ревью UI перед сдачей | `get_audit_checklist` / ReUI `validate_usage` |
|
|
||||||
|
|
||||||
## Запрещено
|
## Запрещено
|
||||||
|
|
||||||
- Писать UI по памяти, не проверив MCP
|
- Писать UI по памяти
|
||||||
- Копипаст с ui.shadcn.com / reui.io без CLI
|
- Ставить ui.shadcn.com/blocks выше ReUI PRO
|
||||||
- Самописные примитивы, если есть item в registry
|
- Самописные примитивы при наличии registry item
|
||||||
- Пропускать MCP «потому что компонент простой»
|
|
||||||
|
|
||||||
## Сервер и инструменты
|
## Сервер
|
||||||
|
|
||||||
- **MCP:** `plugin-shadcn-shadcn` + `user-reui`
|
- **MCP:** `plugin-shadcn-shadcn` (+ `user-reui` primary)
|
||||||
- Перед вызовом — schema инструмента
|
- 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).
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
---
|
---
|
||||||
description: shadcn/ui + ReUI PRO — глобальные UI-принципы EvoFirewall
|
description: shadcn/ui + ReUI PRO — глобальные UI-принципы; ReUI PRO выше shadcn
|
||||||
alwaysApply: true
|
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-задачи:**
|
**Первый шаг 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`
|
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
|
||||||
|
|
||||||
Детали: [`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 и самописные примитивы **запрещены**.
|
Кратко: 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
|
- Monorepo: `apps/web` + `packages/ui` (`@evofw/ui`), pnpm workspaces
|
||||||
- Vite + TanStack Router/Query + shadcn **base-nova** + ReUI **@reui**
|
- 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)
|
- Тема: `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`
|
- 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).
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ pnpm --filter @evofw/web build
|
|||||||
|
|
||||||
## ReUI PRO
|
## 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
|
## Docs
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { AppSwitcher } from '@/components/app-switcher'
|
import { AppSwitcher } from '@/components/app-switcher'
|
||||||
|
import { NavUser } from '@/components/layout/nav-user'
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -93,7 +94,8 @@ export function AppSidebar() {
|
|||||||
<NavSection label="Операции" items={opsNav} pathname={pathname} />
|
<NavSection label="Операции" items={opsNav} pathname={pathname} />
|
||||||
<NavSection label="Система" items={systemNav} pathname={pathname} />
|
<NavSection label="Система" items={systemNav} pathname={pathname} />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter />
|
<SidebarFooter>
|
||||||
</Sidebar>
|
<NavUser />
|
||||||
|
</SidebarFooter> </Sidebar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: <SunIcon className="size-3.5" aria-hidden />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'dark',
|
||||||
|
label: 'Тёмная',
|
||||||
|
icon: <MoonIcon className="size-3.5" aria-hidden />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'system',
|
||||||
|
label: 'Системная',
|
||||||
|
icon: <MonitorIcon className="size-3.5" aria-hidden />,
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
function ThemeSegmentedToggle() {
|
||||||
|
const { theme, setTheme } = useTheme()
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const currentTheme = mounted ? (theme ?? 'system') : 'system'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label="Тема"
|
||||||
|
className="bg-muted/60 inline-flex items-center gap-0.5 rounded-full p-0.5"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{THEMES.map(({ value, label, icon }) => {
|
||||||
|
const isActive = currentTheme === value
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={value}
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={isActive}
|
||||||
|
aria-label={label}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-xs"
|
||||||
|
onClick={() => setTheme(value)}
|
||||||
|
className={cn(
|
||||||
|
'rounded-full',
|
||||||
|
isActive
|
||||||
|
? 'bg-background text-foreground shadow-sm'
|
||||||
|
: 'text-muted-foreground hover:text-foreground',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
render={
|
||||||
|
<SidebarMenuButton
|
||||||
|
size="lg"
|
||||||
|
className="data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Avatar className="size-8 rounded-lg">
|
||||||
|
<AvatarFallback className="rounded-lg text-xs">{fallback}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span className="truncate font-semibold">{name}</span>
|
||||||
|
<span className="truncate text-xs text-muted-foreground">
|
||||||
|
{email || '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDownIcon className="ml-auto size-4" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="w-(--anchor-width) min-w-56 rounded-lg"
|
||||||
|
side={isMobile ? 'bottom' : 'right'}
|
||||||
|
align="end"
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuLabel className="flex items-center gap-2 py-2 font-normal text-foreground">
|
||||||
|
<Avatar className="size-8 rounded-lg">
|
||||||
|
<AvatarFallback className="rounded-lg text-xs">
|
||||||
|
{fallback}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="grid min-w-0 flex-1 text-left text-sm leading-tight">
|
||||||
|
<span className="truncate font-semibold">{name}</span>
|
||||||
|
<span className="truncate text-xs text-muted-foreground">
|
||||||
|
{email || '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem
|
||||||
|
nativeButton={false}
|
||||||
|
render={<Link to="/settings" />}
|
||||||
|
>
|
||||||
|
<SettingsIcon aria-hidden />
|
||||||
|
Настройки
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{authOn ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
nativeButton={false}
|
||||||
|
render={<a href={authPortalUrl()} />}
|
||||||
|
>
|
||||||
|
Auth Portal
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
<DropdownMenuItem className="cursor-default focus:bg-transparent">
|
||||||
|
<PaletteIcon aria-hidden />
|
||||||
|
Тема
|
||||||
|
<div className="ml-auto">
|
||||||
|
<ThemeSegmentedToggle />
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
|
||||||
|
{authOn ? (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem onClick={() => logout()}>
|
||||||
|
<LogOutIcon aria-hidden />
|
||||||
|
Выйти
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
import { Separator } from '@evofw/ui/components/separator'
|
import { Separator } from '@evofw/ui/components/separator'
|
||||||
import { SystemMonitorPopover } from '@/components/layout/system-monitor-popover'
|
import { SystemMonitorPopover } from '@/components/layout/system-monitor-popover'
|
||||||
import { AppsMenu } from '@/components/layout/apps-menu'
|
import { AppsMenu } from '@/components/layout/apps-menu'
|
||||||
import { ModeToggle } from '@/components/mode-toggle'
|
|
||||||
import { SidebarTrigger } from '@evofw/ui/components/sidebar'
|
import { SidebarTrigger } from '@evofw/ui/components/sidebar'
|
||||||
|
|
||||||
export interface RouteBreadcrumbLoaderData {
|
export interface RouteBreadcrumbLoaderData {
|
||||||
@@ -71,7 +70,7 @@ function useDynamicBreadcrumbLabels() {
|
|||||||
}, [matches])
|
}, [matches])
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Header chrome — AppsMenu + SystemMonitor + ModeToggle (EvoBGP etalon). */
|
/** Header chrome — AppsMenu + SystemMonitor; theme in NavUser. */
|
||||||
export function SiteHeader() {
|
export function SiteHeader() {
|
||||||
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
||||||
const dynamicLabels = useDynamicBreadcrumbLabels()
|
const dynamicLabels = useDynamicBreadcrumbLabels()
|
||||||
@@ -114,7 +113,6 @@ export function SiteHeader() {
|
|||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
<AppsMenu />
|
<AppsMenu />
|
||||||
<SystemMonitorPopover />
|
<SystemMonitorPopover />
|
||||||
<ModeToggle />
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger render={<Button variant="ghost" size="icon" />}>
|
|
||||||
<Sun className="size-5 scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
|
||||||
<Moon className="absolute size-5 scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
|
||||||
<span className="sr-only">Сменить тему</span>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem onClick={() => setTheme('light')}>Светлая</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme('dark')}>Тёмная</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme('system')}>Системная</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,12 @@ export {
|
|||||||
} from './resource-page'
|
} from './resource-page'
|
||||||
export {
|
export {
|
||||||
KpiStatGrid,
|
KpiStatGrid,
|
||||||
type KpiStatCard,
|
KpiStatCard,
|
||||||
|
KpiStatCardTile,
|
||||||
|
kpiStatItemKey,
|
||||||
|
type KpiStatItem,
|
||||||
|
type KpiStatCardData,
|
||||||
|
type KpiStatCard as KpiStatCardType,
|
||||||
type KpiStatVariant,
|
type KpiStatVariant,
|
||||||
type OpsKpiCard,
|
type OpsKpiCard,
|
||||||
} from './kpi-stat-grid'
|
} from './kpi-stat-grid'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
}
|
||||||
@@ -1,40 +1,44 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { KeyboardEvent, ReactNode } from 'react'
|
||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from '@tanstack/react-router'
|
||||||
|
|
||||||
import { Frame, FramePanel } from '@/components/reui/frame'
|
import { Frame, FramePanel } from '@/components/reui/frame'
|
||||||
import { Badge } from '@/components/reui/badge'
|
import { Badge } from '@/components/reui/badge'
|
||||||
import { Item, ItemMedia } from '@evofw/ui/components/item'
|
|
||||||
import { cn } from '@evofw/ui/lib/utils'
|
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'
|
import { Skeleton } from '@evofw/ui/components/skeleton'
|
||||||
|
|
||||||
export type KpiStatVariant = 'default' | 'warning' | 'destructive'
|
export type KpiStatVariant = 'default' | 'warning' | 'destructive'
|
||||||
|
|
||||||
export interface KpiStatCard {
|
/**
|
||||||
id: string
|
* KPI tile data — horizontal compact hybrid (icon left + label/Badge + value).
|
||||||
label: string
|
* @see https://reui.io/preview/base/stats-12
|
||||||
value: string | number
|
*/
|
||||||
hint?: string
|
export type KpiStatItem = {
|
||||||
|
id?: string
|
||||||
|
label: ReactNode
|
||||||
|
value: ReactNode
|
||||||
|
hint?: ReactNode
|
||||||
to?: string
|
to?: string
|
||||||
search?: Record<string, unknown>
|
search?: Record<string, unknown>
|
||||||
onSelect?: () => void
|
onSelect?: () => void
|
||||||
|
onClick?: () => void
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
|
active?: boolean
|
||||||
icon?: ReactNode
|
icon?: ReactNode
|
||||||
iconClassName?: string
|
iconClassName?: string
|
||||||
/** Semantic color for the primary value */
|
|
||||||
variant?: KpiStatVariant
|
variant?: KpiStatVariant
|
||||||
footer?: ReactNode
|
footer?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated Use KpiStatCard */
|
/** CFDM-compatible card shape (id required). */
|
||||||
export type OpsKpiCard = KpiStatCard
|
export type KpiStatCardData = KpiStatItem & { id: string }
|
||||||
|
|
||||||
interface KpiStatGridProps {
|
/** @deprecated Use KpiStatCardData */
|
||||||
cards: KpiStatCard[]
|
export type OpsKpiCard = KpiStatCardData
|
||||||
isLoading?: boolean
|
|
||||||
emptyMessage?: ReactNode
|
/** @deprecated Use KpiStatCardData — type alias for CFDM kit parity */
|
||||||
emptyIcon?: ReactNode
|
export type KpiStatCard = KpiStatCardData
|
||||||
className?: string
|
|
||||||
skeletonCount?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current'
|
const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current'
|
||||||
|
|
||||||
@@ -44,107 +48,146 @@ const VALUE_VARIANT_CLASS: Record<KpiStatVariant, string> = {
|
|||||||
destructive: 'text-destructive',
|
destructive: 'text-destructive',
|
||||||
}
|
}
|
||||||
|
|
||||||
function kpiCols(count: number): string {
|
function handleCardKeyDown(onActivate: () => void, event: KeyboardEvent<HTMLDivElement>) {
|
||||||
if (count <= 1) return 'grid-cols-1'
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
if (count === 2) return 'grid-cols-1 @xl:grid-cols-2'
|
event.preventDefault()
|
||||||
if (count === 3) return 'grid-cols-1 @3xl:grid-cols-3'
|
onActivate()
|
||||||
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 resolveFooter(card: KpiStatCard): ReactNode {
|
function resolveActivate(item: KpiStatItem): (() => void) | undefined {
|
||||||
if (card.footer) return card.footer
|
return item.onClick ?? item.onSelect
|
||||||
if (card.hint) {
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<Badge variant="outline" size="sm">
|
<Badge variant="outline" size="sm">
|
||||||
{card.hint}
|
{item.hint}
|
||||||
</Badge>
|
</Badge>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (item.hint) return item.hint
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function KpiStatCardBody({ card }: { card: KpiStatCard }) {
|
function KpiStatCardBody({ item }: { item: KpiStatItem }) {
|
||||||
const footer = resolveFooter(card)
|
const footer = resolveFooter(item)
|
||||||
const valueVariant = card.variant ?? 'default'
|
const valueVariant = item.variant ?? 'default'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-full items-start gap-3">
|
<div className="relative z-10 flex h-full items-start gap-3">
|
||||||
{card.icon ? (
|
{item.icon ? (
|
||||||
<Item
|
<Item
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-background bg-muted flex size-10.5 shrink-0 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-4',
|
'border-background bg-muted flex size-10.5 shrink-0 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-4',
|
||||||
card.iconClassName ?? DEFAULT_ICON_CLASS,
|
item.iconClassName ?? DEFAULT_ICON_CLASS,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ItemMedia variant="icon" className="size-auto">
|
<ItemMedia variant="icon" className="size-auto">
|
||||||
{card.icon}
|
{item.icon}
|
||||||
</ItemMedia>
|
</ItemMedia>
|
||||||
</Item>
|
</Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<span className="text-muted-foreground text-sm font-medium">{card.label}</span>
|
<div className="text-muted-foreground text-sm font-medium">{item.label}</div>
|
||||||
{footer ? <div className="shrink-0">{footer}</div> : null}
|
{footer ? <div className="shrink-0">{footer}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-2xl leading-none font-bold tabular-nums',
|
'text-2xl leading-none font-bold tabular-nums',
|
||||||
VALUE_VARIANT_CLASS[valueVariant],
|
VALUE_VARIANT_CLASS[valueVariant],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{card.value}
|
{item.value}
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function KpiStatCardItem({ card }: { card: KpiStatCard }) {
|
function panelClassName(item: KpiStatItem, className?: string) {
|
||||||
const interactive = Boolean(card.to || card.onSelect)
|
const onActivate = resolveActivate(item)
|
||||||
const panelClass = cn(
|
const clickable = Boolean(item.to || onActivate)
|
||||||
'relative isolate flex h-full flex-col',
|
const selected = isSelected(item)
|
||||||
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',
|
|
||||||
)
|
|
||||||
|
|
||||||
if (card.to) {
|
return cn(
|
||||||
return (
|
'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 = (
|
||||||
<FramePanel className={panelClass}>
|
<FramePanel className={panelClass}>
|
||||||
<Link
|
<Link to={item.to} search={item.search} className="focus-visible:outline-none">
|
||||||
to={card.to}
|
<KpiStatCardBody item={item} />
|
||||||
{...(card.search ? { search: card.search } : {})}
|
|
||||||
className="focus-visible:outline-none"
|
|
||||||
>
|
|
||||||
<KpiStatCardBody card={card} />
|
|
||||||
</Link>
|
</Link>
|
||||||
</FramePanel>
|
</FramePanel>
|
||||||
)
|
)
|
||||||
}
|
} else if (onActivate) {
|
||||||
|
panel = (
|
||||||
if (card.onSelect) {
|
<FramePanel
|
||||||
return (
|
className={panelClass}
|
||||||
|
onClick={onActivate}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => handleCardKeyDown(onActivate, e)}
|
||||||
|
>
|
||||||
|
<KpiStatCardBody item={item} />
|
||||||
|
</FramePanel>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
panel = (
|
||||||
<FramePanel className={panelClass}>
|
<FramePanel className={panelClass}>
|
||||||
<button
|
<KpiStatCardBody item={item} />
|
||||||
type="button"
|
|
||||||
onClick={card.onSelect}
|
|
||||||
className="w-full text-start focus-visible:outline-none"
|
|
||||||
>
|
|
||||||
<KpiStatCardBody card={card} />
|
|
||||||
</button>
|
|
||||||
</FramePanel>
|
</FramePanel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (embedded) {
|
||||||
<FramePanel className={panelClass}>
|
return <Frame className="h-full ring-1 ring-foreground/10">{panel}</Frame>
|
||||||
<KpiStatCardBody card={card} />
|
}
|
||||||
</FramePanel>
|
|
||||||
)
|
return <Frame className="h-full">{panel}</Frame>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated Prefer KpiStatCardTile — kept for existing imports */
|
||||||
|
export function KpiStatCard({
|
||||||
|
item,
|
||||||
|
embedded = false,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
item: KpiStatItem
|
||||||
|
embedded?: boolean
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return <KpiStatCardTile item={item} embedded={embedded} className={className} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function KpiStatGridSkeleton({ count }: { count: number }) {
|
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 (
|
||||||
|
<FramePanel className={panelClass}>
|
||||||
|
<Link to={item.to} search={item.search} className="focus-visible:outline-none">
|
||||||
|
<KpiStatCardBody item={item} />
|
||||||
|
</Link>
|
||||||
|
</FramePanel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onActivate) {
|
||||||
|
return (
|
||||||
|
<FramePanel
|
||||||
|
className={panelClass}
|
||||||
|
onClick={onActivate}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => handleCardKeyDown(onActivate, e)}
|
||||||
|
>
|
||||||
|
<KpiStatCardBody item={item} />
|
||||||
|
</FramePanel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FramePanel className={panelClass}>
|
||||||
|
<KpiStatCardBody item={item} />
|
||||||
|
</FramePanel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* Preview: https://reui.io/preview/base/stats-12
|
||||||
*/
|
*/
|
||||||
export function KpiStatGrid({
|
export function KpiStatGrid({
|
||||||
|
items,
|
||||||
cards,
|
cards,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
emptyMessage,
|
emptyMessage,
|
||||||
emptyIcon,
|
emptyIcon,
|
||||||
className,
|
className,
|
||||||
skeletonCount = 4,
|
skeletonCount = 4,
|
||||||
|
embedded = false,
|
||||||
|
'aria-label': ariaLabel,
|
||||||
}: KpiStatGridProps) {
|
}: KpiStatGridProps) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <KpiStatGridSkeleton count={skeletonCount} />
|
return <KpiStatGridSkeleton count={skeletonCount} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cards.length === 0) {
|
const list = items ?? cards ?? []
|
||||||
|
|
||||||
|
if (list.length === 0 && (emptyMessage || emptyIcon)) {
|
||||||
return (
|
return (
|
||||||
<Frame dense spacing="sm" className={cn('w-full', className)}>
|
<Frame dense spacing="sm" className={cn('w-full', className)}>
|
||||||
<FramePanel className="flex items-center gap-3 p-4">
|
<FramePanel className="flex items-center gap-3 p-4">
|
||||||
@@ -197,13 +295,31 @@ export function KpiStatGrid({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (embedded) {
|
||||||
|
return (
|
||||||
|
<section aria-label={ariaLabel} className={cn('@container w-full', className)}>
|
||||||
|
<div className={cn('grid gap-2', kpiCols(list.length || 1))}>
|
||||||
|
{list.map((item, index) => (
|
||||||
|
<KpiStatCardTile key={kpiStatItemKey(item, index)} item={item} embedded />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Frame className={cn('@container w-full', className)}>
|
<Frame className={cn('@container w-full', className)} aria-label={ariaLabel}>
|
||||||
<div className={cn('grid gap-2', kpiCols(cards.length))}>
|
<div className={cn('grid gap-2', kpiCols(list.length || 1))}>
|
||||||
{cards.map((card) => (
|
{list.map((item, index) => (
|
||||||
<KpiStatCardItem key={card.id} card={card} />
|
<KpiStatCardItem key={kpiStatItemKey(item, index)} item={item} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Frame>
|
</Frame>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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}`
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { Badge } from '@/components/reui/badge'
|
import { Badge } from '@/components/reui/badge'
|
||||||
import { Item, ItemMedia } from '@evofw/ui/components/item'
|
import { Item, ItemMedia } from '@evofw/ui/components/item'
|
||||||
import { cn } from '@evofw/ui/lib/utils'
|
import { cn } from '@evofw/ui/lib/utils'
|
||||||
|
import { kpiCols } from './kpi-cols'
|
||||||
|
|
||||||
export interface QuickActionItem {
|
export interface QuickActionItem {
|
||||||
id: string
|
id: string
|
||||||
@@ -30,16 +31,6 @@ interface QuickActionGridProps {
|
|||||||
|
|
||||||
const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current'
|
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 }) {
|
function QuickActionBody({ action }: { action: QuickActionItem }) {
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-full items-start gap-3">
|
<div className="relative z-10 flex h-full items-start gap-3">
|
||||||
@@ -99,7 +90,7 @@ export function QuickActionGrid({
|
|||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
to={action.to}
|
to={action.to}
|
||||||
{...(action.search ? { search: action.search } : {})}
|
search={action.search}
|
||||||
className="focus-visible:outline-none"
|
className="focus-visible:outline-none"
|
||||||
aria-label={`${action.title}: ${action.description}`}
|
aria-label={`${action.title}: ${action.description}`}
|
||||||
>
|
>
|
||||||
|
|||||||
+70
-22
@@ -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
|
## Surface
|
||||||
|
|
||||||
Project lock: **`surface: frame`**. Ops / list / dashboard / detail / settings — только **Frame**, не shadcn Card как shell. Не смешивать Card и Frame на одном ops-экране.
|
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
|
## Canonical PRO references
|
||||||
|
|
||||||
| Зона | Block | Preview |
|
| Зона | Block | Preview |
|
||||||
|------|-------|---------|
|
|------|-------|---------|
|
||||||
| Shell | `app-shell-12` | https://reui.io/preview/base/app-shell-12 |
|
| Shell | `app-shell-12` (+ cmdk/monitor где нужно) | https://reui.io/preview/base/app-shell-12 · https://reui.io/preview/base/app-shell-7 |
|
||||||
| KPI | `stats-12` | https://reui.io/preview/base/stats-12 |
|
| 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 |
|
| Dashboard | `dashboard-1` | https://reui.io/preview/base/dashboard-1 |
|
||||||
| Lists | `data-grid-filtering-2` | https://reui.io/preview/base/data-grid-filtering-2 |
|
| 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 |
|
| Auth | `auth-13` | https://reui.io/preview/base/auth-13 |
|
||||||
| Empty | `empty-state-12` | https://reui.io/preview/base/empty-state-12 |
|
| 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 |
|
| Component | Role |
|
||||||
|-----------|------|
|
|-----------|------|
|
||||||
| `ResourcePage` | Frame + line tabs + Filters + DataGrid |
|
| `ResourcePage` | Frame + line tabs + Filters + DataGrid |
|
||||||
| `KpiStatGrid` | KPI tiles |
|
| `KpiStatGrid` | EvoBGP hybrid KPI tiles (`items`/`cards`, `variant`, Badge) |
|
||||||
| `OpsDashboard` | KPI + charts + attention |
|
| `QuickActionGrid` | KPI-like quick action tiles (gated by `showQuickActions`) |
|
||||||
|
| `OpsDashboard` | KPI + optional `afterKpi` + charts + attention queue |
|
||||||
| `SettingsShell` | settings nav + Outlet |
|
| `SettingsShell` | settings nav + Outlet |
|
||||||
| `DetailPanel` | detail Frame sections |
|
| `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
|
## 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` |
|
| `SIDEBAR_WIDTH` / `--sidebar-width` | `240px` |
|
||||||
| Header | `h-12`, sticky, `border-b` |
|
| 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` |
|
| `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
|
## 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`
|
2. Cite `previewUrl` + `docsUrl`
|
||||||
3. CLI from `apps/web`: `pnpm dlx shadcn@latest add @reui/...`
|
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`
|
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
|
## License
|
||||||
|
|
||||||
```env
|
```env
|
||||||
# apps/web/.env.local
|
# apps/web/.env.local (gitignored)
|
||||||
REUI_LICENSE_KEY=
|
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
|
## Forbidden
|
||||||
|
|
||||||
- Card as ops list/dashboard shell
|
- 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
|
- Hand-rolled KPI when `KpiStatGrid` exists
|
||||||
- Mixing Card and Frame on one ops screen
|
- Hand-rolled Quick Actions when `QuickActionGrid` exists
|
||||||
- Raw `bg-emerald-*` / `space-y-*`
|
- SectionCards / DataGridCard as design эталон
|
||||||
|
- Mixing Card and Frame surfaces on one ops screen
|
||||||
|
|||||||
Reference in New Issue
Block a user