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
-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 { 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: <ShieldCheckIcon aria-hidden />,
iconClassName: 'text-info',
onSelect: () => onSelectTab('all'),
selected: activeTab === 'all',
},
@@ -55,6 +63,8 @@ export function CertKpiCards({
label: 'OK',
value: okCount,
hint: 'Активные сертификаты',
icon: <CheckCircle2Icon aria-hidden />,
iconClassName: 'text-success',
onSelect: () => onSelectTab('active'),
selected: activeTab === 'active',
},
@@ -63,6 +73,8 @@ export function CertKpiCards({
label: 'Предупреждение',
value: warningCount,
hint: 'Истекают скоро',
icon: <AlertTriangleIcon aria-hidden />,
iconClassName: 'text-warning',
onSelect: () => onSelectTab('warning'),
selected: activeTab === 'warning',
},
@@ -71,6 +83,8 @@ export function CertKpiCards({
label: 'Проблемы',
value: problemCount,
hint: 'Ошибка или истёк',
icon: <ShieldAlertIcon aria-hidden />,
iconClassName: 'text-destructive',
onSelect: () => onSelectTab('expired'),
selected: activeTab === 'expired',
},
+15 -2
View File
@@ -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: <GlobeIcon aria-hidden />,
iconClassName: 'text-info',
onSelect: () => onSelectTab('all'),
selected: activeTab === 'all',
},
@@ -37,6 +44,8 @@ export function DomainKpiCards({
label: 'Без группы',
value: withoutGroup,
hint: 'Не назначена группа',
icon: <FolderOpenIcon aria-hidden />,
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: <CheckCircle2Icon aria-hidden />,
iconClassName: 'text-success',
onSelect: () => onSelectTab('all'),
selected: false,
},
@@ -53,6 +64,8 @@ export function DomainKpiCards({
label: 'С сервисами',
value: withServices,
hint: 'Есть привязки',
icon: <ServerIcon aria-hidden />,
iconClassName: 'text-primary',
onSelect: () => onSelectTab('all'),
selected: false,
},
+8 -2
View File
@@ -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: <FolderTreeIcon aria-hidden />,
iconClassName: 'text-info',
onSelect: () => onSelectTab('all'),
selected: activeTab === 'all',
},
@@ -35,6 +37,8 @@ export function GroupKpiCards({
label: 'С доменами',
value: withDomains,
hint: 'Непустые группы',
icon: <FoldersIcon aria-hidden />,
iconClassName: 'text-success',
onSelect: () => onSelectTab('with_domains'),
selected: activeTab === 'with_domains',
},
@@ -43,6 +47,8 @@ export function GroupKpiCards({
label: 'Пустые',
value: empty,
hint: 'Без доменов',
icon: <FolderOpenIcon aria-hidden />,
iconClassName: 'text-muted-foreground',
onSelect: () => onSelectTab('empty'),
selected: activeTab === 'empty',
},
@@ -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<string, unknown>
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 ? (
<span className="text-muted-foreground text-xs leading-snug">{card.hint}</span>
) : null)
return (
<div className="relative z-10 flex flex-col gap-3">
<span className="text-muted-foreground text-sm">{card.label}</span>
<span className="text-foreground text-xl leading-tight font-semibold tabular-nums">
{card.value}
</span>
{card.hint ? (
<span className="text-muted-foreground text-xs leading-snug">{card.hint}</span>
<div className="relative z-10 flex h-full flex-col items-start gap-4">
{card.icon ? (
<Item
className={cn(
'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.iconClassName ?? DEFAULT_ICON_CLASS,
)}
>
<ItemMedia variant="icon" className="size-auto">
{card.icon}
</ItemMedia>
</Item>
) : 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>
)
}
@@ -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 (
<FramePanel className={panelClass}>
@@ -91,9 +117,12 @@ function KpiStatGridSkeleton({ count }: { count: number }) {
<Frame className="@container w-full">
<div className="grid gap-1 @2xl:grid-cols-2 @4xl:grid-cols-4">
{Array.from({ length: count }).map((_, index) => (
<FramePanel key={index} className="flex flex-col gap-3">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-7 w-14" />
<FramePanel key={index} className="flex flex-col items-start gap-4">
<Skeleton className="size-10.5 rounded-lg" />
<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" />
</FramePanel>
))}
+15 -2
View File
@@ -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: <ServerIcon aria-hidden />,
iconClassName: 'text-info',
onSelect: () => onSelectTab('all'),
selected: activeTab === 'all',
},
@@ -37,6 +44,8 @@ export function ServiceKpiCards({
label: 'Включены',
value: enabled,
hint: 'Активные',
icon: <PowerIcon aria-hidden />,
iconClassName: 'text-success',
onSelect: () => onSelectTab('enabled'),
selected: activeTab === 'enabled',
},
@@ -45,6 +54,8 @@ export function ServiceKpiCards({
label: 'Выключены',
value: disabled,
hint: 'Отключены',
icon: <PowerOffIcon aria-hidden />,
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: <FolderOpenIcon aria-hidden />,
iconClassName: 'text-warning',
onSelect: () => onSelectTab('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,
AlertTriangleIcon,
FolderTreeIcon,
GlobeIcon,
ServerIcon,
ShieldCheckIcon,
} from 'lucide-react'
import {
certificatesQueryOptions,
@@ -173,6 +176,8 @@ function DashboardPage() {
ungroupedCount > 0
? `${ungroupedCount} без группы`
: 'Все в группах',
icon: <GlobeIcon aria-hidden />,
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: <FolderTreeIcon aria-hidden />,
iconClassName: 'text-primary',
to: '/groups',
},
{
@@ -191,6 +198,8 @@ function DashboardPage() {
: ungroupedServiceCount
? `${ungroupedServiceCount} без группы`
: undefined,
icon: <ServerIcon aria-hidden />,
iconClassName: 'text-success',
to: '/services',
search: { domainId: undefined },
},
@@ -202,6 +211,8 @@ function DashboardPage() {
certWarnings > 0
? `${certOk} в норме · ${certWarnings} внимания`
: `${certOk} в норме`,
icon: <ShieldCheckIcon aria-hidden />,
iconClassName: 'text-warning',
to: '/certificates',
},
]