fix: the stop button overlapped the recording counter

Two absolutely-positioned boxes at guessed offsets: right-14 and right-2. They
collided as soon as the counter passed a minute and the text grew — a layout
that only looked right for the first 60 seconds, and did not even manage that at
this font size.

One flex row with a gap, so spacing is a consequence of the content rather than
an arithmetic guess about it. The callsign field also gains right padding while
the badges are shown, so a long call runs under them instead of being typed over.
This commit is contained in:
2026-07-30 21:39:50 +02:00
parent 069728ac68
commit c6088bb2de
+11 -5
View File
@@ -3735,7 +3735,7 @@ export default function App() {
recording. Both sit where the counter is, which has shifted left to recording. Both sit where the counter is, which has shifted left to
make room. */} make room. */}
{recording && recStopped && RECORDABLE_MODES.has(mode.toUpperCase()) && ( {recording && recStopped && RECORDABLE_MODES.has(mode.toUpperCase()) && (
<span className="absolute right-2 top-1/2 -translate-y-1/2 z-10 inline-flex items-center gap-1"> <span className="absolute right-2 top-1/2 -translate-y-1/2 z-10 inline-flex items-center gap-1.5">
<button <button
type="button" type="button"
onMouseDown={(e) => e.preventDefault()} onMouseDown={(e) => e.preventDefault()}
@@ -3759,28 +3759,31 @@ export default function App() {
</span> </span>
</span> </span>
)} )}
{/* Stop and the running counter share ONE flex row. They were two absolute
boxes at guessed offsets and overlapped as soon as the counter passed a
minute a layout that only looked right for the first 60 seconds. */}
{recording && !recStopped && RECORDABLE_MODES.has(mode.toUpperCase()) && ( {recording && !recStopped && RECORDABLE_MODES.has(mode.toUpperCase()) && (
<span className="absolute right-2 top-1/2 -translate-y-1/2 z-10 inline-flex items-center gap-1.5">
<button <button
type="button" type="button"
onMouseDown={(e) => e.preventDefault()} onMouseDown={(e) => e.preventDefault()}
onClick={(e) => { e.stopPropagation(); stopRecording(); }} onClick={(e) => { e.stopPropagation(); stopRecording(); }}
title={t('rec.stop')} title={t('rec.stop')}
className="absolute right-14 top-1/2 -translate-y-1/2 z-10 inline-flex items-center justify-center size-4 rounded text-muted-foreground hover:bg-muted" className="inline-flex items-center justify-center size-4 rounded text-muted-foreground hover:bg-muted"
> >
<span className="size-2 bg-current" /> <span className="size-2 bg-current" />
</button> </button>
)}
{recording && !recStopped && RECORDABLE_MODES.has(mode.toUpperCase()) && (
<button <button
type="button" type="button"
onMouseDown={(e) => e.preventDefault()} onMouseDown={(e) => e.preventDefault()}
onClick={resetRecordingClock} onClick={resetRecordingClock}
title="Click to restart the recording from 0 — drops everything captured so far (incl. pre-roll) so the file holds only your exchange" title="Click to restart the recording from 0 — drops everything captured so far (incl. pre-roll) so the file holds only your exchange"
className="absolute right-2 top-1/2 -translate-y-1/2 z-10 inline-flex items-center gap-1 text-[10px] font-semibold tabular-nums text-destructive whitespace-nowrap cursor-pointer rounded px-1 hover:bg-destructive-muted" className="inline-flex items-center gap-1 text-[10px] font-semibold tabular-nums text-destructive whitespace-nowrap cursor-pointer rounded px-1 hover:bg-destructive-muted"
> >
<span className="size-2 rounded-full bg-destructive animate-pulse" /> <span className="size-2 rounded-full bg-destructive animate-pulse" />
{String(Math.floor(recSeconds / 60)).padStart(2, '0')}:{String(recSeconds % 60).padStart(2, '0')} {String(Math.floor(recSeconds / 60)).padStart(2, '0')}:{String(recSeconds % 60).padStart(2, '0')}
</button> </button>
</span>
)} )}
<Input <Input
ref={callsignRef} ref={callsignRef}
@@ -3789,7 +3792,10 @@ export default function App() {
// exactly the same. A primary-tinted border plus an inset left bar make // exactly the same. A primary-tinted border plus an inset left bar make
// it the obvious one without adding a colour the theme doesn't own. The // it the obvious one without adding a colour the theme doesn't own. The
// contest-dupe state still overrides it — that one is a warning. // contest-dupe state still overrides it — that one is a warning.
// Room on the right for the REC badges while they are shown, so a long
// callsign runs under them instead of being typed over.
className={cn('font-mono text-base font-bold tracking-wider uppercase h-9 bg-muted/40 focus:bg-card', className={cn('font-mono text-base font-bold tracking-wider uppercase h-9 bg-muted/40 focus:bg-card',
recording && RECORDABLE_MODES.has(mode.toUpperCase()) && 'pr-24',
'border-primary/45 shadow-[inset_3px_0_0_0_var(--primary)]', 'border-primary/45 shadow-[inset_3px_0_0_0_var(--primary)]',
contest.active && contestDupe && 'ring-2 ring-destructive !bg-destructive-muted focus:!bg-destructive-muted text-destructive-muted-foreground shadow-none')} contest.active && contestDupe && 'ring-2 ring-destructive !bg-destructive-muted focus:!bg-destructive-muted text-destructive-muted-foreground shadow-none')}
value={callsign} value={callsign}