// Drive Dock Action
// Purpose: Defines the Drive Dock Action module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useState } from 'react';
import { useControlActions, useControlSelector } from '../../controls/index.js';
import { useTelemetrySelector } from '../../context/TelemetryContext.jsx';
import { dockTelemetryEqual, selectDockTelemetry } from '../../context/telemetryViews.js';
import { formatKeyLabel } from '../../controls/keymapUtils.js';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
import { deriveDriveDockStateFromTelemetry } from './driveDockState.js';
import { triggerTouchHaptic } from '../../lib/touchHaptics.js';
function StatusRow({ value, tone = 'neutral' }) {
const toneClasses =
tone === 'good'
? 'border-emerald-200 bg-emerald-600 text-white'
: tone === 'warn'
? 'border-amber-200 bg-amber-600 text-white'
: tone === 'bad'
? 'border-red-200 bg-red-600 text-white'
: 'border-slate-200/30 bg-slate-700 text-slate-100';
return (
{value}
);
}
function KeyPill({ label }) {
if (!label) return null;
return {label};
}
function ActionPill({ label, tone }) {
const toneClasses =
tone === 'emerald'
? 'border-emerald-200/70 bg-emerald-600/70 text-emerald-50'
: tone === 'amber'
? 'border-amber-200/70 bg-amber-600/70 text-amber-50'
: 'border-indigo-200/70 bg-indigo-600/70 text-indigo-50';
return (
{label}
);
}
function DockModal({ instructions, onConfirm, onCancel, pending }) {
return (
Manual Docking Assist
{instructions.summary}
);
}
export default function DriveDockAction({
layout = 'desktop',
expand = false,
fill = false,
driveDockState,
compactHeightClass = '',
}) {
const isMobile = layout === 'mobile';
const roverId = useControlSelector((control) => control.state.roverId);
const keymap = useControlSelector((control) => control.state.keymap);
const actions = useControlActions();
const dockAssist = useManualDockAssist();
const dockTelemetry = useTelemetrySelector(roverId, selectDockTelemetry, dockTelemetryEqual);
const state = driveDockState ?? deriveDriveDockStateFromTelemetry(dockTelemetry);
const { driving, docked, charging, dockingInProgress } = state;
const [pending, setPending] = useState(null);
const [confirmOpen, setConfirmOpen] = useState(false);
const [showModal, setShowModal] = useState(false);
const manualAssistActive = Boolean(dockAssist.active);
const driveDisabled = !roverId || pending !== null;
const dockDisabled = !roverId || pending !== null;
const driveKeyLabel = formatKeyLabel(keymap?.driveMacro?.[0]);
const dockKeyLabel = formatKeyLabel(keymap?.dockMacro?.[0]);
const dockInstructions = {
summary: 'Use assist mode to manually line up with the dock.',
steps: [
'Line up the center dot of your rover with the center of the dock',
'The UI will indicate when you are successfully docked',
'When good contact is made, the dock will stop flashing.',
],
};
const startDriveInstructions = {
summary: 'You must enable driving mode before you can move the rover.',
steps: ['Press the keybind or tap this button to enable driving mode', 'Once ready (should be instant), this dialog will change, and you will be able to move'],
};
const dockValue = docked ? 'Docked' : 'Undocked';
const dockTone = docked ? 'good' : 'bad';
const chargeValue = charging ? 'Charging' : docked ? 'Charging soon...' : '—';
const chargeTone = charging ? 'good' : docked ? 'warn' : 'bad';
const handleReturnToDrive = async () => {
if (!roverId || pending) return;
if (isMobile) triggerTouchHaptic('button');
setPending('drive');
try {
dockAssist.exitAssist();
actions.setMode('drive');
await actions.runMacro('drive-sequence');
} catch (err) {
alert(err.message);
} finally {
setPending(null);
}
};
const handleStartDrive = async () => {
if (!roverId || pending) return;
if (isMobile) triggerTouchHaptic('button');
setConfirmOpen(false);
setShowModal(false);
setPending('drive');
try {
dockAssist.exitAssist();
actions.setMode('drive');
await actions.runMacro('drive-sequence');
} catch (err) {
alert(err.message);
} finally {
setPending(null);
}
};
const handleConfirmDock = async () => {
if (!roverId || pending) return;
if (isMobile) triggerTouchHaptic('button');
setPending('dock');
try {
dockAssist.enterAssist();
} catch (err) {
alert(err.message);
} finally {
setPending(null);
setConfirmOpen(false);
setShowModal(false);
}
};
const handleOpenDock = () => {
if (dockDisabled) return;
if (isMobile) triggerTouchHaptic('button');
if (manualAssistActive) {
dockAssist.exitAssist();
return;
}
setShowModal(true);
setConfirmOpen(true);
};
// The drive/dock card appears inside the mobile controls and can be held or
// tapped repeatedly; attach mobile touch suppression directly to the card so
// Safari does not select labels or open callouts during those interactions.
const baseCardClasses =
'mobile-touch-control flex w-full flex-col gap-0.5 overflow-hidden rounded-xl border-2 px-0.75 py-0.75 text-slate-100 shadow-md transition hover:-translate-y-0.5 hover:shadow-xl focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-60 select-none no-touch-select';
const ctaText = 'text-center';
const ctaLayout = 'items-center justify-between';
const compactLayout = 'items-center justify-center';
const ctaSize = isMobile ? 'text-sm font-semibold' : '';
const emeraldCta =
'border-emerald-300/70 bg-emerald-800 text-emerald-50 hover:bg-emerald-700 focus-visible:ring-emerald-300';
const amberCta =
'border-amber-300/70 bg-amber-900 text-amber-50 hover:bg-amber-800 focus-visible:ring-amber-300';
const indigoCta =
'border-indigo-300/70 bg-indigo-900 text-indigo-50 hover:bg-indigo-800 focus-visible:ring-indigo-300';
const cyanCta =
'border-cyan-300/70 bg-cyan-900 text-cyan-50 hover:bg-cyan-800 focus-visible:ring-cyan-300';
const orangeCta =
'border-amber-300/70 bg-amber-900 text-amber-50 hover:bg-amber-800 focus-visible:ring-amber-300';
const forceExpanded = dockingInProgress;
const expanded = expand || forceExpanded;
const filledHeight = expanded ? 'h-full flex-1' : fill ? 'flex-1' : '';
const compactHeight = isMobile && !expanded ? compactHeightClass : '';
const layoutClass = isMobile && !expanded ? compactLayout : ctaLayout;
const compactDockedDriving = isMobile && !expanded && driving && docked;
if (!driving && !dockingInProgress) {
return (
);
}
if (dockingInProgress) {
const inProgressCopy = {
summary: 'The rover is currently attempting to dock itself due to being idle.',
steps: ['Click to return to driving mode.'],
};
return (
);
}
return (
<>
{showModal || (!isMobile && confirmOpen) ? (
{
setShowModal(false);
setConfirmOpen(false);
}}
onConfirm={handleConfirmDock}
/>
) : null}
>
);
}
function StepList({ steps, tone = 'emerald' }) {
const container =
tone === 'emerald'
? 'bg-emerald-900/60 text-emerald-50/90 border-emerald-700/60'
: tone === 'amber'
? 'bg-amber-900/60 text-amber-50/90 border-amber-700/60'
: 'bg-indigo-900/60 text-indigo-50/90 border-indigo-700/60';
const numberColor =
tone === 'emerald'
? 'text-emerald-200'
: tone === 'amber'
? 'text-amber-200'
: 'text-indigo-200';
return (