From 52665843d6262d0e2fa7bfd777cda1290644d9f0 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 17 Jul 2026 17:14:55 +0700 Subject: [PATCH] =?UTF-8?q?feat(web):=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D1=8B=20KpiStatGrid=20=D0=B8=20SectionCards=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9?= =?UTF-8?q?=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен новый тип KpiStatVariant для управления визуальными состояниями карточек KPI. - Обновлены компоненты SectionCards и KpiStatGrid для поддержки новых свойств, таких как iconClassName и footer с Badge. - Изменены стили и логика отображения для улучшения визуального представления и взаимодействия с пользователем. - Обновлена документация для отражения изменений в интерфейсе. Co-authored-by: Cursor --- apps/web/src/components/reui-kit/index.ts | 7 +- .../src/components/reui-kit/kpi-stat-grid.tsx | 59 +++++++++++++---- apps/web/src/components/section-cards.tsx | 64 ++++++++++--------- apps/web/src/lib/ui-surface.ts | 5 +- apps/web/src/routes/_auth/dashboard.tsx | 62 +++++++++++++++--- docs/ui-design-contract.md | 4 +- 6 files changed, 146 insertions(+), 55 deletions(-) diff --git a/apps/web/src/components/reui-kit/index.ts b/apps/web/src/components/reui-kit/index.ts index 1e3b7c9..ea31935 100644 --- a/apps/web/src/components/reui-kit/index.ts +++ b/apps/web/src/components/reui-kit/index.ts @@ -1,6 +1,11 @@ export { applyFiltersToData, getActiveFilters, renderSingleSelectedLabel } from './filter-utils' 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 { DetailPanel, type DetailMetricCard } from './detail-panel' export { SettingsShell, type SettingsTabConfig } from './settings-shell' 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 c99d5e2..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 } @@ -31,8 +36,13 @@ interface KpiStatGridProps { skeletonCount?: number } -const DEFAULT_ICON_CLASS = - 'text-muted-foreground [&_svg]:text-current' +const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current' + +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' @@ -44,9 +54,24 @@ function kpiCols(count: number): string { 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 footer = resolveFooter(card) + const valueVariant = card.variant ?? 'default' + return ( -
+
{card.icon ? ( ) : null} +
- + {card.value} {card.label} - {card.hint ? ( - {card.hint} - ) : null}
- {card.footer ?
{card.footer}
: null} + + {footer ?
{footer}
: null}
) } @@ -76,7 +105,7 @@ function KpiStatCardBody({ card }: { card: KpiStatCard }) { function KpiStatCardItem({ card }: { card: KpiStatCard }) { const interactive = Boolean(card.to || card.onSelect) const panelClass = cn( - 'relative isolate flex h-full flex-col items-start gap-4', + '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 cursor-pointer transition-colors focus-within:ring-2', @@ -116,12 +145,13 @@ function KpiStatCardItem({ card }: { card: KpiStatCard }) { function KpiStatGridSkeleton({ count }: { count: number }) { return ( -
+
{Array.from({ length: count }).map((_, index) => ( - + + ))}
@@ -129,7 +159,10 @@ function KpiStatGridSkeleton({ count }: { count: number }) { ) } -/** KPI grid — ReUI stats-12. Preview: https://reui.io/preview/base/stats-12 */ +/** + * Hybrid KPI grid — compact stats-12 Frame strip. + * Preview: https://reui.io/preview/base/stats-12 + */ export function KpiStatGrid({ cards, isLoading = false, @@ -157,7 +190,7 @@ export function KpiStatGrid({ return ( -
+
{cards.map((card) => ( ))} diff --git a/apps/web/src/components/section-cards.tsx b/apps/web/src/components/section-cards.tsx index d29c845..a28a16e 100644 --- a/apps/web/src/components/section-cards.tsx +++ b/apps/web/src/components/section-cards.tsx @@ -1,58 +1,64 @@ import type { ReactElement, ReactNode } from 'react' -import { cn } from '@cfdm/ui/lib/utils' -import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid' -import { Badge } from '@cfdm/ui/components/badge' +import { KpiStatGrid, type KpiStatCard, type KpiStatVariant } from '@/components/reui-kit/kpi-stat-grid' +import { Badge } from '@/components/reui/badge' export interface SectionCardItem { label: ReactNode value: string | number | ReactElement hint?: ReactNode icon?: ReactNode + iconClassName?: string badge?: ReactNode - variant?: 'default' | 'warning' | 'destructive' + variant?: KpiStatVariant active?: boolean onClick?: () => void } +const VARIANT_ICON_CLASS: Record = { + default: 'text-muted-foreground', + warning: 'text-warning', + destructive: 'text-destructive', +} + /** * @deprecated Prefer `KpiStatGrid` directly. - * Thin adapter → stats-12 Frame KPI. Preview: https://reui.io/preview/base/stats-12 + * Thin adapter → hybrid stats-12 Frame KPI. Preview: https://reui.io/preview/base/stats-12 */ export function SectionCards({ items, className }: { items: SectionCardItem[]; className?: string }) { const cards: KpiStatCard[] = items.map((item, idx) => { const label = typeof item.label === 'string' ? item.label : `kpi-${idx}` - const footer = - item.badge || item.hint ? ( -
- {item.badge} - {item.hint && !item.badge ? ( - {item.hint} - ) : null} -
- ) : undefined + const variant = item.variant ?? 'default' + + let footer: ReactNode = item.badge + if (!footer && typeof item.hint === 'string') { + footer = ( + + {item.hint} + + ) + } else if (!footer && item.hint) { + footer = item.hint + } else if (!footer && variant !== 'default') { + footer = ( + + {variant === 'warning' ? 'Внимание' : 'Критично'} + + ) + } return { id: label, label, value: item.value as string | number, - hint: typeof item.hint === 'string' ? item.hint : undefined, icon: item.icon, + iconClassName: item.iconClassName ?? VARIANT_ICON_CLASS[variant], + variant, selected: item.active, onSelect: item.onClick, - footer: - item.variant && item.variant !== 'default' && !item.badge ? ( - - {item.variant === 'warning' ? 'Внимание' : 'Критично'} - - ) : ( - footer - ), + footer, } }) diff --git a/apps/web/src/lib/ui-surface.ts b/apps/web/src/lib/ui-surface.ts index 9af8db8..748a90a 100644 --- a/apps/web/src/lib/ui-surface.ts +++ b/apps/web/src/lib/ui-surface.ts @@ -2,7 +2,8 @@ * VPS Tracker UI surface contract (ReUI Pro). * * Ops / list / dashboard / detail / settings: **Frame** only. - * KPI: **stats-12** via `reui-kit/KpiStatGrid`. + * KPI: hybrid compact **stats-12** via `reui-kit/KpiStatGrid` + * (colored icon → value ± variant → label → Badge footer). * Lists: **data-grid-filtering-2** via `reui-kit/ResourcePage`. * * @see https://reui.io/preview/base/stats-12 @@ -11,5 +12,5 @@ */ export const UI_SURFACE = 'frame' as const -/** Grid for ReUI stats-12 KPI rows (3–6 tiles). */ +/** Grid for hybrid compact stats-12 KPI rows (3–6 tiles). */ export const kpiStatGridClassName = '@container w-full' diff --git a/apps/web/src/routes/_auth/dashboard.tsx b/apps/web/src/routes/_auth/dashboard.tsx index fc100c3..2419617 100644 --- a/apps/web/src/routes/_auth/dashboard.tsx +++ b/apps/web/src/routes/_auth/dashboard.tsx @@ -24,7 +24,7 @@ import type { DataGridColumn } from '@/components/data-grid-types' import { dataGridCellStack } from '@/components/data-grid-cells' import { SectionCardsSkeleton, TableSkeleton } from '@/components/skeletons' import { Button } from '@cfdm/ui/components/button' -import { Badge } from '@cfdm/ui/components/badge' +import { Badge } from '@/components/reui/badge' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs' import { StatusBadge } from '@/components/status-badge' import { cn } from '@cfdm/ui/lib/utils' @@ -197,13 +197,22 @@ function DashboardPage() { tabsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) } + const runwayDays = stats?.minRunwayDays + const runwayLow = runwayDays != null && runwayDays < 14 + const kpiCards: KpiStatCard[] = [ { id: 'active-vps', label: 'Активные VPS', value: `${activeCount} из ${totalCount}`, icon: , + iconClassName: 'text-primary', to: '/vps', + footer: ( + + {totalCount} всего + + ), }, { id: 'burn', @@ -215,7 +224,13 @@ function DashboardPage() { ratesData, ), icon: , + iconClassName: 'text-info', to: '/reports', + footer: ( + + оценка + + ), }, { id: 'balance', @@ -227,34 +242,65 @@ function DashboardPage() { ratesData, ), icon: , + iconClassName: 'text-success', to: '/accounts', + footer: ( + + API + + ), }, { id: 'runway', label: 'Runway', - value: stats?.minRunwayDays != null ? `${stats.minRunwayDays} дн` : '—', + value: runwayDays != null ? `${runwayDays} дн` : '—', icon: , + iconClassName: runwayLow ? 'text-warning' : 'text-muted-foreground', + variant: runwayLow ? 'warning' : 'default', to: '/accounts', - footer: - stats?.minRunwayDays != null && stats.minRunwayDays < 14 ? ( - - < 14 дн - - ) : undefined, + footer: runwayLow ? ( + + < 14 дн + + ) : ( + + запас + + ), }, { id: 'expiring', label: 'Истекает 7 дн', value: expiringCount, icon: , + iconClassName: expiringCount > 0 ? 'text-warning' : 'text-muted-foreground', + variant: expiringCount > 0 ? 'warning' : 'default', onSelect: () => navigate({ to: '/vps', search: { health: 'expiring-soon' } }), + footer: ( + 0 ? 'warning-light' : 'success-light'} + size="sm" + > + {expiringCount > 0 ? 'скоро' : 'в норме'} + + ), }, { id: 'issues', label: 'Проблемы', value: issuesCount, icon: , + iconClassName: issuesCount > 0 ? 'text-destructive' : 'text-muted-foreground', + variant: issuesCount > 0 ? 'destructive' : 'default', onSelect: handleGoToIssues, + footer: ( + 0 ? 'destructive-light' : 'success-light'} + size="sm" + > + {issuesCount > 0 ? 'требует внимания' : 'в норме'} + + ), }, ] diff --git a/docs/ui-design-contract.md b/docs/ui-design-contract.md index 6d32fc7..b0e0eb8 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 |