mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
fixing pod collapse expandings
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user