diff --git a/frontend/src/components/DecodesPanel.tsx b/frontend/src/components/DecodesPanel.tsx index d2319f5..0c263aa 100644 --- a/frontend/src/components/DecodesPanel.tsx +++ b/frontend/src/components/DecodesPanel.tsx @@ -71,12 +71,20 @@ interface Props { const DEFAULT_TR = 15; // ROW is the column template, shared by the header and every row so the two can -// never drift. Fixed widths for the short fields, one flexible column for the -// message — and a hard cap on the whole grid, because on a 2500 px screen a -// free-flowing row puts the country a foot away from the callsign it belongs to -// and the eye has to travel the gap on every line. -const ROW = 'grid grid-cols-[3px_112px_56px_60px_1fr_260px_170px_28px] gap-x-3 items-center'; -const ROW_MAX = 'max-w-[1500px]'; +// never drift. Full width and left-aligned — an earlier pass centred it inside a +// maximum width, which on a wide screen opened a huge dead margin down the left +// before the first callsign. +// +// Message is the one elastic column, with a floor so it does not collapse; the +// slack lands there rather than between two fixed columns, which is what read as +// a hole in the middle of every line. +const ROW = 'grid grid-cols-[3px_120px_64px_68px_minmax(280px,1fr)_230px_180px_36px] items-stretch'; + +// CELL draws the column rule. items-stretch above plus a right border here is +// what makes the lines run unbroken from the header to the bottom of the list — +// the thing that turns rows of text into a table you can follow across. +const CELL = 'flex items-center min-w-0 px-3 border-r border-border/40'; +const CELL_LAST = 'flex items-center justify-center min-w-0 px-2'; // The "new" badges. Every one of these is a REASON TO CALL, which is why they // get a column of their own rather than a coloured edge: a stripe says something @@ -122,6 +130,23 @@ function periodStart(at: string, tr: number): number { const hhmmss = (epochSec: number) => new Date(epochSec * 1000).toISOString().slice(11, 19); +// renderMsg prints the decoded line with its leading CQ picked out. +// +// There used to be a separate green "CQ" badge in front of the message, which +// read "CQ CQ PE1NAO JO32" — the badge and the message's own first word saying +// the same thing twice. Highlighting the word already in the line keeps the +// scannability and drops the stutter. +function renderMsg(msg: string) { + const m = /^(CQ(?:\s+DX)?)\s+(.*)$/i.exec(msg); + if (!m) return {msg}; + return ( + <> + {m[1].toUpperCase()} + {' ' + m[2]} + + ); +} + // snrTone colours the report by readability rather than as a gradient: an // operator scanning a period wants "workable" to jump out, and -24 dB is not // three shades worse than -6, it is a different decision. @@ -308,16 +333,16 @@ export function DecodesPanel({ decodes, txMsgs, spotStatus, onCall, myCall }: Pr {/* ── Column header ──────────────────────────────────────────── */} -
-
+
+
- {t('dec.colCall')} - {t('dec.colSnr')} - {t('dec.colGrid')} - {t('dec.colMsg')} - {t('dec.colFlags')} - {t('dec.colCountry')} - L + {t('dec.colCall')} + {t('dec.colSnr')} + {t('dec.colGrid')} + {t('dec.colMsg')} + {t('dec.colFlags')} + {t('dec.colCountry')} + L
@@ -334,8 +359,8 @@ export function DecodesPanel({ decodes, txMsgs, spotStatus, onCall, myCall }: Pr {groups.map((g, gi) => (
{/* Period header — sticky, so the slot being read is always named. */} -
-
+
+
{hhmmss(g.start)} @@ -355,15 +380,14 @@ export function DecodesPanel({ decodes, txMsgs, spotStatus, onCall, myCall }: Pr {/* The operator's own transmission, at the top of its slot: it is what the stations below were answering (or ignoring). */} {g.tx.map((m, i) => ( -
-
- - - {t('dec.tx')} - - {m.msg} - {m.band && {m.band}} -
+
+ + + {t('dec.tx')} + + {m.msg} + {m.band && {m.band}}
))} @@ -375,65 +399,63 @@ export function DecodesPanel({ decodes, txMsgs, spotStatus, onCall, myCall }: Pr const mine = myCall && d.call === myCall.toUpperCase(); const hot = !!entity || extras.length > 0; return ( -
- -
+ + {e?.lotw && ( + L + )} + + ); })}