feat(ui): обновить KPI с card-35 на stats-12 во всех компонентах
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m27s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m52s
Build, Test, and Push CFDM Docker Image / create-release (push) Skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Failing after 6s

Заменены ссылки на блоки card-35 на stats-12 в документации и компонентах, включая KpiStatGrid, CertKpiCards, DomainKpiCards, GroupKpiCards и ServiceKpiCards. Удален неиспользуемый компонент AppCard. Добавлены иконки для улучшения визуализации KPI.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-17 15:30:25 +07:00
co-authored by Cursor
parent 704624cbe7
commit d8340e8a0b
13 changed files with 197 additions and 80 deletions
+3 -3
View File
@@ -58,7 +58,7 @@ Monorepo — [`frontend-monorepo.mdc`](frontend-monorepo.mdc). ReUI — [`reui-m
cd apps/web cd apps/web
pnpm dlx shadcn@latest add button field input ... 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/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 pnpm dlx shadcn@latest apply b2fA --only theme -y
``` ```
@@ -76,7 +76,7 @@ pnpm dlx shadcn@latest apply b2fA --only theme -y
| `<table>`, raw select | DataGrid / `Select` | | `<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` / [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-*` | | `space-y-*` | `flex` + `gap-*` |
| `@/components/ui/*` | `@cfdm/ui/components/*` | | `@/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) | | 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) | | 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) | | 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) | | Settings | `settings/integrations.tsx` | [settings-16](https://reui.io/preview/base/settings-16) |
+5 -5
View File
@@ -27,7 +27,7 @@ alwaysApply: false
| Зона | Preview | | Зона | 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) | | 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 | [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) | | 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 kanban-board.tsx ← kanban + KanbanBoardSkeleton
detail-panel.tsx ← detail: Frame header/metrics detail-panel.tsx ← detail: Frame header/metrics
settings-shell.tsx settings-shell.tsx
ops-dashboard.tsx ← KPI card-35 + charts ops-dashboard.tsx ← KPI stats-12 + charts
catalog-board-toggle.tsx catalog-board-toggle.tsx
empty-state.tsx empty-state.tsx
query-state.tsx query-state.tsx
@@ -73,7 +73,7 @@ apps/web/src/components/ ← shared + domain + layout
| Kanban | `KanbanBoard` / `KanbanBoardSkeleton` | ReUI `kanban` + `Frame` | | Kanban | `KanbanBoard` / `KanbanBoardSkeleton` | ReUI `kanban` + `Frame` |
| Detail | `DetailPanel` | ReUI `Frame` | | Detail | `DetailPanel` | ReUI `Frame` |
| Settings | `SettingsShell` | — | | 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` | | Empty | `EmptyState` | `Empty` |
| Loading / Error | `QueryState` / kit skeletons | `Skeleton`, `Alert` | | Loading / Error | `QueryState` / kit skeletons | `Skeleton`, `Alert` |
| Status | `StatusBadge` | ReUI `Badge` (`success`/`info`/`warning`) | | Status | `StatusBadge` | ReUI `Badge` (`success`/`info`/`warning`) |
@@ -127,7 +127,7 @@ Toggle «Доска» / «К каталогу» — всегда `outline` в `p
## Dashboard KPI ## 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 ## Sidebar
@@ -149,7 +149,7 @@ Toggle «Доска» / «К каталогу» — всегда `outline` в `p
| Shell | [app-shell-12](https://reui.io/preview/base/app-shell-12) | | 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) | | 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) | | 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) | | 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) | | 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) | | Empty | [empty-state-12](https://reui.io/preview/base/empty-state-12) |
+3 -3
View File
@@ -36,7 +36,7 @@ alwaysApply: true
| 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 [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` | | 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/*` |
@@ -68,7 +68,7 @@ alwaysApply: true
**Kit:** `ResourcePage`, `KpiStatGrid`, `OpsDashboard`, `KanbanBoard`, `DetailPanel`, `SettingsShell` **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 ## License
@@ -83,7 +83,7 @@ REUI_LICENSE_KEY=
| Зона | Preview | | Зона | 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 | | 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 |
-50
View File
@@ -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<typeof Card>
export type AppCardHeaderProps = ComponentProps<typeof CardHeader>
export type AppCardTitleProps = ComponentProps<typeof CardTitle>
export type AppCardDescriptionProps = ComponentProps<typeof CardDescription>
export type AppCardActionProps = ComponentProps<typeof CardAction>
export type AppCardContentProps = ComponentProps<typeof CardContent>
export type AppCardFooterProps = ComponentProps<typeof CardFooter>
export function AppCard({ className, ...props }: AppCardProps) {
return <Card className={cn(className)} {...props} />
}
export function AppCardHeader({ className, ...props }: AppCardHeaderProps) {
return <CardHeader className={cn(className)} {...props} />
}
export function AppCardTitle({ className, ...props }: AppCardTitleProps) {
return <CardTitle className={cn(className)} {...props} />
}
export function AppCardDescription({
className,
...props
}: AppCardDescriptionProps) {
return <CardDescription className={cn(className)} {...props} />
}
export function AppCardAction({ className, ...props }: AppCardActionProps) {
return <CardAction className={cn(className)} {...props} />
}
export function AppCardContent({ className, ...props }: AppCardContentProps) {
return <CardContent className={cn(className)} {...props} />
}
export function AppCardFooter({ className, ...props }: AppCardFooterProps) {
return <CardFooter className={cn(className)} {...props} />
}
+15 -1
View File
@@ -1,5 +1,10 @@
import { useMemo } from 'react' 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' import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid'
const ACTIVE_STATUSES = new Set(['active', 'ok', 'synced']) const ACTIVE_STATUSES = new Set(['active', 'ok', 'synced'])
@@ -28,6 +33,7 @@ interface CertKpiCardsProps {
onSelectTab: (tabId: string) => void onSelectTab: (tabId: string) => void
} }
/** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */
export function CertKpiCards({ export function CertKpiCards({
summary, summary,
total, total,
@@ -47,6 +53,8 @@ export function CertKpiCards({
label: 'Всего', label: 'Всего',
value: allCount, value: allCount,
hint: 'Проверенные хосты', hint: 'Проверенные хосты',
icon: <ShieldCheckIcon aria-hidden />,
iconClassName: 'text-info',
onSelect: () => onSelectTab('all'), onSelect: () => onSelectTab('all'),
selected: activeTab === 'all', selected: activeTab === 'all',
}, },
@@ -55,6 +63,8 @@ export function CertKpiCards({
label: 'OK', label: 'OK',
value: okCount, value: okCount,
hint: 'Активные сертификаты', hint: 'Активные сертификаты',
icon: <CheckCircle2Icon aria-hidden />,
iconClassName: 'text-success',
onSelect: () => onSelectTab('active'), onSelect: () => onSelectTab('active'),
selected: activeTab === 'active', selected: activeTab === 'active',
}, },
@@ -63,6 +73,8 @@ export function CertKpiCards({
label: 'Предупреждение', label: 'Предупреждение',
value: warningCount, value: warningCount,
hint: 'Истекают скоро', hint: 'Истекают скоро',
icon: <AlertTriangleIcon aria-hidden />,
iconClassName: 'text-warning',
onSelect: () => onSelectTab('warning'), onSelect: () => onSelectTab('warning'),
selected: activeTab === 'warning', selected: activeTab === 'warning',
}, },
@@ -71,6 +83,8 @@ export function CertKpiCards({
label: 'Проблемы', label: 'Проблемы',
value: problemCount, value: problemCount,
hint: 'Ошибка или истёк', hint: 'Ошибка или истёк',
icon: <ShieldAlertIcon aria-hidden />,
iconClassName: 'text-destructive',
onSelect: () => onSelectTab('expired'), onSelect: () => onSelectTab('expired'),
selected: activeTab === 'expired', selected: activeTab === 'expired',
}, },
+15 -2
View File
@@ -1,5 +1,10 @@
import { useMemo } from 'react' 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 { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid'
import type { DomainListItem } from '@/lib/schemas' import type { DomainListItem } from '@/lib/schemas'
@@ -10,7 +15,7 @@ interface DomainKpiCardsProps {
onSelectTab: (tabId: string) => void 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({ export function DomainKpiCards({
domains, domains,
activeTab, activeTab,
@@ -29,6 +34,8 @@ export function DomainKpiCards({
label: 'Всего', label: 'Всего',
value: list.length, value: list.length,
hint: 'Импортированные зоны', hint: 'Импортированные зоны',
icon: <GlobeIcon aria-hidden />,
iconClassName: 'text-info',
onSelect: () => onSelectTab('all'), onSelect: () => onSelectTab('all'),
selected: activeTab === 'all', selected: activeTab === 'all',
}, },
@@ -37,6 +44,8 @@ export function DomainKpiCards({
label: 'Без группы', label: 'Без группы',
value: withoutGroup, value: withoutGroup,
hint: 'Не назначена группа', hint: 'Не назначена группа',
icon: <FolderOpenIcon aria-hidden />,
iconClassName: 'text-warning',
onSelect: () => onSelectTab('without_group'), onSelect: () => onSelectTab('without_group'),
selected: activeTab === 'without_group', selected: activeTab === 'without_group',
}, },
@@ -45,6 +54,8 @@ export function DomainKpiCards({
label: 'Active', label: 'Active',
value: active, value: active,
hint: 'Статус Cloudflare', hint: 'Статус Cloudflare',
icon: <CheckCircle2Icon aria-hidden />,
iconClassName: 'text-success',
onSelect: () => onSelectTab('all'), onSelect: () => onSelectTab('all'),
selected: false, selected: false,
}, },
@@ -53,6 +64,8 @@ export function DomainKpiCards({
label: 'С сервисами', label: 'С сервисами',
value: withServices, value: withServices,
hint: 'Есть привязки', hint: 'Есть привязки',
icon: <ServerIcon aria-hidden />,
iconClassName: 'text-primary',
onSelect: () => onSelectTab('all'), onSelect: () => onSelectTab('all'),
selected: false, selected: false,
}, },
+8 -2
View File
@@ -1,5 +1,5 @@
import { useMemo } from 'react' 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 { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid'
import type { GroupCatalogRow } from '@/components/columns/groups-columns' import type { GroupCatalogRow } from '@/components/columns/groups-columns'
@@ -10,7 +10,7 @@ interface GroupKpiCardsProps {
onSelectTab: (tabId: string) => void 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({ export function GroupKpiCards({
rows, rows,
activeTab, activeTab,
@@ -27,6 +27,8 @@ export function GroupKpiCards({
label: 'Всего', label: 'Всего',
value: rows.length, value: rows.length,
hint: 'Группы доменов', hint: 'Группы доменов',
icon: <FolderTreeIcon aria-hidden />,
iconClassName: 'text-info',
onSelect: () => onSelectTab('all'), onSelect: () => onSelectTab('all'),
selected: activeTab === 'all', selected: activeTab === 'all',
}, },
@@ -35,6 +37,8 @@ export function GroupKpiCards({
label: 'С доменами', label: 'С доменами',
value: withDomains, value: withDomains,
hint: 'Непустые группы', hint: 'Непустые группы',
icon: <FoldersIcon aria-hidden />,
iconClassName: 'text-success',
onSelect: () => onSelectTab('with_domains'), onSelect: () => onSelectTab('with_domains'),
selected: activeTab === 'with_domains', selected: activeTab === 'with_domains',
}, },
@@ -43,6 +47,8 @@ export function GroupKpiCards({
label: 'Пустые', label: 'Пустые',
value: empty, value: empty,
hint: 'Без доменов', hint: 'Без доменов',
icon: <FolderOpenIcon aria-hidden />,
iconClassName: 'text-muted-foreground',
onSelect: () => onSelectTab('empty'), onSelect: () => onSelectTab('empty'),
selected: activeTab === 'empty', selected: activeTab === 'empty',
}, },
@@ -1,6 +1,7 @@
import type { ReactNode } from 'react' import type { 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 { Item, ItemMedia } from '@cfdm/ui/components/item'
import { cn } from '@cfdm/ui/lib/utils' import { cn } from '@cfdm/ui/lib/utils'
import { Skeleton } from '@cfdm/ui/components/skeleton' import { Skeleton } from '@cfdm/ui/components/skeleton'
@@ -13,6 +14,9 @@ export interface KpiStatCard {
search?: Record<string, unknown> search?: Record<string, unknown>
onSelect?: () => void onSelect?: () => void
selected?: boolean selected?: boolean
icon?: ReactNode
iconClassName?: string
footer?: ReactNode
} }
/** @deprecated Use KpiStatCard */ /** @deprecated Use KpiStatCard */
@@ -27,16 +31,39 @@ interface KpiStatGridProps {
skeletonCount?: number 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 }) { function KpiStatCardBody({ card }: { card: KpiStatCard }) {
const footerContent =
card.footer ??
(card.hint ? (
<span className="text-muted-foreground text-xs leading-snug">{card.hint}</span>
) : null)
return ( return (
<div className="relative z-10 flex flex-col gap-3"> <div className="relative z-10 flex h-full flex-col items-start gap-4">
<span className="text-muted-foreground text-sm">{card.label}</span> {card.icon ? (
<span className="text-foreground text-xl leading-tight font-semibold tabular-nums"> <Item
{card.value} className={cn(
</span> 'border-background bg-muted flex size-10.5 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.hint ? ( card.iconClassName ?? DEFAULT_ICON_CLASS,
<span className="text-muted-foreground text-xs leading-snug">{card.hint}</span> )}
>
<ItemMedia variant="icon" className="size-auto">
{card.icon}
</ItemMedia>
</Item>
) : null} ) : null}
<div className="flex flex-col gap-0.5">
<span className="text-foreground text-2xl leading-none font-bold tabular-nums">
{card.value}
</span>
<span className="text-muted-foreground text-sm font-medium">{card.label}</span>
</div>
{footerContent ? <div className="mt-auto w-full">{footerContent}</div> : null}
</div> </div>
) )
} }
@@ -44,13 +71,12 @@ function KpiStatCardBody({ card }: { card: KpiStatCard }) {
function KpiStatCardItem({ card }: { card: KpiStatCard }) { function KpiStatCardItem({ card }: { card: KpiStatCard }) {
const interactive = Boolean(card.to || card.onSelect) const interactive = Boolean(card.to || card.onSelect)
const panelClass = cn( 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', card.selected && 'ring-primary/30 bg-muted/30 ring-1',
interactive && interactive &&
'hover:bg-muted/40 focus-within:ring-ring transition-colors focus-within:ring-2', 'hover:bg-muted/40 focus-within:ring-ring transition-colors focus-within:ring-2',
) )
// card-35: FramePanel — прямой child грида; ссылка/кнопка внутри панели
if (card.to) { if (card.to) {
return ( return (
<FramePanel className={panelClass}> <FramePanel className={panelClass}>
@@ -91,9 +117,12 @@ function KpiStatGridSkeleton({ count }: { count: number }) {
<Frame className="@container w-full"> <Frame className="@container w-full">
<div className="grid gap-1 @2xl:grid-cols-2 @4xl:grid-cols-4"> <div className="grid gap-1 @2xl:grid-cols-2 @4xl:grid-cols-4">
{Array.from({ length: count }).map((_, index) => ( {Array.from({ length: count }).map((_, index) => (
<FramePanel key={index} className="flex flex-col gap-3"> <FramePanel key={index} className="flex flex-col items-start gap-4">
<Skeleton className="h-4 w-20" /> <Skeleton className="size-10.5 rounded-lg" />
<Skeleton className="h-7 w-14" /> <div className="flex w-full flex-col gap-1.5">
<Skeleton className="h-7 w-14" />
<Skeleton className="h-4 w-20" />
</div>
<Skeleton className="h-3 w-28" /> <Skeleton className="h-3 w-28" />
</FramePanel> </FramePanel>
))} ))}
+15 -2
View File
@@ -1,5 +1,10 @@
import { useMemo } from 'react' 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 { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid'
import type { ServiceCatalogRow } from '@/components/columns/services-columns' import type { ServiceCatalogRow } from '@/components/columns/services-columns'
@@ -10,7 +15,7 @@ interface ServiceKpiCardsProps {
onSelectTab: (tabId: string) => void 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({ export function ServiceKpiCards({
rows, rows,
activeTab, activeTab,
@@ -29,6 +34,8 @@ export function ServiceKpiCards({
label: 'Всего', label: 'Всего',
value: rows.length, value: rows.length,
hint: 'Сервисы', hint: 'Сервисы',
icon: <ServerIcon aria-hidden />,
iconClassName: 'text-info',
onSelect: () => onSelectTab('all'), onSelect: () => onSelectTab('all'),
selected: activeTab === 'all', selected: activeTab === 'all',
}, },
@@ -37,6 +44,8 @@ export function ServiceKpiCards({
label: 'Включены', label: 'Включены',
value: enabled, value: enabled,
hint: 'Активные', hint: 'Активные',
icon: <PowerIcon aria-hidden />,
iconClassName: 'text-success',
onSelect: () => onSelectTab('enabled'), onSelect: () => onSelectTab('enabled'),
selected: activeTab === 'enabled', selected: activeTab === 'enabled',
}, },
@@ -45,6 +54,8 @@ export function ServiceKpiCards({
label: 'Выключены', label: 'Выключены',
value: disabled, value: disabled,
hint: 'Отключены', hint: 'Отключены',
icon: <PowerOffIcon aria-hidden />,
iconClassName: 'text-muted-foreground',
onSelect: () => onSelectTab('disabled'), onSelect: () => onSelectTab('disabled'),
selected: activeTab === 'disabled', selected: activeTab === 'disabled',
}, },
@@ -53,6 +64,8 @@ export function ServiceKpiCards({
label: 'Без группы', label: 'Без группы',
value: ungrouped, value: ungrouped,
hint: down > 0 ? `Down: ${down}` : 'Не в группе', hint: down > 0 ? `Down: ${down}` : 'Не в группе',
icon: <FolderOpenIcon aria-hidden />,
iconClassName: 'text-warning',
onSelect: () => onSelectTab('ungrouped'), onSelect: () => onSelectTab('ungrouped'),
selected: activeTab === 'ungrouped', selected: activeTab === 'ungrouped',
}, },
+15
View File
@@ -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 (36 tiles). */
export const kpiStatGridClassName = '@container w-full'
+11
View File
@@ -5,6 +5,9 @@ import {
ActivityIcon, ActivityIcon,
AlertTriangleIcon, AlertTriangleIcon,
FolderTreeIcon, FolderTreeIcon,
GlobeIcon,
ServerIcon,
ShieldCheckIcon,
} from 'lucide-react' } from 'lucide-react'
import { import {
certificatesQueryOptions, certificatesQueryOptions,
@@ -173,6 +176,8 @@ function DashboardPage() {
ungroupedCount > 0 ungroupedCount > 0
? `${ungroupedCount} без группы` ? `${ungroupedCount} без группы`
: 'Все в группах', : 'Все в группах',
icon: <GlobeIcon aria-hidden />,
iconClassName: 'text-info',
to: '/domains', to: '/domains',
}, },
{ {
@@ -180,6 +185,8 @@ function DashboardPage() {
label: 'Группы', label: 'Группы',
value: groups?.length ?? 0, value: groups?.length ?? 0,
hint: `${groupChartData.filter((g) => g.count > 0).length} с зонами`, hint: `${groupChartData.filter((g) => g.count > 0).length} с зонами`,
icon: <FolderTreeIcon aria-hidden />,
iconClassName: 'text-primary',
to: '/groups', to: '/groups',
}, },
{ {
@@ -191,6 +198,8 @@ function DashboardPage() {
: ungroupedServiceCount : ungroupedServiceCount
? `${ungroupedServiceCount} без группы` ? `${ungroupedServiceCount} без группы`
: undefined, : undefined,
icon: <ServerIcon aria-hidden />,
iconClassName: 'text-success',
to: '/services', to: '/services',
search: { domainId: undefined }, search: { domainId: undefined },
}, },
@@ -202,6 +211,8 @@ function DashboardPage() {
certWarnings > 0 certWarnings > 0
? `${certOk} в норме · ${certWarnings} внимания` ? `${certOk} в норме · ${certWarnings} внимания`
: `${certOk} в норме`, : `${certOk} в норме`,
icon: <ShieldCheckIcon aria-hidden />,
iconClassName: 'text-warning',
to: '/certificates', to: '/certificates',
}, },
] ]
+4
View File
@@ -2,6 +2,10 @@
Wiki home — synced from repository on `main` when this file changes. 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 ## Overview
Manage Cloudflare zones, DNS records, domain groups, and TLS certificate expiry from a single UI. Manage Cloudflare zones, DNS records, domain groups, and TLS certificate expiry from a single UI.
+62
View File
@@ -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