fix(rotor widget): several rotors no longer run off the bottom

The selector row appears above the dial when there is more than one
rotor, and the widget's height is not its own to take — it sits in a
strip sized by the entry form beside it. The row was simply added, so the
SP/LP pair and half the Stop button went off the end.

The dial (24 px), the three button rows (8 px each) and the padding
(4 px) now give that height back between them, which is a selector row
almost exactly. The dial and the controls column stay the same height as
each other, so the two columns still line up.
This commit is contained in:
2026-09-07 09:42:48 +02:00
parent 0430aab78e
commit ed062a040c
2 changed files with 32 additions and 11 deletions
+28 -9
View File
@@ -610,9 +610,22 @@ export function RotorCompass({
return columns;
}, [presets]);
// 192 dial + 6 gap + 154 controls + 16 padding, plus 60 per preset column.
// THE SELECTOR HAS TO COME OUT OF SOMEWHERE.
//
// With more than one rotor a row of buttons appears above the dial, and the
// widget's height is not its own to take: it sits in a strip whose height is
// set by the entry form beside it. The extra row simply pushed the bottom of
// the panel off the end — the SP/LP pair and half the Stop button gone.
//
// So the dial and the button rows give the row back, in proportion: 24 px off
// the dial and 8 off each of the three rows is the height of a selector, and
// nothing has to be dropped.
const tight = !!(rotors && rotors.length > 1);
const dialPx = tight ? 168 : 192;
const rowPx = tight ? 52 : 60;
// 6 gap + 154 controls + 16 padding, plus 60 per preset column.
const controlsWidth = 154 + presetColumns.length * 60;
const widgetWidth = 368 + presetColumns.length * 60;
const widgetWidth = dialPx + 176 + presetColumns.length * 60;
const markMovementCommanded = () => {
movementSeenRef.current = false;
@@ -713,7 +726,8 @@ export function RotorCompass({
const renderPresetColumn = (column: RotorPreset[], columnIndex: number) => (
<div key={`preset-column-${columnIndex}`}
className="w-[54px] min-w-[54px] shrink-0 grid grid-rows-[60px_60px_60px] gap-1.5 min-h-0">
className="w-[54px] min-w-[54px] shrink-0 grid gap-1.5 min-h-0"
style={{ gridTemplateRows: `repeat(3, ${rowPx}px)` }}>
{[0, 2, 4].map((row) => (
<div key={row} className="h-full min-h-0 grid grid-rows-2 gap-1">
{column[row] && renderPresetButton(column[row], columnIndex * 6 + row)}
@@ -749,7 +763,8 @@ export function RotorCompass({
);
const mainControls = (
<div className="w-[154px] min-w-[154px] shrink-0 grid grid-rows-[60px_60px_60px] gap-1.5 min-h-0">
<div className="w-[154px] min-w-[154px] shrink-0 grid gap-1.5 min-h-0"
style={{ gridTemplateRows: `repeat(3, ${rowPx}px)` }}>
{/* Where the antenna is, and under it — smaller, yellow, and only while it
matters — where it was told to go. */}
<div className="h-full min-h-0 rounded-md border border-border bg-background/30 px-1 text-center relative overflow-hidden">
@@ -760,7 +775,8 @@ export function RotorCompass({
it. The green one does not move, so nothing jumps when the mouse
leaves the dial. */}
<div className={cn(
'absolute left-1/2 top-1/2 -translate-x-1/2 font-mono text-[30px] leading-none font-bold tabular-nums whitespace-nowrap transition-all duration-300 ease-out',
'absolute left-1/2 top-1/2 -translate-x-1/2 font-mono leading-none font-bold tabular-nums whitespace-nowrap transition-all duration-300 ease-out',
tight ? 'text-[26px]' : 'text-[30px]',
targetAzimuth != null ? '-translate-y-[72%]' : '-translate-y-1/2',
hoverAzimuth != null ? 'opacity-0 scale-95' : 'opacity-100 scale-100',
displayAzimuth != null ? 'text-success' : 'text-muted-foreground',
@@ -769,7 +785,8 @@ export function RotorCompass({
</div>
<div className={cn(
'absolute left-1/2 top-1/2 -translate-x-1/2 font-mono text-[30px] leading-none font-bold tabular-nums whitespace-nowrap transition-all duration-300 ease-out',
'absolute left-1/2 top-1/2 -translate-x-1/2 font-mono leading-none font-bold tabular-nums whitespace-nowrap transition-all duration-300 ease-out',
tight ? 'text-[26px]' : 'text-[30px]',
targetAzimuth != null ? '-translate-y-[72%]' : '-translate-y-1/2',
hoverAzimuth != null ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none',
)}
@@ -904,7 +921,7 @@ export function RotorCompass({
</div>
{rotors && rotors.length > 1 && (
<div className="flex flex-wrap gap-1 px-2 pt-1.5">
<div className="flex flex-wrap gap-1 px-2 pt-1">
{rotors.map((name, index) => {
const active = (activeRotor ?? 0) === index;
const label = name?.trim() || `Rotor ${index + 1}`;
@@ -922,8 +939,10 @@ export function RotorCompass({
)}
{showControls ? (
<div className="flex items-stretch gap-1.5 p-2 min-h-0">
<div className="w-[192px] min-w-[192px] h-[192px] min-h-[192px] shrink-0">{dial}</div>
// The padding gives its share too: four pixels, which is what the
// selector row still owed after the dial and the buttons had paid.
<div className={cn('flex items-stretch gap-1.5 min-h-0', tight ? 'p-1.5' : 'p-2')}>
<div className="shrink-0" style={{ width: dialPx, minWidth: dialPx, height: dialPx, minHeight: dialPx }}>{dial}</div>
<div className="shrink-0 flex gap-1.5 min-h-0"
style={{ width: `${controlsWidth}px`, minWidth: `${controlsWidth}px` }}>
{mainControls}