dimensionlab-website/src/lib/ui/components/TelemetryCard.svelte
2026-06-18 19:22:55 +02:00

158 lines
3.8 KiB
Svelte

<script lang="ts">
import { formatMetricValue } from "../format";
import type { UiTelemetryCard } from "../types";
import IconGlyph from "./IconGlyph.svelte";
let { card }: { card: UiTelemetryCard } = $props();
let progress = $derived(resolveProgress(card));
let value = $derived(formatMetricValue(card.value));
</script>
<article class="telemetry-card" data-severity={card.severity} data-model-id={card.id}>
<header>
{#if card.icon}
<IconGlyph name={card.icon} size="sm" />
{/if}
<h3>{card.label}</h3>
</header>
<strong>{value}</strong>
{#if progress !== null}
<div class="telemetry-card__bar" aria-hidden="true">
<span style={`--metric-progress: ${progress}%`}></span>
</div>
{/if}
{#if card.sparkline?.length}
<svg class="telemetry-card__sparkline" viewBox="0 0 100 24" aria-hidden="true">
<polyline points={sparklinePoints(card.sparkline)} />
</svg>
{/if}
{#if card.detail || card.description}
<p>{card.detail || card.description}</p>
{/if}
</article>
<style>
.telemetry-card {
display: grid;
min-height: 7.15rem;
border: var(--ui-border);
background: linear-gradient(180deg, rgba(13, 15, 14, 0.82), rgba(2, 3, 2, 0.92));
padding: var(--ui-space-3);
color: var(--ui-color-text);
gap: var(--ui-space-2);
}
.telemetry-card[data-severity="warning"] {
border-color: color-mix(in srgb, var(--ui-color-warning), transparent 42%);
}
.telemetry-card[data-severity="danger"] {
border-color: color-mix(in srgb, var(--ui-color-danger), transparent 22%);
color: var(--ui-color-danger);
}
.telemetry-card[data-severity="stale"],
.telemetry-card[data-severity="unavailable"] {
color: var(--ui-color-stale);
}
.telemetry-card[data-severity="loading"] {
color: var(--ui-color-accent);
}
.telemetry-card[data-severity="loading"] .telemetry-card__bar span {
min-width: 1.25rem;
}
header {
display: flex;
min-width: 0;
align-items: center;
gap: var(--ui-space-2);
}
h3,
p {
margin: 0;
}
h3 {
overflow-wrap: anywhere;
color: inherit;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0;
text-transform: uppercase;
}
strong {
font-family: var(--ui-font-display);
font-size: clamp(2rem, 4vw, 3rem);
font-weight: 800;
letter-spacing: 0;
line-height: 0.82;
}
p {
color: var(--ui-color-muted);
font-size: 0.67rem;
line-height: 1.2;
text-transform: uppercase;
}
.telemetry-card__bar {
height: 0.35rem;
border: var(--ui-border);
background: #030403;
}
.telemetry-card__bar span {
display: block;
width: var(--metric-progress);
height: 100%;
background: currentColor;
}
.telemetry-card__sparkline {
width: 100%;
height: 1.5rem;
color: var(--ui-color-accent);
}
.telemetry-card__sparkline polyline {
fill: none;
stroke: currentColor;
stroke-linecap: square;
stroke-linejoin: miter;
stroke-width: 2;
}
</style>
<script lang="ts" module>
import { clampPercent } from "../format";
function resolveProgress(card: {
progress?: number;
value: { kind: string; value: number | string };
}): number | null {
if (typeof card.progress === "number") return clampPercent(card.progress);
if (card.value.kind !== "percent") return null;
const progress = Number(card.value.value);
return Number.isFinite(progress) ? clampPercent(progress) : null;
}
function sparklinePoints(values: number[]): string {
const max = Math.max(...values, 1);
const step = values.length > 1 ? 100 / (values.length - 1) : 100;
return values
.map((value, index) => {
const x = index * step;
const y = 24 - (Math.max(0, value) / max) * 22;
return `${x.toFixed(2)},${y.toFixed(2)}`;
})
.join(" ");
}
</script>