feat(webui): sparkline, capacity meter, metric tile components

This commit is contained in:
xiaoxixi 2026-07-26 21:28:52 +08:00
parent 5e2771c538
commit 6e719e7fcb
3 changed files with 101 additions and 0 deletions

View File

@ -0,0 +1,30 @@
<script>
let { depth = 0, cap = 1, segments = 8, label = "capacity" } = $props();
const ratio = $derived(cap > 0 ? Math.min(depth / cap, 1) : 0);
const lit = $derived(Math.round(ratio * segments));
const nearFull = $derived(ratio > 0.9);
const barColor = $derived(nearFull ? "var(--danger)" : "var(--signal)");
</script>
<div class="meter" role="meter" aria-label={label} aria-valuemin={0} aria-valuemax={cap} aria-valuenow={depth}>
{#each Array(segments) as _, i}
<span class="seg" class:lit={i < lit} style:--seg-color={barColor}></span>
{/each}
</div>
<style>
.meter { display: flex; gap: 3px; align-items: stretch; height: 100%; }
.seg {
flex: 1;
min-width: 4px;
border-radius: 2px;
background: var(--panel-2);
border: 1px solid var(--line);
transition: background .2s, border-color .2s;
}
.seg.lit {
background: var(--seg-color);
border-color: var(--seg-color);
}
</style>

View File

@ -0,0 +1,23 @@
<script>
import Sparkline from "./Sparkline.svelte";
let { label = "", value = "", sub = "", sparkValues = [], sparkColor = undefined } = $props();
</script>
<div class="tile panel">
<span class="label-caps">{label}</span>
<span class="value mono">{value}</span>
{#if sub}<span class="sub">{sub}</span>{/if}
{#if sparkValues.length > 0}
<div class="spark">
<Sparkline values={sparkValues} color={sparkColor ?? "var(--signal)"} />
</div>
{/if}
</div>
<style>
.tile { display: flex; flex-direction: column; gap: 6px; padding: 14px 16px; }
.value { font-size: 26px; font-weight: 700; line-height: 1.1; color: var(--text); }
.sub { font-size: 11px; color: var(--muted); }
.spark { height: 28px; margin-top: 4px; }
</style>

View File

@ -0,0 +1,48 @@
<script>
let { values = [], color = "var(--signal)" } = $props();
const W = 100;
const H = 28;
const PAD = 2;
const points = $derived.by(() => {
if (!values || values.length === 0) return "";
if (values.length === 1) {
const y = H / 2;
return `${PAD},${y} ${W - PAD},${y}`;
}
let min = values[0];
let max = values[0];
for (let i = 1; i < values.length; i++) {
if (values[i] < min) min = values[i];
if (values[i] > max) max = values[i];
}
const range = max - min || 1;
const step = (W - PAD * 2) / (values.length - 1);
return values
.map((v, i) => {
const x = PAD + i * step;
const y = H - PAD - ((v - min) / range) * (H - PAD * 2);
return `${x.toFixed(2)},${y.toFixed(2)}`;
})
.join(" ");
});
</script>
{#if points}
<svg class="sparkline" viewBox="0 0 {W} {H}" preserveAspectRatio="none" aria-hidden="true">
<polyline
points={points}
fill="none"
stroke={color}
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
/>
</svg>
{/if}
<style>
.sparkline { display: block; width: 100%; height: 100%; }
</style>