diff --git a/.cursor/rules/frontend-shadcn.mdc b/.cursor/rules/frontend-shadcn.mdc index cb40243..3541b27 100644 --- a/.cursor/rules/frontend-shadcn.mdc +++ b/.cursor/rules/frontend-shadcn.mdc @@ -58,7 +58,7 @@ Monorepo — [`frontend-monorepo.mdc`](frontend-monorepo.mdc). ReUI — [`reui-m 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/card-35 @reui/settings-16 @reui/auth-13 +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 ``` @@ -76,7 +76,7 @@ pnpm dlx shadcn@latest apply b2fA --only theme -y | ``, raw select | DataGrid / `Select` | | `bg-emerald-*` | semantic / ReUI `variant` | | Card как ops-shell | ReUI `Frame` | -| Hand-roll KPI | `KpiStatGrid` / [card-35](https://reui.io/preview/base/card-35) | +| Hand-roll KPI | `KpiStatGrid` / [stats-12](https://reui.io/preview/base/stats-12) | | `space-y-*` | `flex` + `gap-*` | | `@/components/ui/*` | `@cfdm/ui/components/*` | @@ -86,7 +86,7 @@ pnpm dlx shadcn@latest apply b2fA --only theme -y |------|------|---------| | Shell | `layout/app-shell.tsx` | [app-shell-12](https://reui.io/preview/base/app-shell-12) | | Login | `routes/login.tsx` | [auth-13](https://reui.io/preview/base/auth-13) | -| Dashboard | `routes/_auth/index.tsx` | [card-35](https://reui.io/preview/base/card-35) / dashboard-1 | +| Dashboard | `routes/_auth/index.tsx` | [stats-12](https://reui.io/preview/base/stats-12) / dashboard-1 | | Lists | `ResourcePage` | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) | | Settings | `settings/integrations.tsx` | [settings-16](https://reui.io/preview/base/settings-16) | diff --git a/.cursor/rules/frontend-ui-patterns.mdc b/.cursor/rules/frontend-ui-patterns.mdc index 9cbc889..f1da28d 100644 --- a/.cursor/rules/frontend-ui-patterns.mdc +++ b/.cursor/rules/frontend-ui-patterns.mdc @@ -27,7 +27,7 @@ alwaysApply: false | Зона | Preview | |------|---------| -| KPI | [card-35](https://reui.io/preview/base/card-35) | +| KPI | [stats-12](https://reui.io/preview/base/stats-12) (primary); [card-35](https://reui.io/preview/base/card-35) compact | | 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) | @@ -50,7 +50,7 @@ apps/web/src/components/ ← shared + domain + layout kanban-board.tsx ← kanban + KanbanBoardSkeleton detail-panel.tsx ← detail: Frame header/metrics settings-shell.tsx - ops-dashboard.tsx ← KPI card-35 + charts + ops-dashboard.tsx ← KPI stats-12 + charts catalog-board-toggle.tsx empty-state.tsx query-state.tsx @@ -73,7 +73,7 @@ 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 [card-35](https://reui.io/preview/base/card-35) | +| Dashboard KPI | `OpsDashboard` / `KpiStatGrid` | ReUI Frame [stats-12](https://reui.io/preview/base/stats-12) | | Empty | `EmptyState` | `Empty` | | Loading / Error | `QueryState` / kit skeletons | `Skeleton`, `Alert` | | Status | `StatusBadge` | ReUI `Badge` (`success`/`info`/`warning`) | @@ -127,7 +127,7 @@ Toggle «Доска» / «К каталогу» — всегда `outline` в `p ## Dashboard KPI -Эталон: [card-35](https://reui.io/preview/base/card-35) — muted **label** + крупный **value** + optional **hint**; клик по карточке через `to` (`Link`), без отдельных hint-links. +Эталон: [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 @@ -149,7 +149,7 @@ Toggle «Доска» / «К каталогу» — всегда `outline` в `p | 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 | [card-35](https://reui.io/preview/base/card-35) — на dashboard, certificates, domains, groups, services | +| 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) | diff --git a/.cursor/rules/reui-mcp.mdc b/.cursor/rules/reui-mcp.mdc index 1ed0fe4..5342bc9 100644 --- a/.cursor/rules/reui-mcp.mdc +++ b/.cursor/rules/reui-mcp.mdc @@ -36,7 +36,7 @@ alwaysApply: true | Data Grid | `@reui` | `@/components/reui/data-grid/*` → `ResourcePage` | | Filters | `@reui` | `@/components/reui/filters` | | Frame surface | `@reui` | `@/components/reui/frame` | -| KPI | block [card-35](https://reui.io/preview/base/card-35) | `reui-kit/KpiStatGrid` | +| KPI | block [stats-12](https://reui.io/preview/base/stats-12) | `reui-kit/KpiStatGrid` | | Semantic badge / alert | `@reui` | `@/components/reui/badge`, `@/components/reui/alert` | | Number / date / autocomplete / color / kanban | `@reui` | `@/components/reui/*` | @@ -68,7 +68,7 @@ alwaysApply: true **Kit:** `ResourcePage`, `KpiStatGrid`, `OpsDashboard`, `KanbanBoard`, `DetailPanel`, `SettingsShell` -**Blocks (reference):** `card-35`, `auth-13`, `app-shell-12`, `settings-16`, `settings-8`, `empty-state-12`, `form-7`, `data-grid-filtering-2`, `dashboard-1`, … +**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 @@ -83,7 +83,7 @@ REUI_LICENSE_KEY= | Зона | Preview | |------|---------| -| KPI | https://reui.io/preview/base/card-35 | +| KPI | https://reui.io/preview/base/stats-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 | diff --git a/apps/web/src/components/app-card.tsx b/apps/web/src/components/app-card.tsx deleted file mode 100644 index 62c8966..0000000 --- a/apps/web/src/components/app-card.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import type { ComponentProps } from 'react' -import { - Card, - CardAction, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from '@cfdm/ui/components/card' -import { cn } from '@cfdm/ui/lib/utils' - -export type AppCardProps = ComponentProps -export type AppCardHeaderProps = ComponentProps -export type AppCardTitleProps = ComponentProps -export type AppCardDescriptionProps = ComponentProps -export type AppCardActionProps = ComponentProps -export type AppCardContentProps = ComponentProps -export type AppCardFooterProps = ComponentProps - -export function AppCard({ className, ...props }: AppCardProps) { - return -} - -export function AppCardHeader({ className, ...props }: AppCardHeaderProps) { - return -} - -export function AppCardTitle({ className, ...props }: AppCardTitleProps) { - return -} - -export function AppCardDescription({ - className, - ...props -}: AppCardDescriptionProps) { - return -} - -export function AppCardAction({ className, ...props }: AppCardActionProps) { - return -} - -export function AppCardContent({ className, ...props }: AppCardContentProps) { - return -} - -export function AppCardFooter({ className, ...props }: AppCardFooterProps) { - return -} diff --git a/apps/web/src/components/cert-kpi-cards.tsx b/apps/web/src/components/cert-kpi-cards.tsx index 0967bbe..2a775e3 100644 --- a/apps/web/src/components/cert-kpi-cards.tsx +++ b/apps/web/src/components/cert-kpi-cards.tsx @@ -1,5 +1,10 @@ import { useMemo } from 'react' -import { ShieldCheckIcon } from 'lucide-react' +import { + AlertTriangleIcon, + CheckCircle2Icon, + ShieldAlertIcon, + ShieldCheckIcon, +} from 'lucide-react' import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid' const ACTIVE_STATUSES = new Set(['active', 'ok', 'synced']) @@ -28,6 +33,7 @@ interface CertKpiCardsProps { onSelectTab: (tabId: string) => void } +/** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */ export function CertKpiCards({ summary, total, @@ -47,6 +53,8 @@ export function CertKpiCards({ label: 'Всего', value: allCount, hint: 'Проверенные хосты', + icon: , + iconClassName: 'text-info', onSelect: () => onSelectTab('all'), selected: activeTab === 'all', }, @@ -55,6 +63,8 @@ export function CertKpiCards({ label: 'OK', value: okCount, hint: 'Активные сертификаты', + icon: , + iconClassName: 'text-success', onSelect: () => onSelectTab('active'), selected: activeTab === 'active', }, @@ -63,6 +73,8 @@ export function CertKpiCards({ label: 'Предупреждение', value: warningCount, hint: 'Истекают скоро', + icon: , + iconClassName: 'text-warning', onSelect: () => onSelectTab('warning'), selected: activeTab === 'warning', }, @@ -71,6 +83,8 @@ export function CertKpiCards({ label: 'Проблемы', value: problemCount, hint: 'Ошибка или истёк', + icon: , + iconClassName: 'text-destructive', onSelect: () => onSelectTab('expired'), selected: activeTab === 'expired', }, diff --git a/apps/web/src/components/domain-kpi-cards.tsx b/apps/web/src/components/domain-kpi-cards.tsx index 4c9e483..98452b3 100644 --- a/apps/web/src/components/domain-kpi-cards.tsx +++ b/apps/web/src/components/domain-kpi-cards.tsx @@ -1,5 +1,10 @@ import { useMemo } from 'react' -import { GlobeIcon } from 'lucide-react' +import { + CheckCircle2Icon, + FolderOpenIcon, + GlobeIcon, + ServerIcon, +} from 'lucide-react' import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid' import type { DomainListItem } from '@/lib/schemas' @@ -10,7 +15,7 @@ interface DomainKpiCardsProps { onSelectTab: (tabId: string) => void } -/** KPI strip — ReUI PRO [card-35](https://reui.io/preview/base/card-35) */ +/** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */ export function DomainKpiCards({ domains, activeTab, @@ -29,6 +34,8 @@ export function DomainKpiCards({ label: 'Всего', value: list.length, hint: 'Импортированные зоны', + icon: , + iconClassName: 'text-info', onSelect: () => onSelectTab('all'), selected: activeTab === 'all', }, @@ -37,6 +44,8 @@ export function DomainKpiCards({ label: 'Без группы', value: withoutGroup, hint: 'Не назначена группа', + icon: , + iconClassName: 'text-warning', onSelect: () => onSelectTab('without_group'), selected: activeTab === 'without_group', }, @@ -45,6 +54,8 @@ export function DomainKpiCards({ label: 'Active', value: active, hint: 'Статус Cloudflare', + icon: , + iconClassName: 'text-success', onSelect: () => onSelectTab('all'), selected: false, }, @@ -53,6 +64,8 @@ export function DomainKpiCards({ label: 'С сервисами', value: withServices, hint: 'Есть привязки', + icon: , + iconClassName: 'text-primary', onSelect: () => onSelectTab('all'), selected: false, }, diff --git a/apps/web/src/components/group-kpi-cards.tsx b/apps/web/src/components/group-kpi-cards.tsx index c29ad79..ed10793 100644 --- a/apps/web/src/components/group-kpi-cards.tsx +++ b/apps/web/src/components/group-kpi-cards.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import { FolderTreeIcon } from 'lucide-react' +import { FolderOpenIcon, FolderTreeIcon, FoldersIcon } from 'lucide-react' import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid' import type { GroupCatalogRow } from '@/components/columns/groups-columns' @@ -10,7 +10,7 @@ interface GroupKpiCardsProps { onSelectTab: (tabId: string) => void } -/** KPI strip — ReUI PRO [card-35](https://reui.io/preview/base/card-35) */ +/** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */ export function GroupKpiCards({ rows, activeTab, @@ -27,6 +27,8 @@ export function GroupKpiCards({ label: 'Всего', value: rows.length, hint: 'Группы доменов', + icon: , + iconClassName: 'text-info', onSelect: () => onSelectTab('all'), selected: activeTab === 'all', }, @@ -35,6 +37,8 @@ export function GroupKpiCards({ label: 'С доменами', value: withDomains, hint: 'Непустые группы', + icon: , + iconClassName: 'text-success', onSelect: () => onSelectTab('with_domains'), selected: activeTab === 'with_domains', }, @@ -43,6 +47,8 @@ export function GroupKpiCards({ label: 'Пустые', value: empty, hint: 'Без доменов', + icon: , + iconClassName: 'text-muted-foreground', onSelect: () => onSelectTab('empty'), selected: activeTab === 'empty', }, 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 b91aa27..0154f8f 100644 --- a/apps/web/src/components/reui-kit/kpi-stat-grid.tsx +++ b/apps/web/src/components/reui-kit/kpi-stat-grid.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from 'react' import { Link } from '@tanstack/react-router' import { Frame, FramePanel } from '@/components/reui/frame' +import { Item, ItemMedia } from '@cfdm/ui/components/item' import { cn } from '@cfdm/ui/lib/utils' import { Skeleton } from '@cfdm/ui/components/skeleton' @@ -13,6 +14,9 @@ export interface KpiStatCard { search?: Record onSelect?: () => void selected?: boolean + icon?: ReactNode + iconClassName?: string + footer?: ReactNode } /** @deprecated Use KpiStatCard */ @@ -27,16 +31,39 @@ interface KpiStatGridProps { skeletonCount?: number } +const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current' + +/** KPI body — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */ function KpiStatCardBody({ card }: { card: KpiStatCard }) { + const footerContent = + card.footer ?? + (card.hint ? ( + {card.hint} + ) : null) + return ( -
- {card.label} - - {card.value} - - {card.hint ? ( - {card.hint} +
+ {card.icon ? ( + + + {card.icon} + + ) : null} + +
+ + {card.value} + + {card.label} +
+ + {footerContent ?
{footerContent}
: null}
) } @@ -44,13 +71,12 @@ function KpiStatCardBody({ card }: { card: KpiStatCard }) { function KpiStatCardItem({ card }: { card: KpiStatCard }) { const interactive = Boolean(card.to || card.onSelect) const panelClass = cn( - 'relative isolate flex flex-col gap-3', + '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 transition-colors focus-within:ring-2', ) - // card-35: FramePanel — прямой child грида; ссылка/кнопка внутри панели if (card.to) { return ( @@ -91,9 +117,12 @@ function KpiStatGridSkeleton({ count }: { count: number }) {
{Array.from({ length: count }).map((_, index) => ( - - - + + +
+ + +
))} diff --git a/apps/web/src/components/service-kpi-cards.tsx b/apps/web/src/components/service-kpi-cards.tsx index 493e42b..1967fda 100644 --- a/apps/web/src/components/service-kpi-cards.tsx +++ b/apps/web/src/components/service-kpi-cards.tsx @@ -1,5 +1,10 @@ import { useMemo } from 'react' -import { ServerIcon } from 'lucide-react' +import { + FolderOpenIcon, + PowerIcon, + PowerOffIcon, + ServerIcon, +} from 'lucide-react' import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid' import type { ServiceCatalogRow } from '@/components/columns/services-columns' @@ -10,7 +15,7 @@ interface ServiceKpiCardsProps { onSelectTab: (tabId: string) => void } -/** KPI strip — ReUI PRO [card-35](https://reui.io/preview/base/card-35) */ +/** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */ export function ServiceKpiCards({ rows, activeTab, @@ -29,6 +34,8 @@ export function ServiceKpiCards({ label: 'Всего', value: rows.length, hint: 'Сервисы', + icon: , + iconClassName: 'text-info', onSelect: () => onSelectTab('all'), selected: activeTab === 'all', }, @@ -37,6 +44,8 @@ export function ServiceKpiCards({ label: 'Включены', value: enabled, hint: 'Активные', + icon: , + iconClassName: 'text-success', onSelect: () => onSelectTab('enabled'), selected: activeTab === 'enabled', }, @@ -45,6 +54,8 @@ export function ServiceKpiCards({ label: 'Выключены', value: disabled, hint: 'Отключены', + icon: , + iconClassName: 'text-muted-foreground', onSelect: () => onSelectTab('disabled'), selected: activeTab === 'disabled', }, @@ -53,6 +64,8 @@ export function ServiceKpiCards({ label: 'Без группы', value: ungrouped, hint: down > 0 ? `Down: ${down}` : 'Не в группе', + icon: , + iconClassName: 'text-warning', onSelect: () => onSelectTab('ungrouped'), selected: activeTab === 'ungrouped', }, diff --git a/apps/web/src/lib/ui-surface.ts b/apps/web/src/lib/ui-surface.ts new file mode 100644 index 0000000..a8b683f --- /dev/null +++ b/apps/web/src/lib/ui-surface.ts @@ -0,0 +1,15 @@ +/** + * CFDM UI surface contract (ReUI Pro). + * + * Ops / list / dashboard / detail / settings: **Frame** only. + * KPI: **stats-12** via `reui-kit/KpiStatGrid` (card-35 = compact strip). + * Lists: **data-grid-filtering-2** via `reui-kit/ResourcePage`. + * + * @see https://reui.io/preview/base/stats-12 + * @see https://reui.io/preview/base/data-grid-filtering-2 + * @see docs/ui-design-contract.md + */ +export const UI_SURFACE = 'frame' as const + +/** Grid for ReUI stats-12 KPI rows (3–6 tiles). */ +export const kpiStatGridClassName = '@container w-full' diff --git a/apps/web/src/routes/_auth/index.tsx b/apps/web/src/routes/_auth/index.tsx index 896ebc0..10a733f 100644 --- a/apps/web/src/routes/_auth/index.tsx +++ b/apps/web/src/routes/_auth/index.tsx @@ -5,6 +5,9 @@ import { ActivityIcon, AlertTriangleIcon, FolderTreeIcon, + GlobeIcon, + ServerIcon, + ShieldCheckIcon, } from 'lucide-react' import { certificatesQueryOptions, @@ -173,6 +176,8 @@ function DashboardPage() { ungroupedCount > 0 ? `${ungroupedCount} без группы` : 'Все в группах', + icon: , + iconClassName: 'text-info', to: '/domains', }, { @@ -180,6 +185,8 @@ function DashboardPage() { label: 'Группы', value: groups?.length ?? 0, hint: `${groupChartData.filter((g) => g.count > 0).length} с зонами`, + icon: , + iconClassName: 'text-primary', to: '/groups', }, { @@ -191,6 +198,8 @@ function DashboardPage() { : ungroupedServiceCount ? `${ungroupedServiceCount} без группы` : undefined, + icon: , + iconClassName: 'text-success', to: '/services', search: { domainId: undefined }, }, @@ -202,6 +211,8 @@ function DashboardPage() { certWarnings > 0 ? `${certOk} в норме · ${certWarnings} внимания` : `${certOk} в норме`, + icon: , + iconClassName: 'text-warning', to: '/certificates', }, ] diff --git a/docs/Home.md b/docs/Home.md index 82fd949..fe1e527 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -2,6 +2,10 @@ Wiki home — synced from repository on `main` when this file changes. +## UI + +Design contract (Frame surface, ReUI kit): [`docs/ui-design-contract.md`](ui-design-contract.md) + ## Overview Manage Cloudflare zones, DNS records, domain groups, and TLS certificate expiry from a single UI. diff --git a/docs/ui-design-contract.md b/docs/ui-design-contract.md new file mode 100644 index 0000000..7cfbc44 --- /dev/null +++ b/docs/ui-design-contract.md @@ -0,0 +1,62 @@ +# UI Design Contract (ops apps) + +Единый контракт для vps-tracker, CFDM и EvoBGP. Surface: **ReUI Frame**. Kit API: `apps/web/src/components/reui-kit/`. + +Карта: [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) + +## Surface + +```ts +// apps/web/src/lib/ui-surface.ts +export const UI_SURFACE = 'frame' as const +``` + +Ops / list / dashboard / detail / settings — только **Frame**, не shadcn Card как shell. Не смешивать Card и Frame на одном ops-экране. + +## Canonical PRO references + +| Зона | Block | Preview | +|------|-------|---------| +| Shell | `app-shell-12` (+ cmdk/monitor где нужно) | https://reui.io/preview/base/app-shell-12 | +| KPI | `stats-12` (primary); `card-35` compact strip | https://reui.io/preview/base/stats-12 · https://reui.io/preview/base/card-35 | +| 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 | +| Auth | `auth-13` | https://reui.io/preview/base/auth-13 | +| Empty | `empty-state-12` | https://reui.io/preview/base/empty-state-12 | +| Forms | `form-7` → Sheet/Drawer | https://reui.io/preview/base/form-7 | + +## Kit API (`reui-kit/`) + +| Component | Role | +|-----------|------| +| `ResourcePage` | Frame + line tabs + Filters + DataGrid | +| `KpiStatGrid` | stats-12 KPI tiles | +| `OpsDashboard` | KPI + charts + attention queue | +| `SettingsShell` | settings nav + Outlet | +| `DetailPanel` | detail Frame sections | +| `filter-utils` | apply/clear ReUI Filters | + +## MCP workflow + +1. MCP `user-reui` — `search` / `get_block` / `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 +5. `validate_usage` / `get_audit_checklist` + +Primitives: MCP `plugin-shadcn-shadcn` + `@cfdm/ui`. + +## Spacing + +- Main: `gap-4 md:gap-6`, `px-4 md:px-6` +- 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-*` + +## Forbidden + +- Card as ops list/dashboard shell +- Hand-rolled data tables when ReUI DataGrid exists +- Hand-rolled KPI grids when `KpiStatGrid` exists +- Mixing Card and Frame surfaces on one ops screen