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" />
<!-- site-metadata:inject -->
<!-- analytics:inject -->
<script type="module" crossorigin src="/assets/index-CAUR32ig.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D2qTDWX0.css">
<script type="module" crossorigin src="/assets/index-D9i-ZujG.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D5gzYj1F.css">
</head>
<body>
<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)} />
)}
{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 = '' }) {
const Icon = ICONS[direction] || FaChevronLeft;
const horizontalEdge = direction === 'up' || direction === 'down';
return (
<button
type="button"
aria-label={label}
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
therefore still a thin piece of that expansion, never a loose button over a pod. */}
{/* ExpansionPanel owns all positioning so this button has exactly one visual
form. Only the chevron direction changes between open and closed states. */}
<Icon aria-hidden="true" />
</button>
);
@@ -4,7 +4,7 @@ import { useCallback } from 'react';
import { HORN_MAX_FREQUENCY } from '../../../../controls/constants.js';
import { useSettingsNamespace } from '../../../../settings/index.js';
import { HORN_SETTINGS_DEFAULTS } from '../../../../settings/namespaces.js';
import ExpansionToggle from './ExpansionToggle.jsx';
import ExpansionPanel from './ExpansionPanel.jsx';
function clampFrequency(value) {
const numeric = Number(value);
@@ -12,7 +12,7 @@ function clampFrequency(value) {
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 waveform = value?.waveform === 'sine' ? 'sine' : 'saw';
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]);
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 (
<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">
<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>
<label className="flex items-center justify-between gap-2 text-slate-300">
<span>Wave</span>
@@ -73,6 +77,6 @@ export default function HornSettingsExpansion({ open, podOpen, onOpenChange }) {
</label>
))}
</div>
</div>
</ExpansionPanel>
);
}
@@ -6,7 +6,7 @@ import { useSessionSelector } from '../../../../context/SessionContext.jsx';
import { useSharedClock } from '../../../../hooks/useSharedClock.js';
import RoverLabel from '../../../RoverLabel/index.jsx';
import CornerPodToggle from './CornerPodToggle.jsx';
import ExpansionToggle from './ExpansionToggle.jsx';
import ExpansionPanel from './ExpansionPanel.jsx';
import usePodVisibility from './usePodVisibility.js';
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 expansion itself, and its position naturally moves into the corner whenever
the conditional timer pod is absent or manually collapsed. */}
{nameOpen ? (
<div className={`flex h-11 items-center gap-2 bg-black/60 px-2 ${showTimer ? '' : 'rounded-br-xl'}`}>
<ExpansionPanel
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" />
<ExpansionToggle direction="up" label="Hide rover name" onClick={() => setNameOpen(false)} />
</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>
)}
</ExpansionPanel>
</div>
);
}
@@ -6,7 +6,7 @@ import { useSessionSelector } from '../../../../context/SessionContext.jsx';
import { useTelemetrySelector } from '../../../../context/TelemetryContext.jsx';
import { hostStatsEqual, selectHostStats, selectSpectatorTelemetry, spectatorTelemetryEqual } from '../../../../context/telemetryViews.js';
import CornerPodToggle from './CornerPodToggle.jsx';
import ExpansionToggle from './ExpansionToggle.jsx';
import ExpansionPanel from './ExpansionPanel.jsx';
import usePodVisibility from './usePodVisibility.js';
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
retained when closed, and the whole panel moves into the corner if the pod closes. */}
{powerOpen ? (
<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'}`}>
<ExpansionToggle direction="right" label="Hide power and computer" onClick={() => setPowerOpen(false)} className="absolute left-0 top-1/2 -translate-y-1/2" />
<ExpansionPanel
open={powerOpen}
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">
<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'} />
@@ -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={FaArrowUp} label="Upload speed" value={upload == null ? '--' : `${upload.toFixed(1)} Mb/s`} colorClass="text-violet-300" />
</div>
</div>
) : (
<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>
)}
</ExpansionPanel>
</div>
{/* 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;
// the archived desktop layout retains its previous DriveDockAction behavior.
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 { formatKeyLabel } from '../../../../controls/keymapUtils.js';
import { useTelemetrySelector } from '../../../../context/TelemetryContext.jsx';
@@ -14,7 +14,7 @@ import useCanControlRover from '../../../../hooks/useCanControlRover.js';
import { useDriverLayout } from '../../../../layouts/driver/DriverLayoutContext.jsx';
import { useSessionSelector } from '../../../../context/SessionContext.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';
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 }) {
const batteryUrgent = batterySeverity === 'urgent';
const batteryLow = batterySeverity === 'low';
if (!open) {
return (
<button
type="button"
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>
);
}
// When the battery pod is collapsed its triangular reopen control owns the
// outermost forty pixels. Docking uses the next top-edge slot in both open
// and collapsed states so the two independent controls never overlap.
const dockPositionClass = cornerOffsetClass === 'right-0' ? 'right-10' : cornerOffsetClass;
if (active) {
return (
<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 (
<div className={`pointer-events-auto absolute top-0 z-20 flex items-stretch ${cornerOffsetClass}`}>
<ExpansionToggle direction="up" label="Hide dock controls" onClick={() => onOpenChange(false)} />
<button
type="button"
aria-label="Start rover docking assist"
disabled={pending || controlsDisabled}
onClick={onDock}
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 ${
batteryUrgent
? 'bg-red-950 text-red-50 ring-red-300/80 hover:bg-red-900 focus-visible:ring-red-200'
: 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>
{/* The expansion toggle stays on the far side of this panel so the battery pod's
triangular control remains unobstructed when both occupy the top-right area. */}
<ExpansionPanel
open={open}
onOpenChange={onOpenChange}
anchorClassName={`absolute top-0 ${dockPositionClass}`}
panelAlign="right"
panelClassName="flex items-start"
openDirection="down"
closeDirection="up"
openLabel="Show rover docking control"
closeLabel="Hide dock controls"
>
<div className="relative">
<button
type="button"
aria-label="Start rover docking assist"
disabled={pending || controlsDisabled}
onClick={onDock}
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 ${
batteryUrgent
? 'bg-red-950 text-red-50 ring-red-300/80 hover:bg-red-900 focus-visible:ring-red-200'
: 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}
</div>
</ExpansionPanel>
);
}