diff --git a/src/components/layout/StatusBar.tsx b/src/components/layout/StatusBar.tsx index 06e3027..98330d9 100644 --- a/src/components/layout/StatusBar.tsx +++ b/src/components/layout/StatusBar.tsx @@ -197,6 +197,34 @@ function useRemoteHostMetrics(connectionId: string | undefined, enabled: boolean return { metrics, stale } } +const DISK_SLOT_THRESHOLDS = [ + { slots: 5, min: 360 }, + { slots: 4, min: 290 }, + { slots: 3, min: 220 }, + { slots: 2, min: 150 }, + { slots: 1, min: 72 }, + { slots: 0, min: 0 }, +] as const +/** Deadband so showing/hiding disks cannot oscillate slot count. */ +const DISK_SLOT_HYSTERESIS_PX = 36 + +function diskSlotsForSpare(spare: number, current: number, withHysteresis: boolean): number { + let next = 0 + for (const row of DISK_SLOT_THRESHOLDS) { + if (spare >= row.min) { + next = row.slots + break + } + } + if (!withHysteresis || next === current) return next + if (next > current) { + const need = DISK_SLOT_THRESHOLDS.find((row) => row.slots === next)?.min ?? 0 + return spare >= need + DISK_SLOT_HYSTERESIS_PX ? next : current + } + const keep = DISK_SLOT_THRESHOLDS.find((row) => row.slots === current)?.min ?? 0 + return spare < keep - DISK_SLOT_HYSTERESIS_PX ? next : current +} + function RemoteHostMetricsStrip({ metrics, stale, @@ -210,7 +238,9 @@ function RemoteHostMetricsStrip({ }) { const { t } = useTranslation('common') const stripRef = useRef(null) - const [diskSlots, setDiskSlots] = useState(1) + const disksRef = useRef(null) + const measuredRef = useRef(false) + const [diskSlots, setDiskSlots] = useState(0) const osLabel = (metrics?.osPrettyName || fallbackOs || '').trim() || null const tz = metrics ? timezoneLabel(metrics) : null @@ -248,14 +278,12 @@ function RemoteHostMetricsStrip({ const updateSlots = () => { const spacer = parent.querySelector('[data-statusbar-spacer]') - const spare = spacer?.clientWidth ?? 0 - // ~70px per chip; fullscreen usually has room for top 5. - if (spare >= 360) setDiskSlots(5) - else if (spare >= 290) setDiskSlots(4) - else if (spare >= 220) setDiskSlots(3) - else if (spare >= 150) setDiskSlots(2) - else if (spare >= 72) setDiskSlots(1) - else setDiskSlots(0) + const disksEl = disksRef.current + // spacer shrinks when disks appear — add disks width back so capacity is stable. + const spare = (spacer?.clientWidth ?? 0) + (disksEl?.offsetWidth ?? 0) + const useHysteresis = measuredRef.current + measuredRef.current = true + setDiskSlots((prev) => diskSlotsForSpare(spare, prev, useHysteresis)) } updateSlots() @@ -264,7 +292,18 @@ function RemoteHostMetricsStrip({ const spacer = parent.querySelector('[data-statusbar-spacer]') if (spacer) observer.observe(spacer) return () => observer.disconnect() - }, [rankedDisks.length, osLabel, tz, transferLabel, metrics?.cpuPercent, metrics?.memPercent]) + }, []) + + // Disk chips mount after first metrics poll; re-check once data arrives. + useEffect(() => { + if (!rankedDisks.length) return + const parent = stripRef.current?.parentElement + if (!parent) return + const spacer = parent.querySelector('[data-statusbar-spacer]') + const spare = (spacer?.clientWidth ?? 0) + (disksRef.current?.offsetWidth ?? 0) + setDiskSlots((prev) => diskSlotsForSpare(spare, prev, measuredRef.current)) + measuredRef.current = true + }, [rankedDisks.length]) const hasAny = osLabel || @@ -303,11 +342,13 @@ function RemoteHostMetricsStrip({ {transferLabel} )} - {visibleDisks.map((d) => ( - - {shortMount(d.mount)} {d.percent}% - - ))} +
+ {visibleDisks.map((d) => ( + + {shortMount(d.mount)} {d.percent}% + + ))} +
) }