From 32248d88a4ddbcb0e9a3fa38039c3089aee6abab Mon Sep 17 00:00:00 2001 From: Denozordec Date: Tue, 21 Jul 2026 00:23:01 +0700 Subject: [PATCH] =?UTF-8?q?chore(web):=20=D1=83=D0=BD=D0=B8=D1=84=D0=B8?= =?UTF-8?q?=D1=86=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20ReUI=20PRO?= =?UTF-8?q?=20kit=20=D0=B8=20rules=20=D0=BF=D0=BE=D0=B4=20EvoBGP=20hybrid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Синхронизированы KpiStatGrid/QuickActionGrid (kpi-cols), design contract и ReUI skill; ReUI PRO выше shadcn в rules. Co-authored-by: Cursor --- .agents/skills/reui/SKILL.md | 2 +- .agents/skills/reui/rules/cli.md | 2 + .agents/skills/reui/rules/components.md | 30 +- .claude/skills/reui/SKILL.md | 2 +- .claude/skills/reui/rules/cli.md | 2 + .claude/skills/reui/rules/components.md | 30 +- .cursor/rules/frontend-shadcn.mdc | 2 +- .cursor/rules/frontend-ui-patterns.mdc | 6 +- .cursor/rules/reui-mcp.mdc | 13 +- .cursor/rules/reui.mdc | 2 +- .cursor/rules/shadcn-mcp.mdc | 55 ++-- .cursor/rules/shadcn-ui-production.mdc | 25 +- .cursor/skills/reui/SKILL.md | 2 +- .cursor/skills/reui/rules/cli.md | 2 + .cursor/skills/reui/rules/components.md | 30 +- .opencode/skills/reui/SKILL.md | 2 +- .opencode/skills/reui/rules/cli.md | 2 + .opencode/skills/reui/rules/components.md | 30 +- apps/web/src/components/reui-kit/index.ts | 12 +- apps/web/src/components/reui-kit/kpi-cols.ts | 10 + .../src/components/reui-kit/kpi-stat-grid.tsx | 268 +++++++++++++----- .../components/reui-kit/quick-action-grid.tsx | 11 +- docs/ui-design-contract.md | 72 +++-- 23 files changed, 445 insertions(+), 167 deletions(-) create mode 100644 apps/web/src/components/reui-kit/kpi-cols.ts diff --git a/.agents/skills/reui/SKILL.md b/.agents/skills/reui/SKILL.md index 160e7e5..d76c1c2 100644 --- a/.agents/skills/reui/SKILL.md +++ b/.agents/skills/reui/SKILL.md @@ -5,7 +5,7 @@ user-invocable: false allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *) --- -> **ReUI skill version `0e224b0281`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer (see `get_agent_skill` -> `install.recommended`) to update this skill. Cloud/tools-only agents have no local file and always read the latest - they can ignore this. +> **ReUI skill version `42d70dcc3d`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer (see `get_agent_skill` -> `install.recommended`) to update this skill. Cloud/tools-only agents have no local file and always read the latest - they can ignore this. # ReUI for Agents diff --git a/.agents/skills/reui/rules/cli.md b/.agents/skills/reui/rules/cli.md index e603164..7c40844 100644 --- a/.agents/skills/reui/rules/cli.md +++ b/.agents/skills/reui/rules/cli.md @@ -29,6 +29,8 @@ REUI_LICENSE_KEY=your-license-key } ``` +The shadcn CLI expands `${REUI_LICENSE_KEY}` from `.env.local` inside `components.json`, but an MCP client config never expands variables, so a ReUI MCP server config must carry the raw token instead (for example `reui_pat_your_token_here`). + The MCP `get_project_context` tool returns the right config. Full guide: https://reui.io/docs/registry ## Installing diff --git a/.agents/skills/reui/rules/components.md b/.agents/skills/reui/rules/components.md index 8767bbb..0c24578 100644 --- a/.agents/skills/reui/rules/components.md +++ b/.agents/skills/reui/rules/components.md @@ -1,6 +1,6 @@ # ReUI components -The 17 ReUI building blocks: `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `filters`, `frame`, `icon-stack`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `sortable`, `stepper`, `timeline`, `tree`. Examples and blocks are composed from these. +The 19 ReUI building blocks: `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `event-calendar`, `filters`, `frame`, `gantt`, `icon-stack`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `sortable`, `stepper`, `timeline`, `tree`. Examples and blocks are composed from these. **Rule one: never guess a component's API. Read it first.** Call **`get_component(name)`** for its inline `api` (props + usage, no web fetch), and **share the result's `docsUrl`** (the component's API documentation page) with the user whenever you work with that component's API, so they have the full reference (the `/llms.txt` index is a further fallback). Then call **`get_examples(name)`** to install a worked example and copy real composition. The contracts below are first-try orientation (required props, composition shape, the one gotcha); the inline `api` is the full reference. No single block fits? Compose: search the components you need, read each `get_component`, install a `get_examples` example per component, and adapt. @@ -32,6 +32,34 @@ Common mistakes: - **Incorrect:** a raw `` / hand-rolled pagination. **Correct:** use `data-grid`; read its API for sticky header, pagination, virtualization, row selection. - **Incorrect:** styling rows/cells with arbitrary classes. **Correct:** drive layout via `tableLayout` and the documented `ColumnMeta` (e.g. `cellClassName`, `headerTitle`). +## event-calendar + +**Required:** events via `events`/`onEventsChange` (controlled) or `defaultEvents` (uncontrolled), plus a height on the root. +**Shape:** + +```tsx + + + + +``` + +**Gotcha:** headless-first: `EventCalendarContent` renders the active view (month/week/day/days/agenda; a resource view activates when `resources` is passed) - there is no per-view JSX to compose. Events are `{ id, title, start, end (exclusive), allDay?, color?, recurrence?, resourceId? }`. Mutations flow through `onEventUpdate`/`canDropEvent` (return `false` to reject); the root needs an explicit height because it is a min-h-0 flex column. + +## gantt + +**Required:** `resources` (the left tree) plus bars via `events`/`defaultEvents` attached by `resourceId`. +**Shape:** + +```tsx + + + + +``` + +**Gotcha:** bars move along the time axis only (never across rows) and are all-day spans with exclusive `end`; `progress` is 0-100. Scales are `day | week | month | quarter | year`. Zoom control, infinite scroll, summary rollups, and row checkboxes are ON by default - turn off what you do not need. Same `onEventUpdate`/`canDropEvent` commit pipeline as `event-calendar`; the root needs an explicit height. + ## kanban **Required:** `value` (`Record`), `onValueChange`, `getItemValue` diff --git a/.claude/skills/reui/SKILL.md b/.claude/skills/reui/SKILL.md index 160e7e5..d76c1c2 100644 --- a/.claude/skills/reui/SKILL.md +++ b/.claude/skills/reui/SKILL.md @@ -5,7 +5,7 @@ user-invocable: false allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *) --- -> **ReUI skill version `0e224b0281`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer (see `get_agent_skill` -> `install.recommended`) to update this skill. Cloud/tools-only agents have no local file and always read the latest - they can ignore this. +> **ReUI skill version `42d70dcc3d`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer (see `get_agent_skill` -> `install.recommended`) to update this skill. Cloud/tools-only agents have no local file and always read the latest - they can ignore this. # ReUI for Agents diff --git a/.claude/skills/reui/rules/cli.md b/.claude/skills/reui/rules/cli.md index e603164..7c40844 100644 --- a/.claude/skills/reui/rules/cli.md +++ b/.claude/skills/reui/rules/cli.md @@ -29,6 +29,8 @@ REUI_LICENSE_KEY=your-license-key } ``` +The shadcn CLI expands `${REUI_LICENSE_KEY}` from `.env.local` inside `components.json`, but an MCP client config never expands variables, so a ReUI MCP server config must carry the raw token instead (for example `reui_pat_your_token_here`). + The MCP `get_project_context` tool returns the right config. Full guide: https://reui.io/docs/registry ## Installing diff --git a/.claude/skills/reui/rules/components.md b/.claude/skills/reui/rules/components.md index 8767bbb..0c24578 100644 --- a/.claude/skills/reui/rules/components.md +++ b/.claude/skills/reui/rules/components.md @@ -1,6 +1,6 @@ # ReUI components -The 17 ReUI building blocks: `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `filters`, `frame`, `icon-stack`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `sortable`, `stepper`, `timeline`, `tree`. Examples and blocks are composed from these. +The 19 ReUI building blocks: `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `event-calendar`, `filters`, `frame`, `gantt`, `icon-stack`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `sortable`, `stepper`, `timeline`, `tree`. Examples and blocks are composed from these. **Rule one: never guess a component's API. Read it first.** Call **`get_component(name)`** for its inline `api` (props + usage, no web fetch), and **share the result's `docsUrl`** (the component's API documentation page) with the user whenever you work with that component's API, so they have the full reference (the `/llms.txt` index is a further fallback). Then call **`get_examples(name)`** to install a worked example and copy real composition. The contracts below are first-try orientation (required props, composition shape, the one gotcha); the inline `api` is the full reference. No single block fits? Compose: search the components you need, read each `get_component`, install a `get_examples` example per component, and adapt. @@ -32,6 +32,34 @@ Common mistakes: - **Incorrect:** a raw `
` / hand-rolled pagination. **Correct:** use `data-grid`; read its API for sticky header, pagination, virtualization, row selection. - **Incorrect:** styling rows/cells with arbitrary classes. **Correct:** drive layout via `tableLayout` and the documented `ColumnMeta` (e.g. `cellClassName`, `headerTitle`). +## event-calendar + +**Required:** events via `events`/`onEventsChange` (controlled) or `defaultEvents` (uncontrolled), plus a height on the root. +**Shape:** + +```tsx + + + + +``` + +**Gotcha:** headless-first: `EventCalendarContent` renders the active view (month/week/day/days/agenda; a resource view activates when `resources` is passed) - there is no per-view JSX to compose. Events are `{ id, title, start, end (exclusive), allDay?, color?, recurrence?, resourceId? }`. Mutations flow through `onEventUpdate`/`canDropEvent` (return `false` to reject); the root needs an explicit height because it is a min-h-0 flex column. + +## gantt + +**Required:** `resources` (the left tree) plus bars via `events`/`defaultEvents` attached by `resourceId`. +**Shape:** + +```tsx + + + + +``` + +**Gotcha:** bars move along the time axis only (never across rows) and are all-day spans with exclusive `end`; `progress` is 0-100. Scales are `day | week | month | quarter | year`. Zoom control, infinite scroll, summary rollups, and row checkboxes are ON by default - turn off what you do not need. Same `onEventUpdate`/`canDropEvent` commit pipeline as `event-calendar`; the root needs an explicit height. + ## kanban **Required:** `value` (`Record`), `onValueChange`, `getItemValue` diff --git a/.cursor/rules/frontend-shadcn.mdc b/.cursor/rules/frontend-shadcn.mdc index 3541b27..e21ec6e 100644 --- a/.cursor/rules/frontend-shadcn.mdc +++ b/.cursor/rules/frontend-shadcn.mdc @@ -35,7 +35,7 @@ Monorepo — [`frontend-monorepo.mdc`](frontend-monorepo.mdc). ReUI — [`reui-m |-----------|------| | `PageShell` | `page-shell.tsx` | | `ResourcePage` | `reui-kit/resource-page.tsx` | -| `OpsDashboard` / `KpiStatGrid` | `reui-kit/ops-dashboard.tsx`, `kpi-stat-grid.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` | diff --git a/.cursor/rules/frontend-ui-patterns.mdc b/.cursor/rules/frontend-ui-patterns.mdc index 038f679..184d82f 100644 --- a/.cursor/rules/frontend-ui-patterns.mdc +++ b/.cursor/rules/frontend-ui-patterns.mdc @@ -27,7 +27,8 @@ alwaysApply: false | Зона | Preview | |------|---------| -| KPI | [stats-12](https://reui.io/preview/base/stats-12) (primary); [card-35](https://reui.io/preview/base/card-35) compact | +| 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) | @@ -73,7 +74,8 @@ apps/web/src/components/ ← shared + domain + layout | 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) | +| 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`) | diff --git a/.cursor/rules/reui-mcp.mdc b/.cursor/rules/reui-mcp.mdc index 5342bc9..8857b60 100644 --- a/.cursor/rules/reui-mcp.mdc +++ b/.cursor/rules/reui-mcp.mdc @@ -36,11 +36,13 @@ alwaysApply: true | 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 `
`, не устаревший DataGridCard. +**Сложные списки** — `ResourcePage` (Frame + data-grid + filters), не raw `
`, не DataGridCard. +**Quick Actions** — только `QuickActionGrid` (не Card / Button grid). ## MCP workflow @@ -66,7 +68,7 @@ alwaysApply: true **Components:** `frame`, `data-grid/*`, `filters`, `kanban`, `badge`, `alert`, `autocomplete`, `number-field`, `date-selector`, `color-picker`, `timeline`, `rating`, `phone-input`, `icon-stack` -**Kit:** `ResourcePage`, `KpiStatGrid`, `OpsDashboard`, `KanbanBoard`, `DetailPanel`, `SettingsShell` +**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`, … @@ -83,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 | @@ -96,8 +98,9 @@ REUI_LICENSE_KEY= - ReUI в `packages/ui` / импорт как `@cfdm/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 ## Чеклист diff --git a/.cursor/rules/reui.mdc b/.cursor/rules/reui.mdc index 48b05d0..2c881c6 100644 --- a/.cursor/rules/reui.mdc +++ b/.cursor/rules/reui.mdc @@ -11,7 +11,7 @@ user-invocable: false allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *) --- -> **ReUI skill version `0e224b0281`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer (see `get_agent_skill` -> `install.recommended`) to update this skill. Cloud/tools-only agents have no local file and always read the latest - they can ignore this. +> **ReUI skill version `42d70dcc3d`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer (see `get_agent_skill` -> `install.recommended`) to update this skill. Cloud/tools-only agents have no local file and always read the latest - they can ignore this. # ReUI for Agents diff --git a/.cursor/rules/shadcn-mcp.mdc b/.cursor/rules/shadcn-mcp.mdc index 0e990e9..41f0268 100644 --- a/.cursor/rules/shadcn-mcp.mdc +++ b/.cursor/rules/shadcn-mcp.mdc @@ -1,49 +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`. -## Порядок (строго) +## Порядок (primitives) -0. **Codegraph** `codegraph_explore` — найти существующие реализации и shared-обёртки (один вызов перед правками) -1. **`get_project_registries`** — какие registry доступны в проекте -2. **`search_items_in_registries`** — компонент, block, example (`query`: `"card"`, `"tabs demo"`, `"dashboard"`, `"scroll-area"`) -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/props с [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 (не заменяет шаги 1–8 для примитивов) -10. **`get_audit_checklist`** — перед merge PR -11. **Codegraph** `codegraph_status` — Pending sync пустой после правок +0. Поиск существующих shared / `reui-kit` +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 | `search` → `types: ["block"]` → examples → add block | -| Нет примитива в `@cfdm/ui` | `search` → `get_add_command_for_items` → add | -| Сомнение в API/props | `get_item_examples_from_registries` | -| Ревью UI перед сдачей | `get_audit_checklist` | +| Новая страница / KPI / list / settings | **`user-reui`** ([`reui-mcp.mdc`](reui-mcp.mdc)) | +| Нет примитива в `@cfdm/ui` | `plugin-shadcn-shadcn` → add | +| Сомнение в props примитива | examples + docs CLI | ## Запрещено -- Писать UI по памяти, не проверив MCP -- Копипаст с ui.shadcn.com без examples/add из MCP -- Самописные примитивы, если есть item в registry -- Пропускать MCP «потому что компонент простой» +- Писать UI по памяти +- Ставить ui.shadcn.com/blocks выше ReUI PRO +- Самописные примитивы при наличии registry item -## Сервер и инструменты +## Сервер -- **MCP server:** `plugin-shadcn-shadcn` -- **Инструменты:** `get_project_registries`, `search_items_in_registries`, `get_item_examples_from_registries`, `get_add_command_for_items`, `view_items_in_registries`, `list_items_in_registries`, `get_audit_checklist` -- Перед вызовом — прочитать schema в `mcps/plugin-shadcn-shadcn/tools/` +- **MCP:** `plugin-shadcn-shadcn` (+ `user-reui` primary) +- Schema: `mcps/plugin-shadcn-shadcn/tools/` -Связанные правила: [`shadcn-ui-production.mdc`](shadcn-ui-production.mdc), [`frontend-shadcn.mdc`](frontend-shadcn.mdc). +Связанные: [`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 279c56b..ca32eb1 100644 --- a/.cursor/rules/shadcn-ui-production.mdc +++ b/.cursor/rules/shadcn-ui-production.mdc @@ -1,26 +1,33 @@ --- -description: shadcn/ui — глобальные UI-принципы проекта; frontend см. frontend-shadcn.mdc +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): [Components](https://ui.shadcn.com/docs/components), [Blocks](https://ui.shadcn.com/blocks), [Monorepo](https://ui.shadcn.com/docs/monorepo). +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). -**Первый шаг любой UI-задачи — MCP `plugin-shadcn-shadcn`** (см. [`shadcn-mcp.mdc`](shadcn-mcp.mdc)): search → examples → add command → CLI. +**Иерархия:** ReUI PRO (`user-reui`) **выше** базового shadcn. Pages / KPI / lists / settings / shell → ReUI; shadcn — primitives. -## Разработка frontend +**Первый шаг UI-задачи:** +1. MCP **`user-reui`** для pages / KPI / data-grid / settings / Frame / Quick Actions (`surface: frame`) +2. MCP **`plugin-shadcn-shadcn`** для primitives `@shadcn` -**Все правила frontend** — в [`frontend-shadcn.mdc`](frontend-shadcn.mdc), [`frontend-ui-patterns.mdc`](frontend-ui-patterns.mdc) и [`frontend-monorepo.mdc`](frontend-monorepo.mdc) (globs: `apps/web/**`, `packages/ui/**`). +См. [`shadcn-mcp.mdc`](shadcn-mcp.mdc), [`reui-mcp.mdc`](reui-mcp.mdc). Contract: [`docs/ui-design-contract.md`](../../docs/ui-design-contract.md). -Кратко: docs → CLI из `apps/web` → Block → композиция → `pnpm --filter web build`. Кастомный CSS и самописные примитивы **запрещены**. +## Frontend + +Детали: [`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 web build`. Кастомный CSS и самописные примитивы **запрещены**. ## Стек - Monorepo: `apps/web` + `packages/ui` (`@cfdm/ui`), pnpm workspaces -- Vite + TanStack Router/Query + shadcn **base-nova** +- 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) -- Тема: `pnpm dlx shadcn@latest apply b2fA --only theme -y` — единственный способ менять базовую тему `globals.css`; ReUI semantic 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` ## Backend → UI diff --git a/.cursor/skills/reui/SKILL.md b/.cursor/skills/reui/SKILL.md index 160e7e5..d76c1c2 100644 --- a/.cursor/skills/reui/SKILL.md +++ b/.cursor/skills/reui/SKILL.md @@ -5,7 +5,7 @@ user-invocable: false allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *) --- -> **ReUI skill version `0e224b0281`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer (see `get_agent_skill` -> `install.recommended`) to update this skill. Cloud/tools-only agents have no local file and always read the latest - they can ignore this. +> **ReUI skill version `42d70dcc3d`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer (see `get_agent_skill` -> `install.recommended`) to update this skill. Cloud/tools-only agents have no local file and always read the latest - they can ignore this. # ReUI for Agents diff --git a/.cursor/skills/reui/rules/cli.md b/.cursor/skills/reui/rules/cli.md index e603164..7c40844 100644 --- a/.cursor/skills/reui/rules/cli.md +++ b/.cursor/skills/reui/rules/cli.md @@ -29,6 +29,8 @@ REUI_LICENSE_KEY=your-license-key } ``` +The shadcn CLI expands `${REUI_LICENSE_KEY}` from `.env.local` inside `components.json`, but an MCP client config never expands variables, so a ReUI MCP server config must carry the raw token instead (for example `reui_pat_your_token_here`). + The MCP `get_project_context` tool returns the right config. Full guide: https://reui.io/docs/registry ## Installing diff --git a/.cursor/skills/reui/rules/components.md b/.cursor/skills/reui/rules/components.md index 8767bbb..0c24578 100644 --- a/.cursor/skills/reui/rules/components.md +++ b/.cursor/skills/reui/rules/components.md @@ -1,6 +1,6 @@ # ReUI components -The 17 ReUI building blocks: `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `filters`, `frame`, `icon-stack`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `sortable`, `stepper`, `timeline`, `tree`. Examples and blocks are composed from these. +The 19 ReUI building blocks: `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `event-calendar`, `filters`, `frame`, `gantt`, `icon-stack`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `sortable`, `stepper`, `timeline`, `tree`. Examples and blocks are composed from these. **Rule one: never guess a component's API. Read it first.** Call **`get_component(name)`** for its inline `api` (props + usage, no web fetch), and **share the result's `docsUrl`** (the component's API documentation page) with the user whenever you work with that component's API, so they have the full reference (the `/llms.txt` index is a further fallback). Then call **`get_examples(name)`** to install a worked example and copy real composition. The contracts below are first-try orientation (required props, composition shape, the one gotcha); the inline `api` is the full reference. No single block fits? Compose: search the components you need, read each `get_component`, install a `get_examples` example per component, and adapt. @@ -32,6 +32,34 @@ Common mistakes: - **Incorrect:** a raw `
` / hand-rolled pagination. **Correct:** use `data-grid`; read its API for sticky header, pagination, virtualization, row selection. - **Incorrect:** styling rows/cells with arbitrary classes. **Correct:** drive layout via `tableLayout` and the documented `ColumnMeta` (e.g. `cellClassName`, `headerTitle`). +## event-calendar + +**Required:** events via `events`/`onEventsChange` (controlled) or `defaultEvents` (uncontrolled), plus a height on the root. +**Shape:** + +```tsx + + + + +``` + +**Gotcha:** headless-first: `EventCalendarContent` renders the active view (month/week/day/days/agenda; a resource view activates when `resources` is passed) - there is no per-view JSX to compose. Events are `{ id, title, start, end (exclusive), allDay?, color?, recurrence?, resourceId? }`. Mutations flow through `onEventUpdate`/`canDropEvent` (return `false` to reject); the root needs an explicit height because it is a min-h-0 flex column. + +## gantt + +**Required:** `resources` (the left tree) plus bars via `events`/`defaultEvents` attached by `resourceId`. +**Shape:** + +```tsx + + + + +``` + +**Gotcha:** bars move along the time axis only (never across rows) and are all-day spans with exclusive `end`; `progress` is 0-100. Scales are `day | week | month | quarter | year`. Zoom control, infinite scroll, summary rollups, and row checkboxes are ON by default - turn off what you do not need. Same `onEventUpdate`/`canDropEvent` commit pipeline as `event-calendar`; the root needs an explicit height. + ## kanban **Required:** `value` (`Record`), `onValueChange`, `getItemValue` diff --git a/.opencode/skills/reui/SKILL.md b/.opencode/skills/reui/SKILL.md index 160e7e5..d76c1c2 100644 --- a/.opencode/skills/reui/SKILL.md +++ b/.opencode/skills/reui/SKILL.md @@ -5,7 +5,7 @@ user-invocable: false allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *) --- -> **ReUI skill version `0e224b0281`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer (see `get_agent_skill` -> `install.recommended`) to update this skill. Cloud/tools-only agents have no local file and always read the latest - they can ignore this. +> **ReUI skill version `42d70dcc3d`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer (see `get_agent_skill` -> `install.recommended`) to update this skill. Cloud/tools-only agents have no local file and always read the latest - they can ignore this. # ReUI for Agents diff --git a/.opencode/skills/reui/rules/cli.md b/.opencode/skills/reui/rules/cli.md index e603164..7c40844 100644 --- a/.opencode/skills/reui/rules/cli.md +++ b/.opencode/skills/reui/rules/cli.md @@ -29,6 +29,8 @@ REUI_LICENSE_KEY=your-license-key } ``` +The shadcn CLI expands `${REUI_LICENSE_KEY}` from `.env.local` inside `components.json`, but an MCP client config never expands variables, so a ReUI MCP server config must carry the raw token instead (for example `reui_pat_your_token_here`). + The MCP `get_project_context` tool returns the right config. Full guide: https://reui.io/docs/registry ## Installing diff --git a/.opencode/skills/reui/rules/components.md b/.opencode/skills/reui/rules/components.md index 8767bbb..0c24578 100644 --- a/.opencode/skills/reui/rules/components.md +++ b/.opencode/skills/reui/rules/components.md @@ -1,6 +1,6 @@ # ReUI components -The 17 ReUI building blocks: `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `filters`, `frame`, `icon-stack`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `sortable`, `stepper`, `timeline`, `tree`. Examples and blocks are composed from these. +The 19 ReUI building blocks: `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `event-calendar`, `filters`, `frame`, `gantt`, `icon-stack`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `sortable`, `stepper`, `timeline`, `tree`. Examples and blocks are composed from these. **Rule one: never guess a component's API. Read it first.** Call **`get_component(name)`** for its inline `api` (props + usage, no web fetch), and **share the result's `docsUrl`** (the component's API documentation page) with the user whenever you work with that component's API, so they have the full reference (the `/llms.txt` index is a further fallback). Then call **`get_examples(name)`** to install a worked example and copy real composition. The contracts below are first-try orientation (required props, composition shape, the one gotcha); the inline `api` is the full reference. No single block fits? Compose: search the components you need, read each `get_component`, install a `get_examples` example per component, and adapt. @@ -32,6 +32,34 @@ Common mistakes: - **Incorrect:** a raw `
` / hand-rolled pagination. **Correct:** use `data-grid`; read its API for sticky header, pagination, virtualization, row selection. - **Incorrect:** styling rows/cells with arbitrary classes. **Correct:** drive layout via `tableLayout` and the documented `ColumnMeta` (e.g. `cellClassName`, `headerTitle`). +## event-calendar + +**Required:** events via `events`/`onEventsChange` (controlled) or `defaultEvents` (uncontrolled), plus a height on the root. +**Shape:** + +```tsx + + + + +``` + +**Gotcha:** headless-first: `EventCalendarContent` renders the active view (month/week/day/days/agenda; a resource view activates when `resources` is passed) - there is no per-view JSX to compose. Events are `{ id, title, start, end (exclusive), allDay?, color?, recurrence?, resourceId? }`. Mutations flow through `onEventUpdate`/`canDropEvent` (return `false` to reject); the root needs an explicit height because it is a min-h-0 flex column. + +## gantt + +**Required:** `resources` (the left tree) plus bars via `events`/`defaultEvents` attached by `resourceId`. +**Shape:** + +```tsx + + + + +``` + +**Gotcha:** bars move along the time axis only (never across rows) and are all-day spans with exclusive `end`; `progress` is 0-100. Scales are `day | week | month | quarter | year`. Zoom control, infinite scroll, summary rollups, and row checkboxes are ON by default - turn off what you do not need. Same `onEventUpdate`/`canDropEvent` commit pipeline as `event-calendar`; the root needs an explicit height. + ## kanban **Required:** `value` (`Record`), `onValueChange`, `getItemValue` diff --git a/apps/web/src/components/reui-kit/index.ts b/apps/web/src/components/reui-kit/index.ts index 466bd24..5ed2012 100644 --- a/apps/web/src/components/reui-kit/index.ts +++ b/apps/web/src/components/reui-kit/index.ts @@ -1,7 +1,17 @@ export { applyFiltersToData, getActiveFilters, renderSingleSelectedLabel } from './filter-utils' export { CertStatusChart, GroupDomainsChart } from './dashboard-analytics' export { ResourcePage, type ResourcePageProps, type ResourcePageTab } from './resource-page' -export { KpiStatGrid, type KpiStatCard, type KpiStatVariant, type OpsKpiCard } from './kpi-stat-grid' +export { + KpiStatGrid, + KpiStatCard, + KpiStatCardTile, + kpiStatItemKey, + type KpiStatItem, + type KpiStatCardData, + type KpiStatCard as KpiStatCardType, + type KpiStatVariant, + type OpsKpiCard, +} from './kpi-stat-grid' export { QuickActionGrid, type QuickActionItem } from './quick-action-grid' export { OpsDashboard } from './ops-dashboard' export { KanbanBoard, KanbanBoardSkeleton, type KanbanBoardProps, type KanbanColumnConfig } from './kanban-board' 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 121215c..05caecc 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 '@cfdm/ui/components/item' import { cn } from '@cfdm/ui/lib/utils' +import { kpiCols } from './kpi-cols' +import { Item, ItemMedia } from '@cfdm/ui/components/item' import { Skeleton } from '@cfdm/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,103 +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 }) { @@ -164,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 ( @@ -193,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 8221473..7f1ea7c 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 '@cfdm/ui/components/item' import { cn } from '@cfdm/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 (
diff --git a/docs/ui-design-contract.md b/docs/ui-design-contract.md index 9a24c05..753e641 100644 --- a/docs/ui-design-contract.md +++ b/docs/ui-design-contract.md @@ -1,12 +1,19 @@ # UI Design Contract (ops apps) -Единый контракт для vps-tracker, CFDM и EvoBGP. Surface: **ReUI Frame**. Kit API: `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) +Карта: [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`** (ReUI 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). @@ -15,7 +22,8 @@ Settings: секции через Frame + `gap` (без hairline `Separator` п | Зона | Block | Preview | |------|-------|---------| | 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 (icon left + label/Badge + value ± variant; EvoBGP visual) | 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 | | Lists | `data-grid-filtering-2` | https://reui.io/preview/base/data-grid-filtering-2 | | Settings | `settings-16` + SettingRow | https://reui.io/preview/base/settings-16 | @@ -28,72 +36,84 @@ Settings: секции через Frame + `gap` (без hairline `Separator` п | Component | Role | |-----------|------| | `ResourcePage` | Frame + line tabs + Filters + DataGrid | -| `KpiStatGrid` | horizontal compact hybrid KPI tiles (`variant`, Badge) | -| `QuickActionGrid` | KPI-like quick action tiles under KPI (gated by `showQuickActions`) | +| `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 | KPI → **QuickActionGrid** → charts / rest | +| 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** production AppShell + ReUI [app-shell-12](https://reui.io/preview/base/app-shell-12). - -При переключении между vps-tracker / CFDM / EvoBGP меняются **только** sidebar nav labels/hrefs и `main` content. Разметка, ширина, фон и hover chrome идентичны. +Эталон разметки: production apps + ReUI [app-shell-12](https://reui.io/preview/base/app-shell-12). +При переключении между apps меняются **только** sidebar nav labels/hrefs и `main` content. | Токен / зона | Значение | |--------------|----------| -| `SIDEBAR_WIDTH` / `--sidebar-width` | `240px` (в `packages/ui` sidebar + Provider style) | -| Sidebar / hover colors | theme `--sidebar` / `--sidebar-accent` из `globals.css` — **без** AppShell `color-mix` override | +| `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 (`SidebarGroupContent`) → icons `size-4` → **NavUser** в `SidebarFooter` | +| 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) | -Запрещено в chrome: `SidebarRail`, sync-row footer, Search/Ctrl+K pill в header, issues Badge в header, muted/hover cascade на right-cluster, Provider `color-mix` для `--sidebar*`, `ModeToggle` в header (тема только в NavUser). +Запрещено в chrome: `SidebarRail`, sync-row footer, Search pill в header, issues Badge в header, `ModeToggle` в header (тема только в NavUser), Provider `color-mix` для `--sidebar*`. -NavUser (footer): avatar + name/email из portal JWT; dropdown — Настройки / Тема (segmented) / Выйти. Preview: [app-shell-1](https://reui.io/preview/base/app-shell-1). +NavUser (footer): avatar + name/email; dropdown — Настройки / Тема (segmented) / Выйти. Preview: [app-shell-1](https://reui.io/preview/base/app-shell-1). -App Switcher: source of truth — auth-portal `GET /api/v1/app-switcher`. Ids: `cfdm` · `vps` · `bgp`. Admin: portal `/admin/apps`. Локальный editor убран. +App Switcher: auth-portal `GET /api/v1/app-switcher`. Ids: `cfdm` · `vps` · `bgp` · `fw`. Admin: portal `/admin/apps`. -QuickActionGrid icons: только semantic **text** (`text-info` / `text-primary` / …) на kit `bg-muted` — без solid `bg-primary` fills. Preview: [stats-12](https://reui.io/preview/base/stats-12). +QuickActionGrid / KPI icons: только semantic **text** (`text-info` / `text-primary` / …) на kit `bg-muted` — без solid fills. ## System monitor -`SystemMonitorPopover` in header after AppsMenu (тема — в NavUser, не ModeToggle). Preview: https://reui.io/preview/base/app-shell-12 · https://reui.io/preview/base/app-shell-7 +`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 rows +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` + `@cfdm/ui`. +Primitives: MCP `plugin-shadcn-shadcn` + project `@scope/ui` (`@cfdm/ui` / `@evobgp/ui` / `@evofw/ui` / `@authportal/ui`). + +## License + +```env +# apps/web/.env.local (gitignored) +REUI_LICENSE_KEY= +``` + +`apps/web/components.json` → `@reui` с `Authorization: Bearer ${REUI_LICENSE_KEY}`. ## Spacing -- AppShell main: `gap-4 md:gap-6`, `px-4 py-4 md:px-6 md:py-5` (shared chrome) -- PageShell / content: `gap-4 md:gap-6` +- 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 (`variant="success"|"info"|"warning"`) — no raw `bg-emerald-*` +- Semantic tokens only — no raw `bg-emerald-*` ## Forbidden - Card as ops list/dashboard shell -- Hand-rolled data tables when ReUI DataGrid exists -- Hand-rolled KPI grids when `KpiStatGrid` exists +- Hand-rolled data tables when ReUI DataGrid / `ResourcePage` exists +- Hand-rolled KPI when `KpiStatGrid` exists +- Hand-rolled Quick Actions when `QuickActionGrid` exists +- SectionCards / DataGridCard as design эталон - Mixing Card and Frame surfaces on one ops screen