Enhance DataTableCard component with accent and icon support
Publish telemt-api gateway Docker image / test (push) Successful in 26s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 2m24s

- Added accent and icon properties to the DataTableCard component for improved visual distinction and context.
- Updated multiple pages to utilize the new accent feature, enhancing the user interface with color-coded cards.
- Incorporated icons in DataTableCard instances across various routes for better representation of content.
This commit is contained in:
Denozordec
2026-04-10 17:19:49 +07:00
parent bfe79ca601
commit 7c6892ce0e
7 changed files with 50 additions and 9 deletions
@@ -1,31 +1,57 @@
<script lang="ts"> <script lang="ts">
import * as Card from '$lib/components/ui/card/index.js'; import * as Card from '$lib/components/ui/card/index.js';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import { kpiAccent, type KpiAccentKey } from '$lib/kpi-accents.js';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
let { let {
title, title,
description, description,
accent,
icon,
class: className, class: className,
toolbar, toolbar,
children children
}: { }: {
/** Без title — как в shadcn dashboard-01: сразу toolbar и таблица */
title?: string; title?: string;
description?: string; description?: string;
accent?: KpiAccentKey;
icon?: Snippet;
toolbar?: Snippet; toolbar?: Snippet;
children: Snippet; children: Snippet;
class?: string; class?: string;
} = $props(); } = $props();
</script> </script>
<Card.Root density="compact" class={cn('overflow-hidden rounded-lg border border-border shadow-sm', className)}> <Card.Root
density="compact"
class={cn(
'overflow-hidden rounded-lg border border-border shadow-sm',
accent && 'border-l-4',
accent && kpiAccent[accent].border,
className
)}
>
{#if title} {#if title}
<Card.Header class="border-b border-border bg-muted/30 px-4 py-3.5"> <Card.Header class="border-b border-border bg-muted/30 px-4 py-3.5">
<Card.Title class="text-base font-semibold">{title}</Card.Title> <div class="flex gap-3">
{#if description} {#if icon && accent}
<Card.Description class="text-xs sm:text-sm">{description}</Card.Description> <div
{/if} class={cn(
'flex size-10 shrink-0 items-center justify-center rounded-lg',
kpiAccent[accent].iconWrap
)}
>
{@render icon()}
</div>
{/if}
<div class="min-w-0 flex-1 space-y-0.5">
<Card.Title class="text-base font-semibold">{title}</Card.Title>
{#if description}
<Card.Description class="text-xs sm:text-sm">{description}</Card.Description>
{/if}
</div>
</div>
</Card.Header> </Card.Header>
{/if} {/if}
{#if toolbar} {#if toolbar}
+7
View File
@@ -45,6 +45,7 @@
import GlobeIcon from '@lucide/svelte/icons/globe'; import GlobeIcon from '@lucide/svelte/icons/globe';
import CloudUploadIcon from '@lucide/svelte/icons/cloud-upload'; import CloudUploadIcon from '@lucide/svelte/icons/cloud-upload';
import LockIcon from '@lucide/svelte/icons/lock'; import LockIcon from '@lucide/svelte/icons/lock';
import UsersIcon from '@lucide/svelte/icons/users';
import { nextSortDir, compareStrings, compareNum, type SortDir } from '$lib/table-sort.js'; import { nextSortDir, compareStrings, compareNum, type SortDir } from '$lib/table-sort.js';
let loading = $state(true); let loading = $state(true);
@@ -619,7 +620,9 @@ async function handleAliasFilterChange(nextAlias: string) {
class="mb-6" class="mb-6"
title="Активные IP (агрегат)" title="Активные IP (агрегат)"
description="С GeoIP при включённой базе на шлюзе" description="С GeoIP при включённой базе на шлюзе"
accent="teal"
> >
{#snippet icon()}<GlobeIcon class="size-5" aria-hidden="true" />{/snippet}
{#snippet toolbar()} {#snippet toolbar()}
<DataTableToolbar> <DataTableToolbar>
<div class="relative max-w-xs flex-1"> <div class="relative max-w-xs flex-1">
@@ -751,7 +754,9 @@ async function handleAliasFilterChange(nextAlias: string) {
class="mb-6" class="mb-6"
title="По нодам" title="По нодам"
description="Health, версия, аптайм, bad connections" description="Health, версия, аптайм, bad connections"
accent="violet"
> >
{#snippet icon()}<ServerIcon class="size-5" aria-hidden="true" />{/snippet}
{#snippet toolbar()} {#snippet toolbar()}
<DataTableToolbar> <DataTableToolbar>
<div class="relative max-w-xs flex-1"> <div class="relative max-w-xs flex-1">
@@ -910,7 +915,9 @@ async function handleAliasFilterChange(nextAlias: string) {
class="mt-6" class="mt-6"
title="Топ пользователей по трафику" title="Топ пользователей по трафику"
description="До 10 записей из summary" description="До 10 записей из summary"
accent="info"
> >
{#snippet icon()}<UsersIcon class="size-5" aria-hidden="true" />{/snippet}
{#snippet toolbar()} {#snippet toolbar()}
<DataTableToolbar> <DataTableToolbar>
<div class="relative max-w-xs flex-1"> <div class="relative max-w-xs flex-1">
+2 -1
View File
@@ -346,7 +346,8 @@
</KpiStatCard> </KpiStatCard>
</div> </div>
<DataTableCard title="Инциденты" description="Список и разбор"> <DataTableCard title="Инциденты" description="Список и разбор" accent="danger">
{#snippet icon()}<SirenIcon class="size-5" aria-hidden="true" />{/snippet}
{#snippet toolbar()} {#snippet toolbar()}
<DataTableToolbar> <DataTableToolbar>
<div class="relative max-w-md flex-1"> <div class="relative max-w-md flex-1">
+3 -1
View File
@@ -17,6 +17,7 @@
import SearchIcon from '@lucide/svelte/icons/search'; import SearchIcon from '@lucide/svelte/icons/search';
import { nextSortDir, compareStrings, type SortDir } from '$lib/table-sort.js'; import { nextSortDir, compareStrings, type SortDir } from '$lib/table-sort.js';
import MapIcon from '@lucide/svelte/icons/map'; import MapIcon from '@lucide/svelte/icons/map';
import GlobeIcon from '@lucide/svelte/icons/globe';
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js'; import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
import { Button } from '$lib/components/ui/button/index.js'; import { Button } from '$lib/components/ui/button/index.js';
import { Badge } from '$lib/components/ui/badge/index.js'; import { Badge } from '$lib/components/ui/badge/index.js';
@@ -617,7 +618,8 @@ $effect(() => {
</Alert> </Alert>
{/if} {/if}
<DataTableCard title="Уникальные IP" description="Снимок шлюза: активные и недавние списки"> <DataTableCard title="Уникальные IP" description="Снимок шлюза: активные и недавние списки" accent="teal">
{#snippet icon()}<GlobeIcon class="size-5" aria-hidden="true" />{/snippet}
{#snippet toolbar()} {#snippet toolbar()}
<DataTableToolbar> <DataTableToolbar>
<div class="relative max-w-md flex-1"> <div class="relative max-w-md flex-1">
+2
View File
@@ -391,7 +391,9 @@
<DataTableCard <DataTableCard
title="Последние инциденты из снимка" title="Последние инциденты из снимка"
description="Всего: {snapshot?.counts?.total ?? snapshot?.incidents?.length ?? 0}" description="Всего: {snapshot?.counts?.total ?? snapshot?.incidents?.length ?? 0}"
accent="warning"
> >
{#snippet icon()}<ActivityIcon class="size-5" aria-hidden="true" />{/snippet}
{#snippet toolbar()} {#snippet toolbar()}
<DataTableToolbar> <DataTableToolbar>
<div class="relative max-w-md flex-1"> <div class="relative max-w-md flex-1">
+2
View File
@@ -260,7 +260,9 @@
<DataTableCard <DataTableCard
title="Ноды" title="Ноды"
description="Контроллер и метрики. Ниже — компактные группы прокси (автозагрузка). «Детально» — обзор с графиками." description="Контроллер и метрики. Ниже — компактные группы прокси (автозагрузка). «Детально» — обзор с графиками."
accent="violet"
> >
{#snippet icon()}<ServerIcon class="size-5" aria-hidden="true" />{/snippet}
<ScrollArea class="w-full" orientation="both"> <ScrollArea class="w-full" orientation="both">
<Table.Root> <Table.Root>
<Table.Header class="sticky top-0 z-10 bg-muted"> <Table.Header class="sticky top-0 z-10 bg-muted">
+2 -1
View File
@@ -440,7 +440,8 @@ let sumActiveIps = $derived.by(() =>
</KpiStatCard> </KpiStatCard>
</div> </div>
<DataTableCard> <DataTableCard title="Пользователи" description="Слияние по имени между серверами" accent="violet">
{#snippet icon()}<UsersIcon class="size-5" aria-hidden="true" />{/snippet}
{#snippet toolbar()} {#snippet toolbar()}
<div class="border-b border-border"> <div class="border-b border-border">
<div class="flex flex-wrap items-center justify-between gap-2 px-3 py-2.5"> <div class="flex flex-wrap items-center justify-between gap-2 px-3 py-2.5">