Docker / build (push) Failing after 22s
Унифицировать оболочку ops-экранов, IconTile, skip-link и контракт UI. Co-authored-by: Cursor <cursoragent@cursor.com>
181 lines
8.7 KiB
Plaintext
181 lines
8.7 KiB
Plaintext
---
|
||
description: Единые UI-паттерны web — shared components, docs workflow, матрица стандартизации
|
||
globs: apps/web/**/*
|
||
alwaysApply: false
|
||
---
|
||
|
||
# Frontend UI Patterns
|
||
|
||
См. также: [`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
|
||
|
||
Проект использует **ReUI Frame** (`surface: frame`), не shadcn Card как оболочку list/ops-экранов. Эталон списка: [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2).
|
||
|
||
## Обязательные референсы по зонам
|
||
|
||
| Зона | Preview |
|
||
|------|---------|
|
||
| KPI | [stats-12](https://reui.io/preview/base/stats-12) — EvoBGP hybrid SoT |
|
||
| Quick Actions | [stats-12](https://reui.io/preview/base/stats-12) · [card-12](https://reui.io/preview/base/card-12) → `QuickActionGrid` |
|
||
| List | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) |
|
||
| Settings | [settings-16](https://reui.io/preview/base/settings-16), [Application Settings](https://reui.io/blocks/application/settings) |
|
||
| Settings rows / Health-check | [settings-2](https://reui.io/preview/base/settings-2), [settings-3](https://reui.io/preview/base/settings-3) |
|
||
| Auth | [auth-13](https://reui.io/preview/base/auth-13) |
|
||
| Empty | [empty-state-12](https://reui.io/preview/base/empty-state-12) |
|
||
| Forms | [form-7](https://reui.io/preview/base/form-7) |
|
||
| Shell | [app-shell-12](https://reui.io/preview/base/app-shell-12) |
|
||
|
||
**SettingRow:** `FieldSeparator` opt-in (`separated`); не между toggle-row и nested fields (Health-check). Settings-секции — отдельные Frame + `gap`, без hairline под PageHeader.
|
||
|
||
## Иерархия компонентов
|
||
|
||
```
|
||
@cfdm/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
|
||
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` | — |
|
||
| List page | `ResourcePage` | ReUI `Frame` + `data-grid` + `filters` + shadcn `Tabs` `variant="line"` |
|
||
| Detail | `DetailPanel` | ReUI `Frame` |
|
||
| Settings | `SettingsShell` | — |
|
||
| Dashboard KPI | `OpsDashboard` / `KpiStatGrid` | ReUI Frame [stats-12](https://reui.io/preview/base/stats-12) hybrid |
|
||
| Quick Actions | `QuickActionGrid` | Frame tiles + Badge «Перейти» |
|
||
| Empty | `EmptyState` | `Empty` |
|
||
| Loading / Error | `QueryState` / kit skeletons | `Skeleton`, `Alert` |
|
||
| Status | `StatusBadge` | ReUI `Badge` (`success`/`info`/`warning`) |
|
||
| Create/Edit | `FormSheet` + `*-edit-sheet.tsx` | `Sheet`, `Field` |
|
||
| Form field | `FormField` | `Field`, `Input`, `Select` |
|
||
| Submit button | `LoadingButton` | `Button`, `Spinner` |
|
||
| Delete confirm | `ConfirmDialog` | `AlertDialog` |
|
||
| Nav | `AppSidebar` (`isActive` обязателен) | `Sidebar` |
|
||
| Breadcrumbs | `SiteHeader` | `Breadcrumb` |
|
||
| Dates | `lib/format.ts` | — |
|
||
|
||
## Header actions
|
||
|
||
Независимые CTA в header → `flex shrink-0 flex-wrap items-center justify-end gap-2`.
|
||
|
||
**Не** оборачивать в `ButtonGroup` (он склеивает кнопки). `ButtonGroup` — только для связанных контролок (filter chips и т.п.).
|
||
|
||
Max 1 primary (`default`) на экран; остальные `outline` / `ghost`.
|
||
|
||
Max 1 primary (`default`) на экран; остальные `outline` / `ghost`.
|
||
|
||
## 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`
|
||
|
||
## Dashboard KPI
|
||
|
||
Эталон: [stats-12](https://reui.io/preview/base/stats-12) — icon tile + value + label + optional badge/footer; клик через `to` (`Link`) / `onSelect`. Compact strip: [card-35](https://reui.io/preview/base/card-35).
|
||
|
||
## Sidebar
|
||
|
||
- Каждый `SidebarMenuButton` получает `isActive` от pathname (`useRouterState`)
|
||
- `--sidebar-accent` в `AppShell` — заметный mix primary (~14%), не 5%
|
||
|
||
## Overlay selection
|
||
|
||
| Сценарий | Компонент |
|
||
|----------|-----------|
|
||
| Create/edit форма | `Sheet` |
|
||
| Destructive confirm | `AlertDialog` via `ConfirmDialog` |
|
||
| Modal preview | `Dialog` |
|
||
|
||
## Block registry
|
||
|
||
| Зона | Block / эталон |
|
||
|------|----------------|
|
||
| Shell | [app-shell-12](https://reui.io/preview/base/app-shell-12) |
|
||
| List + tabs + filters | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) |
|
||
| Line tabs | [c-tabs-2](https://reui.io/preview/base/components/c-tabs-2) |
|
||
| KPI | [stats-12](https://reui.io/preview/base/stats-12) — на dashboard, certificates, domains, groups, services |
|
||
| Login | [auth-13](https://reui.io/preview/base/auth-13) |
|
||
| Settings | [settings-16](https://reui.io/preview/base/settings-16) / [settings-6](https://reui.io/preview/base/settings-6) |
|
||
| Empty | [empty-state-12](https://reui.io/preview/base/empty-state-12) |
|
||
|
||
## Spacing
|
||
|
||
```
|
||
AppShell main: gap-4 md:gap-6, px-4 md:px-6, py-4 md:py-5
|
||
(--sidebar-width: 240px; header h-12; AppSwitcher + AppsMenu — Shared App Shell chrome)
|
||
PageShell: gap-4 md:gap-6
|
||
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
|
||
```
|
||
|
||
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).
|
||
|
||
**Запрещено:** `space-y-*`, raw colors (`bg-emerald-*`), custom empty divs, page-level Spinner / plain «Загрузка…» без Skeleton.
|
||
|
||
## UX/UI (состояния данных)
|
||
|
||
Каждый блок: **default, hover, focus, disabled, empty, loading, error**.
|
||
|
||
- **Loading** — `Skeleton` / `ResourcePage` skeleton / `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`
|