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
+4 -2
View File
@@ -6,13 +6,15 @@
"[NEW] Typing a digital watering hole sets the mode with it. A spot click has always carried one; a frequency typed by hand carried none, so the rig stayed in SSB on 28.074 while the operator waited for decodes. Same table and same tolerance as a spot (±3 kHz of a known FT8/FT4/JS8 frequency), only towards the digital modes: tuning away from one leaves the mode alone, because there the frequency says nothing about what you mean to do.", "[NEW] Typing a digital watering hole sets the mode with it. A spot click has always carried one; a frequency typed by hand carried none, so the rig stayed in SSB on 28.074 while the operator waited for decodes. Same table and same tolerance as a spot (±3 kHz of a known FT8/FT4/JS8 frequency), only towards the digital modes: tuning away from one leaves the mode alone, because there the frequency says nothing about what you mean to do.",
"Yaesu CAT now drives the older radios. The FTDX10, FT-991A, FT-891 and FT-710 write a frequency in nine digits; everything before them — FTDX3000, FTDX5000, FTDX1200, FT-2000, FT-950, FT-450 — writes eight and answers a nine-digit command with a rejection, which is what an FTDX3000 owner saw: every FA refused and a radio that would not follow. The width is taken from the rigs own reply rather than from a table of models, so a set is in the format that radio speaks — including models this backend has never heard of.", "Yaesu CAT now drives the older radios. The FTDX10, FT-991A, FT-891 and FT-710 write a frequency in nine digits; everything before them — FTDX3000, FTDX5000, FTDX1200, FT-2000, FT-950, FT-450 — writes eight and answers a nine-digit command with a rejection, which is what an FTDX3000 owner saw: every FA refused and a radio that would not follow. The width is taken from the rigs own reply rather than from a table of models, so a set is in the format that radio speaks — including models this backend has never heard of.",
"Yaesu: RTTY can be set on USB (Settings → CAT). ADIF records only “RTTY” and the rig has both sidebands, so the log cannot answer for it — the older RTTY-L stays the default, and a station whose FSK controller wants the upper one says so once.", "Yaesu: RTTY can be set on USB (Settings → CAT). ADIF records only “RTTY” and the rig has both sidebands, so the log cannot answer for it — the older RTTY-L stays the default, and a station whose FSK controller wants the upper one says so once.",
"The update no longer relaunches OpsLog through a hidden PowerShell. An unsigned program that replaces itself on disk, clears the mark-of-the-web and then spawns a windowless PowerShell to start another executable is — byte for byte — the shape of a dropper, and Windows Defenders machine-learning model reads the shape, not the intention: 0.27.14 was removed from a station under Trojan:Script/Wacatac.H!ml. The new version simply starts itself and waits its turn on the single-instance lock, which it already knew how to do. Only the rare fallback path, when the running file cannot even be renamed, still needs a helper that outlives the process." "The update no longer relaunches OpsLog through a hidden PowerShell. An unsigned program that replaces itself on disk, clears the mark-of-the-web and then spawns a windowless PowerShell to start another executable is — byte for byte — the shape of a dropper, and Windows Defenders machine-learning model reads the shape, not the intention: 0.27.14 was removed from a station under Trojan:Script/Wacatac.H!ml. The new version simply starts itself and waits its turn on the single-instance lock, which it already knew how to do. Only the rare fallback path, when the running file cannot even be renamed, still needs a helper that outlives the process.",
"Rotor widget: with more than one rotor the panel no longer runs off the bottom. The selector row appears above the dial, and the widgets height is not its own to take — it sits in a strip sized by the entry form beside it — so the SP/LP pair and half the Stop button were cut off. The dial, the button rows and the padding now give that row back between them, in proportion, and nothing is dropped."
], ],
"fr": [ "fr": [
"[NEW] Taper une fréquence dappel numérique règle le mode avec elle. Un clic sur un spot en portait un depuis toujours ; une fréquence tapée à la main nen portait aucun, si bien que le poste restait en SSB sur 28.074 pendant quon attendait les décodages. Même table et même tolérance quun spot (±3 kHz dune fréquence FT8/FT4/JS8 connue), et seulement vers les modes numériques : en sen éloignant le mode nest pas touché, car là la fréquence ne dit rien de ce quon veut faire.", "[NEW] Taper une fréquence dappel numérique règle le mode avec elle. Un clic sur un spot en portait un depuis toujours ; une fréquence tapée à la main nen portait aucun, si bien que le poste restait en SSB sur 28.074 pendant quon attendait les décodages. Même table et même tolérance quun spot (±3 kHz dune fréquence FT8/FT4/JS8 connue), et seulement vers les modes numériques : en sen éloignant le mode nest pas touché, car là la fréquence ne dit rien de ce quon veut faire.",
"Le CAT Yaesu pilote désormais les postes plus anciens. FTDX10, FT-991A, FT-891 et FT-710 écrivent une fréquence sur neuf chiffres ; tout ce qui précède — FTDX3000, FTDX5000, FTDX1200, FT-2000, FT-950, FT-450 — l’écrit sur huit et rejette une commande à neuf chiffres. Cest ce que voyait un possesseur de FTDX3000 : chaque FA refusée et une radio qui ne suivait pas. Le format est pris dans la réponse du poste plutôt que dans une table de modèles : lenvoi part donc dans la langue de cette radio-là, y compris pour des modèles que ce backend ne connaît pas.", "Le CAT Yaesu pilote désormais les postes plus anciens. FTDX10, FT-991A, FT-891 et FT-710 écrivent une fréquence sur neuf chiffres ; tout ce qui précède — FTDX3000, FTDX5000, FTDX1200, FT-2000, FT-950, FT-450 — l’écrit sur huit et rejette une commande à neuf chiffres. Cest ce que voyait un possesseur de FTDX3000 : chaque FA refusée et une radio qui ne suivait pas. Le format est pris dans la réponse du poste plutôt que dans une table de modèles : lenvoi part donc dans la langue de cette radio-là, y compris pour des modèles que ce backend ne connaît pas.",
"Yaesu : le RTTY peut être placé en USB (Réglages → CAT). LADIF nenregistre que « RTTY » et le poste a les deux bandes latérales : le log ne peut pas répondre à sa place. Le RTTY-L ancien reste par défaut, et une station dont linterface FSK veut la supérieure le dit une fois.", "Yaesu : le RTTY peut être placé en USB (Réglages → CAT). LADIF nenregistre que « RTTY » et le poste a les deux bandes latérales : le log ne peut pas répondre à sa place. Le RTTY-L ancien reste par défaut, et une station dont linterface FSK veut la supérieure le dit une fois.",
"La mise à jour ne relance plus OpsLog par un PowerShell caché. Un programme non signé qui se remplace sur le disque, efface la marque « téléchargé depuis Internet » puis lance un PowerShell sans fenêtre pour démarrer un autre exécutable a — à loctet près — la forme dun dropper, et le modèle dapprentissage de Windows Defender lit la forme, pas lintention : la 0.27.14 a été supprimée chez un OM sous Trojan:Script/Wacatac.H!ml. La nouvelle version se lance elle-même et attend son tour sur le verrou dinstance unique, ce quelle savait déjà faire. Seul le repli rare, quand le fichier en cours dexécution ne peut même pas être renommé, garde un assistant qui survit au processus." "La mise à jour ne relance plus OpsLog par un PowerShell caché. Un programme non signé qui se remplace sur le disque, efface la marque « téléchargé depuis Internet » puis lance un PowerShell sans fenêtre pour démarrer un autre exécutable a — à loctet près — la forme dun dropper, et le modèle dapprentissage de Windows Defender lit la forme, pas lintention : la 0.27.14 a été supprimée chez un OM sous Trojan:Script/Wacatac.H!ml. La nouvelle version se lance elle-même et attend son tour sur le verrou dinstance unique, ce quelle savait déjà faire. Seul le repli rare, quand le fichier en cours dexécution ne peut même pas être renommé, garde un assistant qui survit au processus.",
"Widget rotor : avec plusieurs rotors, le panneau ne déborde plus par le bas. La rangée de sélection apparaît au-dessus du cadran, et la hauteur du widget ne lui appartient pas — il occupe une bande dont la hauteur est fixée par la saisie à côté — si bien que la paire SP/LP et la moitié du bouton Stop se retrouvaient coupées. Le cadran, les rangées de boutons et les marges rendent désormais cette hauteur entre eux, chacun pour sa part, sans rien supprimer."
] ]
}, },
{ {
+28 -9
View File
@@ -610,9 +610,22 @@ export function RotorCompass({
return columns; return columns;
}, [presets]); }, [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 controlsWidth = 154 + presetColumns.length * 60;
const widgetWidth = 368 + presetColumns.length * 60; const widgetWidth = dialPx + 176 + presetColumns.length * 60;
const markMovementCommanded = () => { const markMovementCommanded = () => {
movementSeenRef.current = false; movementSeenRef.current = false;
@@ -713,7 +726,8 @@ export function RotorCompass({
const renderPresetColumn = (column: RotorPreset[], columnIndex: number) => ( const renderPresetColumn = (column: RotorPreset[], columnIndex: number) => (
<div key={`preset-column-${columnIndex}`} <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) => ( {[0, 2, 4].map((row) => (
<div key={row} className="h-full min-h-0 grid grid-rows-2 gap-1"> <div key={row} className="h-full min-h-0 grid grid-rows-2 gap-1">
{column[row] && renderPresetButton(column[row], columnIndex * 6 + row)} {column[row] && renderPresetButton(column[row], columnIndex * 6 + row)}
@@ -749,7 +763,8 @@ export function RotorCompass({
); );
const mainControls = ( 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 {/* Where the antenna is, and under it — smaller, yellow, and only while it
matters — where it was told to go. */} 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"> <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 it. The green one does not move, so nothing jumps when the mouse
leaves the dial. */} leaves the dial. */}
<div className={cn( <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', targetAzimuth != null ? '-translate-y-[72%]' : '-translate-y-1/2',
hoverAzimuth != null ? 'opacity-0 scale-95' : 'opacity-100 scale-100', hoverAzimuth != null ? 'opacity-0 scale-95' : 'opacity-100 scale-100',
displayAzimuth != null ? 'text-success' : 'text-muted-foreground', displayAzimuth != null ? 'text-success' : 'text-muted-foreground',
@@ -769,7 +785,8 @@ export function RotorCompass({
</div> </div>
<div className={cn( <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', targetAzimuth != null ? '-translate-y-[72%]' : '-translate-y-1/2',
hoverAzimuth != null ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none', hoverAzimuth != null ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none',
)} )}
@@ -904,7 +921,7 @@ export function RotorCompass({
</div> </div>
{rotors && rotors.length > 1 && ( {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) => { {rotors.map((name, index) => {
const active = (activeRotor ?? 0) === index; const active = (activeRotor ?? 0) === index;
const label = name?.trim() || `Rotor ${index + 1}`; const label = name?.trim() || `Rotor ${index + 1}`;
@@ -922,8 +939,10 @@ export function RotorCompass({
)} )}
{showControls ? ( {showControls ? (
<div className="flex items-stretch gap-1.5 p-2 min-h-0"> // The padding gives its share too: four pixels, which is what the
<div className="w-[192px] min-w-[192px] h-[192px] min-h-[192px] shrink-0">{dial}</div> // 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" <div className="shrink-0 flex gap-1.5 min-h-0"
style={{ width: `${controlsWidth}px`, minWidth: `${controlsWidth}px` }}> style={{ width: `${controlsWidth}px`, minWidth: `${controlsWidth}px` }}>
{mainControls} {mainControls}