From 00bfee4ed255193f47faa6687c7e00d3c4cbbbcc Mon Sep 17 00:00:00 2001 From: rouggy Date: Sat, 25 Jul 2026 11:44:23 +0200 Subject: [PATCH] refactor(flex): single shared MeterBar so Flex/amp/tuner meters are identical The FlexRadio panel, AmpCard and TunerCard each carried their own copy of the MeterBar (LED-bar) component. Even small drift between the copies made the tuner meters look slightly off in height/LED size vs the others. Extracted one shared components/MeterBar.tsx (segments, bar height, padding) and imported it in all three, so every meter renders at exactly the same size. No behaviour change. --- frontend/src/components/AmpCard.tsx | 38 +--------------------- frontend/src/components/FlexPanel.tsx | 41 +----------------------- frontend/src/components/MeterBar.tsx | 46 +++++++++++++++++++++++++++ frontend/src/components/TunerCard.tsx | 40 ++--------------------- 4 files changed, 51 insertions(+), 114 deletions(-) create mode 100644 frontend/src/components/MeterBar.tsx diff --git a/frontend/src/components/AmpCard.tsx b/frontend/src/components/AmpCard.tsx index 59af7b0..188b3e6 100644 --- a/frontend/src/components/AmpCard.tsx +++ b/frontend/src/components/AmpCard.tsx @@ -1,6 +1,7 @@ import { useRef, useState } from 'react'; import { Flame, ChevronDown } from 'lucide-react'; import { cn } from '@/lib/utils'; +import { MeterBar } from '@/components/MeterBar'; import { AmpOperate, AmpPower, AmpPowerLevel, AmpFanMode, FlexAmpOperate } from '../../wailsjs/go/main/App'; // AmpCard renders the amplifier card exactly like the one in the FlexRadio panel, @@ -12,43 +13,6 @@ import { AmpOperate, AmpPower, AmpPowerLevel, AmpFanMode, FlexAmpOperate } from // Controls use the multi-amp API (AmpOperate/AmpPower/… by amp id) so several amps // can each get their own card. -const METER_SEGMENTS = 26; - -function MeterBar({ label, value, unit, lo, hi, accent = '#16a34a', display, segColor, compact }: { - label: string; value: number; unit?: string; lo: number; hi: number; accent?: string; display?: string; - segColor?: (frac: number) => string; compact?: boolean; -}) { - const span = hi - lo; - const pct = span > 0 ? Math.max(0, Math.min(100, ((value - lo) / span) * 100)) : 0; - const lit = Math.round((pct / 100) * METER_SEGMENTS); - return ( -
-
- {label} - - {display !== undefined ? display : ( - <>{Math.abs(value) >= 100 ? value.toFixed(0) : value.toFixed(1)}{unit} - )} - -
-
- {Array.from({ length: METER_SEGMENTS }).map((_, i) => { - const on = i < lit; - const frac = i / METER_SEGMENTS; - const col = segColor ? segColor(frac) : (frac > 0.82 ? '#dc2626' : accent); - return ( -
- ); - })} -
-
- ); -} - function Card({ icon: Icon, title, accent, children, ckey }: { icon: any; title: string; accent?: string; children: React.ReactNode; ckey?: string }) { // Collapsible with persisted state — same behaviour as the FlexRadio panel's Card. const storeKey = 'opslog.cardOpen.' + (ckey || title); diff --git a/frontend/src/components/FlexPanel.tsx b/frontend/src/components/FlexPanel.tsx index 2a0b877..2da3ca8 100644 --- a/frontend/src/components/FlexPanel.tsx +++ b/frontend/src/components/FlexPanel.tsx @@ -21,6 +21,7 @@ import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; import { sMeterRST } from '@/lib/rst'; import { TunerCard } from '@/components/TunerCard'; +import { MeterBar } from '@/components/MeterBar'; import type { TGStatus } from '@/components/TunerGeniusPanel'; type FlexState = { @@ -222,46 +223,6 @@ function OffsetRow({ label, on, onToggle, hz, onHz, disabled, title }: { // MeterBar — a segmented "LED" instrument bar (radio look) scaled by lo/hi. // `display` overrides the numeric readout; `segColor` colours segments by their // 0..1 position (zones); the top ~18% light red by default (overload/peak). -const METER_SEGMENTS = 26; -function MeterBar({ label, value, unit, lo, hi, accent = '#16a34a', extra, display, segColor, onClick, title, compact }: { - label: string; value: number; unit?: string; lo: number; hi: number; accent?: string; extra?: string; display?: string; - segColor?: (frac: number) => string; onClick?: () => void; title?: string; compact?: boolean; -}) { - const span = hi - lo; - const pct = span > 0 ? Math.max(0, Math.min(100, ((value - lo) / span) * 100)) : 0; - const lit = Math.round((pct / 100) * METER_SEGMENTS); - return ( -
-
- {label} - - {display !== undefined ? display : ( - <>{Math.abs(value) >= 100 ? value.toFixed(0) : value.toFixed(1)}{unit} - )} - -
- {/* LED bar — recessed track + gradient segments for a cleaner instrument look. */} -
- {Array.from({ length: METER_SEGMENTS }).map((_, i) => { - const on = i < lit; - const frac = i / METER_SEGMENTS; - const col = segColor ? segColor(frac) : (frac > 0.82 ? '#dc2626' : accent); - return ( -
- ); - })} -
- {extra && !compact &&
{extra}
} -
- ); -} - function Card({ icon: Icon, title, accent, children, ckey, open: openProp, onToggle }: { icon: any; title: string; accent?: string; children: React.ReactNode; ckey?: string; open?: boolean; onToggle?: () => void; // controlled mode — lets sibling cards share one collapse state diff --git a/frontend/src/components/MeterBar.tsx b/frontend/src/components/MeterBar.tsx new file mode 100644 index 0000000..ca0b919 --- /dev/null +++ b/frontend/src/components/MeterBar.tsx @@ -0,0 +1,46 @@ +import { cn } from '@/lib/utils'; + +// MeterBar is the ONE LED-bar meter used across the FlexRadio panel, the amplifier +// cards and the Tuner Genius card, so every meter renders at exactly the same size +// (segment count, bar height, padding). Keep it the single source of truth — don't +// re-declare a local copy in a panel, or the meters drift out of sync. +export const METER_SEGMENTS = 26; + +export function MeterBar({ label, value, unit, lo, hi, accent = '#16a34a', extra, display, segColor, onClick, title, compact }: { + label: string; value: number; unit?: string; lo: number; hi: number; accent?: string; extra?: string; display?: string; + segColor?: (frac: number) => string; onClick?: () => void; title?: string; compact?: boolean; +}) { + const span = hi - lo; + const pct = span > 0 ? Math.max(0, Math.min(100, ((value - lo) / span) * 100)) : 0; + const lit = Math.round((pct / 100) * METER_SEGMENTS); + return ( +
+
+ {label} + + {display !== undefined ? display : ( + <>{Math.abs(value) >= 100 ? value.toFixed(0) : value.toFixed(1)}{unit} + )} + +
+ {/* LED bar — recessed track + gradient segments for a cleaner instrument look. */} +
+ {Array.from({ length: METER_SEGMENTS }).map((_, i) => { + const on = i < lit; + const frac = i / METER_SEGMENTS; + const col = segColor ? segColor(frac) : (frac > 0.82 ? '#dc2626' : accent); + return ( +
+ ); + })} +
+ {extra && !compact &&
{extra}
} +
+ ); +} diff --git a/frontend/src/components/TunerCard.tsx b/frontend/src/components/TunerCard.tsx index acc37fb..247984f 100644 --- a/frontend/src/components/TunerCard.tsx +++ b/frontend/src/components/TunerCard.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { Gauge, Radio, ChevronDown } from 'lucide-react'; import { cn } from '@/lib/utils'; +import { MeterBar } from '@/components/MeterBar'; import type { TGStatus, TGChannel } from '@/components/TunerGeniusPanel'; import { TunerGeniusAutotune, TunerGeniusSetBypass, TunerGeniusSetOperate, TunerGeniusActivate } from '../../wailsjs/go/main/App'; @@ -9,43 +10,8 @@ import { TunerGeniusAutotune, TunerGeniusSetBypass, TunerGeniusSetOperate, Tuner // mirrors the native app's two channels (A / B) with their source, frequency and // antenna, a live PWR / SWR pair, and the Tune / Bypass / Operate actions. It // drives the backend directly (no local state) — the caller's ~1.5s poll -// reconciles the display, just like AmpCard. - -const METER_SEGMENTS = 26; - -function MeterBar({ label, value, unit, lo, hi, accent = '#16a34a', display, segColor }: { - label: string; value: number; unit?: string; lo: number; hi: number; accent?: string; display?: string; - segColor?: (frac: number) => string; -}) { - const span = hi - lo; - const pct = span > 0 ? Math.max(0, Math.min(100, ((value - lo) / span) * 100)) : 0; - const lit = Math.round((pct / 100) * METER_SEGMENTS); - return ( -
-
- {label} - - {display !== undefined ? display : ( - <>{Math.abs(value) >= 100 ? value.toFixed(0) : value.toFixed(1)}{unit} - )} - -
-
- {Array.from({ length: METER_SEGMENTS }).map((_, i) => { - const on = i < lit; - const frac = i / METER_SEGMENTS; - const col = segColor ? segColor(frac) : (frac > 0.82 ? '#dc2626' : accent); - return ( -
- ); - })} -
-
- ); -} +// reconciles the display, just like AmpCard. Meters come from the shared MeterBar +// so they're the exact same size as the Flex/amp meters. function Card({ icon: Icon, title, accent, children, ckey }: { icon: any; title: string; accent?: string; children: React.ReactNode; ckey?: string }) { // Collapsible with persisted state — same behaviour as the FlexRadio panel's Card.