loading screen, better HUD, etc.

This commit is contained in:
legop3
2026-08-20 14:38:07 -04:00
parent 5ab62c3633
commit f77a969098
18 changed files with 235 additions and 100 deletions
@@ -18,42 +18,69 @@ import ExpansionPanel from '../CornerPods/ExpansionPanel.jsx';
import usePodVisibility from '../CornerPods/usePodVisibility.js';
function DockedAction({ driveKeyLabel, pending, controlsDisabled, error, onUndock }) {
const [hidden, setHidden] = useState(false);
const waitingForTurn = controlsDisabled && !pending;
/* The dismissal belongs to this mounted docked episode. DockingHud unmounts
this component when the rover leaves the base, and its roverId key remounts
it for a different assignment, so no persistence or reset effect is needed. */
if (hidden && !pending) return null;
const mainToneClass = waitingForTurn
? 'cursor-not-allowed bg-slate-950/95 ring-slate-400/70'
: 'bg-emerald-950/90 ring-emerald-300/80 hover:bg-emerald-900/95 focus-visible:ring-emerald-200 disabled:cursor-wait disabled:opacity-75';
const hideToneClass = waitingForTurn
? 'bg-slate-950/95 ring-slate-400/70 hover:bg-slate-900'
: 'bg-emerald-950/90 ring-emerald-300/80 hover:bg-emerald-900/95 focus-visible:ring-emerald-200';
return (
<div className="pointer-events-none absolute inset-0 z-30 flex items-center justify-center p-6">
<button
type="button"
disabled={pending || controlsDisabled}
onClick={onUndock}
className={`pointer-events-auto flex w-[min(32rem,80%)] flex-col items-center gap-2 px-8 py-7 text-center text-white shadow-2xl ring-2 transition focus-visible:outline-none focus-visible:ring-4 ${
waitingForTurn
? 'cursor-not-allowed bg-slate-950/95 ring-slate-400/70'
: 'bg-emerald-950/90 ring-emerald-300/80 hover:bg-emerald-900/95 focus-visible:ring-emerald-200 disabled:cursor-wait disabled:opacity-75'
}`}
>
<strong className="text-3xl leading-tight">{pending ? 'Undocking…' : 'Your rover is docked'}</strong>
{pending ? (
null
) : waitingForTurn ? (
/* A disabled action must explain the ownership constraint instead of
continuing to advertise a click and keybind that cannot succeed. */
<span className="text-lg font-semibold leading-snug text-slate-300">
Wait for your turn to undock.
</span>
) : (
<span className="text-lg font-semibold leading-snug text-emerald-50">
Click here
{driveKeyLabel ? (
<>
{' '}or press <KeyPill label={driveKeyLabel} />
</>
) : null}
{' '}to undock and drive the rover
</span>
)}
{error ? <span className="text-sm font-semibold text-red-200">{error}</span> : null}
</button>
</div>
<>
{/* The docked shield is owned by the dismissible action so hiding the
prompt also reveals the video and ordinary HUD instead of leaving an
unexplained dark, input-blocking layer behind. */}
<div className="pointer-events-auto absolute inset-0 z-[25] bg-black/75" aria-hidden="true" />
<div className="pointer-events-none absolute inset-0 z-30 flex items-center justify-center p-6">
<div className="relative w-[min(32rem,80%)]">
<button
type="button"
disabled={pending || controlsDisabled}
onClick={onUndock}
className={`pointer-events-auto flex w-full flex-col items-center gap-2 px-8 py-7 text-center text-white shadow-2xl ring-2 transition focus-visible:outline-none focus-visible:ring-4 ${mainToneClass}`}
>
<strong className="text-3xl leading-tight">{pending ? 'Undocking…' : 'Your rover is docked'}</strong>
{pending ? (
null
) : waitingForTurn ? (
/* A disabled action must explain the ownership constraint instead of
continuing to advertise a click and keybind that cannot succeed. */
<span className="text-lg font-semibold leading-snug text-slate-300">
Wait for your turn to undock.
</span>
) : (
<span className="text-lg font-semibold leading-snug text-emerald-50">
Click here
{driveKeyLabel ? (
<>
{' '}or press <KeyPill label={driveKeyLabel} />
</>
) : null}
{' '}to undock and drive the rover
</span>
)}
{error ? <span className="text-sm font-semibold text-red-200">{error}</span> : null}
</button>
{!pending ? (
<button
type="button"
onClick={() => setHidden(true)}
className={`pointer-events-auto absolute left-1/2 top-full -translate-x-1/2 rounded-b-lg px-6 py-1.5 text-sm font-bold text-white shadow-xl ring-2 transition focus-visible:outline-none focus-visible:ring-4 ${hideToneClass}`}
>
Hide
</button>
) : null}
</div>
</div>
</>
);
}
@@ -318,15 +345,12 @@ export default function DockingHud({ roverId }) {
return (
<>
{/* This single layer both dims and blocks the ordinary rover HUD. A passive
undocked rover is still moving autonomously, so it gets a lighter but equally
blocking shield. Explicit pending state keeps the correct shield mounted until
the complete drive sequence finishes even as telemetry changes underneath it. */}
{/* Automatic docking keeps its own lighter blocking shield. The ordinary
docked shield lives inside DockedAction because the new Hide control
must dismiss the prompt and its dimming as one coherent surface. */}
<div
className={`absolute inset-0 z-[25] transition-all duration-300 ${
docked || pendingAction === 'undocking'
? 'pointer-events-auto bg-black/75 opacity-100'
: autoDocking || pendingAction === 'resuming'
autoDocking || pendingAction === 'resuming'
? 'pointer-events-auto bg-black/55 opacity-100'
: 'pointer-events-none opacity-0'
}`}
@@ -335,6 +359,7 @@ export default function DockingHud({ roverId }) {
{docked || pendingAction === 'undocking' ? (
<DockedAction
key={roverId}
driveKeyLabel={driveKeyLabel}
pending={pendingAction === 'undocking'}
controlsDisabled={!canControl}
@@ -0,0 +1,40 @@
// Initial Session Overlay
// Purpose: Hides incomplete driver-page placeholders until the first authoritative session snapshot arrives.
// Scope: Provides startup presentation only; it deliberately does not delay or alter page initialization underneath.
import { useSessionSelector } from '../../context/SessionContext.jsx';
import spinnerImage from '../../assets/spinner.png';
import './styles.css';
export default function InitialSessionOverlay() {
const connected = useSessionSelector((state) => Boolean(state.connected));
const sessionReady = useSessionSelector((state) => state.session !== null);
if (sessionReady) return null;
return (
<div
className="fixed inset-0 z-[1000] flex items-center justify-center bg-black text-slate-200"
role="status"
aria-live="polite"
aria-label={connected ? 'Loading session' : 'Connecting'}
>
<div className="flex flex-col items-center gap-3">
{/* The source image is intentionally constrained to a small fixed box;
its intrinsic pixel dimensions must never determine overlay layout.
Reduced-motion users still see the identifying image without either
the rotation or continuously changing color. */}
<div className="initial-session-spinner-rotation h-20 w-20" aria-hidden="true">
<img
src={spinnerImage}
alt=""
draggable="false"
className="initial-session-spinner-image h-full w-full select-none object-contain"
/>
</div>
<div className="text-sm font-medium text-slate-300">
{connected ? 'Loading session…' : 'Connecting…'}
</div>
</div>
</div>
);
}
@@ -0,0 +1,35 @@
/*
These animations belong only to InitialSessionOverlay. Keeping them beside
the component avoids adding feature-specific behavior to the global styles.
Rotation and hue use separate elements so their independent infinite cycles
cannot overwrite one another's transform or filter properties.
*/
@keyframes initial-session-spinner-rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes initial-session-spinner-hue {
from {
filter: hue-rotate(0deg);
}
to {
filter: hue-rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
.initial-session-spinner-rotation {
animation: initial-session-spinner-rotate 1.8s linear infinite;
}
.initial-session-spinner-image {
animation: initial-session-spinner-hue 2.8s linear infinite;
}
}
@@ -19,9 +19,9 @@ function ControlRow({ label, keyLabel }) {
function DesktopQuickstart({ keymap }) {
return (
<div className="space-y-0.5">
<p className="text-sm text-slate-200">1. Press "Start Driving" put your rover into driving mode.</p>
<p className="text-sm text-slate-200">1. Click "Your rover is docked" to undock.</p>
<div className="space-y-0.5">
<p className="text-sm text-slate-200">2. Use the drive controls to move your rover:</p>
<p className="text-sm text-slate-200">2. Drive with these keybindings:</p>
<div className="space-y-0.5">
<ControlRow label="Forward" keyLabel={formatKeyLabel(keymap?.driveForward?.[0])} />
<ControlRow label="Backward" keyLabel={formatKeyLabel(keymap?.driveBackward?.[0])} />
@@ -31,7 +31,8 @@ function DesktopQuickstart({ keymap }) {
<ControlRow label="Move slower" keyLabel={formatKeyLabel(keymap?.slowModifier?.[0])} />
</div>
</div>
<p className="text-sm text-slate-200">3. When done, enter "Docking Assist" and line up the front sensor with the dock sensor.</p>
<p className="text-sm text-slate-200">3. Use the video HUD for rover controls and information.</p>
<p className="text-sm text-slate-200">4. Click "Dock rover" when finished.</p>
</div>
);
}
@@ -39,10 +40,11 @@ function DesktopQuickstart({ keymap }) {
function MobileQuickstart() {
return (
<div className="space-y-0.5 text-sm text-slate-200">
<p>1. Press "Start Driving" put your rover into driving mode.</p>
<p>2. Touch and hold in Joystick area to move.</p>
<p>3. Use the other column for motor, horn, and camera controls.</p>
<p>4. When done, enter "Docking Assist" and line up the front sensor with the dock sensor.</p>
<p>1. Tap "Your rover is docked" to undock.</p>
<p>2. Hold and drag on the drive pad.</p>
<p>3. Choose Precision, Normal, or Turbo above the drive pad.</p>
<p>4. Use the other column for rover controls.</p>
<p>5. Tap "Dock and charge" when finished.</p>
</div>
);
}
@@ -71,7 +73,6 @@ export default function QuickstartOverlay({
layout,
showOnLoad,
onToggleShowOnLoad,
onOpenHelp,
onClose,
}) {
const rawKeymap = useControlSelector((control) => control.state.keymap);