mirror of
https://github.com/MeshCore-Beacon/beacon-web.git
synced 2026-09-16 10:22:34 +00:00
Integrate the stats feature onto main: live mesh KPIs, observations time-series, payload/node-type donuts, top-nodes/observers leaderboards, radio presets, scope stats, and per-observer telemetry (battery, airtime, noise, queue, receive errors) with 1h/6h/24h bucketing. - adapt to the multi-IATA region model (useRegion -> single iata for /stats/*) - rename stats getScopes -> getStatsScopes (avoids /scopes name-list collision) - donut legend kept right but scrollable + truncating so it never clips - balanced mesh grid; observer empty-state for telemetry-less observers - normalize telemetry t to ms (raw path emits seconds, bucketed emits ms) - add echarts 5.5.1; lazy-load StatsOverview chunk
51 lines
1.7 KiB
TypeScript
51 lines
1.7 KiB
TypeScript
import type { ReactNode } from "react";
|
|
|
|
export interface SegmentedOption {
|
|
value: string;
|
|
label: string;
|
|
icon?: ReactNode;
|
|
}
|
|
|
|
interface SegmentedProps {
|
|
options: SegmentedOption[];
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
ariaLabel: string;
|
|
size?: "sm" | "md";
|
|
className?: string;
|
|
}
|
|
|
|
// A contained "pill group" segmented control bound to the palette: the active pill gets a primary
|
|
// tint + inset ring, inactive pills are muted. Used for the Stats sub-tabs (md, with icons) and the
|
|
// time-range selector (sm). Active state is conveyed with aria-pressed, not color alone.
|
|
export function Segmented({ options, value, onChange, ariaLabel, size = "sm", className }: SegmentedProps) {
|
|
const pad = size === "md" ? "px-3 py-1.5 text-xs" : "px-2.5 py-1 text-[11px]";
|
|
return (
|
|
<div
|
|
role="group"
|
|
aria-label={ariaLabel}
|
|
className={`inline-flex items-center gap-0.5 rounded-md border border-border bg-bg-raised p-0.5 ${className ?? ""}`}
|
|
>
|
|
{options.map((o) => {
|
|
const active = o.value === value;
|
|
return (
|
|
<button
|
|
key={o.value}
|
|
type="button"
|
|
aria-pressed={active}
|
|
onClick={() => onChange(o.value)}
|
|
className={`flex items-center gap-1.5 rounded font-mono font-semibold tracking-wide transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary ${pad} ${
|
|
active
|
|
? "bg-primary/15 text-text-bright ring-1 ring-inset ring-primary/30"
|
|
: "text-text-muted hover:text-text-normal"
|
|
}`}
|
|
>
|
|
{o.icon}
|
|
{o.label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|