style(details): size the Info tab fields to their contents

Six equal columns gave a US county name the same width as a two-digit CQ
zone: the county truncated while the zones, the prefix and the DXCC
number sat mostly empty. The row is twelve columns now, spans set per
field.

The bearing and the two distances are read-only and never exceed
"12345 km", so they move to a flex row at a fixed width and the address —
the one field here that was never wide enough — takes the remainder.
This commit is contained in:
2026-08-01 01:22:35 +02:00
parent c21d6cc23a
commit 589e0b9b6a
3 changed files with 65 additions and 43 deletions
+56 -39
View File
@@ -122,9 +122,17 @@ function numOrUndef(v: string): number | undefined {
}
// Compact field helper to keep the JSX dense.
function Field({ label, span = 1, children }: { label: string; span?: 1 | 2 | 3 | 4 | 6; children: React.ReactNode }) {
// Written out rather than built as `col-span-${n}`: Tailwind scans for literal
// class names, so an interpolated one is never emitted.
const SPAN_CLASS: Record<number, string> = {
1: '', 2: 'col-span-2', 3: 'col-span-3', 4: 'col-span-4', 5: 'col-span-5',
6: 'col-span-6', 7: 'col-span-7', 8: 'col-span-8', 9: 'col-span-9',
10: 'col-span-10', 11: 'col-span-11', 12: 'col-span-12',
};
function Field({ label, span = 1, className, children }: { label: string; span?: number; className?: string; children: React.ReactNode }) {
return (
<div className={cn('flex flex-col min-w-0', span === 2 && 'col-span-2', span === 3 && 'col-span-3', span === 4 && 'col-span-4', span === 6 && 'col-span-6')}>
<div className={cn('flex flex-col min-w-0', SPAN_CLASS[span], className)}>
<Label className="mb-1">{label}</Label>
{children}
</div>
@@ -243,14 +251,18 @@ export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, qth,
)}
{open === 'info' && (
<div className="grid grid-cols-6 gap-2 px-3 py-2.5">
<Field label={t('detp.statePref')}>
<div className="grid grid-cols-12 gap-2 px-3 py-2.5">
{/* Twelve columns, not six: the fields hold very different things. A US
county name is long, a CQ zone is two digits — on an even six-column
grid they were the same width, so the county truncated while the
zones sat mostly empty. */}
<Field label={t('detp.statePref')} span={2}>
<Input value={details.state} onChange={(e) => onChange({ state: e.target.value })} />
</Field>
<Field label={t('detp.county')}>
<Field label={t('detp.county')} span={4}>
<Input value={details.cnty} onChange={(e) => onChange({ cnty: e.target.value })} />
</Field>
<Field label={t('detp.prefix')}>
<Field label={t('detp.prefix')} span={2}>
<Input className="font-mono uppercase" value={prefix} readOnly tabIndex={-1} />
</Field>
<Field label={t('detp.cqZone')}>
@@ -264,7 +276,7 @@ export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, qth,
{/* DXCC # closes the top row (next to the zones); Continent and
Azimuth SP live in the main entry strip / bandeau. The long-path
bearing and distances move to the row below. */}
<Field label={t('detp.dxcc')}>
<Field label={t('detp.dxcc')} span={2}>
<Input
readOnly
tabIndex={-1}
@@ -273,40 +285,45 @@ export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, qth,
placeholder="—"
/>
</Field>
<Field label={t('detp.azimuthLp')}>
<Input
readOnly
tabIndex={-1}
className="font-mono bg-muted/40 cursor-default"
value={path ? fmtDeg(path.bearingLong) : ''}
placeholder="—"
/>
</Field>
<Field label={t('detp.distanceSp')}>
<Input
readOnly
tabIndex={-1}
className="font-mono bg-muted/40 cursor-default"
value={path ? fmtKm(path.distanceShort) : ''}
placeholder="—"
/>
</Field>
<Field label={t('detp.distanceLp')}>
<Input
readOnly
tabIndex={-1}
className="font-mono bg-muted/40 cursor-default"
value={path ? fmtKm(path.distanceLong) : ''}
placeholder="—"
/>
</Field>
<Field label={t('detp.address')} span={3}>
<Input value={details.address} onChange={(e) => onChange({ address: e.target.value })} />
</Field>
<Field label={t('detp.qslMessage')} span={3}>
{/* The bearing and the two distances are read-only and hold at most
"12345 km", so they take a fixed width and the address — the one
field here that is never wide enough — gets everything left over. */}
<div className="col-span-12 flex gap-2">
<Field label={t('detp.azimuthLp')} className="w-20 shrink-0">
<Input
readOnly
tabIndex={-1}
className="font-mono bg-muted/40 cursor-default"
value={path ? fmtDeg(path.bearingLong) : ''}
placeholder="—"
/>
</Field>
<Field label={t('detp.distanceSp')} className="w-24 shrink-0">
<Input
readOnly
tabIndex={-1}
className="font-mono bg-muted/40 cursor-default"
value={path ? fmtKm(path.distanceShort) : ''}
placeholder="—"
/>
</Field>
<Field label={t('detp.distanceLp')} className="w-24 shrink-0">
<Input
readOnly
tabIndex={-1}
className="font-mono bg-muted/40 cursor-default"
value={path ? fmtKm(path.distanceLong) : ''}
placeholder="—"
/>
</Field>
<Field label={t('detp.address')} className="flex-1 min-w-0">
<Input value={details.address} onChange={(e) => onChange({ address: e.target.value })} />
</Field>
</div>
<Field label={t('detp.qslMessage')} span={7}>
<Input value={details.qsl_msg} onChange={(e) => onChange({ qsl_msg: e.target.value })} />
</Field>
<Field label={t('detp.qslVia')} span={2}>
<Field label={t('detp.qslVia')} span={5}>
<Input value={details.qsl_via} onChange={(e) => onChange({ qsl_via: e.target.value })} />
</Field>
</div>