-
{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