diff --git a/apps/web/src/components/cert-kpi-cards.tsx b/apps/web/src/components/cert-kpi-cards.tsx index 2a775e3..ce36f36 100644 --- a/apps/web/src/components/cert-kpi-cards.tsx +++ b/apps/web/src/components/cert-kpi-cards.tsx @@ -75,6 +75,7 @@ export function CertKpiCards({ hint: 'Истекают скоро', icon: , iconClassName: 'text-warning', + variant: warningCount > 0 ? 'warning' : 'default', onSelect: () => onSelectTab('warning'), selected: activeTab === 'warning', }, @@ -85,6 +86,7 @@ export function CertKpiCards({ hint: 'Ошибка или истёк', icon: , iconClassName: 'text-destructive', + variant: problemCount > 0 ? 'destructive' : 'default', 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 98452b3..4d6d157 100644 --- a/apps/web/src/components/domain-kpi-cards.tsx +++ b/apps/web/src/components/domain-kpi-cards.tsx @@ -46,6 +46,7 @@ export function DomainKpiCards({ hint: 'Не назначена группа', icon: , iconClassName: 'text-warning', + variant: withoutGroup > 0 ? 'warning' : 'default', onSelect: () => onSelectTab('without_group'), selected: activeTab === 'without_group', }, diff --git a/apps/web/src/components/group-kpi-cards.tsx b/apps/web/src/components/group-kpi-cards.tsx index ed10793..989862b 100644 --- a/apps/web/src/components/group-kpi-cards.tsx +++ b/apps/web/src/components/group-kpi-cards.tsx @@ -49,6 +49,7 @@ export function GroupKpiCards({ hint: 'Без доменов', icon: , iconClassName: 'text-muted-foreground', + variant: empty > 0 ? 'warning' : 'default', onSelect: () => onSelectTab('empty'), selected: activeTab === 'empty', }, diff --git a/apps/web/src/components/reui-kit/index.ts b/apps/web/src/components/reui-kit/index.ts index 8ffa94f..13cf689 100644 --- a/apps/web/src/components/reui-kit/index.ts +++ b/apps/web/src/components/reui-kit/index.ts @@ -1,7 +1,7 @@ export { applyFiltersToData, getActiveFilters, renderSingleSelectedLabel } from './filter-utils' export { CertStatusChart, GroupDomainsChart } from './dashboard-analytics' export { ResourcePage, type ResourcePageProps, type ResourcePageTab } from './resource-page' -export { KpiStatGrid, type KpiStatCard, type OpsKpiCard } from './kpi-stat-grid' +export { KpiStatGrid, type KpiStatCard, type KpiStatVariant, type OpsKpiCard } from './kpi-stat-grid' export { OpsDashboard } from './ops-dashboard' export { KanbanBoard, KanbanBoardSkeleton, type KanbanBoardProps, type KanbanColumnConfig } from './kanban-board' export { DetailPanel, type DetailMetricCard } from './detail-panel' 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 0154f8f..64582e2 100644 --- a/apps/web/src/components/reui-kit/kpi-stat-grid.tsx +++ b/apps/web/src/components/reui-kit/kpi-stat-grid.tsx @@ -1,10 +1,13 @@ import type { ReactNode } from 'react' import { Link } from '@tanstack/react-router' import { Frame, FramePanel } from '@/components/reui/frame' +import { Badge } from '@/components/reui/badge' import { Item, ItemMedia } from '@cfdm/ui/components/item' import { cn } from '@cfdm/ui/lib/utils' import { Skeleton } from '@cfdm/ui/components/skeleton' +export type KpiStatVariant = 'default' | 'warning' | 'destructive' + export interface KpiStatCard { id: string label: string @@ -16,6 +19,8 @@ export interface KpiStatCard { selected?: boolean icon?: ReactNode iconClassName?: string + /** Semantic color for the primary value */ + variant?: KpiStatVariant footer?: ReactNode } @@ -33,16 +38,40 @@ interface KpiStatGridProps { const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current' -/** KPI body — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */ +const VALUE_VARIANT_CLASS: Record = { + default: 'text-foreground', + warning: 'text-warning', + destructive: 'text-destructive', +} + +function kpiCols(count: number): string { + if (count <= 1) return 'grid-cols-1' + if (count === 2) return 'grid-cols-1 @xl:grid-cols-2' + if (count === 3) return 'grid-cols-1 @3xl:grid-cols-3' + if (count === 4) return 'grid-cols-1 @3xl:grid-cols-2 @6xl:grid-cols-4' + if (count === 5) return 'grid-cols-2 @3xl:grid-cols-3 xl:grid-cols-5' + if (count === 6) return 'grid-cols-2 sm:grid-cols-3 xl:grid-cols-6' + return 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4' +} + +function resolveFooter(card: KpiStatCard): ReactNode { + if (card.footer) return card.footer + if (card.hint) { + return ( + + {card.hint} + + ) + } + return null +} + function KpiStatCardBody({ card }: { card: KpiStatCard }) { - const footerContent = - card.footer ?? - (card.hint ? ( - {card.hint} - ) : null) + const footer = resolveFooter(card) + const valueVariant = card.variant ?? 'default' return ( -
+
{card.icon ? ( - + {card.value} {card.label}
- {footerContent ?
{footerContent}
: null} + {footer ?
{footer}
: null}
) } @@ -74,17 +108,13 @@ function KpiStatCardItem({ card }: { card: KpiStatCard }) { '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', + 'hover:bg-muted/40 focus-within:ring-ring cursor-pointer transition-colors focus-within:ring-2', ) if (card.to) { return ( - + @@ -115,15 +145,13 @@ function KpiStatCardItem({ card }: { card: KpiStatCard }) { function KpiStatGridSkeleton({ count }: { count: number }) { return ( -
+
{Array.from({ length: count }).map((_, index) => ( - + -
- - -
- + + +
))}
@@ -131,6 +159,10 @@ function KpiStatGridSkeleton({ count }: { count: number }) { ) } +/** + * Hybrid KPI grid — compact stats-12 Frame strip. + * Preview: https://reui.io/preview/base/stats-12 + */ export function KpiStatGrid({ cards, isLoading = false, @@ -158,7 +190,7 @@ export function KpiStatGrid({ return ( -
+
{cards.map((card) => ( ))} diff --git a/apps/web/src/components/service-kpi-cards.tsx b/apps/web/src/components/service-kpi-cards.tsx index 1967fda..c024d25 100644 --- a/apps/web/src/components/service-kpi-cards.tsx +++ b/apps/web/src/components/service-kpi-cards.tsx @@ -66,6 +66,7 @@ export function ServiceKpiCards({ hint: down > 0 ? `Down: ${down}` : 'Не в группе', icon: , iconClassName: 'text-warning', + variant: ungrouped > 0 || down > 0 ? 'warning' : 'default', onSelect: () => onSelectTab('ungrouped'), selected: activeTab === 'ungrouped', }, diff --git a/apps/web/src/routes/_auth/index.tsx b/apps/web/src/routes/_auth/index.tsx index 10a733f..3db8d31 100644 --- a/apps/web/src/routes/_auth/index.tsx +++ b/apps/web/src/routes/_auth/index.tsx @@ -178,6 +178,7 @@ function DashboardPage() { : 'Все в группах', icon: , iconClassName: 'text-info', + variant: ungroupedCount > 0 ? 'warning' : 'default', to: '/domains', }, { @@ -200,6 +201,7 @@ function DashboardPage() { : undefined, icon: , iconClassName: 'text-success', + variant: attentionCount > 0 ? 'destructive' : ungroupedServiceCount > 0 ? 'warning' : 'default', to: '/services', search: { domainId: undefined }, }, @@ -213,6 +215,7 @@ function DashboardPage() { : `${certOk} в норме`, icon: , iconClassName: 'text-warning', + variant: certWarnings > 0 ? 'warning' : 'default', to: '/certificates', }, ] diff --git a/docs/ui-design-contract.md b/docs/ui-design-contract.md index 7cfbc44..771b329 100644 --- a/docs/ui-design-contract.md +++ b/docs/ui-design-contract.md @@ -18,7 +18,7 @@ Ops / list / dashboard / detail / settings — только **Frame**, не shad | Зона | 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 | +| KPI | hybrid `stats-12` (compact Frame strip: colored icon → value ± variant → label → Badge footer) | https://reui.io/preview/base/stats-12 | | 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 | @@ -31,7 +31,7 @@ Ops / list / dashboard / detail / settings — только **Frame**, не shad | Component | Role | |-----------|------| | `ResourcePage` | Frame + line tabs + Filters + DataGrid | -| `KpiStatGrid` | stats-12 KPI tiles | +| `KpiStatGrid` | hybrid compact stats-12 KPI tiles (`variant`, Badge footer) | | `OpsDashboard` | KPI + charts + attention queue | | `SettingsShell` | settings nav + Outlet | | `DetailPanel` | detail Frame sections |