mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
moving a LOT of stuff around in web ui
This commit is contained in:
@@ -1,443 +0,0 @@
|
||||
// Main Application Shell
|
||||
// Purpose: Composes the primary rover control interface and page-level layout. Scope: Orchestrates high-level panels, overlays, and feature modules for the default route.
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import TelemetryPanel from './components/TelemetryPanel/index.jsx';
|
||||
import PiHostStatsCard from './components/PiHostStatsCard/index.jsx';
|
||||
import ReplaySourcesPanel from './components/ReplaySourcesPanel/index.jsx';
|
||||
import AlertFeed from './components/AlertFeed/index.jsx';
|
||||
import AuxColumn from './components/MobileControls/AuxColumn.jsx';
|
||||
import MovementColumn from './components/MobileControls/MovementColumn.jsx';
|
||||
import {
|
||||
ControlSystemProvider,
|
||||
KeyboardInputManager,
|
||||
GamepadInputManager,
|
||||
useControlSelector,
|
||||
} from './controls/index.js';
|
||||
import RoomCameraPanel from './components/RoomCameraPanel/index.jsx';
|
||||
import KinectPanel from './components/KinectPanel/index.jsx';
|
||||
import BalanceBoardPanel from './components/BalanceBoardPanel/index.jsx';
|
||||
import DriverVideo from './components/DriverVideo/index.jsx';
|
||||
import RightPaneTabs from './components/RightPaneTabs/index.jsx';
|
||||
import ModeGateOverlay from './components/ModeGateOverlay/index.jsx';
|
||||
import HomeAssistantControls from './components/HomeAssistantControls/index.jsx';
|
||||
import TurnAlertListener from './components/TurnAlertListener/index.jsx';
|
||||
import RawUserPilePanel from './components/RawUserPilePanel/index.jsx';
|
||||
import OverseerPreferencePanel from './components/OverseerPreferencePanel/index.jsx';
|
||||
import SocialButtonsGrid from './components/SocialButtonsGrid/index.jsx';
|
||||
import ChatPanel from './components/ChatPanel/index.jsx';
|
||||
import FullscreenPrompt from './components/FullscreenPrompt/index.jsx';
|
||||
import FloatingFullscreenButton from './components/FloatingFullscreenButton/index.jsx';
|
||||
import { useFullscreenPrompt } from './hooks/useFullscreenPrompt.js';
|
||||
import { useSettingsNamespace } from './settings/index.js';
|
||||
import HelpOverlay from './components/HelpOverlay/index.jsx';
|
||||
import QuickstartOverlay from './components/QuickstartOverlay/index.jsx';
|
||||
import HelpPanel from './components/HelpPanel/index.jsx';
|
||||
import SettingsPanel from './components/SettingsPanel/index.jsx';
|
||||
import Tabs, { Tab, TabList, TabPanel, TabPanels } from './components/Tabs/index.jsx';
|
||||
import useDefaultNickname from './hooks/useDefaultNickname.js';
|
||||
import useUserIdentitySync from './hooks/useUserIdentitySync.js';
|
||||
import useIncomingInterInstanceTransfer from './hooks/useIncomingInterInstanceTransfer.js';
|
||||
import GlobalObjectiveBanner from './components/GlobalObjectiveBanner/index.jsx';
|
||||
import RoverQueuesPanel from './components/RoverQueuesPanel/index.jsx';
|
||||
import PtzQueueCard from './components/PtzCamera/index.jsx';
|
||||
import VipPanel from './components/VipPanel/index.jsx';
|
||||
import { useSessionSelector } from './context/SessionContext.jsx';
|
||||
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 FleetReportsCard from './components/FleetReportsCard/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';
|
||||
import DriverAdCard from './components/DriverAdCard/index.jsx';
|
||||
import {
|
||||
DEFAULT_PAGE_THEME_KEY,
|
||||
getPageThemeClass,
|
||||
themeGapClass,
|
||||
themeStackClass,
|
||||
} from './themes/index.js';
|
||||
import useLayoutMode from './hooks/useLayoutMode.js';
|
||||
|
||||
function DesktopLayout({ layout, onOpenHelpOverlay }) {
|
||||
return (
|
||||
<div className={`flex h-full ${themeGapClass} overflow-hidden`}>
|
||||
<div className={`flex min-w-0 flex-[1.22] flex-col ${themeGapClass} overflow-y-auto pr-0`}>
|
||||
<DriverVideo />
|
||||
<PiHostStatsCard />
|
||||
{/*
|
||||
The ad owns its empty-state gate and uses auto top margin to consume
|
||||
any spare column height. Keeping it as the final desktop-only child
|
||||
pins it to the bottom without introducing layout measurement code or
|
||||
exposing it on mobile and alternate application routes.
|
||||
*/}
|
||||
<DriverAdCard className="mt-auto" />
|
||||
{/* <TelemetryPanel /> */}
|
||||
</div>
|
||||
<div className={`flex min-w-0 flex-1 flex-col ${themeGapClass} overflow-y-auto`}>
|
||||
<GlobalObjectiveBanner layout={layout} />
|
||||
<RightPaneTabs layout={layout} onOpenHelpOverlay={onOpenHelpOverlay} />
|
||||
{/* <SessionSnapshot /> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileFeatureTabs({
|
||||
layout,
|
||||
onOpenHelpOverlay,
|
||||
roomPanelId,
|
||||
}) {
|
||||
const [activeTab, setActiveTab] = useState('chat');
|
||||
const isVerified = useSessionSelector((state) => Boolean(state.session?.isVerified));
|
||||
const ownRoverId = useSessionSelector((state) => String(state.session?.assignment?.roverId || '').trim());
|
||||
const ownAudioForward = useSessionSelector((state) => {
|
||||
const roverId = String(state.session?.assignment?.roverId || '').trim();
|
||||
return roverId ? state.session?.audioForward?.[roverId] || null : null;
|
||||
});
|
||||
const pttActive = useControlSelector((control) => Boolean(control.state.mic?.pttActive));
|
||||
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
|
||||
const vipDotClass = isVerified ? 'bg-emerald-400' : 'bg-amber-400';
|
||||
const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
|
||||
const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live';
|
||||
const vipMicActive = Boolean(
|
||||
ownRoverId &&
|
||||
isVerified &&
|
||||
(pttMode === 'clip' ? pttActive : (openMicEnabled || pttActive)),
|
||||
);
|
||||
const vipClipPlaying = Boolean(
|
||||
ownRoverId &&
|
||||
isVerified &&
|
||||
pttMode === 'clip' &&
|
||||
ownAudioForward?.source === 'upload' &&
|
||||
ownAudioForward?.state === 'playing',
|
||||
);
|
||||
const showOverseerPreferencePanel = useSessionSelector((state) => {
|
||||
const vote = state.session?.overseerVote;
|
||||
|
||||
// Match the panel's server-owned voting gate so the mobile chat row can
|
||||
// collapse to a single-column layout when voting is unavailable, including
|
||||
// disabled service and direct-address mode.
|
||||
return Boolean(vote?.votingEnabled);
|
||||
});
|
||||
const handleTabChange = useCallback(
|
||||
(tab) => {
|
||||
setActiveTab(tab);
|
||||
},
|
||||
[],
|
||||
);
|
||||
return (
|
||||
<section className="text-base">
|
||||
<Tabs defaultTab="chat" currentTab={activeTab} onTabChange={handleTabChange}>
|
||||
<TabList>
|
||||
<Tab id="chat">Chat</Tab>
|
||||
<Tab id="activities">Activities</Tab>
|
||||
<Tab id="vip" highlight={vipClipPlaying ? 'green' : vipMicActive ? 'pink' : 'none'}>
|
||||
<span className="inline-flex items-center gap-0.5">
|
||||
<span>VIP</span>
|
||||
<span
|
||||
className={`inline-block h-1.5 w-1.5 rounded-full ${vipDotClass}`}
|
||||
aria-hidden="true"
|
||||
title={isVerified ? 'Verified' : 'Not verified'}
|
||||
/>
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab id="roomcontrols">Room Controls</Tab>
|
||||
<Tab id="help">Help</Tab>
|
||||
<Tab id="settings">Settings</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel id="chat">
|
||||
<div className={themeStackClass}>
|
||||
<ChatPanel nicknameLayout="stacked" />
|
||||
<div className={themeStackClass}>
|
||||
<div className={`grid ${themeGapClass} md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]`}>
|
||||
<SocialButtonsGrid />
|
||||
</div>
|
||||
<div
|
||||
className={`grid ${themeGapClass} ${
|
||||
showOverseerPreferencePanel
|
||||
? 'grid-cols-[minmax(0,1fr)_minmax(0,1fr)]'
|
||||
: 'grid-cols-[minmax(0,1fr)]'
|
||||
}`}
|
||||
>
|
||||
{/*
|
||||
The overseer vote panel is server-vote-gated. When voting
|
||||
is unavailable, the raw user pile should reclaim the row
|
||||
instead of sitting in a half-empty two-column layout.
|
||||
*/}
|
||||
{showOverseerPreferencePanel ? <OverseerPreferencePanel /> : null}
|
||||
<RawUserPilePanel hideNicknameForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
{/* activities tab */}
|
||||
<TabPanel id="activities">
|
||||
<div className={`flex flex-col ${themeGapClass}`}>
|
||||
<NeatoCard />
|
||||
<LiftCard />
|
||||
<BalanceBoardPanel />
|
||||
<BarcodeGamesPanel />
|
||||
<OdometerPanel />
|
||||
<ButtonBoxPanel />
|
||||
<KinectPanel />
|
||||
{/* Fleet reports owns its optional feature gate and intentionally
|
||||
remains last so Activities keeps lightweight controls first. */}
|
||||
<FleetReportsCard />
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel id="vip" keepMounted>
|
||||
<VipPanel isActive={activeTab === 'vip'} layout={layout} />
|
||||
</TabPanel>
|
||||
<TabPanel id="roomcontrols">
|
||||
<div className={themeStackClass}>
|
||||
{/* {showTelemetry ? <TelemetryPanel /> : null} */}
|
||||
<HomeAssistantControls />
|
||||
<RoomCameraPanel panelId={roomPanelId} />
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel id="help">
|
||||
<HelpPanel layout={layout} onOpenOverlay={onOpenHelpOverlay} />
|
||||
</TabPanel>
|
||||
<TabPanel id="settings">
|
||||
<div className={themeStackClass}>
|
||||
<SettingsPanel />
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function MobilePortraitLayout({ onOpenHelpOverlay, swapMobileControlColumns = false }) {
|
||||
const columnHeight = 'h-[min(60svh,24rem)]';
|
||||
const firstColumn = swapMobileControlColumns
|
||||
? <MovementColumn layout="portrait" className={columnHeight} />
|
||||
: <AuxColumn layout="portrait" className={columnHeight} />;
|
||||
const secondColumn = swapMobileControlColumns
|
||||
? <AuxColumn layout="portrait" className={columnHeight} />
|
||||
: <MovementColumn layout="portrait" className={columnHeight} />;
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col ${themeGapClass}`}>
|
||||
<DriverVideo layoutFormat="mobile-portrait" />
|
||||
{/* Portrait owns the two-column placement because the same reusable mobile
|
||||
columns sit in different grid contexts in portrait and landscape. */}
|
||||
<section className="mobile-touch-control text-white">
|
||||
<div className="mobile-touch-control grid grid-cols-2 gap-0.5 items-stretch">
|
||||
{firstColumn}
|
||||
{secondColumn}
|
||||
</div>
|
||||
</section>
|
||||
<div className={`grid ${themeGapClass} grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]`}>
|
||||
<ReplaySourcesPanel panelId="replay-sources-mobile-portrait" />
|
||||
<div className="space-y-0.5">
|
||||
<RoverQueuesPanel />
|
||||
<PtzQueueCard layout="mobile-portrait" />
|
||||
</div>
|
||||
</div>
|
||||
{/* <ControlSummary /> */}
|
||||
<MobileFeatureTabs
|
||||
layout="mobile-portrait"
|
||||
onOpenHelpOverlay={onOpenHelpOverlay}
|
||||
roomPanelId="mobile-portrait-room"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileLandscapeLayout({ onOpenHelpOverlay, swapMobileControlColumns = false }) {
|
||||
const columnClass = 'self-start h-[min(100svh,32rem)]';
|
||||
const firstColumn = swapMobileControlColumns
|
||||
? <MovementColumn layout="landscape" className={columnClass} />
|
||||
: <AuxColumn layout="landscape" className={columnClass} />;
|
||||
const secondColumn = swapMobileControlColumns
|
||||
? <AuxColumn layout="landscape" className={columnClass} />
|
||||
: <MovementColumn layout="landscape" className={columnClass} />;
|
||||
return (
|
||||
<div className={`flex flex-col ${themeGapClass}`}>
|
||||
<section className={`grid min-h-screen grid-cols-[minmax(0,0.7fr)_minmax(0,2.1fr)_minmax(0,0.7fr)] ${themeGapClass}`}>
|
||||
{firstColumn}
|
||||
<div>
|
||||
<DriverVideo layoutFormat="mobile-landscape" />
|
||||
<div className={`grid ${themeGapClass} grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]`}>
|
||||
<ReplaySourcesPanel panelId="replay-sources-mobile-landscape" />
|
||||
<div className="space-y-0.5">
|
||||
<RoverQueuesPanel />
|
||||
<PtzQueueCard layout="mobile-landscape" />
|
||||
</div>
|
||||
</div>
|
||||
{/* <TelemetryPanel /> */}
|
||||
</div>
|
||||
{secondColumn}
|
||||
</section>
|
||||
<div className={`flex flex-col ${themeGapClass} pb-0`}>
|
||||
<MobileFeatureTabs
|
||||
layout="mobile-landscape"
|
||||
onOpenHelpOverlay={onOpenHelpOverlay}
|
||||
roomPanelId="mobile-landscape-room"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const layout = useLayoutMode();
|
||||
const isDesktop = layout === 'desktop';
|
||||
const fullscreen = useFullscreenPrompt(layout);
|
||||
const { value: pageSettings } = useSettingsNamespace('page', {
|
||||
backgroundTheme: DEFAULT_PAGE_THEME_KEY,
|
||||
});
|
||||
// Resolve the cookie value through the shared catalog before painting the page. This prevents
|
||||
// an obsolete or hand-edited key from stripping the background class from every exposed seam.
|
||||
const pageBackgroundClass = getPageThemeClass(pageSettings?.backgroundTheme);
|
||||
|
||||
return (
|
||||
<div className={`${pageBackgroundClass} text-slate-100 ${isDesktop ? 'h-screen overflow-hidden' : 'ios-safe-screen min-h-screen'}`}>
|
||||
<AppWithProviders layout={layout} isDesktop={isDesktop} fullscreen={fullscreen} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AppWithProviders({ layout, isDesktop, fullscreen }) {
|
||||
useDefaultNickname();
|
||||
useIncomingInterInstanceTransfer();
|
||||
useUserIdentitySync({ identitySurface: 'driver' });
|
||||
useTelemetryVisualPolicy({ mobile: !isDesktop });
|
||||
const {
|
||||
visible: fullscreenVisible,
|
||||
mode: fullscreenMode,
|
||||
isIOS: fullscreenIsIOS,
|
||||
nativeSupported: fullscreenNativeSupported,
|
||||
enterFullscreen,
|
||||
dismiss,
|
||||
showPrompt,
|
||||
} = fullscreen;
|
||||
|
||||
const {
|
||||
value: helpSettings,
|
||||
save: saveHelpSettings,
|
||||
} = useSettingsNamespace('help', { showOnLoad: true });
|
||||
const {
|
||||
value: quickstartSettings,
|
||||
status: quickstartStatus,
|
||||
save: saveQuickstartSettings,
|
||||
} = useSettingsNamespace('quickstart', { showOnLoad: true });
|
||||
const { value: pageSettings } = useSettingsNamespace('page', {
|
||||
swapMobileControlColumns: false,
|
||||
});
|
||||
const swapMobileControlColumns = Boolean(pageSettings?.swapMobileControlColumns);
|
||||
const fullscreenButtonSide = swapMobileControlColumns ? 'left' : 'right';
|
||||
const showFloatingFullscreenButton = !isDesktop && (fullscreenIsIOS || fullscreenNativeSupported);
|
||||
const [helpVisible, setHelpVisible] = useState(false);
|
||||
const [quickstartVisible, setQuickstartVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (quickstartStatus === 'ready') {
|
||||
setQuickstartVisible(quickstartSettings?.showOnLoad !== false);
|
||||
}
|
||||
}, [quickstartStatus, quickstartSettings?.showOnLoad]);
|
||||
|
||||
const openHelp = useCallback(() => {
|
||||
setHelpVisible(true);
|
||||
}, []);
|
||||
const closeHelp = useCallback(() => {
|
||||
setHelpVisible(false);
|
||||
}, []);
|
||||
const closeQuickstart = useCallback(() => {
|
||||
setQuickstartVisible(false);
|
||||
}, []);
|
||||
const handleFloatingFullscreen = useCallback(async () => {
|
||||
if (fullscreenIsIOS) {
|
||||
showPrompt();
|
||||
return;
|
||||
}
|
||||
const entered = await enterFullscreen();
|
||||
if (!entered) {
|
||||
showPrompt();
|
||||
}
|
||||
}, [enterFullscreen, fullscreenIsIOS, showPrompt]);
|
||||
const setQuickstartShowOnLoad = useCallback(
|
||||
(enabled) => {
|
||||
const next = Boolean(enabled);
|
||||
saveQuickstartSettings((current) => ({ ...(current ?? {}), showOnLoad: next }));
|
||||
if (!next) {
|
||||
setQuickstartVisible(false);
|
||||
}
|
||||
},
|
||||
[saveQuickstartSettings],
|
||||
);
|
||||
const openHelpFromQuickstart = useCallback(() => {
|
||||
setQuickstartVisible(false);
|
||||
setHelpVisible(true);
|
||||
}, []);
|
||||
const handleFullscreenPromptEnter = useCallback(async () => {
|
||||
const entered = await enterFullscreen();
|
||||
return entered;
|
||||
}, [enterFullscreen]);
|
||||
const handleFullscreenPromptDismiss = useCallback(() => {
|
||||
dismiss();
|
||||
}, [dismiss]);
|
||||
|
||||
const renderedLayout = useMemo(
|
||||
() =>
|
||||
isDesktop
|
||||
? <DesktopLayout layout={layout} onOpenHelpOverlay={openHelp} />
|
||||
: layout === 'mobile-landscape'
|
||||
? <MobileLandscapeLayout onOpenHelpOverlay={openHelp} swapMobileControlColumns={swapMobileControlColumns} />
|
||||
: <MobilePortraitLayout onOpenHelpOverlay={openHelp} swapMobileControlColumns={swapMobileControlColumns} />,
|
||||
[isDesktop, layout, openHelp, swapMobileControlColumns],
|
||||
);
|
||||
|
||||
return (
|
||||
<ControlSystemProvider>
|
||||
<KeyboardInputManager />
|
||||
<GamepadInputManager />
|
||||
<main className={`flex w-full flex-col ${themeGapClass} text-base ${isDesktop ? 'h-full overflow-hidden' : ''}`}>
|
||||
{!isDesktop ? <GlobalObjectiveBanner layout={layout} /> : null}
|
||||
{renderedLayout}
|
||||
</main>
|
||||
<AlertFeed />
|
||||
<DuplicateIdentityOverlay />
|
||||
<RewardRunOverlay />
|
||||
<TurnAlertListener />
|
||||
<ModeGateOverlay />
|
||||
<SocketConnectionPill />
|
||||
|
||||
<HelpOverlay
|
||||
visible={helpVisible}
|
||||
layout={layout}
|
||||
onClose={closeHelp}
|
||||
showOnLoad={helpSettings?.showOnLoad !== false}
|
||||
onToggleShowOnLoad={(enabled) => saveHelpSettings((current) => ({ ...(current ?? {}), showOnLoad: Boolean(enabled) }))}
|
||||
/>
|
||||
<FullscreenPrompt
|
||||
visible={fullscreenVisible}
|
||||
mode={fullscreenMode}
|
||||
onEnterFullscreen={handleFullscreenPromptEnter}
|
||||
onDismiss={handleFullscreenPromptDismiss}
|
||||
/>
|
||||
<QuickstartOverlay
|
||||
visible={quickstartVisible}
|
||||
layout={layout}
|
||||
showOnLoad={quickstartSettings?.showOnLoad !== false}
|
||||
onToggleShowOnLoad={setQuickstartShowOnLoad}
|
||||
onOpenHelp={openHelpFromQuickstart}
|
||||
onClose={closeQuickstart}
|
||||
/>
|
||||
{showFloatingFullscreenButton ? (
|
||||
<FloatingFullscreenButton
|
||||
side={fullscreenButtonSide}
|
||||
onClick={handleFloatingFullscreen}
|
||||
/>
|
||||
) : null}
|
||||
</ControlSystemProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -2,6 +2,7 @@
|
||||
// Purpose: Defines the Battery Bar 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 React from 'react';
|
||||
import './styles.css';
|
||||
import { WARN_DISPLAY_PERCENT } from '../../lib/battery.js';
|
||||
|
||||
const WARN_FLASH_MS = 1600;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
@keyframes batteryTickWarn { 0%, 49% { background-color: rgba(255,255,255,.95); } 50%, 100% { background-color: rgba(239,68,68,.95); } }
|
||||
.battery-tick-warn { animation: batteryTickWarn .4s steps(2,end) infinite; }
|
||||
@keyframes batteryUrgentFlash { 0%, 49% { opacity: 1; } 50%, 100% { opacity: .25; } }
|
||||
.battery-urgent-flash { animation: batteryUrgentFlash .4s steps(2,end) infinite; }
|
||||
@@ -3,6 +3,7 @@
|
||||
// Scope: Keeps timeline updates isolated from controlled form inputs so incoming chat activity does not
|
||||
// force the composer DOM to re-commit while a user is simply watching or driving.
|
||||
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import './styles.css';
|
||||
import { useChatActions, useChatTimeline } from '../../context/ChatContext.jsx';
|
||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import { useSettingsNamespace } from '../../settings/index.js';
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.chat-composer { container-type: inline-size; display: flex; flex-wrap: wrap; align-items: stretch; gap: 0.125rem; min-width: 0; overflow: hidden; }
|
||||
.chat-composer-nickname { flex: 0 0 5rem; min-width: 0; order: 1; }
|
||||
.chat-composer-input { flex: 1 1 0%; min-width: 0; order: 2; }
|
||||
.chat-composer-send { flex: 0 0 auto; align-self: stretch; order: 3; }
|
||||
.chat-composer-tts { display: flex; flex: 1 1 100%; align-items: center; gap: 0.125rem; min-width: 0; overflow: hidden; order: 4; }
|
||||
@container (min-width: 44rem) {
|
||||
.chat-composer { flex-wrap: nowrap; }
|
||||
.chat-composer-nickname { flex-basis: 7rem; order: 1; }
|
||||
.chat-composer-input { order: 2; }
|
||||
.chat-composer-tts { flex: 0 1 auto; order: 3; }
|
||||
.chat-composer-send { order: 4; }
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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 '../MobileControls/mobileControls.css';
|
||||
import { useControlActions, useControlSelector } from '../../controls/index.js';
|
||||
import { useTelemetrySelector } from '../../context/TelemetryContext.jsx';
|
||||
import { dockTelemetryEqual, selectDockTelemetry } from '../../context/telemetryViews.js';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Purpose: Renders a direct press target for rover GPIO-backed toggles such as the headlight and laser.
|
||||
// Scope: Owns optimistic button state and touch/click de-duplication while callers provide device labels and actions.
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import '../MobileControls/mobileControls.css';
|
||||
import { triggerTouchHaptic } from '../../lib/touchHaptics.js';
|
||||
|
||||
function isBoolean(value) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Purpose: Defines the Horn Control 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 { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import '../MobileControls/mobileControls.css';
|
||||
import { useSettingsNamespace } from '../../settings/index.js';
|
||||
import { HORN_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
||||
import { HORN_MAX_FREQUENCY } from '../../controls/constants.js';
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// Purpose: Defines the Hud Chat Input 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 { memo, useMemo, useState } from 'react';
|
||||
import './styles.css';
|
||||
import '../../MobileControls/mobileControls.css';
|
||||
import { useChatActions } from '../../../context/ChatContext.jsx';
|
||||
import { useSessionSelector } from '../../../context/SessionContext.jsx';
|
||||
import { useSettingsNamespace } from '../../../settings/index.js';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.mobile-text-entry { font-size: 16px; -webkit-text-size-adjust: 100%; touch-action: manipulation; }
|
||||
@@ -2,6 +2,7 @@
|
||||
// Purpose: Renders remote rover servers discovered through the inter-instance directory.
|
||||
// Scope: Owns external server metadata presentation while reusing RoverQueuesPanel for rover/queue rows.
|
||||
import { useMemo } from 'react';
|
||||
import './styles.css';
|
||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import CardFrame from '../CardFrame/index.jsx';
|
||||
import RoverQueuesPanel from '../RoverQueuesPanel/index.jsx';
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.inter-instance-overlay-frame { max-width: calc(100vw - .5rem); }
|
||||
.inter-instance-overlay-body { max-height: 82vh; }
|
||||
@media (min-width: 1024px) {
|
||||
.inter-instance-overlay-scale { zoom: 1.5; }
|
||||
.inter-instance-overlay-frame { max-width: calc((100vw - .5rem) / 1.5); }
|
||||
.inter-instance-overlay-body { max-height: 54.6667vh; }
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Purpose: Assembles the mobile auxiliary controls column, which is the left column by default.
|
||||
// Scope: Owns mobile aux/camera/headlight/laser/horn wiring while reusing desktop variation components where intended.
|
||||
import { useCallback, useRef } from 'react';
|
||||
import './mobileControls.css';
|
||||
import { useControlActions, useControlSelector } from '../../controls/index.js';
|
||||
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
|
||||
import HornControl from '../HornControl/index.jsx';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Purpose: Provides the mobile movement control pad and speed mode selector.
|
||||
// Scope: Converts touch pad cells into keyboard-style drive vectors; movement column owns drive/dock placement.
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import './mobileControls.css';
|
||||
import { useControlActions, useControlSelector } from '../../controls/index.js';
|
||||
import { normalizeKeymapEntries } from '../../controls/keymapUtils.js';
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.no-touch-select { user-select: none; -webkit-user-select: none; -ms-user-select: none; -webkit-touch-callout: none; }
|
||||
.mobile-touch-control {
|
||||
/* Suppress browser gestures that compete with deliberate control presses. */
|
||||
user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none;
|
||||
-webkit-touch-callout: none; -webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation; overscroll-behavior: contain;
|
||||
}
|
||||
.mobile-drag-control { touch-action: none; }
|
||||
@@ -2,6 +2,7 @@
|
||||
// Purpose: Defines the Mode Gate Overlay 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 { useMemo } from 'react';
|
||||
import '../InterInstancePanel/styles.css';
|
||||
import AuthPanel from '../AuthPanel/index.jsx';
|
||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import { useSharedClock } from '../../hooks/useSharedClock.js';
|
||||
|
||||
@@ -14,6 +14,16 @@ function useTabsContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useTabIsActive(id) {
|
||||
/*
|
||||
Tab content that manages a mounted media lifecycle needs to know whether it
|
||||
is selected without having the parent shell relay activeTab as a prop. This
|
||||
reads the same Tabs context that already controls TabPanel visibility.
|
||||
*/
|
||||
return useTabsContext().activeTab === id;
|
||||
}
|
||||
|
||||
const TAB_VARIANTS = {
|
||||
primary: {
|
||||
base: 'flex-1 px-0.5 py-0.5 text-sm font-medium focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-slate-500 rounded-md border border-slate-800',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Purpose: Provides the verified-user entry point and fullscreen controller for the single Reolink PTZ camera.
|
||||
// Scope: Owns PTZ UI state only; server-side PTZ ownership, rover handoff, and command authorization remain authoritative.
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import '../MobileControls/mobileControls.css';
|
||||
import { createPortal } from 'react-dom';
|
||||
import ChatPanel from '../ChatPanel/index.jsx';
|
||||
import CardFrame from '../CardFrame/index.jsx';
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// Main Application Shell
|
||||
// Purpose: Composes the primary rover control interface and page-level layout. Scope: Orchestrates high-level panels, overlays, and feature modules for the default route.
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import AlertFeed from '../components/AlertFeed/index.jsx';
|
||||
import {
|
||||
ControlSystemProvider,
|
||||
KeyboardInputManager,
|
||||
GamepadInputManager,
|
||||
} from '../controls/index.js';
|
||||
import ModeGateOverlay from '../components/ModeGateOverlay/index.jsx';
|
||||
import TurnAlertListener from '../components/TurnAlertListener/index.jsx';
|
||||
import { useSettingsNamespace } from '../settings/index.js';
|
||||
import HelpOverlay from '../components/HelpOverlay/index.jsx';
|
||||
import QuickstartOverlay from '../components/QuickstartOverlay/index.jsx';
|
||||
import useDefaultNickname from '../hooks/useDefaultNickname.js';
|
||||
import useUserIdentitySync from '../hooks/useUserIdentitySync.js';
|
||||
import useIncomingInterInstanceTransfer from '../hooks/useIncomingInterInstanceTransfer.js';
|
||||
import RewardRunOverlay from '../components/RewardRunOverlay/index.jsx';
|
||||
import SocketConnectionPill from '../components/SocketConnectionPill/index.jsx';
|
||||
import DuplicateIdentityOverlay from '../components/DuplicateIdentityOverlay/index.jsx';
|
||||
import {
|
||||
DEFAULT_PAGE_THEME_KEY,
|
||||
getPageThemeClass,
|
||||
themeGapClass,
|
||||
} from '../themes/index.js';
|
||||
import useLayoutMode from '../hooks/useLayoutMode.js';
|
||||
import { DriverLayoutProvider } from '../layouts/driver/DriverLayoutContext.jsx';
|
||||
import DriverLayoutRoot from '../layouts/driver/DriverLayoutRoot.jsx';
|
||||
|
||||
/* Driver-page compositions live under layouts/driver. App retains only global providers, overlays, and route-level state. */
|
||||
function DriverPageRoot() {
|
||||
const layout = useLayoutMode();
|
||||
const { value: pageSettings } = useSettingsNamespace('page', {
|
||||
backgroundTheme: DEFAULT_PAGE_THEME_KEY,
|
||||
});
|
||||
// Resolve the cookie value through the shared catalog before painting the page. This prevents
|
||||
// an obsolete or hand-edited key from stripping the background class from every exposed seam.
|
||||
const pageBackgroundClass = getPageThemeClass(pageSettings?.backgroundTheme);
|
||||
|
||||
return (
|
||||
<div className={`${pageBackgroundClass} text-slate-100`}>
|
||||
<DriverPageContent layout={layout} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DriverPageContent({ layout }) {
|
||||
useDefaultNickname();
|
||||
useIncomingInterInstanceTransfer();
|
||||
useUserIdentitySync({ identitySurface: 'driver' });
|
||||
|
||||
const {
|
||||
value: helpSettings,
|
||||
save: saveHelpSettings,
|
||||
} = useSettingsNamespace('help', { showOnLoad: true });
|
||||
const {
|
||||
value: quickstartSettings,
|
||||
status: quickstartStatus,
|
||||
save: saveQuickstartSettings,
|
||||
} = useSettingsNamespace('quickstart', { showOnLoad: true });
|
||||
const [helpVisible, setHelpVisible] = useState(false);
|
||||
const [quickstartVisible, setQuickstartVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (quickstartStatus === 'ready') {
|
||||
setQuickstartVisible(quickstartSettings?.showOnLoad !== false);
|
||||
}
|
||||
}, [quickstartStatus, quickstartSettings?.showOnLoad]);
|
||||
|
||||
const openHelp = useCallback(() => {
|
||||
setHelpVisible(true);
|
||||
}, []);
|
||||
const closeHelp = useCallback(() => {
|
||||
setHelpVisible(false);
|
||||
}, []);
|
||||
const closeQuickstart = useCallback(() => {
|
||||
setQuickstartVisible(false);
|
||||
}, []);
|
||||
const setQuickstartShowOnLoad = useCallback(
|
||||
(enabled) => {
|
||||
const next = Boolean(enabled);
|
||||
saveQuickstartSettings((current) => ({ ...(current ?? {}), showOnLoad: next }));
|
||||
if (!next) {
|
||||
setQuickstartVisible(false);
|
||||
}
|
||||
},
|
||||
[saveQuickstartSettings],
|
||||
);
|
||||
const openHelpFromQuickstart = useCallback(() => {
|
||||
setQuickstartVisible(false);
|
||||
setHelpVisible(true);
|
||||
}, []);
|
||||
return (
|
||||
<ControlSystemProvider>
|
||||
<KeyboardInputManager />
|
||||
<GamepadInputManager />
|
||||
<main className={`relative flex w-full flex-col ${themeGapClass} text-base`}>
|
||||
<DriverLayoutProvider layout={layout} openHelp={openHelp}>
|
||||
<DriverLayoutRoot />
|
||||
</DriverLayoutProvider>
|
||||
</main>
|
||||
<AlertFeed />
|
||||
<DuplicateIdentityOverlay />
|
||||
<RewardRunOverlay />
|
||||
<TurnAlertListener />
|
||||
<ModeGateOverlay />
|
||||
<SocketConnectionPill />
|
||||
|
||||
<HelpOverlay
|
||||
visible={helpVisible}
|
||||
layout={layout}
|
||||
onClose={closeHelp}
|
||||
showOnLoad={helpSettings?.showOnLoad !== false}
|
||||
onToggleShowOnLoad={(enabled) => saveHelpSettings((current) => ({ ...(current ?? {}), showOnLoad: Boolean(enabled) }))}
|
||||
/>
|
||||
<QuickstartOverlay
|
||||
visible={quickstartVisible}
|
||||
layout={layout}
|
||||
showOnLoad={quickstartSettings?.showOnLoad !== false}
|
||||
onToggleShowOnLoad={setQuickstartShowOnLoad}
|
||||
onOpenHelp={openHelpFromQuickstart}
|
||||
onClose={closeQuickstart}
|
||||
/>
|
||||
</ControlSystemProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default DriverPageRoot;
|
||||
@@ -25,15 +25,6 @@ body {
|
||||
scrollbar-color: rgba(148, 163, 184, 0.55) transparent;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
@@ -59,40 +50,6 @@ body {
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.inter-instance-overlay-frame {
|
||||
/* The unscaled frame always stays inside the viewport on phones and on
|
||||
desktop browsers that do not apply the larger presentation below. */
|
||||
max-width: calc(100vw - 0.5rem);
|
||||
}
|
||||
|
||||
.inter-instance-overlay-body {
|
||||
max-height: 82vh;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.inter-instance-overlay-scale {
|
||||
/*
|
||||
`zoom` enlarges the complete interface—including typography, controls,
|
||||
spacing, and hit targets—while participating in layout. A transform
|
||||
would only enlarge the paint result and could overlap or clip sibling
|
||||
content because the browser would still reserve the original size.
|
||||
*/
|
||||
zoom: 1.5;
|
||||
}
|
||||
|
||||
.inter-instance-overlay-frame {
|
||||
/* Reserve the inverse width before the 1.5x zoom so the final rendered
|
||||
frame still fits inside the physical desktop viewport. */
|
||||
max-width: calc((100vw - 0.5rem) / 1.5);
|
||||
}
|
||||
|
||||
.inter-instance-overlay-body {
|
||||
/* 54.6667vh becomes approximately 82vh after the 1.5x desktop zoom. */
|
||||
max-height: 54.6667vh;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.panel {
|
||||
@apply bg-black text-white p-0 rounded-md;
|
||||
}
|
||||
@@ -125,166 +82,4 @@ body {
|
||||
@apply px-0.5 py-0.5 text-sm font-medium text-white transition-colors bg-rose-600 hover:bg-rose-500 rounded-md;
|
||||
}
|
||||
|
||||
.chat-composer {
|
||||
/* The chat bar lives in several panel widths, so container queries are more
|
||||
accurate than viewport media queries. The default narrow layout preserves
|
||||
a usable chat row first, then moves all TTS controls to a second row. */
|
||||
container-type: inline-size;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
gap: 0.125rem;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-composer-nickname {
|
||||
/* Keep nickname deliberately compact so the message input and Send button
|
||||
remain usable on mobile while still allowing longer typed nicknames. */
|
||||
flex: 0 0 5rem;
|
||||
min-width: 0;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.chat-composer-input {
|
||||
/* This input is the pressure valve for the first row. It can shrink to the
|
||||
remaining space, but the TTS controls no longer steal that row on mobile. */
|
||||
flex: 1 1 0%;
|
||||
min-width: 0;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.chat-composer-send {
|
||||
/* Send must stay with the message input in the narrow two-row layout. */
|
||||
flex: 0 0 auto;
|
||||
align-self: stretch;
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.chat-composer-tts {
|
||||
/* The full-width basis is what creates the second row when the composer is
|
||||
narrow; the controls themselves still remain one compact TTS strip. */
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
order: 4;
|
||||
}
|
||||
|
||||
@container (min-width: 44rem) {
|
||||
.chat-composer {
|
||||
/* Once there is enough room for a meaningful message field, all controls
|
||||
return to the single-row desktop layout. */
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.chat-composer-nickname {
|
||||
flex-basis: 7rem;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.chat-composer-input {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.chat-composer-tts {
|
||||
flex: 0 1 auto;
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.chat-composer-send {
|
||||
order: 4;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@keyframes batteryTickWarn {
|
||||
0%,
|
||||
49% {
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
background-color: rgba(239, 68, 68, 0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.battery-tick-warn {
|
||||
animation: batteryTickWarn 0.4s steps(2, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes batteryUrgentFlash {
|
||||
0%,
|
||||
49% {
|
||||
opacity: 1;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
.battery-urgent-flash {
|
||||
animation: batteryUrgentFlash 0.4s steps(2, end) infinite;
|
||||
}
|
||||
|
||||
.no-touch-select {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.mobile-touch-control {
|
||||
/*
|
||||
Mobile rover controls are press surfaces, not document text. These flags
|
||||
deliberately stack the browser-specific knobs because iOS Safari can still
|
||||
show callouts, selection handles, tap highlights, or delayed gesture behavior
|
||||
when only the standard property is present.
|
||||
*/
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.mobile-drag-control {
|
||||
/*
|
||||
Drag controls need the stricter touch-action value so the browser does not
|
||||
reinterpret a held thumb as page pan, pinch zoom, double-tap zoom, or text
|
||||
selection while the control is actively tracking pointer movement.
|
||||
*/
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.mobile-text-entry {
|
||||
/*
|
||||
iOS Safari zooms focused form fields whose computed text size is below
|
||||
16px. Keep HUD chat inputs at that threshold instead of fighting focus with
|
||||
JavaScript, because the input still needs normal editing and caret behavior.
|
||||
*/
|
||||
font-size: 16px;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.ios-safe-screen {
|
||||
/*
|
||||
viewport-fit=cover stops iOS from reserving automatic bars on both ends of
|
||||
a homescreen app. We then add back only the unsafe notch edges so controls
|
||||
do not sit under camera hardware, while the opposite edge stays flush.
|
||||
*/
|
||||
/* padding-top: env(safe-area-inset-top); */
|
||||
/* padding-left: env(safe-area-inset-left); */
|
||||
/* padding-right: env(safe-area-inset-right); */
|
||||
/* padding-bottom: 0; */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Desktop Driver Layout
|
||||
// Purpose: Owns the concrete two-pane desktop driver-page composition.
|
||||
// Scope: Places existing cards without abstracting or changing their presentation.
|
||||
import DriverVideo from '../../../components/DriverVideo/index.jsx';
|
||||
import PiHostStatsCard from '../../../components/PiHostStatsCard/index.jsx';
|
||||
import DriverAdCard from '../../../components/DriverAdCard/index.jsx';
|
||||
import GlobalObjectiveBanner from '../../../components/GlobalObjectiveBanner/index.jsx';
|
||||
import { themeGapClass } from '../../../themes/index.js';
|
||||
import DesktopRightPaneTabs from '../DesktopRightPaneTabs/index.jsx';
|
||||
import { useTelemetryVisualPolicy } from '../../../context/TelemetryContext.jsx';
|
||||
|
||||
export default function DesktopLayout() {
|
||||
useTelemetryVisualPolicy({ mobile: false });
|
||||
return (
|
||||
<div className={`flex h-screen ${themeGapClass} overflow-hidden`}>
|
||||
<div className={`flex min-w-0 flex-[1.22] flex-col ${themeGapClass} overflow-y-auto pr-0`}>
|
||||
<DriverVideo />
|
||||
<PiHostStatsCard />
|
||||
{/* The self-gating ad remains pinned to the bottom of the left pane. */}
|
||||
<DriverAdCard className="mt-auto" />
|
||||
</div>
|
||||
<div className={`flex min-w-0 flex-1 flex-col ${themeGapClass} overflow-y-auto`}>
|
||||
<GlobalObjectiveBanner layout="desktop" />
|
||||
<DesktopRightPaneTabs />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+32
-65
@@ -1,41 +1,34 @@
|
||||
// Right Pane Tabs
|
||||
// Purpose: Defines the Right Pane Tabs 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 RoomCameraPanel from '../RoomCameraPanel/index.jsx';
|
||||
import KinectPanel from '../KinectPanel/index.jsx';
|
||||
import BalanceBoardPanel from '../BalanceBoardPanel/index.jsx';
|
||||
import HomeAssistantControls from '../HomeAssistantControls/index.jsx';
|
||||
import SettingsPanel from '../SettingsPanel/index.jsx';
|
||||
import HelpPanel from '../HelpPanel/index.jsx';
|
||||
import ChatPanel from '../ChatPanel/index.jsx';
|
||||
import { LinkButtonsPanel } from '../UserListPanel/index.jsx';
|
||||
import ReplaySourcesPanel from '../ReplaySourcesPanel/index.jsx';
|
||||
import PtzQueueCard from '../PtzCamera/index.jsx';
|
||||
import Tabs, { Tab, TabList, TabPanel, TabPanels } from '../Tabs/index.jsx';
|
||||
import TopDownMap from '../TopDownMap/index.jsx';
|
||||
import DriveDockAction from '../DriveDockAction/index.jsx';
|
||||
import { useDriveDockState } from '../DriveDockAction/driveDockState.js';
|
||||
import { useControlActions, useControlSelector } from '../../controls/index.js';
|
||||
import RoverQueuesPanel from '../RoverQueuesPanel/index.jsx';
|
||||
import RawUserPilePanel from '../RawUserPilePanel/index.jsx';
|
||||
import { formatKeyLabel } from '../../controls/keymapUtils.js';
|
||||
import GPIOToggleControl from '../GPIOToggleControl/index.jsx';
|
||||
import HornControl from '../HornControl/index.jsx';
|
||||
import CameraTiltControl from '../CameraTiltControl/index.jsx';
|
||||
import VipPanel from '../VipPanel/index.jsx';
|
||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
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 FleetReportsCard from '../FleetReportsCard/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 RoomCameraPanel from '../../../components/RoomCameraPanel/index.jsx';
|
||||
import HomeAssistantControls from '../../../components/HomeAssistantControls/index.jsx';
|
||||
import ChatPanel from '../../../components/ChatPanel/index.jsx';
|
||||
import { LinkButtonsPanel } from '../../../components/UserListPanel/index.jsx';
|
||||
import ReplaySourcesPanel from '../../../components/ReplaySourcesPanel/index.jsx';
|
||||
import PtzQueueCard from '../../../components/PtzCamera/index.jsx';
|
||||
import Tabs, { Tab, TabList, TabPanel, TabPanels } from '../../../components/Tabs/index.jsx';
|
||||
import TopDownMap from '../../../components/TopDownMap/index.jsx';
|
||||
import DriveDockAction from '../../../components/DriveDockAction/index.jsx';
|
||||
import { useDriveDockState } from '../../../components/DriveDockAction/driveDockState.js';
|
||||
import { useControlActions, useControlSelector } from '../../../controls/index.js';
|
||||
import RoverQueuesPanel from '../../../components/RoverQueuesPanel/index.jsx';
|
||||
import RawUserPilePanel from '../../../components/RawUserPilePanel/index.jsx';
|
||||
import { formatKeyLabel } from '../../../controls/keymapUtils.js';
|
||||
import GPIOToggleControl from '../../../components/GPIOToggleControl/index.jsx';
|
||||
import HornControl from '../../../components/HornControl/index.jsx';
|
||||
import CameraTiltControl from '../../../components/CameraTiltControl/index.jsx';
|
||||
import { useSessionSelector } from '../../../context/SessionContext.jsx';
|
||||
import { useSettingsNamespace } from '../../../settings/index.js';
|
||||
import OverseerPreferencePanel from '../../../components/OverseerPreferencePanel/index.jsx';
|
||||
import CardFrame from '../../../components/CardFrame/index.jsx';
|
||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
|
||||
import { themeGapClass, themeStackClass } from '../../themes/index.js';
|
||||
import { useManualDockAssist } from '../../../features/manualDockAssist/useManualDockAssist.js';
|
||||
import { themeGapClass, themeStackClass } from '../../../themes/index.js';
|
||||
import ActivitiesTab from '../tabs/shared/ActivitiesTab/index.jsx';
|
||||
import VipTab from '../tabs/shared/VipTab/index.jsx';
|
||||
import HelpTab from '../tabs/shared/HelpTab/index.jsx';
|
||||
import SettingsTab from '../tabs/shared/SettingsTab/index.jsx';
|
||||
|
||||
const CHAT_DOCK_INITIAL_HEIGHT = 224;
|
||||
const CHAT_DOCK_MIN_HEIGHT = 144;
|
||||
@@ -217,7 +210,7 @@ function QueueReplayLinksRow() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
|
||||
export default function RightPaneTabs() {
|
||||
const [activeTab, setActiveTab] = useState('telemetry');
|
||||
const chatDockRef = useRef(null);
|
||||
const [chatDockHeight, setChatDockHeight] = useState(CHAT_DOCK_INITIAL_HEIGHT);
|
||||
@@ -420,36 +413,10 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
{/* activities tab */}
|
||||
<TabPanel id="activities">
|
||||
<div className={`flex flex-col ${themeGapClass}`}>
|
||||
<NeatoCard />
|
||||
<LiftCard />
|
||||
<BalanceBoardPanel />
|
||||
<BarcodeGamesPanel />
|
||||
<OdometerPanel />
|
||||
<ButtonBoxPanel />
|
||||
<KinectPanel />
|
||||
{/* The compact report is a terminal summary for Activities; the
|
||||
component itself disappears when the server feature is off. */}
|
||||
<FleetReportsCard />
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
{/* VIP tab */}
|
||||
<TabPanel id="vip" keepMounted>
|
||||
<VipPanel isActive={activeTab === 'vip'} layout={layout} />
|
||||
</TabPanel>
|
||||
|
||||
{/* help tab */}
|
||||
<TabPanel id="help">
|
||||
<HelpPanel layout={layout} onOpenOverlay={onOpenHelpOverlay} />
|
||||
</TabPanel>
|
||||
|
||||
{/* settings tab */}
|
||||
<TabPanel id="settings">
|
||||
<SettingsPanel />
|
||||
</TabPanel>
|
||||
<ActivitiesTab />
|
||||
<VipTab />
|
||||
<HelpTab />
|
||||
<SettingsTab />
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</section>
|
||||
@@ -0,0 +1,38 @@
|
||||
// Driver Layout Context
|
||||
// Purpose: Exposes the existing global Help-overlay action to deeply placed tab content.
|
||||
// Scope: Avoids threading a layout callback through otherwise concrete composition files.
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
const DriverLayoutContext = createContext(null);
|
||||
|
||||
export function DriverLayoutProvider({ layout, openHelp, children }) {
|
||||
/*
|
||||
Layout mode is classified once at the driver-route boundary. Concrete
|
||||
layouts and tabs read it here instead of each installing a resize listener
|
||||
or receiving chains of layout/configuration props.
|
||||
*/
|
||||
return <DriverLayoutContext.Provider value={{ layout, openHelp }}>{children}</DriverLayoutContext.Provider>;
|
||||
}
|
||||
|
||||
/*
|
||||
This hook intentionally lives beside its tiny route-local provider so the
|
||||
driver layout contract remains discoverable in one file. It is not a React
|
||||
component export, so the Fast Refresh rule needs this narrow exception.
|
||||
*/
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useOpenDriverHelp() {
|
||||
const context = useContext(DriverLayoutContext);
|
||||
if (!context) {
|
||||
throw new Error('useOpenDriverHelp must be used within DriverLayoutProvider.');
|
||||
}
|
||||
return context.openHelp;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useDriverLayout() {
|
||||
const context = useContext(DriverLayoutContext);
|
||||
if (!context) {
|
||||
throw new Error('useDriverLayout must be used within DriverLayoutProvider.');
|
||||
}
|
||||
return context.layout;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Driver Layout Root
|
||||
// Purpose: Selects and frames the concrete desktop or mobile driver composition.
|
||||
// Scope: Owns layout-only mobile framing so App.jsx does not place page cards.
|
||||
import DesktopLayout from './DesktopLayout/index.jsx';
|
||||
import MobilePortraitLayout from './MobilePortraitLayout/index.jsx';
|
||||
import MobileLandscapeLayout from './MobileLandscapeLayout/index.jsx';
|
||||
import { useDriverLayout } from './DriverLayoutContext.jsx';
|
||||
|
||||
export default function DriverLayoutRoot() {
|
||||
const layout = useDriverLayout();
|
||||
|
||||
if (layout === 'desktop') {
|
||||
return <DesktopLayout />;
|
||||
}
|
||||
|
||||
return layout === 'mobile-landscape' ? <MobileLandscapeLayout /> : <MobilePortraitLayout />;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Mobile Landscape Driver Layout
|
||||
// Purpose: Owns the concrete landscape video, controls, and secondary-card placement.
|
||||
import AuxColumn from '../../../components/MobileControls/AuxColumn.jsx';
|
||||
import MovementColumn from '../../../components/MobileControls/MovementColumn.jsx';
|
||||
import DriverVideo from '../../../components/DriverVideo/index.jsx';
|
||||
import { useSettingsNamespace } from '../../../settings/index.js';
|
||||
import { themeGapClass } from '../../../themes/index.js';
|
||||
import MobileLayoutFrame from '../MobileLayoutFrame/index.jsx';
|
||||
import MobileSecondaryContent from '../MobileSecondaryContent/index.jsx';
|
||||
|
||||
export default function MobileLandscapeLayout() {
|
||||
const { value: pageSettings } = useSettingsNamespace('page', { swapMobileControlColumns: false });
|
||||
const swap = Boolean(pageSettings?.swapMobileControlColumns);
|
||||
const columnClass = 'self-start h-[min(100svh,32rem)]';
|
||||
const firstColumn = swap
|
||||
? <MovementColumn layout="landscape" className={columnClass} />
|
||||
: <AuxColumn layout="landscape" className={columnClass} />;
|
||||
const secondColumn = swap
|
||||
? <AuxColumn layout="landscape" className={columnClass} />
|
||||
: <MovementColumn layout="landscape" className={columnClass} />;
|
||||
|
||||
return (
|
||||
<MobileLayoutFrame>
|
||||
<div className={`flex flex-col ${themeGapClass}`}>
|
||||
{/* The center column deliberately contains only the driver video. */}
|
||||
<section className={`grid grid-cols-[minmax(0,0.7fr)_minmax(0,2.1fr)_minmax(0,0.7fr)] ${themeGapClass}`}>
|
||||
{firstColumn}
|
||||
<div className="min-w-0 self-start">
|
||||
<DriverVideo layoutFormat="mobile-landscape" />
|
||||
</div>
|
||||
{secondColumn}
|
||||
</section>
|
||||
<MobileSecondaryContent />
|
||||
</div>
|
||||
</MobileLayoutFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Mobile Driver Layout Frame
|
||||
// Purpose: Owns behavior and framing shared exclusively by portrait and landscape driver layouts.
|
||||
// Scope: Keeps fullscreen, mobile telemetry policy, objective banner, and snap framing out of the shared driver root.
|
||||
import { useCallback } from 'react';
|
||||
import FloatingFullscreenButton from '../../../components/FloatingFullscreenButton/index.jsx';
|
||||
import FullscreenPrompt from '../../../components/FullscreenPrompt/index.jsx';
|
||||
import GlobalObjectiveBanner from '../../../components/GlobalObjectiveBanner/index.jsx';
|
||||
import { useTelemetryVisualPolicy } from '../../../context/TelemetryContext.jsx';
|
||||
import { useFullscreenPrompt } from '../../../hooks/useFullscreenPrompt.js';
|
||||
import { useSettingsNamespace } from '../../../settings/index.js';
|
||||
import { themeGapClass } from '../../../themes/index.js';
|
||||
import { useDriverLayout } from '../DriverLayoutContext.jsx';
|
||||
import './styles.css';
|
||||
|
||||
export default function MobileLayoutFrame({ children }) {
|
||||
const layout = useDriverLayout();
|
||||
useTelemetryVisualPolicy({ mobile: true });
|
||||
const fullscreen = useFullscreenPrompt(layout);
|
||||
const { value: pageSettings } = useSettingsNamespace('page', { swapMobileControlColumns: false });
|
||||
const buttonSide = pageSettings?.swapMobileControlColumns ? 'left' : 'right';
|
||||
const showButton = fullscreen.isIOS || fullscreen.nativeSupported;
|
||||
|
||||
const handleFloatingFullscreen = useCallback(async () => {
|
||||
if (fullscreen.isIOS) {
|
||||
fullscreen.showPrompt();
|
||||
return;
|
||||
}
|
||||
const entered = await fullscreen.enterFullscreen();
|
||||
if (!entered) fullscreen.showPrompt();
|
||||
}, [fullscreen]);
|
||||
|
||||
return (
|
||||
<div className={`driver-mobile-layout min-h-screen flex flex-col ${themeGapClass}`}>
|
||||
{/* The absolute marker creates the literal page-top snap without taking flex space. */}
|
||||
<div className="mobile-top-snap" aria-hidden="true" />
|
||||
<GlobalObjectiveBanner layout={layout} />
|
||||
{children}
|
||||
<FullscreenPrompt
|
||||
visible={fullscreen.visible}
|
||||
mode={fullscreen.mode}
|
||||
onEnterFullscreen={fullscreen.enterFullscreen}
|
||||
onDismiss={fullscreen.dismiss}
|
||||
/>
|
||||
{showButton ? <FloatingFullscreenButton side={buttonSide} onClick={handleFloatingFullscreen} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
@media (max-width: 1023px) {
|
||||
html:has(.driver-mobile-layout) {
|
||||
/* These three driver-only positions deliberately use strict page snapping. */
|
||||
scroll-snap-type: y mandatory;
|
||||
}
|
||||
|
||||
.driver-mobile-layout .mobile-top-snap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
pointer-events: none;
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: always;
|
||||
}
|
||||
|
||||
.driver-mobile-layout .mobile-content-snap,
|
||||
.driver-mobile-layout .mobile-tabs-snap {
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: always;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Mobile Portrait Driver Layout
|
||||
// Purpose: Owns the concrete portrait video, controls, and secondary-card placement.
|
||||
import AuxColumn from '../../../components/MobileControls/AuxColumn.jsx';
|
||||
import MovementColumn from '../../../components/MobileControls/MovementColumn.jsx';
|
||||
import DriverVideo from '../../../components/DriverVideo/index.jsx';
|
||||
import { useSettingsNamespace } from '../../../settings/index.js';
|
||||
import { themeGapClass } from '../../../themes/index.js';
|
||||
import MobileLayoutFrame from '../MobileLayoutFrame/index.jsx';
|
||||
import MobileSecondaryContent from '../MobileSecondaryContent/index.jsx';
|
||||
|
||||
export default function MobilePortraitLayout() {
|
||||
const { value: pageSettings } = useSettingsNamespace('page', { swapMobileControlColumns: false });
|
||||
const swap = Boolean(pageSettings?.swapMobileControlColumns);
|
||||
const columnHeight = 'h-[min(60svh,24rem)]';
|
||||
const firstColumn = swap
|
||||
? <MovementColumn layout="portrait" className={columnHeight} />
|
||||
: <AuxColumn layout="portrait" className={columnHeight} />;
|
||||
const secondColumn = swap
|
||||
? <AuxColumn layout="portrait" className={columnHeight} />
|
||||
: <MovementColumn layout="portrait" className={columnHeight} />;
|
||||
|
||||
return (
|
||||
<MobileLayoutFrame>
|
||||
<div className={`flex flex-col ${themeGapClass}`}>
|
||||
<DriverVideo layoutFormat="mobile-portrait" />
|
||||
{/* Preserve the existing two-column portrait control surface exactly. */}
|
||||
<section className="mobile-touch-control text-white">
|
||||
<div className="mobile-touch-control grid grid-cols-2 gap-0.5 items-stretch">
|
||||
{firstColumn}
|
||||
{secondColumn}
|
||||
</div>
|
||||
</section>
|
||||
<MobileSecondaryContent />
|
||||
</div>
|
||||
</MobileLayoutFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Mobile Driver Secondary Content
|
||||
// Purpose: Owns the identical replay, queue, tabs, and terminal-ad composition used by both mobile layouts.
|
||||
import DriverAdCard from '../../../components/DriverAdCard/index.jsx';
|
||||
import PtzQueueCard from '../../../components/PtzCamera/index.jsx';
|
||||
import ReplaySourcesPanel from '../../../components/ReplaySourcesPanel/index.jsx';
|
||||
import RoverQueuesPanel from '../../../components/RoverQueuesPanel/index.jsx';
|
||||
import { themeGapClass } from '../../../themes/index.js';
|
||||
import { useDriverLayout } from '../DriverLayoutContext.jsx';
|
||||
import MobileTabs from '../MobileTabs/index.jsx';
|
||||
|
||||
export default function MobileSecondaryContent() {
|
||||
const layout = useDriverLayout();
|
||||
const portrait = layout === 'mobile-portrait';
|
||||
const replayPanelId = portrait
|
||||
? 'replay-sources-mobile-portrait'
|
||||
: 'replay-sources-mobile-landscape';
|
||||
|
||||
return (
|
||||
<div className={`mobile-content-snap flex flex-col ${themeGapClass} ${portrait ? '' : 'pb-0'}`}>
|
||||
<div className={`grid ${themeGapClass} grid-cols-2`}>
|
||||
<div className="space-y-0.5">
|
||||
<ReplaySourcesPanel panelId={replayPanelId} />
|
||||
<PtzQueueCard layout={layout} />
|
||||
</div>
|
||||
<RoverQueuesPanel />
|
||||
</div>
|
||||
<MobileTabs />
|
||||
<DriverAdCard />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Mobile Driver Tabs
|
||||
// Purpose: Owns the shared mobile tab selector, state, and per-tab modules.
|
||||
import { useCallback, useState } from 'react';
|
||||
import Tabs, { Tab, TabList, TabPanels } from '../../../components/Tabs/index.jsx';
|
||||
import { useSessionSelector } from '../../../context/SessionContext.jsx';
|
||||
import { useControlSelector } from '../../../controls/index.js';
|
||||
import { useSettingsNamespace } from '../../../settings/index.js';
|
||||
import MobileChatTab from '../tabs/mobile/ChatTab/index.jsx';
|
||||
import RoomControlsTab from '../tabs/mobile/RoomControlsTab/index.jsx';
|
||||
import ActivitiesTab from '../tabs/shared/ActivitiesTab/index.jsx';
|
||||
import VipTab from '../tabs/shared/VipTab/index.jsx';
|
||||
import HelpTab from '../tabs/shared/HelpTab/index.jsx';
|
||||
import SettingsTab from '../tabs/shared/SettingsTab/index.jsx';
|
||||
|
||||
export default function MobileTabs() {
|
||||
const [activeTab, setActiveTab] = useState('chat');
|
||||
const isVerified = useSessionSelector((state) => Boolean(state.session?.isVerified));
|
||||
const ownRoverId = useSessionSelector((state) => String(state.session?.assignment?.roverId || '').trim());
|
||||
const ownAudioForward = useSessionSelector((state) => ownRoverId ? state.session?.audioForward?.[ownRoverId] || null : null);
|
||||
const pttActive = useControlSelector((control) => Boolean(control.state.mic?.pttActive));
|
||||
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
|
||||
const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live';
|
||||
const vipMicActive = Boolean(ownRoverId && isVerified && (pttMode === 'clip' ? pttActive : (vipAudio?.openMicEnabled || pttActive)));
|
||||
const vipClipPlaying = Boolean(ownRoverId && isVerified && pttMode === 'clip' && ownAudioForward?.source === 'upload' && ownAudioForward?.state === 'playing');
|
||||
const handleTabChange = useCallback((tab) => setActiveTab(tab), []);
|
||||
|
||||
return (
|
||||
<section className="mobile-tabs-snap text-base">
|
||||
<Tabs defaultTab="chat" currentTab={activeTab} onTabChange={handleTabChange}>
|
||||
<TabList>
|
||||
<Tab id="chat">Chat</Tab>
|
||||
<Tab id="activities">Activities</Tab>
|
||||
<Tab id="vip" highlight={vipClipPlaying ? 'green' : vipMicActive ? 'pink' : 'none'}>
|
||||
<span className="inline-flex items-center gap-0.5">
|
||||
<span>VIP</span>
|
||||
<span className={`inline-block h-1.5 w-1.5 rounded-full ${isVerified ? 'bg-emerald-400' : 'bg-amber-400'}`} aria-hidden="true" title={isVerified ? 'Verified' : 'Not verified'} />
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab id="roomcontrols">Room Controls</Tab>
|
||||
<Tab id="help">Help</Tab>
|
||||
<Tab id="settings">Settings</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<MobileChatTab />
|
||||
<ActivitiesTab />
|
||||
<VipTab />
|
||||
<RoomControlsTab />
|
||||
<HelpTab />
|
||||
<SettingsTab />
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Mobile Chat Tab
|
||||
// Purpose: Owns the mobile chat and compact supporting-card composition.
|
||||
import ChatPanel from '../../../../../components/ChatPanel/index.jsx';
|
||||
import SocialButtonsGrid from '../../../../../components/SocialButtonsGrid/index.jsx';
|
||||
import OverseerPreferencePanel from '../../../../../components/OverseerPreferencePanel/index.jsx';
|
||||
import RawUserPilePanel from '../../../../../components/RawUserPilePanel/index.jsx';
|
||||
import { TabPanel } from '../../../../../components/Tabs/index.jsx';
|
||||
import { useSessionSelector } from '../../../../../context/SessionContext.jsx';
|
||||
import { isFeatureEnabled } from '../../../../../lib/features.js';
|
||||
import { themeGapClass, themeStackClass } from '../../../../../themes/index.js';
|
||||
|
||||
export default function MobileChatTab() {
|
||||
const showOverseerPreferencePanel = useSessionSelector((state) => Boolean(state.session?.overseerVote?.votingEnabled));
|
||||
const showSocialButtons = useSessionSelector((state) => {
|
||||
/* Match the card's own gate so its absence cannot reserve an empty column. */
|
||||
const socials = Array.isArray(state.session?.socials) ? state.session.socials : [];
|
||||
return isFeatureEnabled(state, 'socials') && socials.length > 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<TabPanel id="chat">
|
||||
<div className={themeStackClass}>
|
||||
<ChatPanel nicknameLayout="stacked" />
|
||||
<div className={`grid items-start ${themeGapClass} ${showSocialButtons ? 'grid-cols-[minmax(0,1fr)_minmax(0,1fr)]' : 'grid-cols-[minmax(0,1fr)]'}`}>
|
||||
{showSocialButtons ? <SocialButtonsGrid /> : null}
|
||||
<div className={`flex min-w-0 flex-col ${themeGapClass} ${showSocialButtons ? '' : 'max-w-sm'}`}>
|
||||
{showOverseerPreferencePanel ? <OverseerPreferencePanel /> : null}
|
||||
<RawUserPilePanel hideNicknameForm compact />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Mobile Room Controls Tab
|
||||
// Purpose: Owns the concrete mobile room-controls card order.
|
||||
import { TabPanel } from '../../../../../components/Tabs/index.jsx';
|
||||
import HomeAssistantControls from '../../../../../components/HomeAssistantControls/index.jsx';
|
||||
import RoomCameraPanel from '../../../../../components/RoomCameraPanel/index.jsx';
|
||||
import { themeStackClass } from '../../../../../themes/index.js';
|
||||
import { useDriverLayout } from '../../../DriverLayoutContext.jsx';
|
||||
|
||||
export default function RoomControlsTab() {
|
||||
const layout = useDriverLayout();
|
||||
const panelId = layout === 'mobile-landscape' ? 'mobile-landscape-room' : 'mobile-portrait-room';
|
||||
return (
|
||||
<TabPanel id="roomcontrols">
|
||||
<div className={themeStackClass}>
|
||||
<HomeAssistantControls />
|
||||
<RoomCameraPanel panelId={panelId} />
|
||||
</div>
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Driver Activities Tab
|
||||
// Purpose: Owns the shared desktop/mobile ordering of activity cards.
|
||||
import { TabPanel } from '../../../../../components/Tabs/index.jsx';
|
||||
import NeatoCard from '../../../../../components/NeatoCard/index.jsx';
|
||||
import LiftCard from '../../../../../components/LiftCard/index.jsx';
|
||||
import BalanceBoardPanel from '../../../../../components/BalanceBoardPanel/index.jsx';
|
||||
import BarcodeGamesPanel from '../../../../../components/BarcodeGamesPanel/index.jsx';
|
||||
import OdometerPanel from '../../../../../components/OdometerPanel/index.jsx';
|
||||
import ButtonBoxPanel from '../../../../../components/ButtonBoxPanel/index.jsx';
|
||||
import KinectPanel from '../../../../../components/KinectPanel/index.jsx';
|
||||
import FleetReportsCard from '../../../../../components/FleetReportsCard/index.jsx';
|
||||
import { themeGapClass } from '../../../../../themes/index.js';
|
||||
|
||||
export default function ActivitiesTab() {
|
||||
return (
|
||||
<TabPanel id="activities">
|
||||
<div className={`flex flex-col ${themeGapClass}`}>
|
||||
<NeatoCard />
|
||||
<LiftCard />
|
||||
<BalanceBoardPanel />
|
||||
<BarcodeGamesPanel />
|
||||
<OdometerPanel />
|
||||
<ButtonBoxPanel />
|
||||
<KinectPanel />
|
||||
{/* Fleet reports retains its existing terminal position and self-gate. */}
|
||||
<FleetReportsCard />
|
||||
</div>
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Driver Help Tab
|
||||
// Purpose: Owns the shared driver help panel placement.
|
||||
import { TabPanel } from '../../../../../components/Tabs/index.jsx';
|
||||
import HelpPanel from '../../../../../components/HelpPanel/index.jsx';
|
||||
import { useOpenDriverHelp } from '../../../DriverLayoutContext.jsx';
|
||||
import { useDriverLayout } from '../../../DriverLayoutContext.jsx';
|
||||
|
||||
export default function HelpTab() {
|
||||
const openHelp = useOpenDriverHelp();
|
||||
const layout = useDriverLayout();
|
||||
return (
|
||||
<TabPanel id="help">
|
||||
<HelpPanel layout={layout} onOpenOverlay={openHelp} />
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Driver Settings Tab
|
||||
// Purpose: Owns the shared driver settings panel placement.
|
||||
import { TabPanel } from '../../../../../components/Tabs/index.jsx';
|
||||
import SettingsPanel from '../../../../../components/SettingsPanel/index.jsx';
|
||||
import { themeStackClass } from '../../../../../themes/index.js';
|
||||
import { useDriverLayout } from '../../../DriverLayoutContext.jsx';
|
||||
|
||||
export default function SettingsTab() {
|
||||
const layout = useDriverLayout();
|
||||
const wrapped = layout !== 'desktop';
|
||||
return (
|
||||
<TabPanel id="settings">
|
||||
{wrapped ? <div className={themeStackClass}><SettingsPanel /></div> : <SettingsPanel />}
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Driver VIP Tab
|
||||
// Purpose: Owns the shared keep-mounted VIP panel lifecycle.
|
||||
import { TabPanel, useTabIsActive } from '../../../../../components/Tabs/index.jsx';
|
||||
import VipPanel from '../../../../../components/VipPanel/index.jsx';
|
||||
import { useDriverLayout } from '../../../DriverLayoutContext.jsx';
|
||||
|
||||
export default function VipTab() {
|
||||
const layout = useDriverLayout();
|
||||
const isActive = useTabIsActive('vip');
|
||||
return (
|
||||
<TabPanel id="vip" keepMounted>
|
||||
<VipPanel isActive={isActive} layout={layout} />
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -7,7 +7,7 @@ import './index.css'
|
||||
// Theme artwork is a separate style concern from global component utilities. Loading its dedicated
|
||||
// entrypoint here keeps every route consistent without returning theme definitions to index.css.
|
||||
import './themes/styles/index.css'
|
||||
import App from './App.jsx'
|
||||
import DriverPageRoot from './driver/DriverPageRoot.jsx'
|
||||
import { SocketProvider } from './context/SocketContext.jsx'
|
||||
import { SessionProvider } from './context/SessionContext.jsx'
|
||||
import { TelemetryProvider } from './context/TelemetryContext.jsx'
|
||||
@@ -38,7 +38,7 @@ createRoot(document.getElementById('root')).render(
|
||||
<BrowserRouter>
|
||||
<AnalyticsReporter />
|
||||
<Routes>
|
||||
<Route path="/" element={<App />} />
|
||||
<Route path="/" element={<DriverPageRoot />} />
|
||||
<Route path="/spectate" element={<SpectatorApp />} />
|
||||
<Route path="/mini" element={<MiniSummaryApp />} />
|
||||
<Route path="/display" element={<ServerDisplayApp />} />
|
||||
|
||||
Reference in New Issue
Block a user