mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
new dock / drive / docking buttons and dialog
This commit is contained in:
@@ -5,40 +5,22 @@ import { useControlSystem } from '../controls/index.js';
|
||||
import { formatKeyLabel } from '../controls/keymapUtils.js';
|
||||
import TopDownMap from './TopDownMap.jsx';
|
||||
import RoverRoster from './RoverRoster.jsx';
|
||||
import DriveDockAction, { useDriveDockState } from './DriveDockAction.jsx';
|
||||
|
||||
export default function ControlSummary() {
|
||||
const { session, requestControl } = useSession();
|
||||
const {
|
||||
state: { roverId, keymap },
|
||||
actions,
|
||||
} = useControlSystem();
|
||||
const frame = useTelemetryFrame(roverId);
|
||||
const sensors = frame?.sensors || {};
|
||||
const driveDockState = useDriveDockState(roverId);
|
||||
const roster = session?.roster ?? [];
|
||||
const [pending, setPending] = useState({});
|
||||
|
||||
const canRequest = useMemo(() => session?.role && session.role !== 'spectator', [session?.role]);
|
||||
|
||||
const driving = useMemo(
|
||||
() => (sensors.oiMode?.label || '').toLowerCase() === 'full',
|
||||
[sensors.oiMode?.label],
|
||||
);
|
||||
const docked = Boolean(sensors.chargingSources?.homeBase);
|
||||
const charging = Boolean(
|
||||
sensors.chargingState?.label && sensors.chargingState.label.toLowerCase() !== 'not charging',
|
||||
);
|
||||
|
||||
const handleStartDrive = () => {
|
||||
if (!roverId) return;
|
||||
actions.setMode('drive');
|
||||
actions.runMacro('drive-sequence');
|
||||
};
|
||||
|
||||
const handleDock = () => {
|
||||
if (!roverId) return;
|
||||
actions.setMode('dock');
|
||||
actions.runMacro('seek-dock');
|
||||
};
|
||||
const hideInlineControls = driveDockState.docked && !driveDockState.driving;
|
||||
|
||||
async function handleRequest(targetRoverId) {
|
||||
if (!targetRoverId) return;
|
||||
@@ -61,30 +43,10 @@ export default function ControlSummary() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid h-full grid-rows-[1fr_1fr_auto] gap-0.75">
|
||||
<ActionButton
|
||||
title="Start Driving"
|
||||
description="Enable driving mode, then begin driving."
|
||||
statuses={[{ label: driving ? 'Ready' : 'Press to enter driving mode', active: driving }]}
|
||||
tone="emerald"
|
||||
onClick={handleStartDrive}
|
||||
disabled={!roverId}
|
||||
keyAction="driveMacro"
|
||||
keymap={keymap}
|
||||
/>
|
||||
<ActionButton
|
||||
title="Dock and Charge"
|
||||
description="Line up about a foot from the dock, then press to attempt auto-dock."
|
||||
statuses={[
|
||||
{ label: docked ? 'Docked' : 'Not docked', active: docked },
|
||||
{ label: charging ? 'Charging' : 'Not charging', active: charging },
|
||||
]}
|
||||
tone="indigo"
|
||||
onClick={handleDock}
|
||||
disabled={!roverId}
|
||||
keyAction="dockMacro"
|
||||
keymap={keymap}
|
||||
/>
|
||||
<InlineCameraTilt keymap={keymap} />
|
||||
<div className="row-span-2">
|
||||
<DriveDockAction layout="desktop" expand driveDockState={driveDockState} />
|
||||
</div>
|
||||
{!hideInlineControls ? <InlineCameraTilt keymap={keymap} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
<RoverRoster
|
||||
@@ -107,49 +69,6 @@ export default function ControlSummary() {
|
||||
);
|
||||
}
|
||||
|
||||
function ActionButton({ title, description, statuses, onClick, disabled, tone, keyAction, keymap }) {
|
||||
const colors =
|
||||
tone === 'indigo'
|
||||
? 'bg-indigo-700 hover:bg-indigo-600 focus-visible:ring-indigo-400'
|
||||
: 'bg-emerald-700 hover:bg-emerald-600 focus-visible:ring-emerald-400';
|
||||
const keyLabel = keyAction ? formatKeyLabel(keymap?.[keyAction]?.[0]) : '';
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`flex h-full w-full flex-col items-center justify-center gap-0.5 border border-slate-700 px-1 py-1 text-center text-white transition focus-visible:outline-none focus-visible:ring-1 ${colors} disabled:cursor-not-allowed disabled:opacity-50`}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="text-base font-semibold text-slate-100">{title}</span>
|
||||
{keyLabel ? <KeyPill label={keyLabel} /> : null}
|
||||
</div>
|
||||
<p className="text-sm text-slate-300 text-center">{description}</p>
|
||||
<div className="flex flex-wrap items-center justify-center gap-0.5">
|
||||
{statuses.map((status) => (
|
||||
<StatusPill key={status.label} active={status.active} label={status.label} />
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function KeyPill({ label }) {
|
||||
return <span className="rounded border border-white/40 px-1 text-[0.7rem] text-white">{label}</span>;
|
||||
}
|
||||
|
||||
function StatusPill({ label, active }) {
|
||||
return (
|
||||
<span
|
||||
className={`rounded px-1.5 py-0.5 text-[0.7rem] font-semibold ${
|
||||
active ? 'bg-emerald-600 text-white' : 'bg-red-500 text-white'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function InlineCameraTilt({ keymap }) {
|
||||
const {
|
||||
state: { roverId, camera },
|
||||
@@ -226,6 +145,10 @@ function InlineCameraTilt({ keymap }) {
|
||||
);
|
||||
}
|
||||
|
||||
function KeyPill({ label }) {
|
||||
return <span className="rounded border border-white/40 px-1 text-[0.7rem] text-white">{label}</span>;
|
||||
}
|
||||
|
||||
function formatDegrees(value) {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) return '—';
|
||||
return `${value > 0 ? '+' : ''}${value.toFixed(1)}°`;
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useControlSystem } from '../controls/index.js';
|
||||
import { useTelemetryFrame } from '../context/TelemetryContext.jsx';
|
||||
import { formatKeyLabel } from '../controls/keymapUtils.js';
|
||||
|
||||
export function deriveDriveDockState(frame) {
|
||||
const sensors = frame?.sensors || {};
|
||||
const oiLabel = sensors.oiMode?.label || 'Unknown';
|
||||
const oiNormalized = oiLabel.toLowerCase();
|
||||
const chargingLabel = sensors.chargingState?.label || '';
|
||||
const docked = Boolean(sensors.chargingSources?.homeBase);
|
||||
const charging = docked && chargingLabel.toLowerCase() !== 'not charging' && chargingLabel !== '';
|
||||
const driving = oiNormalized === 'full';
|
||||
const dockedNotCharging = docked && !charging;
|
||||
const dockingInProgress = !docked && !charging && oiNormalized === 'passive';
|
||||
return { driving, docked, charging, dockedNotCharging, dockingInProgress, oiLabel, chargingLabel };
|
||||
}
|
||||
|
||||
export function useDriveDockState(roverId) {
|
||||
const frame = useTelemetryFrame(roverId);
|
||||
return useMemo(() => deriveDriveDockState(frame), [frame]);
|
||||
}
|
||||
|
||||
function StatusPill({ label, active }) {
|
||||
return (
|
||||
<span
|
||||
className={`rounded px-1.5 py-0.5 text-[0.75rem] font-semibold ${
|
||||
active ? 'bg-emerald-600 text-white' : 'bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function KeyPill({ label }) {
|
||||
if (!label) return null;
|
||||
return <span className="rounded border border-white/40 px-1 text-[0.7rem] text-white">{label}</span>;
|
||||
}
|
||||
|
||||
function DockModal({ instructions, onConfirm, onCancel, pending }) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-40 flex items-center justify-center bg-black/70 p-1">
|
||||
<div className="surface w-full max-w-md space-y-0.5 border border-indigo-700 bg-indigo-950/90 p-1 text-slate-100 shadow-2xl">
|
||||
<div className="space-y-0.25">
|
||||
<p className="text-lg font-semibold text-indigo-50">Dock Rover</p>
|
||||
<p className="text-sm text-slate-200">{instructions.summary}</p>
|
||||
<StepList steps={instructions.steps} tone="indigo" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-0.5 text-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="bg-slate-700 px-0.5 py-1 font-semibold text-slate-100 transition hover:bg-slate-600"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={pending}
|
||||
onClick={onConfirm}
|
||||
className="bg-indigo-600 px-0.5 py-1 font-semibold text-indigo-50 transition hover:bg-indigo-500 disabled:opacity-50"
|
||||
>
|
||||
{pending ? 'Docking…' : 'Confirm Dock'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DriveDockAction({ layout = 'desktop', expand = false, driveDockState }) {
|
||||
const isMobile = layout === 'mobile';
|
||||
const {
|
||||
state: { roverId, keymap },
|
||||
actions,
|
||||
} = useControlSystem();
|
||||
const frame = useTelemetryFrame(roverId);
|
||||
const state = driveDockState ?? deriveDriveDockState(frame);
|
||||
const { driving, docked, charging, dockedNotCharging, dockingInProgress, oiLabel } = state;
|
||||
const [pending, setPending] = useState(null);
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
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: 'You are about to trigger an automatic docking attempt! To have a successful dock:',
|
||||
steps: ['Line up the rover straight in front of the dock', 'Make sure the rover is about one foot or half a meter away from the dock', 'Press Confirm Dock to begin the docking process'],
|
||||
};
|
||||
const dockButtonCaption = 'Click this button to initate auto-dock.';
|
||||
|
||||
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 handleReturnToDrive = async () => {
|
||||
if (!roverId || pending) return;
|
||||
setPending('drive');
|
||||
try {
|
||||
actions.setMode('drive');
|
||||
await actions.runMacro('drive-sequence');
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
setPending(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartDrive = async () => {
|
||||
if (!roverId || pending) return;
|
||||
setConfirmOpen(false);
|
||||
setShowModal(false);
|
||||
setPending('drive');
|
||||
try {
|
||||
actions.setMode('drive');
|
||||
await actions.runMacro('drive-sequence');
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
setPending(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmDock = async () => {
|
||||
if (!roverId || pending) return;
|
||||
setPending('dock');
|
||||
try {
|
||||
actions.setMode('dock');
|
||||
await actions.runMacro('seek-dock');
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
setPending(null);
|
||||
setConfirmOpen(false);
|
||||
setShowModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenDock = () => {
|
||||
if (dockDisabled) return;
|
||||
setShowModal(true);
|
||||
setConfirmOpen(true);
|
||||
};
|
||||
|
||||
const baseCardClasses =
|
||||
'flex w-full flex-col gap-0.5 border border-slate-700 px-0.75 py-0.75 text-slate-100 shadow';
|
||||
const filledHeight = expand ? 'h-full flex-1' : '';
|
||||
|
||||
if (!driving && !dockingInProgress) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleStartDrive}
|
||||
disabled={driveDisabled}
|
||||
className={`${baseCardClasses} ${filledHeight} items-center justify-between bg-emerald-800 hover:bg-emerald-700 text-center transition disabled:cursor-not-allowed disabled:opacity-60`}
|
||||
>
|
||||
<div className="space-y-0.25 w-full">
|
||||
<div className="flex items-center justify-center gap-0.5">
|
||||
<span className="text-base font-semibold text-emerald-50">Start Driving</span>
|
||||
{!isMobile && driveKeyLabel ? <KeyPill label={driveKeyLabel} /> : null}
|
||||
</div>
|
||||
<p className="text-sm text-emerald-50/90">{startDriveInstructions.summary}</p>
|
||||
<StepList steps={startDriveInstructions.steps} tone="emerald" />
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-0.5">
|
||||
<StatusPill label={`OI: ${oiLabel}`} active={oiLabel.toLowerCase() === 'full'} />
|
||||
<StatusPill label={docked ? 'Docked!' : 'Not docked'} active={docked} />
|
||||
<StatusPill label={charging ? 'Charging!' : 'Not charging'} active={charging} />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (dockingInProgress) {
|
||||
const inProgressCopy = {
|
||||
summary: 'Docking attempt in progress.',
|
||||
steps: ['The rover should be slowly wiggling towards the dock', 'If it is obviously not working, press this button to enter driving mode and try again'],
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled={driveDisabled}
|
||||
onClick={handleReturnToDrive}
|
||||
className={`${baseCardClasses} ${filledHeight} items-center bg-amber-900 text-center transition hover:bg-amber-800 disabled:cursor-not-allowed disabled:opacity-50`}
|
||||
>
|
||||
<div className="space-y-0.25 w-full">
|
||||
<div className="flex items-center justify-center gap-0.5">
|
||||
<span className="text-base font-semibold text-amber-50">Docking in Progress</span>
|
||||
</div>
|
||||
<p className="text-sm text-amber-50/90">{inProgressCopy.summary}</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-0.5">
|
||||
<StatusPill label={docked ? 'Docked' : 'Not docked'} active={docked} />
|
||||
<StatusPill label={charging ? 'Charging' : 'Not charging'} active={charging} />
|
||||
</div>
|
||||
<StepList steps={inProgressCopy.steps} tone="amber" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
disabled={dockDisabled}
|
||||
onClick={handleOpenDock}
|
||||
className={
|
||||
isMobile
|
||||
? 'flex w-full items-center justify-center border border-slate-700 bg-indigo-900 px-0.75 py-0.75 text-sm font-semibold text-indigo-50 shadow transition hover:bg-indigo-800 disabled:cursor-not-allowed disabled:opacity-50'
|
||||
: `${baseCardClasses} ${filledHeight} items-center bg-indigo-900 text-center transition hover:bg-indigo-800 disabled:cursor-not-allowed disabled:opacity-50`
|
||||
}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-0.5 w-full">
|
||||
<span className="text-base font-semibold text-indigo-50">Dock and Charge</span>
|
||||
{!isMobile && dockKeyLabel ? <KeyPill label={dockKeyLabel} /> : null}
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<>
|
||||
<p className="text-sm text-indigo-50/90">{dockButtonCaption}</p>
|
||||
<div className="flex w-full flex-col gap-0.5">
|
||||
<StatusPill label={docked ? 'Docked' : 'Not docked'} active={docked} />
|
||||
<StatusPill label={charging ? 'Charging' : 'Not charging'} active={charging} />
|
||||
{dockedNotCharging ? (
|
||||
<StatusPill label="Not charging yet" active={false} />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-xs text-indigo-100/80">
|
||||
Line up straight, about 1 foot away, before docking.
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{showModal || (!isMobile && confirmOpen) ? (
|
||||
<DockModal
|
||||
instructions={dockInstructions}
|
||||
pending={pending === 'dock'}
|
||||
onCancel={() => {
|
||||
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 (
|
||||
<div className={`space-y-0.5 rounded border px-0.5 py-0.35 text-left ${container}`}>
|
||||
{steps.map((step, idx) => (
|
||||
<div key={step} className="text-[0.85rem] leading-snug break-words">
|
||||
<div className="flex items-start">
|
||||
<span className={`mr-0.35 align-top text-[0.75rem] font-semibold ${numberColor}`}>{idx + 1}.</span>
|
||||
<span className="align-top">{step}</span>
|
||||
</div>
|
||||
{idx < steps.length - 1 ? <div className="mt-0.35 h-px bg-white/10" /> : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useControlSystem } from '../controls/index.js';
|
||||
import { clampUnit } from '../controls/controlMath.js';
|
||||
import DriveModeToggle from './controls/DriveModeToggle.jsx';
|
||||
import DriveDockAction, { useDriveDockState } from './DriveDockAction.jsx';
|
||||
|
||||
const SOURCE = 'mobile-joystick';
|
||||
const JOYSTICK_RADIUS = 80;
|
||||
@@ -134,8 +134,10 @@ function MobileJoystickPanel({ layout }) {
|
||||
const {
|
||||
state: { roverId, camera },
|
||||
pipeline,
|
||||
actions: { setDriveVector, registerInputState, stopAllMotion, setServoAngle, toggleNightVision },
|
||||
actions: { setDriveVector, registerInputState, setServoAngle, toggleNightVision },
|
||||
} = useControlSystem();
|
||||
const driveDockState = useDriveDockState(roverId);
|
||||
const dockedNotDriving = driveDockState.docked && !driveDockState.driving;
|
||||
const disabled = !roverId;
|
||||
const cameraConfig = camera?.config;
|
||||
const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig);
|
||||
@@ -196,47 +198,52 @@ function MobileJoystickPanel({ layout }) {
|
||||
setServoAngle(next);
|
||||
};
|
||||
|
||||
const containerClass = `flex flex-col gap-0.5 text-slate-100 ${
|
||||
dockedNotDriving ? 'h-screen max-h-screen' : ''
|
||||
}`;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5 text-slate-100">
|
||||
|
||||
<DriveModeToggle size="compact" />
|
||||
{nightVisionAvailable && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleNightVision()}
|
||||
disabled={disabled}
|
||||
className="bg-amber-600 px-0.5 py-1 text-sm font-semibold text-amber-50 transition hover:bg-amber-500 disabled:opacity-40"
|
||||
>
|
||||
Toggle Night Vision
|
||||
</button>
|
||||
)}
|
||||
{cameraEnabled && (
|
||||
<div className="bg-zinc-950 p-0.5 text-xs">
|
||||
<div className="flex items-center justify-between text-[0.75rem] text-slate-400">
|
||||
<span>Camera Tilt</span>
|
||||
<span className="font-mono text-slate-200">{cameraValue.toFixed(1)}°</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={cameraMin}
|
||||
max={cameraMax}
|
||||
step={0.5}
|
||||
value={cameraValue}
|
||||
onChange={handleCameraSlider}
|
||||
className="mt-0.5 w-full accent-cyan-400"
|
||||
<div className={containerClass}>
|
||||
<DriveDockAction layout="mobile" expand={dockedNotDriving} driveDockState={driveDockState} />
|
||||
{!dockedNotDriving ? (
|
||||
<>
|
||||
{nightVisionAvailable && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleNightVision()}
|
||||
disabled={disabled}
|
||||
className="bg-amber-600 px-0.5 py-1 text-sm font-semibold text-amber-50 transition hover:bg-amber-500 disabled:opacity-40"
|
||||
>
|
||||
Toggle Night Vision
|
||||
</button>
|
||||
)}
|
||||
{cameraEnabled && (
|
||||
<div className="bg-zinc-950 p-0.5 text-xs">
|
||||
<div className="flex items-center justify-between text-[0.75rem] text-slate-400">
|
||||
<span>Camera Tilt</span>
|
||||
<span className="font-mono text-slate-200">{cameraValue.toFixed(1)}°</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={cameraMin}
|
||||
max={cameraMax}
|
||||
step={0.5}
|
||||
value={cameraValue}
|
||||
onChange={handleCameraSlider}
|
||||
className="mt-0.5 w-full accent-cyan-400"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<FloatingJoystick
|
||||
disabled={disabled}
|
||||
layout={layout}
|
||||
radius={joystickRadius}
|
||||
onMove={handleMove}
|
||||
onStop={handleStop}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<FloatingJoystick
|
||||
disabled={disabled}
|
||||
layout={layout}
|
||||
radius={joystickRadius}
|
||||
onMove={handleMove}
|
||||
onStop={handleStop}
|
||||
/>
|
||||
{/* <button type="button" onClick={stopAllMotion} disabled={disabled} className="button-danger w-full disabled:opacity-40">
|
||||
Panic Stop
|
||||
</button> */}
|
||||
</>
|
||||
) : null}
|
||||
{/* Panic stop button can be re-enabled here if needed */}
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user