style(cluster): one header row in the docked pane

The pane drew its own title bar above the grid's toolbar, so the cluster spent
two lines on headers where Recent QSOs beside it spent one — in a pane often
only a few spots tall. The grid's toolbar now takes what the caller wants on its
left, and the pane passes its title, live count and Filters button there.
Reported by VK4DX.
This commit is contained in:
2026-08-28 13:15:41 +02:00
parent f7b9aa2181
commit 30f74583ff
3 changed files with 24 additions and 9 deletions
+11 -5
View File
@@ -6236,13 +6236,19 @@ export default function App() {
case 'cluster':
return (
<div className="h-full w-full min-h-0 flex flex-col bg-card border border-border rounded-lg overflow-hidden">
<div className="flex items-center justify-between px-2 py-1 border-b border-border/60 shrink-0">
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Cluster</span>
{clusterFiltersToggleBtn}
</div>
<div className="flex-1 min-h-0 flex">
<div className="flex-1 min-w-0 flex flex-col min-h-0">
<ClusterGrid key={`clg-${activeProfileId ?? 'x'}`} rows={clusterRenderedRows as any} spotStatus={spotStatus} onSpotClick={handleSpotClick} onSpotSelect={handleSpotSelect} />
{/* Title, count and Filters ride INSIDE the grid's toolbar: two
header rows cost a pane that is often only a few spots tall
one of the few lines it has. */}
<ClusterGrid key={`clg-${activeProfileId ?? 'x'}`} rows={clusterRenderedRows as any} spotStatus={spotStatus} onSpotClick={handleSpotClick} onSpotSelect={handleSpotSelect}
headerLeft={(
<>
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground shrink-0">DX Cluster</span>
<Badge variant="secondary" className="text-[10px] shrink-0">{spots.length} live</Badge>
{clusterFiltersToggleBtn}
</>
)} />
</div>
{clusterShowFilters && renderClusterFilters()}
</div>
+9 -2
View File
@@ -84,6 +84,11 @@ type Props = {
// stray click while looking took the operator off the station they were
// working. Looking and going are now two different gestures.
onSpotSelect?: (s: ClusterSpot) => void;
// Anything the caller wants on the LEFT of the toolbar — the pane's title, its
// live count, its Filters button. Docked in a pane, the title used to sit on a
// row of its own above this one, so the cluster ate two lines of a short pane
// where Recent QSOs beside it ate one. Reported by VK4DX.
headerLeft?: React.ReactNode;
};
const COL_STATE_KEY = 'hamlog.clusterColState.v1';
@@ -523,7 +528,7 @@ const GROUP_ORDER = ['Spot', 'Geo'];
const CLG_GRP_KEYS: Record<string, string> = { Spot: 'clg2.grpSpot', Geo: 'clg2.grpGeo' };
const groupLabel = (t: TFn, g: string): string => t(CLG_GRP_KEYS[g] ?? g);
export function ClusterGrid({ rows, spotStatus, onSpotClick, onSpotSelect }: Props) {
export function ClusterGrid({ rows, spotStatus, onSpotClick, onSpotSelect, headerLeft }: Props) {
const { t } = useI18n();
const gridRef = useRef<any>(null);
const [pickerOpen, setPickerOpen] = useState(false);
@@ -689,7 +694,9 @@ export function ClusterGrid({ rows, spotStatus, onSpotClick, onSpotSelect }: Pro
return (
<>
<div className="flex items-center justify-end gap-2 px-2.5 py-1 border-b border-border/60 bg-muted/20">
<div className="flex items-center gap-2 px-2.5 py-1 border-b border-border/60 bg-muted/20">
{headerLeft}
<div className="flex-1" />
<Button variant="ghost" size="sm" className="h-7 text-[11px]" onClick={() => gridRef.current?.api?.setFilterModel(null)}
title={t('clg2.clearFiltersTitle')}>
<FilterX className="size-3.5" /> {t('clg2.clearFilters')}