move neato and lift into activities

This commit is contained in:
legop3
2026-06-23 12:36:21 -04:00
parent 0a351b9a46
commit 5634004ebd
9 changed files with 72 additions and 81 deletions
+4
View File
@@ -43,6 +43,8 @@ import { useTelemetryVisualPolicy } from './context/TelemetryContext.jsx';
import ButtonBoxPanel from './components/ButtonBoxPanel/index.jsx';
import BarcodeGamesPanel from './components/BarcodeGamesPanel/index.jsx';
import OdometerPanel from './components/OdometerPanel/index.jsx';
import LiftCard from './components/LiftCard/index.jsx';
import NeatoCard from './components/NeatoCard/index.jsx';
import RewardRunOverlay from './components/RewardRunOverlay/index.jsx';
import SocketConnectionPill from './components/SocketConnectionPill/index.jsx';
import DuplicateIdentityOverlay from './components/DuplicateIdentityOverlay/index.jsx';
@@ -195,6 +197,8 @@ function MobileFeatureTabs({
<TabPanel id="activities">
<div className={`flex flex-col ${themeGapClass}`}>
<BarcodeGamesPanel />
<LiftCard />
<NeatoCard />
<OdometerPanel />
<ButtonBoxPanel />
<KinectPanel />
@@ -1,7 +1,8 @@
// Vip Lift Card
// Lift Card
// Purpose: Renders a shared lift controller panel synced from server session state.
// Scope: Presents verified-user controls while reflecting global busy/position/cooldown state.
// Scope: Presents public activity controls while reflecting global busy/position/cooldown state.
import { useEffect, useMemo, useState } from 'react';
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import CardFrame from '../CardFrame/index.jsx';
function badgeClass(tone) {
@@ -19,10 +20,17 @@ function positionLabel(value) {
return '--';
}
export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
export default function LiftCard() {
/*
The lift card is a complete Activities-tab feature, so it reads the shared
lift state and socket actions directly. Keeping that wiring inside the card
means each tab only decides where the card appears, and the command policy
cannot accidentally diverge between mobile and desktop layouts.
*/
const lift = useSessionSelector((state) => state.session?.lift || null);
const { liftUp, liftDown } = useSessionActions();
const [working, setWorking] = useState('');
const [nowMs, setNowMs] = useState(() => Date.now());
const wrapClass = fullWidth ? 'w-full' : 'w-full max-w-xl';
const configured = Boolean(lift?.configured);
const connected = Boolean(lift?.enabled && lift?.connected);
@@ -71,7 +79,7 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
<CardFrame
title="Lift Controls"
className={`relative ${wrapClass}`}
className="relative w-full"
bodyClassName="text-sm text-slate-200"
actions={
<span className={`inline-flex w-auto rounded px-1 py-0.25 text-xs font-semibold ${badgeClass(statusTone)}`}>
@@ -108,7 +116,7 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
<button
type="button"
disabled={!canRun}
onClick={() => run('down', onDown)}
onClick={() => run('down', liftDown)}
className={`button-dark w-full text-sm disabled:opacity-50 ${position === 'down' || activeTarget === 'down' ? 'bg-emerald-500 text-white hover:bg-emerald-500' : ''}`}
>
{working === 'down' || activeTarget === 'down' ? 'Lowering...' : 'Down'}
@@ -116,7 +124,7 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
<button
type="button"
disabled={!canRun}
onClick={() => run('up', onUp)}
onClick={() => run('up', liftUp)}
className={`button-dark w-full text-sm disabled:opacity-50 ${position === 'up' || activeTarget === 'up' ? 'bg-emerald-500 text-white hover:bg-emerald-500' : ''}`}
>
{working === 'up' || activeTarget === 'up' ? 'Raising...' : 'Up'}
@@ -1,7 +1,8 @@
// Vip Neato Card
// Purpose: Defines the Vip Neato Card module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
// Neato Card
// Purpose: Defines the public Neato activity card and the local helpers/components used in this file.
// Scope: Keeps Neato state display and commands together because each button's availability depends on the same shared robot telemetry.
import { useState } from 'react';
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import CardFrame from '../CardFrame/index.jsx';
function normalizeState(value) {
@@ -39,17 +40,21 @@ function StatusTile({ label, value, tone = 'muted', valueClass = '', hideLabel =
);
}
export default function VipNeatoCard({
neato,
onStart,
onSendHome,
onLocate,
onClearErrors,
onPowerCycle,
fullWidth = false,
}) {
export default function NeatoCard() {
/*
Neato is a standalone public activity card. It owns its session selector and
command actions so callers do not need to know the socket event names or
keep a parallel list of Neato props in every tab layout.
*/
const neato = useSessionSelector((state) => state.session?.neato || null);
const {
neatoStart,
neatoSendHome,
neatoLocate,
neatoClearErrors,
neatoPowerCycle,
} = useSessionActions();
const [working, setWorking] = useState('');
const wrapClass = fullWidth ? 'w-full' : 'w-full max-w-xl';
const configured = Boolean(neato?.configured);
const connected = Boolean(neato?.enabled && neato?.connected);
@@ -102,7 +107,7 @@ export default function VipNeatoCard({
<CardFrame
title="Neato Controls"
className={wrapClass}
className="w-full"
bodyClassName="text-sm text-slate-200"
actions={
<span className={`inline-flex w-auto rounded px-1 py-0.25 text-xs font-semibold ${metricToneClass(headerTone)}`}>
@@ -121,7 +126,7 @@ export default function VipNeatoCard({
<button
type="button"
disabled={!canRunStart || Boolean(working)}
onClick={() => runAction('start', onStart)}
onClick={() => runAction('start', neatoStart)}
className="rounded-md border border-sky-300 bg-emerald-600 px-1 py-1 text-base font-semibold text-white transition hover:border-sky-500 hover:bg-emerald-500 disabled:opacity-50"
>
{working === 'start' ? 'Starting...' : 'Start cleaning'}
@@ -129,7 +134,7 @@ export default function VipNeatoCard({
<button
type="button"
disabled={!canRunSendHome || Boolean(working)}
onClick={() => runAction('sendHome', onSendHome)}
onClick={() => runAction('sendHome', neatoSendHome)}
className="rounded-md border border-sky-300 bg-sky-600 px-1 py-1 text-base font-semibold text-white transition hover:border-sky-500 hover:bg-sky-500 disabled:opacity-50"
>
{working === 'sendHome' ? 'Sending...' : 'Send to dock'}
@@ -138,7 +143,7 @@ export default function VipNeatoCard({
<button
type="button"
disabled={!canRunLocate || Boolean(working)}
onClick={() => runAction('locate', onLocate)}
onClick={() => runAction('locate', neatoLocate)}
className="w-full rounded-md border border-sky-300 bg-fuchsia-600 px-1 py-0.5 text-xs font-semibold text-white transition hover:border-sky-500 hover:bg-fuchsia-500 disabled:opacity-50"
>
{working === 'locate' ? 'Playing...' : 'Play sound'}
@@ -146,7 +151,7 @@ export default function VipNeatoCard({
<button
type="button"
disabled={!canRunClearErrors || Boolean(working)}
onClick={() => runAction('clearErrors', onClearErrors)}
onClick={() => runAction('clearErrors', neatoClearErrors)}
className="w-full rounded-md border border-sky-300 bg-amber-500 px-1 py-0.5 text-xs font-semibold text-slate-900 transition hover:border-sky-500 hover:bg-amber-400 disabled:opacity-50"
>
{working === 'clearErrors' ? 'Clearing...' : 'Clear errors'}
@@ -154,7 +159,7 @@ export default function VipNeatoCard({
<button
type="button"
disabled={!canRunPowerCycle || Boolean(working)}
onClick={() => runAction('powerCycle', onPowerCycle)}
onClick={() => runAction('powerCycle', neatoPowerCycle)}
className="w-full rounded-md border border-rose-300 bg-rose-600 px-1 py-0.5 text-xs font-semibold text-white transition hover:border-rose-500 hover:bg-rose-500 disabled:opacity-50"
>
{working === 'powerCycle' ? 'Cycling...' : 'Power cycle'}
@@ -28,6 +28,8 @@ import { useSettingsNamespace } from '../../settings/index.js';
import ButtonBoxPanel from '../ButtonBoxPanel/index.jsx';
import BarcodeGamesPanel from '../BarcodeGamesPanel/index.jsx';
import OdometerPanel from '../OdometerPanel/index.jsx';
import LiftCard from '../LiftCard/index.jsx';
import NeatoCard from '../NeatoCard/index.jsx';
import OverseerPreferencePanel from '../OverseerPreferencePanel/index.jsx';
import CardFrame from '../CardFrame/index.jsx';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
@@ -367,6 +369,8 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
<TabPanel id="activities">
<div className={`flex flex-col ${themeGapClass}`}>
<BarcodeGamesPanel />
<LiftCard />
<NeatoCard />
<OdometerPanel />
<ButtonBoxPanel />
<KinectPanel />
+1 -25
View File
@@ -9,8 +9,6 @@ import VipAudioUploadCard from '../vip/VipAudioUploadCard/index.jsx';
import VipVerificationCard from '../vip/VipVerificationCard.jsx';
import VipIdentityCard from '../vip/VipIdentityCard.jsx';
import VipPrivateRoverAccessCard from '../vip/VipPrivateRoverAccessCard.jsx';
import VipNeatoCard from '../vip/VipNeatoCard.jsx';
import VipLiftCard from '../vip/VipLiftCard.jsx';
import VipProfileImageCard from '../vip/VipProfileImageCard.jsx';
export default function VipPanel({ isActive = true }) {
@@ -24,13 +22,6 @@ export default function VipPanel({ isActive = true }) {
startMicWhip,
readyMicWhip,
stopMicWhip,
neatoStart,
neatoSendHome,
neatoLocate,
neatoClearErrors,
neatoPowerCycle,
liftUp,
liftDown,
} = useSessionActions();
const { value: identity, save: saveIdentity } = useSettingsNamespace('identity', { cookieUserId: '' });
const { value: profile } = useSettingsNamespace('profile', { nickname: '' });
@@ -77,21 +68,6 @@ export default function VipPanel({ isActive = true }) {
<div className="lg:col-span-2">
{isVerified ? (
<div className="space-y-2">
<VipNeatoCard
neato={session?.neato || null}
onStart={neatoStart}
onSendHome={neatoSendHome}
onLocate={neatoLocate}
onClearErrors={neatoClearErrors}
onPowerCycle={neatoPowerCycle}
fullWidth
/>
<VipLiftCard
lift={session?.lift || null}
onUp={liftUp}
onDown={liftDown}
fullWidth
/>
<VipAudioUploadCard
ownRoverId={ownRoverId}
audioForwardByRover={session?.audioForward || {}}
@@ -105,7 +81,7 @@ export default function VipPanel({ isActive = true }) {
) : (
<section className="surface h-full">
<div className="flex h-full flex-col items-center justify-center text-center text-xs text-slate-400">
Verify your account to unlock VIP controls.
Verify your account to unlock VIP features.
</div>
</section>
)}