style(watchlist): the toolbar is two atomic groups — no more orphan chip

Add-controls left, filters right, justify-between: when the pane narrows, the
whole filter group drops below as a block instead of shedding its last chip.
Shorter chip labels (Active / Needed) and a slimmer search keep both groups on
one line at the tab's normal width.
This commit is contained in:
2026-08-29 01:16:53 +02:00
parent f0d3bed8a2
commit 72253f99fa
2 changed files with 9 additions and 6 deletions
+7 -4
View File
@@ -236,8 +236,9 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
</span> </span>
</div> </div>
{/* toolbar */} {/* toolbar */}
<div className="flex items-center gap-x-2 gap-y-1.5 flex-wrap"> <div className="flex items-start justify-between gap-x-3 gap-y-1.5 flex-wrap">
<Input className="h-8 w-44 font-mono placeholder:font-sans placeholder:normal-case" placeholder={t('wl.addPh')} value={addCall} <div className="flex items-center gap-2 shrink-0">
<Input className="h-8 w-40 font-mono placeholder:font-sans placeholder:normal-case" placeholder={t('wl.addPh')} value={addCall}
onChange={(e) => setAddCall(e.target.value.toUpperCase())} onChange={(e) => setAddCall(e.target.value.toUpperCase())}
onKeyDown={(e) => { if (e.key === 'Enter') void add(); }} /> onKeyDown={(e) => { if (e.key === 'Enter') void add(); }} />
<label className="flex items-center gap-1.5 text-xs cursor-pointer" title={t('wl.contestHint')}> <label className="flex items-center gap-1.5 text-xs cursor-pointer" title={t('wl.contestHint')}>
@@ -252,10 +253,11 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
value={pattern} value={pattern}
onChange={(e) => setPattern(e.target.value.toUpperCase())} onChange={(e) => setPattern(e.target.value.toUpperCase())}
onBlur={() => { void SetWatchlistContestPattern(pattern.trim()); }} /> onBlur={() => { void SetWatchlistContestPattern(pattern.trim()); }} />
<div className="flex-1" /> </div>
<div className="flex items-center gap-2 shrink-0">
<div className="relative"> <div className="relative">
<Search className="size-3.5 absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" /> <Search className="size-3.5 absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
<Input className="h-8 w-32 pl-7 text-sm" placeholder={t('wl.searchPh')} value={search} <Input className="h-8 w-28 pl-7 text-sm" placeholder={t('wl.searchPh')} value={search}
onChange={(e) => setSearch(e.target.value)} /> onChange={(e) => setSearch(e.target.value)} />
</div> </div>
{/* families kept together, told apart — per review of the DXHunter port, {/* families kept together, told apart — per review of the DXHunter port,
@@ -285,6 +287,7 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
{t('wl.neededOnly')} {t('wl.neededOnly')}
</button> </button>
</div> </div>
</div>
{error && <div className="text-xs text-danger px-1">{error}</div>} {error && <div className="text-xs text-danger px-1">{error}</div>}
{/* cards */} {/* cards */}
+2 -2
View File
@@ -42,7 +42,7 @@ const en: Dict = {
'wl.addPh': 'Callsign or prefix', 'wl.add': 'Add', 'wl.contest': 'Contest', 'wl.addAsContest': 'as contest', 'wl.cTotal': 'Watchlist:', 'wl.cActive': 'Active:', 'wl.cNeeded': 'Needed:', 'wl.allModes': 'All modes', 'wl.modeFilter': 'Only show spots in this mode', 'wl.patternPh': 'Auto contest (e.g. WWA)', 'wl.patternHint': 'Auto-add as contest: any spotted callsign CONTAINING this text joins the watchlist as a contest entry by itself. Empty = off; collected entries stay.', 'wl.addPh': 'Callsign or prefix', 'wl.add': 'Add', 'wl.contest': 'Contest', 'wl.addAsContest': 'as contest', 'wl.cTotal': 'Watchlist:', 'wl.cActive': 'Active:', 'wl.cNeeded': 'Needed:', 'wl.allModes': 'All modes', 'wl.modeFilter': 'Only show spots in this mode', 'wl.patternPh': 'Auto contest (e.g. WWA)', 'wl.patternHint': 'Auto-add as contest: any spotted callsign CONTAINING this text joins the watchlist as a contest entry by itself. Empty = off; collected entries stay.',
'wl.contestHint': 'Contest station: worked/needed is judged against the current UTC day — at 00:00 UTC every slot can be worked again.', 'wl.contestHint': 'Contest station: worked/needed is judged against the current UTC day — at 00:00 UTC every slot can be worked again.',
'wl.searchPh': 'Search…', 'wl.famAll': 'All', 'wl.famNormal': 'DX', 'wl.famContest': 'Contest', 'wl.searchPh': 'Search…', 'wl.famAll': 'All', 'wl.famNormal': 'DX', 'wl.famContest': 'Contest',
'wl.activeOnly': 'Active only', 'wl.neededOnly': 'Needed only', 'wl.activeOnly': 'Active', 'wl.neededOnly': 'Needed',
'wl.empty': 'Add the callsigns or prefixes you are hunting — a prefix catches the portable forms too (RI0SP matches RI0SP/MM). Spots from the cluster appear under each entry with what they are worth.', 'wl.empty': 'Add the callsigns or prefixes you are hunting — a prefix catches the portable forms too (RI0SP matches RI0SP/MM). Spots from the cluster appear under each entry with what they are worth.',
'wl.noneMatch': 'Nothing matches the current filters.', 'wl.noneMatch': 'Nothing matches the current filters.',
'wl.onAir': 'ON AIR', 'wl.expedition': 'DXpedition', 'wl.onAir': 'ON AIR', 'wl.expedition': 'DXpedition',
@@ -566,7 +566,7 @@ const fr: Dict = {
'wl.addPh': 'Indicatif ou préfixe', 'wl.add': 'Ajouter', 'wl.contest': 'Contest', 'wl.addAsContest': 'comme contest', 'wl.cTotal': 'Watchlist :', 'wl.cActive': 'Actives :', 'wl.cNeeded': 'Manquantes :', 'wl.allModes': 'Tous les modes', 'wl.modeFilter': 'Ne montrer que les spots de ce mode', 'wl.patternPh': 'Auto contest (ex. WWA)', 'wl.patternHint': "Ajout auto comme contest : tout indicatif spotté CONTENANT ce texte rejoint la watchlist en entrée contest tout seul. Vide = désactivé ; les entrées déjà collectées restent.", 'wl.addPh': 'Indicatif ou préfixe', 'wl.add': 'Ajouter', 'wl.contest': 'Contest', 'wl.addAsContest': 'comme contest', 'wl.cTotal': 'Watchlist :', 'wl.cActive': 'Actives :', 'wl.cNeeded': 'Manquantes :', 'wl.allModes': 'Tous les modes', 'wl.modeFilter': 'Ne montrer que les spots de ce mode', 'wl.patternPh': 'Auto contest (ex. WWA)', 'wl.patternHint': "Ajout auto comme contest : tout indicatif spotté CONTENANT ce texte rejoint la watchlist en entrée contest tout seul. Vide = désactivé ; les entrées déjà collectées restent.",
'wl.contestHint': "Station contest : contacté/manquant est jugé sur la journée UTC courante — à 00:00 UTC chaque créneau redevient à faire.", 'wl.contestHint': "Station contest : contacté/manquant est jugé sur la journée UTC courante — à 00:00 UTC chaque créneau redevient à faire.",
'wl.searchPh': 'Chercher…', 'wl.famAll': 'Tous', 'wl.famNormal': 'DX', 'wl.famContest': 'Contest', 'wl.searchPh': 'Chercher…', 'wl.famAll': 'Tous', 'wl.famNormal': 'DX', 'wl.famContest': 'Contest',
'wl.activeOnly': 'Actifs seulement', 'wl.neededOnly': 'Manquants seulement', 'wl.activeOnly': 'Actifs', 'wl.neededOnly': 'Manquants',
'wl.empty': "Ajoutez les indicatifs ou préfixes que vous chassez — un préfixe attrape aussi les formes portables (RI0SP matche RI0SP/MM). Les spots du cluster apparaissent sous chaque entrée avec ce qu'ils valent.", 'wl.empty': "Ajoutez les indicatifs ou préfixes que vous chassez — un préfixe attrape aussi les formes portables (RI0SP matche RI0SP/MM). Les spots du cluster apparaissent sous chaque entrée avec ce qu'ils valent.",
'wl.noneMatch': 'Rien ne correspond aux filtres actuels.', 'wl.noneMatch': 'Rien ne correspond aux filtres actuels.',
'wl.onAir': 'ON AIR', 'wl.expedition': 'DXpédition', 'wl.onAir': 'ON AIR', 'wl.expedition': 'DXpédition',