new help system!!

This commit is contained in:
legop3
2025-12-04 23:50:44 -05:00
parent edeac5e6ff
commit 9f0bb8bd24
14 changed files with 572 additions and 74 deletions
+1 -1
View File
@@ -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"
>
+204
View File
@@ -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;
+44
View File
@@ -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&apos;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>
);
}
+18 -28
View File
@@ -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>
);
}
+2 -2
View File
@@ -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>
+8 -1
View File
@@ -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>
)}