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.