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