feat(webui): sparkline, capacity meter, metric tile components
This commit is contained in:
parent
5e2771c538
commit
6e719e7fcb
30
webui/src/lib/components/CapacityMeter.svelte
Normal file
30
webui/src/lib/components/CapacityMeter.svelte
Normal 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>
|
||||||
23
webui/src/lib/components/MetricTile.svelte
Normal file
23
webui/src/lib/components/MetricTile.svelte
Normal 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>
|
||||||
48
webui/src/lib/components/Sparkline.svelte
Normal file
48
webui/src/lib/components/Sparkline.svelte
Normal 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>
|
||||||
Loading…
x
Reference in New Issue
Block a user