fixing pod collapse expandings

This commit is contained in:
legop3
2026-08-19 14:20:57 -04:00
parent 6ff60f7d0e
commit a5ec1dcb2d
15 changed files with 142 additions and 105 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -12,8 +12,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<!-- site-metadata:inject --> <!-- site-metadata:inject -->
<!-- analytics:inject --> <!-- analytics:inject -->
<script type="module" crossorigin src="/assets/index-CAUR32ig.js"></script> <script type="module" crossorigin src="/assets/index-D9i-ZujG.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D2qTDWX0.css"> <link rel="stylesheet" crossorigin href="/assets/index-D5gzYj1F.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
@@ -84,7 +84,7 @@ export default function BottomLeftPod({ roverId }) {
) : ( ) : (
<CornerPodToggle corner="bottom-left" expanded={false} label="Show rover controls" onClick={() => setOpen(true)} /> <CornerPodToggle corner="bottom-left" expanded={false} label="Show rover controls" onClick={() => setOpen(true)} />
)} )}
{hornDevice ? <HornSettingsExpansion open={hornSettingsOpen} podOpen={open} onOpenChange={setHornSettingsOpen} /> : null} {hornDevice ? <HornSettingsExpansion open={hornSettingsOpen} onOpenChange={setHornSettingsOpen} /> : null}
</> </>
); );
} }
@@ -0,0 +1,39 @@
// HUD Expansion Panel
// Purpose: Gives every independent HUD expansion one fixed toggle anchor and identical open/closed behavior.
import ExpansionToggle from './ExpansionToggle.jsx';
export default function ExpansionPanel({
open,
onOpenChange,
anchorClassName = '',
panelClassName = '',
panelAlign = 'left',
panelVerticalAlign = 'top',
openDirection,
closeDirection,
openLabel,
closeLabel,
children,
}) {
const panelAlignmentClass = panelAlign === 'right' ? 'right-0' : 'left-0';
const panelVerticalAlignmentClass = panelVerticalAlign === 'bottom' ? 'bottom-0' : 'top-0';
return (
<div className={`pointer-events-auto z-20 h-3 w-8 ${anchorClassName}`}>
{/* The content is positioned behind the fixed 32-by-12 anchor. Opening or
closing changes only whether this panel exists; it never relocates or
restyles the toggle that the user just clicked. */}
{open ? (
<div className={`absolute z-0 ${panelAlignmentClass} ${panelVerticalAlignmentClass} ${panelClassName}`}>
{children}
</div>
) : null}
<ExpansionToggle
direction={open ? closeDirection : openDirection}
label={open ? closeLabel : openLabel}
onClick={() => onOpenChange(!open)}
className="absolute left-0 top-0"
/>
</div>
);
}
@@ -6,16 +6,15 @@ const ICONS = { down: FaChevronDown, left: FaChevronLeft, right: FaChevronRight,
export default function ExpansionToggle({ direction, label, onClick, className = '' }) { export default function ExpansionToggle({ direction, label, onClick, className = '' }) {
const Icon = ICONS[direction] || FaChevronLeft; const Icon = ICONS[direction] || FaChevronLeft;
const horizontalEdge = direction === 'up' || direction === 'down';
return ( return (
<button <button
type="button" type="button"
aria-label={label} aria-label={label}
onClick={onClick} onClick={onClick}
className={`flex shrink-0 items-center justify-center bg-black/60 text-[0.5rem] text-white/75 hover:bg-black hover:text-white ${horizontalEdge ? 'h-3 w-8' : 'h-8 w-3'} ${className}`} className={`z-10 flex h-3 w-8 shrink-0 items-center justify-center bg-black/60 text-[0.5rem] text-white/75 hover:bg-black hover:text-white ${className}`}
> >
{/* The black strip is intentionally retained around the chevron. A collapsed expansion is {/* ExpansionPanel owns all positioning so this button has exactly one visual
therefore still a thin piece of that expansion, never a loose button over a pod. */} form. Only the chevron direction changes between open and closed states. */}
<Icon aria-hidden="true" /> <Icon aria-hidden="true" />
</button> </button>
); );
@@ -4,7 +4,7 @@ import { useCallback } from 'react';
import { HORN_MAX_FREQUENCY } from '../../../../controls/constants.js'; import { HORN_MAX_FREQUENCY } from '../../../../controls/constants.js';
import { useSettingsNamespace } from '../../../../settings/index.js'; import { useSettingsNamespace } from '../../../../settings/index.js';
import { HORN_SETTINGS_DEFAULTS } from '../../../../settings/namespaces.js'; import { HORN_SETTINGS_DEFAULTS } from '../../../../settings/namespaces.js';
import ExpansionToggle from './ExpansionToggle.jsx'; import ExpansionPanel from './ExpansionPanel.jsx';
function clampFrequency(value) { function clampFrequency(value) {
const numeric = Number(value); const numeric = Number(value);
@@ -12,7 +12,7 @@ function clampFrequency(value) {
return Math.min(HORN_MAX_FREQUENCY, Math.round(numeric)); return Math.min(HORN_MAX_FREQUENCY, Math.round(numeric));
} }
export default function HornSettingsExpansion({ open, podOpen, onOpenChange }) { export default function HornSettingsExpansion({ open, onOpenChange }) {
const { value, save } = useSettingsNamespace('horn', HORN_SETTINGS_DEFAULTS); const { value, save } = useSettingsNamespace('horn', HORN_SETTINGS_DEFAULTS);
const waveform = value?.waveform === 'sine' ? 'sine' : 'saw'; const waveform = value?.waveform === 'sine' ? 'sine' : 'saw';
const frequencies = [...(Array.isArray(value?.freqs) ? value.freqs : HORN_SETTINGS_DEFAULTS.freqs), 0, 0, 0, 0] const frequencies = [...(Array.isArray(value?.freqs) ? value.freqs : HORN_SETTINGS_DEFAULTS.freqs), 0, 0, 0, 0]
@@ -31,21 +31,25 @@ export default function HornSettingsExpansion({ open, podOpen, onOpenChange }) {
}); });
}, [save]); }, [save]);
if (!open) {
return (
<div className={`pointer-events-auto absolute left-10 z-20 flex h-3 w-8 bg-black/60 ${podOpen ? 'bottom-40' : 'bottom-0'}`}>
<ExpansionToggle direction="up" label="Show horn settings" onClick={() => onOpenChange(true)} />
</div>
);
}
return ( return (
<div className={`pointer-events-auto absolute left-0 z-20 w-52 rounded-tr-xl bg-black/60 p-2 text-xs text-white ${podOpen ? 'bottom-40' : 'bottom-0'}`}> /* Horn settings belongs to the left video wall, not to the peripheral pod.
Keeping this anchor fixed above the pod's maximum footprint means the
expansion never follows or overlaps the pod as that separate UI closes.
Its arrows mirror advanced info on the opposite wall: open points into
the screen and close points back toward the wall. */
<ExpansionPanel
open={open}
onOpenChange={onOpenChange}
anchorClassName="absolute bottom-40 left-0"
panelVerticalAlign="bottom"
panelClassName="w-52 rounded-tr-xl bg-black/60 p-2 pb-4 text-xs text-white"
openDirection="right"
closeDirection="left"
openLabel="Show horn settings"
closeLabel="Hide horn settings"
>
<div className="mb-2 flex items-center justify-between gap-2"> <div className="mb-2 flex items-center justify-between gap-2">
<span className="font-semibold text-cyan-100">Horn settings</span> <span className="font-semibold text-cyan-100">Horn settings</span>
{/* This arrow belongs to the expansion. Closing the peripheral pod never changes
hornSettings visibility; it only moves this panel into the vacated corner. */}
<ExpansionToggle direction="down" label="Hide horn settings" onClick={() => onOpenChange(false)} />
</div> </div>
<label className="flex items-center justify-between gap-2 text-slate-300"> <label className="flex items-center justify-between gap-2 text-slate-300">
<span>Wave</span> <span>Wave</span>
@@ -73,6 +77,6 @@ export default function HornSettingsExpansion({ open, podOpen, onOpenChange }) {
</label> </label>
))} ))}
</div> </div>
</div> </ExpansionPanel>
); );
} }
@@ -6,7 +6,7 @@ import { useSessionSelector } from '../../../../context/SessionContext.jsx';
import { useSharedClock } from '../../../../hooks/useSharedClock.js'; import { useSharedClock } from '../../../../hooks/useSharedClock.js';
import RoverLabel from '../../../RoverLabel/index.jsx'; import RoverLabel from '../../../RoverLabel/index.jsx';
import CornerPodToggle from './CornerPodToggle.jsx'; import CornerPodToggle from './CornerPodToggle.jsx';
import ExpansionToggle from './ExpansionToggle.jsx'; import ExpansionPanel from './ExpansionPanel.jsx';
import usePodVisibility from './usePodVisibility.js'; import usePodVisibility from './usePodVisibility.js';
export default function TopLeftPod({ roverId }) { export default function TopLeftPod({ roverId }) {
@@ -159,16 +159,18 @@ export default function TopLeftPod({ roverId }) {
{/* The rover name is an independent edge expansion. Its visibility control lives in {/* The rover name is an independent edge expansion. Its visibility control lives in
the expansion itself, and its position naturally moves into the corner whenever the expansion itself, and its position naturally moves into the corner whenever
the conditional timer pod is absent or manually collapsed. */} the conditional timer pod is absent or manually collapsed. */}
{nameOpen ? ( <ExpansionPanel
<div className={`flex h-11 items-center gap-2 bg-black/60 px-2 ${showTimer ? '' : 'rounded-br-xl'}`}> open={nameOpen}
onOpenChange={setNameOpen}
anchorClassName={`relative shrink-0 ${showTimer || !turnActive ? '' : 'ml-10'}`}
panelClassName={`flex h-11 min-w-max items-center gap-2 bg-black/60 px-2 pt-3 ${showTimer ? '' : 'rounded-br-xl'}`}
openDirection="down"
closeDirection="up"
openLabel="Show rover name"
closeLabel="Hide rover name"
>
<RoverLabel roverId={roverId} fallback={roverId} className="px-2 py-1 text-base" /> <RoverLabel roverId={roverId} fallback={roverId} className="px-2 py-1 text-base" />
<ExpansionToggle direction="up" label="Hide rover name" onClick={() => setNameOpen(false)} /> </ExpansionPanel>
</div>
) : (
<div className={`flex h-3 w-8 bg-black/60 ${showTimer ? '' : 'ml-10'}`}>
<ExpansionToggle direction="down" label="Show rover name" onClick={() => setNameOpen(true)} />
</div>
)}
</div> </div>
); );
} }
@@ -6,7 +6,7 @@ import { useSessionSelector } from '../../../../context/SessionContext.jsx';
import { useTelemetrySelector } from '../../../../context/TelemetryContext.jsx'; import { useTelemetrySelector } from '../../../../context/TelemetryContext.jsx';
import { hostStatsEqual, selectHostStats, selectSpectatorTelemetry, spectatorTelemetryEqual } from '../../../../context/telemetryViews.js'; import { hostStatsEqual, selectHostStats, selectSpectatorTelemetry, spectatorTelemetryEqual } from '../../../../context/telemetryViews.js';
import CornerPodToggle from './CornerPodToggle.jsx'; import CornerPodToggle from './CornerPodToggle.jsx';
import ExpansionToggle from './ExpansionToggle.jsx'; import ExpansionPanel from './ExpansionPanel.jsx';
import usePodVisibility from './usePodVisibility.js'; import usePodVisibility from './usePodVisibility.js';
function finite(value) { function finite(value) {
@@ -135,9 +135,17 @@ export default function TopRightPod({ roverId }) {
{/* Advanced power is an independently persisted right-edge expansion. Its own arrow is {/* Advanced power is an independently persisted right-edge expansion. Its own arrow is
retained when closed, and the whole panel moves into the corner if the pod closes. */} retained when closed, and the whole panel moves into the corner if the pod closes. */}
{powerOpen ? ( <ExpansionPanel
<div className={`absolute right-0 w-56 rounded-bl-xl bg-black/60 p-1.5 pl-4 text-white ${batteryOpen ? 'top-[8.5rem]' : 'top-0'}`}> open={powerOpen}
<ExpansionToggle direction="right" label="Hide power and computer" onClick={() => setPowerOpen(false)} className="absolute left-0 top-1/2 -translate-y-1/2" /> onOpenChange={setPowerOpen}
anchorClassName={`absolute right-0 ${batteryOpen ? 'top-[8.5rem]' : 'top-10'}`}
panelAlign="right"
panelClassName="w-56 rounded-bl-xl bg-black/60 p-1.5 pt-4 text-white"
openDirection="left"
closeDirection="right"
openLabel="Show power and computer"
closeLabel="Hide power and computer"
>
<div className="space-y-1.5"> <div className="space-y-1.5">
<MetricRow icon={FaBolt} label="Roomba voltage" value={voltage == null ? '--' : `${(voltage / 1000).toFixed(1)} V`} percent={voltagePercent} iconClass="text-sky-300" fillClass="bg-sky-400" /> <MetricRow icon={FaBolt} label="Roomba voltage" value={voltage == null ? '--' : `${(voltage / 1000).toFixed(1)} V`} percent={voltagePercent} iconClass="text-sky-300" fillClass="bg-sky-400" />
<MetricRow icon={FaBolt} label="Roomba current" value={`${current > 0 ? '+' : ''}${Math.round(current)} mA`} percent={currentPercent * 100} iconClass={current < 0 ? 'text-amber-300' : 'text-emerald-300'} fillClass={current < 0 ? 'bg-amber-400' : 'bg-emerald-400'} /> <MetricRow icon={FaBolt} label="Roomba current" value={`${current > 0 ? '+' : ''}${Math.round(current)} mA`} percent={currentPercent * 100} iconClass={current < 0 ? 'text-amber-300' : 'text-emerald-300'} fillClass={current < 0 ? 'bg-amber-400' : 'bg-emerald-400'} />
@@ -148,12 +156,7 @@ export default function TopRightPod({ roverId }) {
<SpeedTile icon={FaArrowDown} label="Download speed" value={download == null ? '--' : `${download.toFixed(1)} Mb/s`} colorClass="text-sky-300" /> <SpeedTile icon={FaArrowDown} label="Download speed" value={download == null ? '--' : `${download.toFixed(1)} Mb/s`} colorClass="text-sky-300" />
<SpeedTile icon={FaArrowUp} label="Upload speed" value={upload == null ? '--' : `${upload.toFixed(1)} Mb/s`} colorClass="text-violet-300" /> <SpeedTile icon={FaArrowUp} label="Upload speed" value={upload == null ? '--' : `${upload.toFixed(1)} Mb/s`} colorClass="text-violet-300" />
</div> </div>
</div> </ExpansionPanel>
) : (
<div className={`absolute right-0 flex h-8 w-3 bg-black/60 ${batteryOpen ? 'top-[8.5rem]' : 'top-10'}`}>
<ExpansionToggle direction="left" label="Show power and computer" onClick={() => setPowerOpen(true)} />
</div>
)}
</div> </div>
{/* Battery danger is a stage-level warning, so it belongs near the user's focus instead {/* Battery danger is a stage-level warning, so it belongs near the user's focus instead
@@ -3,7 +3,7 @@
// Scope: Owns presentation and the existing manual-assist lifecycle for the current driver HUD; // Scope: Owns presentation and the existing manual-assist lifecycle for the current driver HUD;
// the archived desktop layout retains its previous DriveDockAction behavior. // the archived desktop layout retains its previous DriveDockAction behavior.
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { FaChargingStation, FaChevronDown } from 'react-icons/fa'; import { FaChargingStation } from 'react-icons/fa';
import { useControlActions, useControlSelector } from '../../../../controls/index.js'; import { useControlActions, useControlSelector } from '../../../../controls/index.js';
import { formatKeyLabel } from '../../../../controls/keymapUtils.js'; import { formatKeyLabel } from '../../../../controls/keymapUtils.js';
import { useTelemetrySelector } from '../../../../context/TelemetryContext.jsx'; import { useTelemetrySelector } from '../../../../context/TelemetryContext.jsx';
@@ -14,7 +14,7 @@ import useCanControlRover from '../../../../hooks/useCanControlRover.js';
import { useDriverLayout } from '../../../../layouts/driver/DriverLayoutContext.jsx'; import { useDriverLayout } from '../../../../layouts/driver/DriverLayoutContext.jsx';
import { useSessionSelector } from '../../../../context/SessionContext.jsx'; import { useSessionSelector } from '../../../../context/SessionContext.jsx';
import KeyPill from '../../../vip/VipAudioUploadCard/KeyPill.jsx'; import KeyPill from '../../../vip/VipAudioUploadCard/KeyPill.jsx';
import ExpansionToggle from '../CornerPods/ExpansionToggle.jsx'; import ExpansionPanel from '../CornerPods/ExpansionPanel.jsx';
import usePodVisibility from '../CornerPods/usePodVisibility.js'; import usePodVisibility from '../CornerPods/usePodVisibility.js';
function DockedAction({ driveKeyLabel, pending, controlsDisabled, error, onUndock }) { function DockedAction({ driveKeyLabel, pending, controlsDisabled, error, onUndock }) {
@@ -101,29 +101,10 @@ function AutoDockingAction({ driveKeyLabel, pending, controlsDisabled, error, on
function DockAssistAction({ active, pending, controlsDisabled, error, dockKeyLabel, onDock, onCancel, cornerOffsetClass, open, onOpenChange, batterySeverity }) { function DockAssistAction({ active, pending, controlsDisabled, error, dockKeyLabel, onDock, onCancel, cornerOffsetClass, open, onOpenChange, batterySeverity }) {
const batteryUrgent = batterySeverity === 'urgent'; const batteryUrgent = batterySeverity === 'urgent';
const batteryLow = batterySeverity === 'low'; const batteryLow = batterySeverity === 'low';
if (!open) { // When the battery pod is collapsed its triangular reopen control owns the
return ( // outermost forty pixels. Docking uses the next top-edge slot in both open
<button // and collapsed states so the two independent controls never overlap.
type="button" const dockPositionClass = cornerOffsetClass === 'right-0' ? 'right-10' : cornerOffsetClass;
aria-label="Show rover docking control"
title="Dock rover"
onClick={() => onOpenChange(true)}
className={`pointer-events-auto absolute top-0 z-20 flex h-6 w-10 items-center justify-center gap-1 rounded-bl text-[0.6rem] transition ${
batteryUrgent
? 'bg-red-950 text-red-100 hover:bg-red-900'
: batteryLow
? 'bg-amber-950 text-amber-100 hover:bg-amber-900'
: 'bg-indigo-950/75 text-indigo-100 hover:bg-indigo-900'
} ${cornerOffsetClass === 'right-0' ? 'right-10' : cornerOffsetClass}`}
>
{/* The collapsed tab retains feature identity instead of becoming an
anonymous expansion arrow whose purpose must be remembered. */}
<FaChargingStation aria-hidden="true" />
<FaChevronDown aria-hidden="true" />
</button>
);
}
if (active) { if (active) {
return ( return (
<div className="pointer-events-none absolute inset-0 z-[60] flex items-center justify-center"> <div className="pointer-events-none absolute inset-0 z-[60] flex items-center justify-center">
@@ -147,29 +128,38 @@ function DockAssistAction({ active, pending, controlsDisabled, error, dockKeyLab
} }
return ( return (
<div className={`pointer-events-auto absolute top-0 z-20 flex items-stretch ${cornerOffsetClass}`}> <ExpansionPanel
<ExpansionToggle direction="up" label="Hide dock controls" onClick={() => onOpenChange(false)} /> open={open}
<button onOpenChange={onOpenChange}
type="button" anchorClassName={`absolute top-0 ${dockPositionClass}`}
aria-label="Start rover docking assist" panelAlign="right"
disabled={pending || controlsDisabled} panelClassName="flex items-start"
onClick={onDock} openDirection="down"
className={`flex items-center gap-1.5 rounded-bl-xl px-4 py-2 text-base font-bold shadow-xl ring-1 transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-wait disabled:opacity-75 ${ closeDirection="up"
batteryUrgent openLabel="Show rover docking control"
? 'bg-red-950 text-red-50 ring-red-300/80 hover:bg-red-900 focus-visible:ring-red-200' closeLabel="Hide dock controls"
: batteryLow >
? 'bg-amber-950 text-amber-50 ring-amber-300/80 hover:bg-amber-900 focus-visible:ring-amber-200' <div className="relative">
: 'bg-indigo-950/60 text-indigo-50 ring-indigo-300/70 hover:bg-indigo-900 focus-visible:ring-indigo-200' <button
}`} type="button"
> aria-label="Start rover docking assist"
<FaChargingStation className="shrink-0" aria-hidden="true" /> disabled={pending || controlsDisabled}
<span>{pending ? 'Starting…' : batteryUrgent ? 'Dock now' : batteryLow ? 'Dock soon' : 'Dock rover'}</span> onClick={onDock}
{dockKeyLabel && !pending ? <KeyPill label={dockKeyLabel} /> : null} className={`flex items-center gap-1.5 rounded-bl-xl px-4 pb-2 pt-4 text-base font-bold shadow-xl ring-1 transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-wait disabled:opacity-75 ${
</button> batteryUrgent
{/* The expansion toggle stays on the far side of this panel so the battery pod's ? 'bg-red-950 text-red-50 ring-red-300/80 hover:bg-red-900 focus-visible:ring-red-200'
triangular control remains unobstructed when both occupy the top-right area. */} : batteryLow
? 'bg-amber-950 text-amber-50 ring-amber-300/80 hover:bg-amber-900 focus-visible:ring-amber-200'
: 'bg-indigo-950/60 text-indigo-50 ring-indigo-300/70 hover:bg-indigo-900 focus-visible:ring-indigo-200'
}`}
>
<FaChargingStation className="shrink-0" aria-hidden="true" />
<span>{pending ? 'Starting…' : batteryUrgent ? 'Dock now' : batteryLow ? 'Dock soon' : 'Dock rover'}</span>
{dockKeyLabel && !pending ? <KeyPill label={dockKeyLabel} /> : null}
</button>
</div>
{error ? <div className="mt-2 max-w-64 bg-red-950/90 px-3 py-2 text-sm font-semibold text-red-100">{error}</div> : null} {error ? <div className="mt-2 max-w-64 bg-red-950/90 px-3 py-2 text-sm font-semibold text-red-100">{error}</div> : null}
</div> </ExpansionPanel>
); );
} }