mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
new help system!!
This commit is contained in:
+73
-26
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import TelemetryPanel from './components/TelemetryPanel.jsx';
|
||||
import DrivePanel from './components/DrivePanel.jsx';
|
||||
import AlertFeed from './components/AlertFeed.jsx';
|
||||
@@ -22,6 +22,8 @@ import UserListPanel from './components/UserListPanel.jsx';
|
||||
import ChatPanel from './components/ChatPanel.jsx';
|
||||
import FullscreenPrompt from './components/FullscreenPrompt.jsx';
|
||||
import { useFullscreenPrompt } from './hooks/useFullscreenPrompt.js';
|
||||
import { useSettingsNamespace } from './settings/index.js';
|
||||
import HelpOverlay from './components/HelpOverlay.jsx';
|
||||
|
||||
function useLayoutMode() {
|
||||
const [mode, setMode] = useState(() => {
|
||||
@@ -53,7 +55,7 @@ function useLayoutMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
function DesktopLayout({ layout }) {
|
||||
function DesktopLayout({ layout, onOpenHelpOverlay }) {
|
||||
return (
|
||||
<div className="flex h-full gap-0.5 overflow-hidden">
|
||||
<div className="flex min-w-0 flex-[1.8] flex-col gap-0.5 overflow-y-auto pr-0.5">
|
||||
@@ -69,7 +71,7 @@ function DesktopLayout({ layout }) {
|
||||
<LogPanel />
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5 overflow-y-auto">
|
||||
<RightPaneTabs layout={layout} />
|
||||
<RightPaneTabs layout={layout} onOpenHelpOverlay={onOpenHelpOverlay} />
|
||||
{/* <SessionSnapshot /> */}
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,36 +130,81 @@ function MobileLandscapeLayout() {
|
||||
function App() {
|
||||
const layout = useLayoutMode();
|
||||
const isDesktop = layout === 'desktop';
|
||||
const { visible: fullscreenVisible, mode: fullscreenMode, enterFullscreen, dismiss } = useFullscreenPrompt(layout);
|
||||
const renderedLayout =
|
||||
isDesktop
|
||||
? <DesktopLayout layout={layout} />
|
||||
: layout === 'mobile-landscape'
|
||||
? <MobileLandscapeLayout />
|
||||
: <MobilePortraitLayout />;
|
||||
const fullscreen = useFullscreenPrompt(layout);
|
||||
|
||||
return (
|
||||
<div className={`bg-black text-slate-100 ${isDesktop ? 'h-screen overflow-hidden' : 'min-h-screen'}`}>
|
||||
<SettingsProvider>
|
||||
<ControlSystemProvider>
|
||||
<KeyboardInputManager />
|
||||
<GamepadInputManager />
|
||||
<main className={`flex w-full flex-col gap-0.5 text-base ${isDesktop ? 'h-full overflow-hidden' : ''}`}>
|
||||
{renderedLayout}
|
||||
</main>
|
||||
<AlertFeed />
|
||||
<TurnAlertListener />
|
||||
<ModeGateOverlay />
|
||||
<FullscreenPrompt
|
||||
visible={fullscreenVisible}
|
||||
mode={fullscreenMode}
|
||||
onEnterFullscreen={enterFullscreen}
|
||||
onDismiss={dismiss}
|
||||
/>
|
||||
</ControlSystemProvider>
|
||||
<AppWithProviders layout={layout} isDesktop={isDesktop} fullscreen={fullscreen} />
|
||||
</SettingsProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AppWithProviders({ layout, isDesktop, fullscreen }) {
|
||||
const {
|
||||
visible: fullscreenVisible,
|
||||
mode: fullscreenMode,
|
||||
enterFullscreen,
|
||||
dismiss,
|
||||
} = fullscreen;
|
||||
|
||||
const {
|
||||
value: helpSettings,
|
||||
save: saveHelpSettings,
|
||||
} = useSettingsNamespace('help', { showOnLoad: true });
|
||||
const [helpVisible, setHelpVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (helpSettings?.showOnLoad !== false) {
|
||||
setHelpVisible(true);
|
||||
}
|
||||
}, [helpSettings?.showOnLoad]);
|
||||
|
||||
const openHelp = useCallback(() => setHelpVisible(true), []);
|
||||
const closeHelp = useCallback(() => setHelpVisible(false), []);
|
||||
const setShowOnLoad = useCallback(
|
||||
(enabled) => {
|
||||
saveHelpSettings((current) => ({ ...(current ?? {}), showOnLoad: Boolean(enabled) }));
|
||||
},
|
||||
[saveHelpSettings],
|
||||
);
|
||||
|
||||
const renderedLayout = useMemo(
|
||||
() =>
|
||||
isDesktop
|
||||
? <DesktopLayout layout={layout} onOpenHelpOverlay={openHelp} />
|
||||
: layout === 'mobile-landscape'
|
||||
? <MobileLandscapeLayout />
|
||||
: <MobilePortraitLayout />,
|
||||
[isDesktop, layout, openHelp],
|
||||
);
|
||||
|
||||
return (
|
||||
<ControlSystemProvider>
|
||||
<KeyboardInputManager />
|
||||
<GamepadInputManager />
|
||||
<main className={`flex w-full flex-col gap-0.5 text-base ${isDesktop ? 'h-full overflow-hidden' : ''}`}>
|
||||
{renderedLayout}
|
||||
</main>
|
||||
<AlertFeed />
|
||||
<TurnAlertListener />
|
||||
<ModeGateOverlay />
|
||||
<HelpOverlay
|
||||
visible={helpVisible}
|
||||
layout={layout}
|
||||
onClose={closeHelp}
|
||||
showOnLoad={helpSettings?.showOnLoad !== false}
|
||||
onToggleShowOnLoad={setShowOnLoad}
|
||||
/>
|
||||
<FullscreenPrompt
|
||||
visible={fullscreenVisible}
|
||||
mode={fullscreenMode}
|
||||
onEnterFullscreen={enterFullscreen}
|
||||
onDismiss={dismiss}
|
||||
/>
|
||||
</ControlSystemProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function DiscordInviteButton({text = "Join our Discord!"}) {
|
||||
href={discordInvite}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"npm
|
||||
className="inline-flex items-center px-3 py-2 text-white rainbow-animate-bg transition justify-center"
|
||||
className="inline-flex items-center w-full h-full text-white rainbow-animate-bg transition justify-center"
|
||||
// animated rainbow backgound
|
||||
// className="inline-flex items-center px-3 py-2 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white rounded hover:from-indigo-600 hover:via-purple-600 hover:to-pink-600 transition"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import { useMemo } from 'react';
|
||||
import { formatKeyLabel } from '../controls/keymapUtils.js';
|
||||
import { getHelpContent } from '../help/content.js';
|
||||
|
||||
function KeyPill({ actionId, keymap }) {
|
||||
const value = keymap?.[actionId]?.[0] ?? '';
|
||||
return (
|
||||
<span className="rounded border border-slate-600 bg-slate-900/40 px-1 text-[0.7rem] text-slate-200">
|
||||
{formatKeyLabel(value)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSegments(segments, keymap) {
|
||||
return segments.map((segment, idx) => {
|
||||
if (typeof segment === 'string') {
|
||||
return <span key={`text-${idx}`}>{segment}</span>;
|
||||
}
|
||||
if (segment?.action) {
|
||||
return <KeyPill key={`pill-${segment.action}-${idx}`} actionId={segment.action} keymap={keymap} />;
|
||||
}
|
||||
if (segment?.text) {
|
||||
return <span key={`span-${idx}`}>{segment.text}</span>;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
function renderLine(line, keymap, idx) {
|
||||
if (typeof line === 'string') return line;
|
||||
if (Array.isArray(line?.segments)) return renderSegments(line.segments, keymap);
|
||||
if (line && typeof line === 'object') {
|
||||
// Fallback: render any stray object as text if possible
|
||||
if (typeof line.text === 'string') return line.text;
|
||||
if (Array.isArray(line)) return renderSegments(line, keymap);
|
||||
}
|
||||
return String(line ?? '');
|
||||
}
|
||||
|
||||
function Hero({ hero, keymap }) {
|
||||
if (!hero) return null;
|
||||
return (
|
||||
<div className="surface space-y-0.25 px-0.5 py-0.5">
|
||||
<div className="flex flex-wrap items-center justify-between gap-0.5">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">{hero.title}</p>
|
||||
{hero.subtitle && <p className="text-xs text-slate-300">{hero.subtitle}</p>}
|
||||
</div>
|
||||
{hero.chips && (
|
||||
<div className="flex flex-wrap gap-0.25 text-[0.7rem] text-slate-200">
|
||||
{hero.chips.map((chip) => (
|
||||
<span key={chip} className="rounded border border-slate-700 px-1 py-[2px]">
|
||||
{chip}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{hero.bullets && (
|
||||
<ul className="space-y-0.25 text-[0.85rem] text-slate-200">
|
||||
{hero.bullets.map((line, idx) => {
|
||||
const key = Array.isArray(line?.segments) ? `hero-${idx}` : `hero-${idx}`;
|
||||
return (
|
||||
<li key={key} className="surface-muted px-0.5 py-0.25">
|
||||
{renderLine(line, keymap, idx)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ListBlock({ block, keymap }) {
|
||||
return (
|
||||
<div className="space-y-0.25">
|
||||
<p className="text-xs font-semibold text-slate-200">{block.title}</p>
|
||||
<ul className="space-y-0.25 text-[0.8rem] text-slate-300">
|
||||
{block.items.map((item, idx) => {
|
||||
const key = `item-${idx}`;
|
||||
return (
|
||||
<li key={key} className="surface-muted flex flex-wrap items-center gap-0.25 px-0.5 py-0.25">
|
||||
{renderLine(item, keymap, idx)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CalloutBlock({ block }) {
|
||||
const toneClass = block.tone === 'info' ? 'border-cyan-500/40' : 'border-slate-700';
|
||||
return (
|
||||
<div className={`surface space-y-0.25 border ${toneClass} px-0.5 py-0.5`}>
|
||||
<p className="text-xs font-semibold text-slate-100">{block.title}</p>
|
||||
{block.body && (
|
||||
<ul className="space-y-0.25 text-[0.8rem] text-slate-300">
|
||||
{block.body.map((line, idx) => (
|
||||
<li key={`callout-${idx}`} className="surface-muted px-0.5 py-0.25">
|
||||
{renderLine(line, {})}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function KeyboardGroup({ group, keymap }) {
|
||||
return (
|
||||
<div className="space-y-0.25 surface">
|
||||
<p className="px-0.5 py-0.25 text-[0.75rem] font-semibold text-slate-200">{group.title}</p>
|
||||
<div className="space-y-0.25 px-0.5 pb-0.25">
|
||||
{group.items.map((item) => (
|
||||
<div key={item.action} className="surface-muted flex items-center justify-between gap-0.5 px-0.5 py-0.25 text-[0.8rem]">
|
||||
<span className="text-slate-200">{item.label}</span>
|
||||
<KeyPill actionId={item.action} keymap={keymap} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function KeyboardBlock({ block, keymap }) {
|
||||
if (!block) return null;
|
||||
return (
|
||||
<div className="space-y-0.25">
|
||||
<div className="flex items-center justify-between text-xs text-slate-200">
|
||||
<span className="font-semibold">{block.title}</span>
|
||||
{block.footnote && <span className="text-[0.7rem] text-slate-400">{block.footnote}</span>}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-0.5">
|
||||
{block.groups?.map((group) => (
|
||||
<KeyboardGroup key={group.id} group={group} keymap={keymap} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GamepadBlock({ block }) {
|
||||
if (!block) return null;
|
||||
return (
|
||||
<div className="space-y-0.25">
|
||||
<p className="text-xs font-semibold text-slate-200">{block.title}</p>
|
||||
<ul className="space-y-0.25 text-[0.8rem] text-slate-300">
|
||||
{block.items?.map((line, idx) => (
|
||||
<li key={`gamepad-${idx}`} className="surface-muted px-0.5 py-0.25">
|
||||
{renderLine(line, {})}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BlockRenderer({ block, keymap }) {
|
||||
if (!block) return null;
|
||||
switch (block.type) {
|
||||
case 'list':
|
||||
return <ListBlock block={block} keymap={keymap} />;
|
||||
case 'callout':
|
||||
return <CalloutBlock block={block} />;
|
||||
case 'keyboard':
|
||||
return <KeyboardBlock block={block} keymap={keymap} />;
|
||||
case 'gamepad':
|
||||
return <GamepadBlock block={block} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function HelpContentView({ layout, keymap }) {
|
||||
const content = useMemo(() => getHelpContent(layout), [layout]);
|
||||
const bindings = keymap || {};
|
||||
const mainBlocks = content.main || [];
|
||||
const asideBlocks = content.aside || [];
|
||||
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
<Hero hero={content.hero} keymap={bindings} />
|
||||
<div
|
||||
className="grid gap-0.5"
|
||||
style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))' }}
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
{mainBlocks.map((block, idx) => (
|
||||
<BlockRenderer key={block.title || idx} block={block} keymap={bindings} />
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{asideBlocks.map((block, idx) => (
|
||||
<BlockRenderer key={block.title || idx} block={block} keymap={bindings} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HelpContentView;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useControlSystem } from '../controls/index.js';
|
||||
import HelpContentView from './HelpContentView.jsx';
|
||||
|
||||
export default function HelpOverlay({ visible, layout, onClose, showOnLoad, onToggleShowOnLoad }) {
|
||||
if (!visible) return null;
|
||||
|
||||
const { state } = useControlSystem();
|
||||
|
||||
const handleCheckbox = (event) => {
|
||||
const keepShowing = !event.target.checked;
|
||||
onToggleShowOnLoad?.(keepShowing);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-30 flex items-center justify-center bg-black/80 px-0.5 py-0.5">
|
||||
<div className="pointer-events-auto surface w-full max-w-4xl max-h-[90vh] overflow-hidden shadow-2xl">
|
||||
<div className="flex items-center justify-between border-b border-slate-700 px-0.5 py-0.25 text-sm text-slate-200">
|
||||
<span className="font-semibold">Help & controls</span>
|
||||
<div className="flex items-center gap-0.5 text-[0.8rem] text-slate-300">
|
||||
<label className="flex items-center gap-0.25">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!showOnLoad}
|
||||
onChange={handleCheckbox}
|
||||
className="accent-cyan-500"
|
||||
/>
|
||||
<span>Don't show again</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="button-dark px-2 py-0.25 text-[0.8rem]"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-[85vh] overflow-y-auto p-0.5">
|
||||
<HelpContentView layout={layout} keymap={state?.keymap || {}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +1,22 @@
|
||||
export default function HelpPanel({ layout }) {
|
||||
const presets = {
|
||||
desktop: [
|
||||
'Use WASD + Shift to drive. Video must stay focused.',
|
||||
'Macros on the left prep the rover before moving.',
|
||||
'Switch tabs on the right for telemetry, room, and advanced tools.',
|
||||
],
|
||||
'mobile-portrait': [
|
||||
'Keep joystick centered when not driving.',
|
||||
'Auxiliary buttons sit beside the joystick – use them carefully.',
|
||||
'Scroll to access drive/telemetry if screen space is limited.',
|
||||
],
|
||||
'mobile-landscape': [
|
||||
'Joystick and video are side by side for game-style control.',
|
||||
'Rotate back to portrait if you need admin controls quickly.',
|
||||
'Tabs are desktop-only; mobile shows sections stacked.',
|
||||
],
|
||||
};
|
||||
const tips = presets[layout] || presets.desktop;
|
||||
import { useControlSystem } from '../controls/index.js';
|
||||
import HelpContentView from './HelpContentView.jsx';
|
||||
|
||||
export default function HelpPanel({ layout, onOpenOverlay }) {
|
||||
const { state } = useControlSystem();
|
||||
|
||||
return (
|
||||
<section className="panel-section space-y-0.5 text-base">
|
||||
<p className="text-sm text-slate-400">Help</p>
|
||||
<ul className="space-y-0.5 text-sm">
|
||||
{tips.map((tip) => (
|
||||
<li key={tip} className="surface text-slate-200">
|
||||
{tip}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<section className="panel-section space-y-0.5 text-sm">
|
||||
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||
<span>Help</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenOverlay}
|
||||
className="button-dark px-1 py-0.25 text-[0.75rem]"
|
||||
>
|
||||
Open full help
|
||||
</button>
|
||||
</div>
|
||||
<HelpContentView layout={layout} keymap={state?.keymap || {}} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import SettingsPanel from './SettingsPanel.jsx';
|
||||
import HelpPanel from './HelpPanel.jsx';
|
||||
import Tabs, { Tab, TabList, TabPanel, TabPanels } from './Tabs.jsx';
|
||||
|
||||
export default function RightPaneTabs({ layout }) {
|
||||
export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
|
||||
return (
|
||||
<section className="panel text-base">
|
||||
<Tabs defaultTab="telemetry">
|
||||
@@ -36,7 +36,7 @@ export default function RightPaneTabs({ layout }) {
|
||||
<SettingsPanel />
|
||||
</TabPanel>
|
||||
<TabPanel id="help">
|
||||
<HelpPanel layout={layout} />
|
||||
<HelpPanel layout={layout} onOpenOverlay={onOpenHelpOverlay} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useSession } from '../context/SessionContext.jsx';
|
||||
import { useSettingsNamespace } from '../settings/index.js';
|
||||
import { useSocket } from '../context/SocketContext.jsx';
|
||||
import NicknameForm from './NicknameForm.jsx';
|
||||
import DiscordInviteButton from './DiscordInviteButton.jsx';
|
||||
|
||||
function roleColors(role) {
|
||||
switch (role) {
|
||||
@@ -128,8 +129,14 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
|
||||
className={`panel-section space-y-0.5 text-base ${fillHeight ? 'flex h-full min-h-0 flex-col overflow-hidden' : ''} ${className}`}
|
||||
>
|
||||
{!hideNicknameForm && (
|
||||
<div className="space-y-0.5">
|
||||
<div className="space-y-0.5 flex">
|
||||
<div className='w-1/2'>
|
||||
<NicknameForm />
|
||||
|
||||
</div>
|
||||
<div className='w-1/2'>
|
||||
<DiscordInviteButton />
|
||||
</div>
|
||||
{!canSetNickname && <p className="text-xs text-slate-500">Spectators cannot set nicknames.</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
export const HELP_LAYOUTS = ['desktop', 'mobile-portrait', 'mobile-landscape'];
|
||||
|
||||
// Block-based help content; each layout defines a hero plus main/aside blocks.
|
||||
// Text supports inline key pills via segments: strings or { action: 'driveMacro' }.
|
||||
export const HELP_CONTENT = {
|
||||
desktop: {
|
||||
main: [
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Chat and nicknames',
|
||||
items: [
|
||||
'Set a nickname in the user list panel, on the bottom left of the page below the rover video.',
|
||||
{ segments: ['Toggle chat focus with ', { action: 'chatFocus' }, '. Press ', { action: 'chatFocus'}, ' again to send.'] },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Driving the rover',
|
||||
items: [
|
||||
{ segments: ['Press the "Start Driving" button onscreen, or press ' , { action: 'driveMacro' }, ' on your keyboard to put the rover into driving mode.'] },
|
||||
'Refer to the controls for the controls for the rover.'
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Docking the rover',
|
||||
items: [
|
||||
'Line up the rover to the dock, about a foot away, then:',
|
||||
{ segments: ['Press the "Dock and Charge" button onscreen, or press ', { action: 'dockMacro' }, ' on your keyboard to start docking.'] },
|
||||
'Wait for the rover to confirm it is docked and charging before leaving it unattended.',
|
||||
],
|
||||
},
|
||||
],
|
||||
aside: [
|
||||
{
|
||||
type: 'keyboard',
|
||||
title: 'Keyboard controls',
|
||||
footnote: 'Per-browser; adjust in Settings → Keybindings.',
|
||||
groups: [
|
||||
{
|
||||
id: 'movement',
|
||||
title: 'Movement',
|
||||
items: [
|
||||
{ action: 'driveForward', label: 'Forward' },
|
||||
{ action: 'driveBackward', label: 'Backward' },
|
||||
{ action: 'driveLeft', label: 'Turn left' },
|
||||
{ action: 'driveRight', label: 'Turn right' },
|
||||
{ action: 'boostModifier', label: 'Boost speed' },
|
||||
{ action: 'slowModifier', label: 'Precision speed' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'macros',
|
||||
title: 'Rover modes & chat',
|
||||
items: [
|
||||
{ action: 'driveMacro', label: 'Drive macro' },
|
||||
{ action: 'dockMacro', label: 'Dock macro' },
|
||||
{ action: 'chatFocus', label: 'Chat focus' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'camera',
|
||||
title: 'Camera',
|
||||
items: [
|
||||
{ action: 'cameraUp', label: 'Tilt up' },
|
||||
{ action: 'cameraDown', label: 'Tilt down' },
|
||||
{ action: 'nightVisionToggle', label: 'Toggle night vision' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'motors',
|
||||
title: 'Rover Aux Motors',
|
||||
items: [
|
||||
{ action: 'auxMainForward', label: 'Main brush forward' },
|
||||
{ action: 'auxMainReverse', label: 'Main brush reverse' },
|
||||
{ action: 'auxSideForward', label: 'Side brush forward' },
|
||||
{ action: 'auxSideReverse', label: 'Side brush reverse' },
|
||||
{ action: 'auxVacuumFast', label: 'Vacuum max' },
|
||||
{ action: 'auxVacuumSlow', label: 'Vacuum low' },
|
||||
{ action: 'auxAllForward', label: 'All motors forward' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'gamepad',
|
||||
title: 'Gamepad / joystick',
|
||||
items: [
|
||||
'Gamepad controls are not mapped by default, this is because of how terribly inconsistent gamepad implementations are across browsers and devices.',
|
||||
'You can map gamepad controls in Settings → Controller.',
|
||||
'Use at your own risk, it may not be perfect depending on your setup.'
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
'mobile-portrait': {
|
||||
main: [
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Chat and nicknames',
|
||||
items: [
|
||||
'Set a nickname in the user list panel below.',
|
||||
'Tap in the chat box to send messages in the chat.'
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Driving the rover',
|
||||
items: [
|
||||
{ segments: ['Press the "Start Driving" button onscreen to put the rover into driving mode.'] },
|
||||
'Look below the rover video. Use the joystick on the right to move the rover, and hold the buttons on the left to run the aux motors.'
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Docking the rover',
|
||||
items: [
|
||||
'Line up the rover to the dock, about a foot away, then:',
|
||||
{ segments: ['Press the "Dock and Charge" button onscreen to start docking.'] },
|
||||
'Wait for the rover to confirm it is docked and charging before leaving it unattended.',
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
'mobile-landscape': {
|
||||
main: [
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Chat and nicknames',
|
||||
items: [
|
||||
'Scroll down to see more of the page.',
|
||||
'Set a nickname in the user list panel below.',
|
||||
'Tap in the chat box to send messages in the chat.'
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Driving the rover',
|
||||
items: [
|
||||
{ segments: ['Press the "Start Driving" button, or the "Drive" button to put the rover into driving mode.'] },
|
||||
'Use the joystick to the right of the video feed to move the rover, and hold the buttons on the left to run the aux motors.'
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Docking the rover',
|
||||
items: [
|
||||
'Line up the rover to the dock, about a foot away, then:',
|
||||
{ segments: ['Press the "Dock and Charge" or the "Dock" button onscreen to start docking.'] },
|
||||
'Wait for the rover to confirm it is docked and charging before leaving it unattended.',
|
||||
],
|
||||
}
|
||||
],
|
||||
// aside: [
|
||||
// {
|
||||
// type: 'keyboard',
|
||||
// title: 'Keyboard (if attached)',
|
||||
// footnote: 'Per-browser; adjust in Settings → Controls.',
|
||||
// groups: [
|
||||
// {
|
||||
// id: 'movement',
|
||||
// title: 'Movement',
|
||||
// items: [
|
||||
// { action: 'driveForward', label: 'Forward' },
|
||||
// { action: 'driveBackward', label: 'Backward' },
|
||||
// { action: 'driveLeft', label: 'Turn left' },
|
||||
// { action: 'driveRight', label: 'Turn right' },
|
||||
// { action: 'boostModifier', label: 'Boost' },
|
||||
// { action: 'slowModifier', label: 'Precision' },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// id: 'camera',
|
||||
// title: 'Camera',
|
||||
// items: [
|
||||
// { action: 'cameraUp', label: 'Tilt up' },
|
||||
// { action: 'cameraDown', label: 'Tilt down' },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// id: 'macros',
|
||||
// title: 'Macros & chat',
|
||||
// items: [
|
||||
// { action: 'driveMacro', label: 'Drive macro' },
|
||||
// { action: 'dockMacro', label: 'Dock macro' },
|
||||
// { action: 'nightVisionToggle', label: 'Toggle night vision' },
|
||||
// { action: 'chatFocus', label: 'Chat focus' },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// type: 'gamepad',
|
||||
// title: 'Gamepad / joystick',
|
||||
// items: [
|
||||
// 'Left stick drives; right stick/D-pad: camera when mapped.',
|
||||
// 'Buttons can trigger macros or aux motors; adjust deadzones if drifting.',
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
},
|
||||
};
|
||||
|
||||
export function getHelpContent(layout) {
|
||||
return HELP_CONTENT[layout] || HELP_CONTENT.desktop;
|
||||
}
|
||||
+1
-1
@@ -71,7 +71,7 @@ body {
|
||||
270deg,
|
||||
#ff0000,
|
||||
#ff7f00,
|
||||
#ffff00,
|
||||
#ffff00b3,
|
||||
#00ff00,
|
||||
#0000ff,
|
||||
#4b0082,
|
||||
|
||||
Reference in New Issue
Block a user