feat(web): обновить компоненты KpiStatGrid и SectionCards для улучшения интерфейса
Docker / build (push) Failing after 19s
Docker / build (push) Failing after 19s
- Добавлен новый тип KpiStatVariant для управления визуальными состояниями карточек KPI. - Обновлены компоненты SectionCards и KpiStatGrid для поддержки новых свойств, таких как iconClassName и footer с Badge. - Изменены стили и логика отображения для улучшения визуального представления и взаимодействия с пользователем. - Обновлена документация для отражения изменений в интерфейсе. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
export { applyFiltersToData, getActiveFilters, renderSingleSelectedLabel } from './filter-utils'
|
export { applyFiltersToData, getActiveFilters, renderSingleSelectedLabel } from './filter-utils'
|
||||||
export { ResourcePage, type ResourcePageProps, type ResourcePageTab } from './resource-page'
|
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 { OpsDashboard } from './ops-dashboard'
|
||||||
export { DetailPanel, type DetailMetricCard } from './detail-panel'
|
export { DetailPanel, type DetailMetricCard } from './detail-panel'
|
||||||
export { SettingsShell, type SettingsTabConfig } from './settings-shell'
|
export { SettingsShell, type SettingsTabConfig } from './settings-shell'
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
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 { Badge } from '@/components/reui/badge'
|
||||||
import { Item, ItemMedia } from '@cfdm/ui/components/item'
|
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'
|
||||||
|
|
||||||
|
export type KpiStatVariant = 'default' | 'warning' | 'destructive'
|
||||||
|
|
||||||
export interface KpiStatCard {
|
export interface KpiStatCard {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
@@ -16,6 +19,8 @@ export interface KpiStatCard {
|
|||||||
selected?: boolean
|
selected?: boolean
|
||||||
icon?: ReactNode
|
icon?: ReactNode
|
||||||
iconClassName?: string
|
iconClassName?: string
|
||||||
|
/** Semantic color for the primary value */
|
||||||
|
variant?: KpiStatVariant
|
||||||
footer?: ReactNode
|
footer?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,8 +36,13 @@ interface KpiStatGridProps {
|
|||||||
skeletonCount?: number
|
skeletonCount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_ICON_CLASS =
|
const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current'
|
||||||
'text-muted-foreground [&_svg]:text-current'
|
|
||||||
|
const VALUE_VARIANT_CLASS: Record<KpiStatVariant, string> = {
|
||||||
|
default: 'text-foreground',
|
||||||
|
warning: 'text-warning',
|
||||||
|
destructive: 'text-destructive',
|
||||||
|
}
|
||||||
|
|
||||||
function kpiCols(count: number): string {
|
function kpiCols(count: number): string {
|
||||||
if (count <= 1) return 'grid-cols-1'
|
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'
|
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 (
|
||||||
|
<Badge variant="outline" size="sm">
|
||||||
|
{card.hint}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function KpiStatCardBody({ card }: { card: KpiStatCard }) {
|
function KpiStatCardBody({ card }: { card: KpiStatCard }) {
|
||||||
|
const footer = resolveFooter(card)
|
||||||
|
const valueVariant = card.variant ?? 'default'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-full flex-col items-start gap-4">
|
<div className="relative z-10 flex h-full flex-col items-start gap-3">
|
||||||
{card.icon ? (
|
{card.icon ? (
|
||||||
<Item
|
<Item
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -59,16 +84,20 @@ function KpiStatCardBody({ card }: { card: KpiStatCard }) {
|
|||||||
</ItemMedia>
|
</ItemMedia>
|
||||||
</Item>
|
</Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<span className="text-foreground text-2xl leading-none font-bold tabular-nums">
|
<span
|
||||||
|
className={cn(
|
||||||
|
'text-2xl leading-none font-bold tabular-nums',
|
||||||
|
VALUE_VARIANT_CLASS[valueVariant],
|
||||||
|
)}
|
||||||
|
>
|
||||||
{card.value}
|
{card.value}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground text-sm font-medium">{card.label}</span>
|
<span className="text-muted-foreground text-sm font-medium">{card.label}</span>
|
||||||
{card.hint ? (
|
|
||||||
<span className="text-muted-foreground text-xs leading-snug">{card.hint}</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
{card.footer ? <div className="mt-auto w-full">{card.footer}</div> : null}
|
|
||||||
|
{footer ? <div className="mt-auto w-full">{footer}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -76,7 +105,7 @@ 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 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',
|
card.selected && 'ring-primary/30 bg-muted/30 ring-1',
|
||||||
interactive &&
|
interactive &&
|
||||||
'hover:bg-muted/40 focus-within:ring-ring cursor-pointer transition-colors focus-within:ring-2',
|
'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 }) {
|
function KpiStatGridSkeleton({ count }: { count: number }) {
|
||||||
return (
|
return (
|
||||||
<Frame className="@container w-full">
|
<Frame className="@container w-full">
|
||||||
<div className={cn('grid gap-1', kpiCols(count))}>
|
<div className={cn('grid gap-2', kpiCols(count))}>
|
||||||
{Array.from({ length: count }).map((_, index) => (
|
{Array.from({ length: count }).map((_, index) => (
|
||||||
<FramePanel key={index} className="flex flex-col gap-4">
|
<FramePanel key={index} className="flex flex-col gap-3">
|
||||||
<Skeleton className="size-10.5 rounded-lg" />
|
<Skeleton className="size-10.5 rounded-lg" />
|
||||||
<Skeleton className="h-7 w-14" />
|
<Skeleton className="h-7 w-14" />
|
||||||
<Skeleton className="h-4 w-20" />
|
<Skeleton className="h-4 w-20" />
|
||||||
|
<Skeleton className="mt-auto h-4.5 w-16 rounded-full" />
|
||||||
</FramePanel>
|
</FramePanel>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -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({
|
export function KpiStatGrid({
|
||||||
cards,
|
cards,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
@@ -157,7 +190,7 @@ export function KpiStatGrid({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Frame className={cn('@container w-full', className)}>
|
<Frame className={cn('@container w-full', className)}>
|
||||||
<div className={cn('grid gap-1', kpiCols(cards.length))}>
|
<div className={cn('grid gap-2', kpiCols(cards.length))}>
|
||||||
{cards.map((card) => (
|
{cards.map((card) => (
|
||||||
<KpiStatCardItem key={card.id} card={card} />
|
<KpiStatCardItem key={card.id} card={card} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,58 +1,64 @@
|
|||||||
import type { ReactElement, ReactNode } from 'react'
|
import type { ReactElement, ReactNode } from 'react'
|
||||||
import { cn } from '@cfdm/ui/lib/utils'
|
import { KpiStatGrid, type KpiStatCard, type KpiStatVariant } from '@/components/reui-kit/kpi-stat-grid'
|
||||||
import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid'
|
import { Badge } from '@/components/reui/badge'
|
||||||
import { Badge } from '@cfdm/ui/components/badge'
|
|
||||||
|
|
||||||
export interface SectionCardItem {
|
export interface SectionCardItem {
|
||||||
label: ReactNode
|
label: ReactNode
|
||||||
value: string | number | ReactElement
|
value: string | number | ReactElement
|
||||||
hint?: ReactNode
|
hint?: ReactNode
|
||||||
icon?: ReactNode
|
icon?: ReactNode
|
||||||
|
iconClassName?: string
|
||||||
badge?: ReactNode
|
badge?: ReactNode
|
||||||
variant?: 'default' | 'warning' | 'destructive'
|
variant?: KpiStatVariant
|
||||||
active?: boolean
|
active?: boolean
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VARIANT_ICON_CLASS: Record<KpiStatVariant, string> = {
|
||||||
|
default: 'text-muted-foreground',
|
||||||
|
warning: 'text-warning',
|
||||||
|
destructive: 'text-destructive',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Prefer `KpiStatGrid` directly.
|
* @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 }) {
|
export function SectionCards({ items, className }: { items: SectionCardItem[]; className?: string }) {
|
||||||
const cards: KpiStatCard[] = items.map((item, idx) => {
|
const cards: KpiStatCard[] = items.map((item, idx) => {
|
||||||
const label = typeof item.label === 'string' ? item.label : `kpi-${idx}`
|
const label = typeof item.label === 'string' ? item.label : `kpi-${idx}`
|
||||||
const footer =
|
const variant = item.variant ?? 'default'
|
||||||
item.badge || item.hint ? (
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
let footer: ReactNode = item.badge
|
||||||
{item.badge}
|
if (!footer && typeof item.hint === 'string') {
|
||||||
{item.hint && !item.badge ? (
|
footer = (
|
||||||
<span className="text-muted-foreground text-xs">{item.hint}</span>
|
<Badge variant="outline" size="sm">
|
||||||
) : null}
|
{item.hint}
|
||||||
</div>
|
</Badge>
|
||||||
) : undefined
|
)
|
||||||
|
} else if (!footer && item.hint) {
|
||||||
|
footer = item.hint
|
||||||
|
} else if (!footer && variant !== 'default') {
|
||||||
|
footer = (
|
||||||
|
<Badge
|
||||||
|
variant={variant === 'warning' ? 'warning-light' : 'destructive-light'}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{variant === 'warning' ? 'Внимание' : 'Критично'}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: label,
|
id: label,
|
||||||
label,
|
label,
|
||||||
value: item.value as string | number,
|
value: item.value as string | number,
|
||||||
hint: typeof item.hint === 'string' ? item.hint : undefined,
|
|
||||||
icon: item.icon,
|
icon: item.icon,
|
||||||
|
iconClassName: item.iconClassName ?? VARIANT_ICON_CLASS[variant],
|
||||||
|
variant,
|
||||||
selected: item.active,
|
selected: item.active,
|
||||||
onSelect: item.onClick,
|
onSelect: item.onClick,
|
||||||
footer:
|
footer,
|
||||||
item.variant && item.variant !== 'default' && !item.badge ? (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={cn(
|
|
||||||
item.variant === 'warning' && 'border-warning/50 text-warning-foreground',
|
|
||||||
item.variant === 'destructive' && 'border-destructive/50 text-destructive',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.variant === 'warning' ? 'Внимание' : 'Критично'}
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
footer
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
* VPS Tracker UI surface contract (ReUI Pro).
|
* VPS Tracker UI surface contract (ReUI Pro).
|
||||||
*
|
*
|
||||||
* Ops / list / dashboard / detail / settings: **Frame** only.
|
* 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`.
|
* Lists: **data-grid-filtering-2** via `reui-kit/ResourcePage`.
|
||||||
*
|
*
|
||||||
* @see https://reui.io/preview/base/stats-12
|
* @see https://reui.io/preview/base/stats-12
|
||||||
@@ -11,5 +12,5 @@
|
|||||||
*/
|
*/
|
||||||
export const UI_SURFACE = 'frame' as const
|
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'
|
export const kpiStatGridClassName = '@container w-full'
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import type { DataGridColumn } from '@/components/data-grid-types'
|
|||||||
import { dataGridCellStack } from '@/components/data-grid-cells'
|
import { dataGridCellStack } from '@/components/data-grid-cells'
|
||||||
import { SectionCardsSkeleton, TableSkeleton } from '@/components/skeletons'
|
import { SectionCardsSkeleton, TableSkeleton } from '@/components/skeletons'
|
||||||
import { Button } from '@cfdm/ui/components/button'
|
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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import { cn } from '@cfdm/ui/lib/utils'
|
import { cn } from '@cfdm/ui/lib/utils'
|
||||||
@@ -197,13 +197,22 @@ function DashboardPage() {
|
|||||||
tabsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
tabsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const runwayDays = stats?.minRunwayDays
|
||||||
|
const runwayLow = runwayDays != null && runwayDays < 14
|
||||||
|
|
||||||
const kpiCards: KpiStatCard[] = [
|
const kpiCards: KpiStatCard[] = [
|
||||||
{
|
{
|
||||||
id: 'active-vps',
|
id: 'active-vps',
|
||||||
label: 'Активные VPS',
|
label: 'Активные VPS',
|
||||||
value: `${activeCount} из ${totalCount}`,
|
value: `${activeCount} из ${totalCount}`,
|
||||||
icon: <ServerIcon className="size-4" />,
|
icon: <ServerIcon className="size-4" />,
|
||||||
|
iconClassName: 'text-primary',
|
||||||
to: '/vps',
|
to: '/vps',
|
||||||
|
footer: (
|
||||||
|
<Badge variant="outline" size="sm">
|
||||||
|
{totalCount} всего
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'burn',
|
id: 'burn',
|
||||||
@@ -215,7 +224,13 @@ function DashboardPage() {
|
|||||||
ratesData,
|
ratesData,
|
||||||
),
|
),
|
||||||
icon: <TrendingUpIcon className="size-4" />,
|
icon: <TrendingUpIcon className="size-4" />,
|
||||||
|
iconClassName: 'text-info',
|
||||||
to: '/reports',
|
to: '/reports',
|
||||||
|
footer: (
|
||||||
|
<Badge variant="info-light" size="sm">
|
||||||
|
оценка
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'balance',
|
id: 'balance',
|
||||||
@@ -227,34 +242,65 @@ function DashboardPage() {
|
|||||||
ratesData,
|
ratesData,
|
||||||
),
|
),
|
||||||
icon: <WalletIcon className="size-4" />,
|
icon: <WalletIcon className="size-4" />,
|
||||||
|
iconClassName: 'text-success',
|
||||||
to: '/accounts',
|
to: '/accounts',
|
||||||
|
footer: (
|
||||||
|
<Badge variant="success-light" size="sm">
|
||||||
|
API
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'runway',
|
id: 'runway',
|
||||||
label: 'Runway',
|
label: 'Runway',
|
||||||
value: stats?.minRunwayDays != null ? `${stats.minRunwayDays} дн` : '—',
|
value: runwayDays != null ? `${runwayDays} дн` : '—',
|
||||||
icon: <ClockIcon className="size-4" />,
|
icon: <ClockIcon className="size-4" />,
|
||||||
|
iconClassName: runwayLow ? 'text-warning' : 'text-muted-foreground',
|
||||||
|
variant: runwayLow ? 'warning' : 'default',
|
||||||
to: '/accounts',
|
to: '/accounts',
|
||||||
footer:
|
footer: runwayLow ? (
|
||||||
stats?.minRunwayDays != null && stats.minRunwayDays < 14 ? (
|
<Badge variant="warning-light" size="sm">
|
||||||
<Badge variant="outline" className="text-xs">
|
< 14 дн
|
||||||
< 14 дн
|
</Badge>
|
||||||
</Badge>
|
) : (
|
||||||
) : undefined,
|
<Badge variant="outline" size="sm">
|
||||||
|
запас
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'expiring',
|
id: 'expiring',
|
||||||
label: 'Истекает 7 дн',
|
label: 'Истекает 7 дн',
|
||||||
value: expiringCount,
|
value: expiringCount,
|
||||||
icon: <AlertTriangleIcon className="size-4" />,
|
icon: <AlertTriangleIcon className="size-4" />,
|
||||||
|
iconClassName: expiringCount > 0 ? 'text-warning' : 'text-muted-foreground',
|
||||||
|
variant: expiringCount > 0 ? 'warning' : 'default',
|
||||||
onSelect: () => navigate({ to: '/vps', search: { health: 'expiring-soon' } }),
|
onSelect: () => navigate({ to: '/vps', search: { health: 'expiring-soon' } }),
|
||||||
|
footer: (
|
||||||
|
<Badge
|
||||||
|
variant={expiringCount > 0 ? 'warning-light' : 'success-light'}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{expiringCount > 0 ? 'скоро' : 'в норме'}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'issues',
|
id: 'issues',
|
||||||
label: 'Проблемы',
|
label: 'Проблемы',
|
||||||
value: issuesCount,
|
value: issuesCount,
|
||||||
icon: <HashIcon className="size-4" />,
|
icon: <HashIcon className="size-4" />,
|
||||||
|
iconClassName: issuesCount > 0 ? 'text-destructive' : 'text-muted-foreground',
|
||||||
|
variant: issuesCount > 0 ? 'destructive' : 'default',
|
||||||
onSelect: handleGoToIssues,
|
onSelect: handleGoToIssues,
|
||||||
|
footer: (
|
||||||
|
<Badge
|
||||||
|
variant={issuesCount > 0 ? 'destructive-light' : 'success-light'}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{issuesCount > 0 ? 'требует внимания' : 'в норме'}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Ops / list / dashboard / detail / settings — только **Frame**, не shad
|
|||||||
| Зона | Block | Preview |
|
| Зона | Block | Preview |
|
||||||
|------|-------|---------|
|
|------|-------|---------|
|
||||||
| Shell | `app-shell-12` (+ cmdk/monitor где нужно) | https://reui.io/preview/base/app-shell-12 |
|
| 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 |
|
| Dashboard | `dashboard-1` | https://reui.io/preview/base/dashboard-1 |
|
||||||
| Lists | `data-grid-filtering-2` | https://reui.io/preview/base/data-grid-filtering-2 |
|
| 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 |
|
| Settings | `settings-16` + SettingRow | https://reui.io/preview/base/settings-16 |
|
||||||
@@ -31,7 +31,7 @@ Ops / list / dashboard / detail / settings — только **Frame**, не shad
|
|||||||
| Component | Role |
|
| Component | Role |
|
||||||
|-----------|------|
|
|-----------|------|
|
||||||
| `ResourcePage` | Frame + line tabs + Filters + DataGrid |
|
| `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 |
|
| `OpsDashboard` | KPI + charts + attention queue |
|
||||||
| `SettingsShell` | settings nav + Outlet |
|
| `SettingsShell` | settings nav + Outlet |
|
||||||
| `DetailPanel` | detail Frame sections |
|
| `DetailPanel` | detail Frame sections |
|
||||||
|
|||||||
Reference in New Issue
Block a user