refactor(web): extract shared kpi metrics grid pattern
WEB-18: KpiMetricsGrid в ui/patterns; рефакторинг overview, network, modules, directories, operations, schedule, monitoring. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
<script lang="ts">
|
||||
import type { Component } from 'svelte';
|
||||
import { Badge } from '$lib/ui/core/badge/index.js';
|
||||
import { Button } from '$lib/ui/core/button/index.js';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription
|
||||
} from '$lib/ui/core/card/index.js';
|
||||
import CardSkeleton from '$lib/ui/patterns/feedback/card-skeleton.svelte';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
||||
|
||||
export type KpiAccent = {
|
||||
border: string;
|
||||
bg: string;
|
||||
iconBg: string;
|
||||
iconText: string;
|
||||
};
|
||||
|
||||
export type KpiCardItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
description: string;
|
||||
icon: Component;
|
||||
accent: KpiAccent;
|
||||
badge: string;
|
||||
badgeClass?: string;
|
||||
badgeVariant?: 'default' | 'secondary' | 'destructive' | 'outline';
|
||||
valueClass?: string;
|
||||
error?: string | null;
|
||||
href?: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
cards: KpiCardItem[];
|
||||
loading?: boolean;
|
||||
skeletonCount?: number;
|
||||
class?: string;
|
||||
};
|
||||
|
||||
let { cards, loading = false, skeletonCount = 3, class: className }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class={cn('grid gap-4', className)}>
|
||||
{#if loading}
|
||||
{#each Array(skeletonCount) as _, i (i)}
|
||||
<CardSkeleton />
|
||||
{/each}
|
||||
{:else}
|
||||
{#each cards as card (card.id)}
|
||||
{@const Icon = card.icon}
|
||||
{@const a = card.accent}
|
||||
<Card
|
||||
class={cn(
|
||||
'overflow-hidden border-l-4 shadow-sm',
|
||||
card.href ? 'transition-colors hover:border-primary/35' : '',
|
||||
a.border,
|
||||
a.bg
|
||||
)}
|
||||
>
|
||||
<CardHeader class="pb-2">
|
||||
{#if card.href}
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<CardDescription class="flex min-w-0 items-center gap-2">
|
||||
<span
|
||||
class={cn(
|
||||
'flex size-9 shrink-0 items-center justify-center rounded-lg',
|
||||
a.iconBg
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Icon class={cn('size-4', a.iconText)} />
|
||||
</span>
|
||||
<span class="truncate">{card.label}</span>
|
||||
</CardDescription>
|
||||
<Button variant="ghost" size="icon-sm" href={card.href}>
|
||||
<ArrowRight class="size-3.5" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<CardDescription class="flex min-w-0 items-center gap-2">
|
||||
<span
|
||||
class={cn('flex size-9 shrink-0 items-center justify-center rounded-lg', a.iconBg)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Icon class={cn('size-4', a.iconText)} />
|
||||
</span>
|
||||
<span class="truncate">{card.label}</span>
|
||||
</CardDescription>
|
||||
{/if}
|
||||
<CardTitle class={cn('font-bold tabular-nums', card.valueClass ?? 'text-3xl')}
|
||||
>{card.value}</CardTitle
|
||||
>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<Badge variant={card.badgeVariant ?? 'outline'} class={card.badgeClass}
|
||||
>{card.badge}</Badge
|
||||
>
|
||||
{#if card.error}
|
||||
<p class="text-xs text-destructive">{card.error}</p>
|
||||
{:else}
|
||||
<p class="text-xs text-muted-foreground">{card.description}</p>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user