mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 10:00:46 -04:00
LOTS OF STYLING STUFF FINALLY THAT I HAD TO DO FINALYL AAUUFH
This commit is contained in:
+1
-1
@@ -81,7 +81,7 @@ function DesktopLayout({ layout, onOpenHelpOverlay }) {
|
|||||||
<div className={`flex min-w-0 flex-[1.22] flex-col ${themeGapClass} overflow-y-auto pr-0`}>
|
<div className={`flex min-w-0 flex-[1.22] flex-col ${themeGapClass} overflow-y-auto pr-0`}>
|
||||||
<DriverVideo />
|
<DriverVideo />
|
||||||
<TelemetryPanel />
|
<TelemetryPanel />
|
||||||
<LogPanel />
|
{/* <LogPanel /> */}
|
||||||
</div>
|
</div>
|
||||||
<div className={`flex min-w-0 flex-1 flex-col ${themeGapClass} overflow-y-auto`}>
|
<div className={`flex min-w-0 flex-1 flex-col ${themeGapClass} overflow-y-auto`}>
|
||||||
<GlobalObjectiveBanner layout={layout} />
|
<GlobalObjectiveBanner layout={layout} />
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
// Admin Ip Log Panel
|
// Admin Ip Log Panel
|
||||||
// Purpose: Defines the Admin Ip Log Panel module and the local helpers/components used in this file.
|
// Purpose: Defines the Admin Ip Log Panel module and the local helpers/components used in this file.
|
||||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
export default function AdminIpLogPanel({ entries }) {
|
export default function AdminIpLogPanel({ entries }) {
|
||||||
const logs = entries || [];
|
const logs = entries || [];
|
||||||
return (
|
return (
|
||||||
<div className="panel-section space-y-0.5 text-base">
|
<CardFrame title="Admin IP log" meta={logs.length} bodyClassName="space-y-0.5 text-base">
|
||||||
<div className="flex items-center justify-between text-sm text-slate-400">
|
<div className="h-64 overflow-y-auto font-mono text-xs">
|
||||||
<span>Admin IP log</span>
|
|
||||||
<span>{logs.length}</span>
|
|
||||||
</div>
|
|
||||||
<div className="surface h-64 overflow-y-auto font-mono text-xs">
|
|
||||||
{logs.length === 0 ? (
|
{logs.length === 0 ? (
|
||||||
<p>No admin log entries yet.</p>
|
<p>No admin log entries yet.</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -17,7 +15,7 @@ export default function AdminIpLogPanel({ entries }) {
|
|||||||
.slice()
|
.slice()
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((entry) => (
|
.map((entry) => (
|
||||||
<div key={entry.id} className="surface">
|
<div key={entry.id}>
|
||||||
<span className="text-amber-400">{entry.ts ? new Date(entry.ts).toLocaleTimeString() : '--'}</span>{' '}
|
<span className="text-amber-400">{entry.ts ? new Date(entry.ts).toLocaleTimeString() : '--'}</span>{' '}
|
||||||
{entry.label && <span className="text-teal-400">[{entry.label}]</span>}{' '}
|
{entry.label && <span className="text-teal-400">[{entry.label}]</span>}{' '}
|
||||||
<span className="text-slate-200">{entry.message}</span>{' '}
|
<span className="text-slate-200">{entry.message}</span>{' '}
|
||||||
@@ -28,6 +26,6 @@ export default function AdminIpLogPanel({ entries }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-500">Admin-only log stream; IPs never appear in user data.</p>
|
<p className="text-xs text-slate-500">Admin-only log stream; IPs never appear in user data.</p>
|
||||||
</div>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import LlmCommentaryPanel from './LlmCommentaryPanel.jsx';
|
|||||||
import OverseerControlPanel from './OverseerControlPanel.jsx';
|
import OverseerControlPanel from './OverseerControlPanel.jsx';
|
||||||
import ReplaySnapshotHealth from './ReplaySnapshotHealth.jsx';
|
import ReplaySnapshotHealth from './ReplaySnapshotHealth.jsx';
|
||||||
import AdminIpLogPanel from './AdminIpLogPanel.jsx';
|
import AdminIpLogPanel from './AdminIpLogPanel.jsx';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
const MODES = [
|
const MODES = [
|
||||||
{ key: 'open', label: 'Open' },
|
{ key: 'open', label: 'Open' },
|
||||||
@@ -279,10 +280,7 @@ export default function AdminPanelContent() {
|
|||||||
|
|
||||||
if (!isAdmin) return null;
|
if (!isAdmin) return null;
|
||||||
|
|
||||||
return (
|
const actions = (
|
||||||
<section className="panel-section space-y-0.5 text-base">
|
|
||||||
<div className="flex items-center justify-between gap-0.5 text-sm">
|
|
||||||
<span>Admin controls</span>
|
|
||||||
<select value={currentMode} onChange={handleModeChange} className="field-input text-sm">
|
<select value={currentMode} onChange={handleModeChange} className="field-input text-sm">
|
||||||
{MODES.map((mode) => (
|
{MODES.map((mode) => (
|
||||||
<option key={mode.key} value={mode.key}>
|
<option key={mode.key} value={mode.key}>
|
||||||
@@ -290,7 +288,10 @@ export default function AdminPanelContent() {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardFrame title="Admin controls" actions={actions} bodyClassName="space-y-0.5 text-base">
|
||||||
<div className="flex gap-0.5 text-xs">
|
<div className="flex gap-0.5 text-xs">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -514,6 +515,6 @@ export default function AdminPanelContent() {
|
|||||||
clearingHistory={clearingOverseerHistory}
|
clearingHistory={clearingOverseerHistory}
|
||||||
/>
|
/>
|
||||||
<AdminIpLogPanel entries={adminLogs} />
|
<AdminIpLogPanel entries={adminLogs} />
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useSessionActions } from '../../context/SessionContext.jsx';
|
import { useSessionActions } from '../../context/SessionContext.jsx';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
export default function AuthPanel() {
|
export default function AuthPanel() {
|
||||||
const { login, setRole } = useSessionActions();
|
const { login } = useSessionActions();
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -25,8 +26,7 @@ export default function AuthPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panel-section space-y-0.5 text-base">
|
<CardFrame title="Admin login" bodyClassName="space-y-0.5 text-base">
|
||||||
<p className="text-sm text-slate-400">Admin login</p>
|
|
||||||
<form className="flex flex-col gap-0.5" onSubmit={handleLogin}>
|
<form className="flex flex-col gap-0.5" onSubmit={handleLogin}>
|
||||||
<input
|
<input
|
||||||
className="field-input"
|
className="field-input"
|
||||||
@@ -53,6 +53,6 @@ export default function AuthPanel() {
|
|||||||
Spectator
|
Spectator
|
||||||
</button>
|
</button>
|
||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ function formatDegrees(value) {
|
|||||||
|
|
||||||
function KeyPill({ label }) {
|
function KeyPill({ label }) {
|
||||||
if (!label) return null;
|
if (!label) return null;
|
||||||
return <span className="rounded border border-white/40 px-1 text-[0.7rem] text-white">{label}</span>;
|
return <span className="rounded bg-slate-800 px-1 py-0.5 text-[0.6rem] font-semibold text-slate-200">{label}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CameraTiltControl({
|
export default function CameraTiltControl({
|
||||||
@@ -143,10 +143,14 @@ export default function CameraTiltControl({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`space-y-0.5 ${className}`.trim()}>
|
<div className={`space-y-0.5 ${disabled ? 'opacity-50' : ''} ${className}`.trim()}>
|
||||||
<div className={`flex items-center justify-between ${labelRowClass}`.trim()}>
|
<div className={`flex items-center justify-between ${labelRowClass}`.trim()}>
|
||||||
<span className={labelClass}>{label}</span>
|
<span className={labelClass}>{label}</span>
|
||||||
{showValue ? <span className={valueClass}>{formatDegrees(value)}</span> : null}
|
{showValue ? (
|
||||||
|
<span className={`rounded bg-slate-800 px-1 py-0.5 text-[0.7rem] font-semibold ${valueClass}`.trim()}>
|
||||||
|
{formatDegrees(value)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{slider}
|
{slider}
|
||||||
|
|||||||
@@ -53,14 +53,14 @@ export default function CardFrame({
|
|||||||
const cardStyle = accentRgb ? { borderColor: rgba(accentRgb, 0.35) } : undefined;
|
const cardStyle = accentRgb ? { borderColor: rgba(accentRgb, 0.35) } : undefined;
|
||||||
const headerStyle = accentRgb
|
const headerStyle = accentRgb
|
||||||
? {
|
? {
|
||||||
backgroundImage: `linear-gradient(90deg, rgba(23,23,23,0.96) 0%, rgba(38,38,38,0.94) 58%, ${rgba(accentRgb, 0.18)} 100%)`,
|
backgroundImage: `linear-gradient(90deg, rgba(23,23,23,0.96) 0%, rgba(38,38,38,0.94) 30%, ${rgba(accentRgb, 0.28)} 100%)`,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={cx(
|
className={cx(
|
||||||
'panel-section border border-neutral-500/60 bg-neutral-900/95',
|
'panel-section border-2 border-neutral-500/60 bg-neutral-900/95',
|
||||||
clipOverflow ? 'overflow-hidden' : 'overflow-visible',
|
clipOverflow ? 'overflow-hidden' : 'overflow-visible',
|
||||||
fillHeight && 'flex h-full min-h-0 flex-col',
|
fillHeight && 'flex h-full min-h-0 flex-col',
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -121,15 +121,15 @@ export function ChatIdentity({ message, toolsToggle = null }) {
|
|||||||
|
|
||||||
function chatRowClass(message) {
|
function chatRowClass(message) {
|
||||||
if (isBotMessage(message)) {
|
if (isBotMessage(message)) {
|
||||||
return 'surface-muted flex flex-col gap-0.5 border border-emerald-500/40 bg-emerald-900/15 text-sm';
|
return 'flex flex-col gap-0.5 rounded-md border border-emerald-500/40 bg-emerald-950 px-0.5 py-0.5 text-sm text-neutral-100';
|
||||||
}
|
}
|
||||||
const isAdmin =
|
const isAdmin =
|
||||||
message.role === 'admin' || message.role === 'lockdown' || message.role === 'lockdown-admin';
|
message.role === 'admin' || message.role === 'lockdown' || message.role === 'lockdown-admin';
|
||||||
return `surface-muted flex flex-col gap-0.5 text-sm ${
|
return `flex flex-col gap-0.5 rounded-md bg-neutral-800 px-0.5 py-0.5 text-sm text-neutral-100 ${
|
||||||
isAdmin
|
isAdmin
|
||||||
? 'border border-amber-400/30'
|
? 'border border-amber-400/30'
|
||||||
: message.fromDiscord
|
: message.fromDiscord
|
||||||
? 'border border-indigo-400/30 bg-indigo-900/20'
|
? 'border border-indigo-400/30 bg-indigo-950'
|
||||||
: ''
|
: ''
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export default function ChatPanel({
|
|||||||
fillHeight={fillHeight}
|
fillHeight={fillHeight}
|
||||||
bodyClassName="space-y-0.5 text-base"
|
bodyClassName="space-y-0.5 text-base"
|
||||||
>
|
>
|
||||||
<div className={`surface overflow-y-auto space-y-0.5 px-0 ${listClass}`} ref={listRef}>
|
<div className={`overflow-y-auto space-y-0.5 px-0 ${listClass}`} ref={listRef}>
|
||||||
{sorted.length === 0 && typingRows.length === 0 ? (
|
{sorted.length === 0 && typingRows.length === 0 ? (
|
||||||
<p className="text-sm text-slate-500">No messages yet.</p>
|
<p className="text-sm text-slate-500">No messages yet.</p>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import OvercurrentOverlay from '../HudOverlays/OvercurrentOverlay/index.jsx';
|
|||||||
import LowBatteryOverlay from '../HudOverlays/LowBatteryOverlay/index.jsx';
|
import LowBatteryOverlay from '../HudOverlays/LowBatteryOverlay/index.jsx';
|
||||||
import DriverBottomStrip from '../HudOverlays/DriverBottomStrip/index.jsx';
|
import DriverBottomStrip from '../HudOverlays/DriverBottomStrip/index.jsx';
|
||||||
import HudChatInput from '../HudOverlays/HudChatInput/index.jsx';
|
import HudChatInput from '../HudOverlays/HudChatInput/index.jsx';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
export default function DriverVideo({ layoutFormat = 'desktop' }) {
|
export default function DriverVideo({ layoutFormat = 'desktop' }) {
|
||||||
const roverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
|
const roverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
|
||||||
@@ -20,7 +21,7 @@ export default function DriverVideo({ layoutFormat = 'desktop' }) {
|
|||||||
|
|
||||||
if (!roverId) {
|
if (!roverId) {
|
||||||
return (
|
return (
|
||||||
<section className="panel">
|
<CardFrame hideHeader className="shrink-0">
|
||||||
<div className="panel-muted content-center text-center text-sm text-slate-400 aspect-[4/3]">
|
<div className="panel-muted content-center text-center text-sm text-slate-400 aspect-[4/3]">
|
||||||
<p>You are not assigned to a rover.</p>
|
<p>You are not assigned to a rover.</p>
|
||||||
<p className="mt-0">
|
<p className="mt-0">
|
||||||
@@ -29,13 +30,13 @@ export default function DriverVideo({ layoutFormat = 'desktop' }) {
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mobileHud = layoutFormat !== 'desktop';
|
const mobileHud = layoutFormat !== 'desktop';
|
||||||
return (
|
return (
|
||||||
<section className="panel">
|
<CardFrame hideHeader className="shrink-0">
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<div className="relative w-full overflow-hidden bg-black aspect-[4/3]">
|
<div className="relative w-full overflow-hidden bg-black aspect-[4/3]">
|
||||||
<RoverMediaPlayer roverId={roverId} videoMode={videoMode} />
|
<RoverMediaPlayer roverId={roverId} videoMode={videoMode} />
|
||||||
@@ -58,6 +59,6 @@ export default function DriverVideo({ layoutFormat = 'desktop' }) {
|
|||||||
</div>
|
</div>
|
||||||
<DriverBottomStrip mobileHud={mobileHud} />
|
<DriverBottomStrip mobileHud={mobileHud} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
createProfileForPad,
|
createProfileForPad,
|
||||||
} from '../../controls/inputs/gamepadBindings.js';
|
} from '../../controls/inputs/gamepadBindings.js';
|
||||||
import { useGamepadHubState } from '../../controls/inputs/gamepadHub.js';
|
import { useGamepadHubState } from '../../controls/inputs/gamepadHub.js';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
import SliderField from './SliderField.jsx';
|
import SliderField from './SliderField.jsx';
|
||||||
import { ACTIONS, NUMBER_FORMAT } from './constants.js';
|
import { ACTIONS, NUMBER_FORMAT } from './constants.js';
|
||||||
import {
|
import {
|
||||||
@@ -222,20 +223,16 @@ export default function GamepadMappingSettings() {
|
|||||||
}, [activePad, activeProfile]);
|
}, [activePad, activeProfile]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel-section space-y-0.5 text-sm">
|
<CardFrame
|
||||||
<div className="flex items-center justify-between">
|
title="Controller"
|
||||||
<div>
|
meta={activePad ? 'Move sticks or press buttons to bind' : 'Connect a controller to configure.'}
|
||||||
<p className="text-xs text-slate-400">Controller</p>
|
bodyClassName="space-y-0.5 text-sm"
|
||||||
<p className="text-[0.65rem] text-slate-500">
|
>
|
||||||
{activePad ? 'Move sticks or press buttons to bind' : 'Connect a controller to configure.'}
|
|
||||||
</p>
|
|
||||||
{captureAction && (
|
{captureAction && (
|
||||||
<p className="mt-0 text-[0.7rem] text-emerald-400">Capturing {captureAction.label}…</p>
|
<p className="mt-0 text-[0.7rem] text-emerald-400">Capturing {captureAction.label}…</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-0.5 surface">
|
<div className="space-y-0.5">
|
||||||
<p className="text-[0.7rem] text-slate-500">Connected controller</p>
|
<p className="text-[0.7rem] text-slate-500">Connected controller</p>
|
||||||
{hubState.pads.length === 0 ? (
|
{hubState.pads.length === 0 ? (
|
||||||
<p className="text-[0.7rem] text-slate-400">No controller detected.</p>
|
<p className="text-[0.7rem] text-slate-400">No controller detected.</p>
|
||||||
@@ -257,7 +254,7 @@ export default function GamepadMappingSettings() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-0.5 surface">
|
<div className="space-y-0.5">
|
||||||
<p className="text-[0.7rem] text-slate-500">Calibration</p>
|
<p className="text-[0.7rem] text-slate-500">Calibration</p>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<SliderField
|
<SliderField
|
||||||
@@ -296,7 +293,7 @@ export default function GamepadMappingSettings() {
|
|||||||
value={activeProfile.calibration?.auxSideScale ?? 0.55}
|
value={activeProfile.calibration?.auxSideScale ?? 0.55}
|
||||||
onChange={(value) => updateCalibration({ auxSideScale: value })}
|
onChange={(value) => updateCalibration({ auxSideScale: value })}
|
||||||
/>
|
/>
|
||||||
<label className="surface-muted block p-0.5">
|
<label className="block p-0.5">
|
||||||
<div className="flex items-center justify-between text-xs text-slate-300">
|
<div className="flex items-center justify-between text-xs text-slate-300">
|
||||||
<span className="font-semibold text-slate-100">Camera mode</span>
|
<span className="font-semibold text-slate-100">Camera mode</span>
|
||||||
<select
|
<select
|
||||||
@@ -324,14 +321,14 @@ export default function GamepadMappingSettings() {
|
|||||||
|
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
{Object.entries(grouped).map(([section, actions]) => (
|
{Object.entries(grouped).map(([section, actions]) => (
|
||||||
<div key={section} className="space-y-0.5 surface">
|
<div key={section} className="space-y-0.5">
|
||||||
<p className="text-[0.7rem] text-slate-500">{section}</p>
|
<p className="text-[0.7rem] text-slate-500">{section}</p>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
{actions.map((action) => {
|
{actions.map((action) => {
|
||||||
const binding = activeProfile.bindings?.[action.id];
|
const binding = activeProfile.bindings?.[action.id];
|
||||||
const source = binding?.sources?.[0] ?? null;
|
const source = binding?.sources?.[0] ?? null;
|
||||||
return (
|
return (
|
||||||
<div key={action.id} className="surface-muted flex items-center justify-between text-xs">
|
<div key={action.id} className="flex items-center justify-between text-xs">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold text-slate-100">{action.label}</p>
|
<p className="font-semibold text-slate-100">{action.label}</p>
|
||||||
<p className="text-[0.65rem] text-slate-400">{formatSource(source)}</p>
|
<p className="text-[0.65rem] text-slate-400">{formatSource(source)}</p>
|
||||||
@@ -390,7 +387,7 @@ export default function GamepadMappingSettings() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-0.5 surface">
|
<div className="space-y-0.5">
|
||||||
<p className="text-[0.7rem] text-slate-500">Diagnostics</p>
|
<p className="text-[0.7rem] text-slate-500">Diagnostics</p>
|
||||||
{!activePad ? (
|
{!activePad ? (
|
||||||
<p className="text-[0.7rem] text-slate-400">No controller detected.</p>
|
<p className="text-[0.7rem] text-slate-400">No controller detected.</p>
|
||||||
@@ -435,6 +432,6 @@ export default function GamepadMappingSettings() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { NUMBER_FORMAT } from './constants.js';
|
|||||||
|
|
||||||
export default function SliderField({ label, description, min, max, step, value, onChange }) {
|
export default function SliderField({ label, description, min, max, step, value, onChange }) {
|
||||||
return (
|
return (
|
||||||
<label className="surface-muted block p-0.5">
|
<label className="block p-0.5">
|
||||||
<div className="flex items-center justify-between text-xs text-slate-300">
|
<div className="flex items-center justify-between text-xs text-slate-300">
|
||||||
<span className="font-semibold text-slate-100">{label}</span>
|
<span className="font-semibold text-slate-100">{label}</span>
|
||||||
<span className="font-mono text-slate-400">{NUMBER_FORMAT.format(value)}</span>
|
<span className="font-mono text-slate-400">{NUMBER_FORMAT.format(value)}</span>
|
||||||
|
|||||||
@@ -2,15 +2,13 @@
|
|||||||
// Purpose: Defines the Help Panel module and the local helpers/components used in this file.
|
// Purpose: Defines the Help Panel module and the local helpers/components used in this file.
|
||||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||||
import { useControlSystem } from '../../controls/index.js';
|
import { useControlSystem } from '../../controls/index.js';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
import HelpContentView from '../HelpContentView/index.jsx';
|
import HelpContentView from '../HelpContentView/index.jsx';
|
||||||
|
|
||||||
export default function HelpPanel({ layout, onOpenOverlay }) {
|
export default function HelpPanel({ layout, onOpenOverlay }) {
|
||||||
const { state } = useControlSystem();
|
const { state } = useControlSystem();
|
||||||
|
|
||||||
return (
|
const actions = (
|
||||||
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onOpenOverlay}
|
onClick={onOpenOverlay}
|
||||||
@@ -18,8 +16,11 @@ export default function HelpPanel({ layout, onOpenOverlay }) {
|
|||||||
>
|
>
|
||||||
Open full help
|
Open full help
|
||||||
</button>
|
</button>
|
||||||
</div>
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardFrame title="Help" actions={actions} bodyClassName="space-y-0.5 text-sm">
|
||||||
<HelpContentView layout={layout} keymap={state?.keymap || {}} />
|
<HelpContentView layout={layout} keymap={state?.keymap || {}} />
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,8 +104,8 @@ function EntityRow({ entity, connected, controlsLocked, onToggle, onSetColor, on
|
|||||||
: controlsLocked
|
: controlsLocked
|
||||||
? 'border-slate-800 bg-slate-900 text-slate-300 cursor-not-allowed'
|
? 'border-slate-800 bg-slate-900 text-slate-300 cursor-not-allowed'
|
||||||
: isOn
|
: isOn
|
||||||
? 'border-emerald-700 bg-emerald-900/80 text-emerald-50 hover:bg-emerald-800'
|
? 'border-emerald-700 bg-emerald-900 text-emerald-50 hover:bg-emerald-800'
|
||||||
: 'border-rose-800 bg-rose-900/80 text-rose-50 hover:bg-rose-800';
|
: 'border-rose-800 bg-rose-900 text-rose-50 hover:bg-rose-800';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!supportsColor || draggingRef.current) return;
|
if (!supportsColor || draggingRef.current) return;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function DriverBottomStrip({ roverId = null, mobileHud = false })
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<LightBumpBars roverId={effectiveRoverId} />
|
<LightBumpBars roverId={effectiveRoverId} />
|
||||||
<div className="panel-section space-y-0.5 text-sm">
|
<div className="space-y-0.5 text-sm">
|
||||||
<BatteryBar visual={batteryVisual} compact={mobileHud} />
|
<BatteryBar visual={batteryVisual} compact={mobileHud} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { canonicalizeKeyInput, formatKeyLabel } from '../../controls/keymapUtils
|
|||||||
import { setKeyboardCaptureLocked } from '../../controls/inputs/keyboardCaptureLock.js';
|
import { setKeyboardCaptureLocked } from '../../controls/inputs/keyboardCaptureLock.js';
|
||||||
import { useSettingsNamespace } from '../../settings/index.js';
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
import { INPUT_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
import { INPUT_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
const KEY_ACTIONS = [
|
const KEY_ACTIONS = [
|
||||||
{ id: 'driveForward', label: 'Drive Forward', group: 'Driving' },
|
{ id: 'driveForward', label: 'Drive Forward', group: 'Driving' },
|
||||||
@@ -118,7 +119,7 @@ function useKeyCapture(onCapture) {
|
|||||||
|
|
||||||
function SpeedField({ label, description, value, onChange, min = 0, max = 500, step = 5 }) {
|
function SpeedField({ label, description, value, onChange, min = 0, max = 500, step = 5 }) {
|
||||||
return (
|
return (
|
||||||
<label className="surface-muted block p-0.5">
|
<label className="block p-0.5">
|
||||||
<div className="flex items-center justify-between text-xs text-slate-300">
|
<div className="flex items-center justify-between text-xs text-slate-300">
|
||||||
<span className="font-semibold text-slate-100">{label}</span>
|
<span className="font-semibold text-slate-100">{label}</span>
|
||||||
<input
|
<input
|
||||||
@@ -212,17 +213,17 @@ export default function KeymapSettings() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel-section space-y-0.5 text-sm">
|
<CardFrame
|
||||||
<div className="flex items-center justify-between">
|
title="Keyboard layout"
|
||||||
<div>
|
meta="Per-browser · click to change a binding"
|
||||||
<p className="text-xs text-slate-400">Keyboard layout</p>
|
actions={
|
||||||
<p className="text-[0.7rem] text-slate-500">Per-browser · click to change a binding</p>
|
|
||||||
</div>
|
|
||||||
<button type="button" onClick={() => resetKeyBindings()} className="button-dark text-xs">
|
<button type="button" onClick={() => resetKeyBindings()} className="button-dark text-xs">
|
||||||
Reset defaults
|
Reset defaults
|
||||||
</button>
|
</button>
|
||||||
</div>
|
}
|
||||||
<div className="space-y-0.5 surface">
|
bodyClassName="space-y-0.5 text-sm"
|
||||||
|
>
|
||||||
|
<div className="space-y-0.5">
|
||||||
<p className="text-[0.7rem] text-slate-500">Keyboard speeds</p>
|
<p className="text-[0.7rem] text-slate-500">Keyboard speeds</p>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<SpeedField
|
<SpeedField
|
||||||
@@ -245,7 +246,7 @@ export default function KeymapSettings() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0.5 surface">
|
<div className="space-y-0.5">
|
||||||
<p className="text-[0.7rem] text-slate-500">Camera tilt</p>
|
<p className="text-[0.7rem] text-slate-500">Camera tilt</p>
|
||||||
<SpeedField
|
<SpeedField
|
||||||
label="Tilt speed"
|
label="Tilt speed"
|
||||||
@@ -259,14 +260,14 @@ export default function KeymapSettings() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
{Object.entries(grouped).map(([group, actions]) => (
|
{Object.entries(grouped).map(([group, actions]) => (
|
||||||
<div key={group} className="space-y-0.5 surface">
|
<div key={group} className="space-y-0.5">
|
||||||
<p className="text-[0.75rem] text-slate-400">{group}</p>
|
<p className="text-[0.75rem] text-slate-400">{group}</p>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
{actions.map((action) => {
|
{actions.map((action) => {
|
||||||
const value = currentKey(action.id);
|
const value = currentKey(action.id);
|
||||||
const isActive = active === action.id;
|
const isActive = active === action.id;
|
||||||
return (
|
return (
|
||||||
<div key={action.id} className="surface-muted flex items-center justify-between text-xs">
|
<div key={action.id} className="flex items-center justify-between text-xs">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold text-slate-100">{action.label}</p>
|
<p className="font-semibold text-slate-100">{action.label}</p>
|
||||||
<p className="text-[0.65rem] text-slate-400">{formatKeyLabel(value)}</p>
|
<p className="text-[0.65rem] text-slate-400">{formatKeyLabel(value)}</p>
|
||||||
@@ -294,6 +295,6 @@ export default function KeymapSettings() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useControlSystem } from '../../controls/index.js';
|
import { useControlSystem } from '../../controls/index.js';
|
||||||
import { OVERCURRENT_GROUPS } from '../../controls/overcurrentLimiter.js';
|
import { OVERCURRENT_GROUPS } from '../../controls/overcurrentLimiter.js';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
const GROUP_LABELS = {
|
const GROUP_LABELS = {
|
||||||
drive: 'Drive wheels',
|
drive: 'Drive wheels',
|
||||||
@@ -32,11 +33,11 @@ export default function OvercurrentLimiterPanel() {
|
|||||||
const groups = useMemo(() => OVERCURRENT_GROUPS.map((group) => group.key), []);
|
const groups = useMemo(() => OVERCURRENT_GROUPS.map((group) => group.key), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel-section space-y-0.5 text-sm">
|
<CardFrame
|
||||||
<div className="flex items-center justify-between text-xs text-slate-400">
|
title="Overcurrent limiter"
|
||||||
<span>Overcurrent limiter</span>
|
meta={overcurrentLimiter?.adminImmune ? 'Admin immune' : 'Active'}
|
||||||
<span>{overcurrentLimiter?.adminImmune ? 'Admin immune' : 'Active'}</span>
|
bodyClassName="space-y-0.5 text-sm"
|
||||||
</div>
|
>
|
||||||
{!roverId ? (
|
{!roverId ? (
|
||||||
<p className="text-xs text-slate-500">Assign a rover to view limiter status.</p>
|
<p className="text-xs text-slate-500">Assign a rover to view limiter status.</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -46,7 +47,7 @@ export default function OvercurrentLimiterPanel() {
|
|||||||
const over = overcurrentLimiter?.overcurrent?.groups?.[key] ?? false;
|
const over = overcurrentLimiter?.overcurrent?.groups?.[key] ?? false;
|
||||||
const scale = overcurrentLimiter?.scales?.perGroup?.[key] ?? 1;
|
const scale = overcurrentLimiter?.scales?.perGroup?.[key] ?? 1;
|
||||||
return (
|
return (
|
||||||
<div key={key} className="surface space-y-0.5">
|
<div key={key} className="space-y-0.5">
|
||||||
<div className="flex items-center justify-between text-xs">
|
<div className="flex items-center justify-between text-xs">
|
||||||
<span className="text-slate-200">{GROUP_LABELS[key] || key}</span>
|
<span className="text-slate-200">{GROUP_LABELS[key] || key}</span>
|
||||||
<span className={over ? 'text-red-300' : 'text-slate-400'}>
|
<span className={over ? 'text-red-300' : 'text-slate-400'}>
|
||||||
@@ -67,13 +68,13 @@ export default function OvercurrentLimiterPanel() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div className="surface text-[0.7rem] text-slate-400">
|
<div className="text-[0.7rem] text-slate-400">
|
||||||
<div>{`Down rate ${overcurrentLimiter?.config?.downRatePerSec}/s · Up rate ${overcurrentLimiter?.config?.upRatePerSec}/s`}</div>
|
<div>{`Down rate ${overcurrentLimiter?.config?.downRatePerSec}/s · Up rate ${overcurrentLimiter?.config?.upRatePerSec}/s`}</div>
|
||||||
<div>{`Release delay ${overcurrentLimiter?.config?.releaseDelaySec}s`}</div>
|
<div>{`Release delay ${overcurrentLimiter?.config?.releaseDelaySec}s`}</div>
|
||||||
<div>{`Output rate ${overcurrentLimiter?.config?.outputRateMs}ms`}</div>
|
<div>{`Output rate ${overcurrentLimiter?.config?.outputRateMs}ms`}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export default function RawUserPilePanel({
|
|||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<div className="grid gap-0.5 md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
|
<div className="grid gap-0.5 md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="surface flex w-full items-center px-0 py-0">
|
<div className="flex w-full items-center px-0 py-0">
|
||||||
<NicknameForm compact={compact} />
|
<NicknameForm compact={compact} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,7 +82,7 @@ export default function RawUserPilePanel({
|
|||||||
|
|
||||||
<div className={`space-y-0.5 ${fillHeight ? 'flex flex-1 min-h-0 flex-col' : ''}`}>
|
<div className={`space-y-0.5 ${fillHeight ? 'flex flex-1 min-h-0 flex-col' : ''}`}>
|
||||||
<div
|
<div
|
||||||
className={`surface flex flex-wrap content-start items-start gap-0.5 px-0 pb-0 ${baseListClass} ${compact ? 'text-[0.8rem]' : ''}`}
|
className={`flex flex-wrap content-start items-start gap-0.5 px-0 pb-0 ${baseListClass} ${compact ? 'text-[0.8rem]' : ''}`}
|
||||||
>
|
>
|
||||||
{sorted.length === 0 ? (
|
{sorted.length === 0 ? (
|
||||||
<p className="text-sm text-slate-500">Waiting for users…</p>
|
<p className="text-sm text-slate-500">Waiting for users…</p>
|
||||||
@@ -90,7 +90,7 @@ export default function RawUserPilePanel({
|
|||||||
sorted.map((user) => (
|
sorted.map((user) => (
|
||||||
<span
|
<span
|
||||||
key={user.socketId}
|
key={user.socketId}
|
||||||
className={`rounded bg-slate-800/80 px-1 py-0.25 text-[0.7rem] ${roleColors(user.role)}`}
|
className={`rounded px-1 py-0.25 text-[0.7rem] ${roleColors(user.role)}`}
|
||||||
>
|
>
|
||||||
{formatLabel(user, selfId)}
|
{formatLabel(user, selfId)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -76,10 +76,15 @@ function DriveDockPanel() {
|
|||||||
const cameraDisabled = Boolean(!roverId || dockAssist.cameraLocked);
|
const cameraDisabled = Boolean(!roverId || dockAssist.cameraLocked);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`panel-section grid h-full min-h-0 grid-rows-[minmax(0,1fr)_auto] ${themeGapClass}`}>
|
<CardFrame
|
||||||
|
hideHeader
|
||||||
|
|
||||||
|
className="h-full"
|
||||||
|
bodyClassName={`grid h-full min-h-0 grid-rows-[minmax(0,1fr)_auto] ${themeGapClass}`}
|
||||||
|
>
|
||||||
<DriveDockAction layout="desktop" expand driveDockState={driveDockState} />
|
<DriveDockAction layout="desktop" expand driveDockState={driveDockState} />
|
||||||
{!hideInlineControls ? (
|
{!hideInlineControls ? (
|
||||||
<div className={`surface ${themeStackClass} p-0 text-sm text-slate-200`}>
|
<div className={`${themeStackClass} p-0 text-sm text-slate-200`}>
|
||||||
{nightVisionAvailable && (
|
{nightVisionAvailable && (
|
||||||
<NightVisionControl
|
<NightVisionControl
|
||||||
nightVisionOn={nightVisionState?.nightVisionOn}
|
nightVisionOn={nightVisionState?.nightVisionOn}
|
||||||
@@ -108,18 +113,18 @@ function DriveDockPanel() {
|
|||||||
onChange={setServoAngle}
|
onChange={setServoAngle}
|
||||||
keyDownLabel={downLabel}
|
keyDownLabel={downLabel}
|
||||||
keyUpLabel={upLabel}
|
keyUpLabel={upLabel}
|
||||||
className={`${themeStackClass} px-1 py-1`}
|
className={`${themeStackClass} rounded-xl border-2 border-emerald-300/70 bg-emerald-900 px-1 py-1 text-emerald-50`}
|
||||||
labelRowClass="text-xs text-slate-300"
|
labelRowClass="text-xs text-emerald-100"
|
||||||
labelClass=""
|
labelClass="text-sm font-semibold"
|
||||||
valueClass="font-mono text-slate-100"
|
valueClass="font-mono text-slate-100"
|
||||||
sliderClass="w-full"
|
sliderClass="w-full"
|
||||||
accentClass="accent-emerald-400"
|
accentClass="accent-emerald-400"
|
||||||
endpointClass="text-[0.7rem] text-slate-400"
|
endpointClass="text-[0.7rem] text-emerald-100/80"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import CardFrame from '../CardFrame/index.jsx';
|
|||||||
|
|
||||||
function EmptyState() {
|
function EmptyState() {
|
||||||
return (
|
return (
|
||||||
<div className="panel-section space-y-0.5 text-sm">
|
<CardFrame title="Room cameras" bodyClassName="space-y-0.5 text-sm">
|
||||||
<p className="text-center text-slate-400">No room cameras configured.</p>
|
<p className="text-center text-slate-400">No room cameras configured.</p>
|
||||||
<p className="text-center text-slate-500">Add entries to server config to populate this list.</p>
|
<p className="text-center text-slate-500">Add entries to server config to populate this list.</p>
|
||||||
</div>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ export default function RoomCameraPanel({
|
|||||||
{cameras.map((camera) => {
|
{cameras.map((camera) => {
|
||||||
const feed = feedMap[camera.id] || null;
|
const feed = feedMap[camera.id] || null;
|
||||||
return (
|
return (
|
||||||
<article key={camera.id} className="w-full space-y-0.5 rounded bg-zinc-950 p-0.5 shadow-inner shadow-black/40">
|
<article key={camera.id} className="w-full space-y-0.5 p-0.5">
|
||||||
{/* <header className="space-y-0.5">
|
{/* <header className="space-y-0.5">
|
||||||
<p className="text-lg font-semibold text-white">{camera.name || camera.id}</p>
|
<p className="text-lg font-semibold text-white">{camera.name || camera.id}</p>
|
||||||
{camera.description && <p className="text-xs text-slate-500">{camera.description}</p>}
|
{camera.description && <p className="text-xs text-slate-500">{camera.description}</p>}
|
||||||
|
|||||||
@@ -3,16 +3,16 @@
|
|||||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useSession } from '../../context/SessionContext.jsx';
|
import { useSession } from '../../context/SessionContext.jsx';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
export default function SessionSnapshot() {
|
export default function SessionSnapshot() {
|
||||||
const { session } = useSession();
|
const { session } = useSession();
|
||||||
const payload = useMemo(() => JSON.stringify(session ?? {}, null, 2), [session]);
|
const payload = useMemo(() => JSON.stringify(session ?? {}, null, 2), [session]);
|
||||||
return (
|
return (
|
||||||
<div className="panel-section flex min-h-0 flex-col gap-0.5 text-xs">
|
<CardFrame title="Session snapshot" className="min-h-0" bodyClassName="flex min-h-0 flex-col gap-0.5 text-xs">
|
||||||
<p className="text-sm text-slate-400">Session snapshot</p>
|
<pre className="min-h-0 flex-1 overflow-y-auto font-mono text-[0.7rem] text-lime-300">
|
||||||
<pre className="surface min-h-0 flex-1 overflow-y-auto font-mono text-[0.7rem] text-lime-300">
|
|
||||||
{payload}
|
{payload}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import OvercurrentLimiterPanel from '../OvercurrentLimiterPanel/index.jsx';
|
|||||||
import Tabs, { Tab, TabList, TabPanel, TabPanels } from '../Tabs/index.jsx';
|
import Tabs, { Tab, TabList, TabPanel, TabPanels } from '../Tabs/index.jsx';
|
||||||
import SessionSnapshot from '../SessionSnapshot/index.jsx';
|
import SessionSnapshot from '../SessionSnapshot/index.jsx';
|
||||||
import SocketLogPanel from '../SocketLogPanel/index.jsx';
|
import SocketLogPanel from '../SocketLogPanel/index.jsx';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
import { useHudMapSetting } from '../../hooks/useHudMapSetting.js';
|
import { useHudMapSetting } from '../../hooks/useHudMapSetting.js';
|
||||||
import { useSettingsNamespace } from '../../settings/index.js';
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
import { useSocket } from '../../context/SocketContext.jsx';
|
import { useSocket } from '../../context/SocketContext.jsx';
|
||||||
@@ -121,8 +122,7 @@ export default function SettingsPanel() {
|
|||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="page">
|
<TabPanel id="page">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<section className="panel-section space-y-0.5 text-sm">
|
<CardFrame title="HUD" bodyClassName="space-y-0.5 text-sm">
|
||||||
<p className="text-slate-400">HUD</p>
|
|
||||||
<label className="flex items-center gap-0.5 text-slate-200">
|
<label className="flex items-center gap-0.5 text-slate-200">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -133,9 +133,8 @@ export default function SettingsPanel() {
|
|||||||
<span>Show top-down map in HUD (desktop)</span>
|
<span>Show top-down map in HUD (desktop)</span>
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-slate-500">Mobile HUD keeps the map on by default.</p>
|
<p className="text-xs text-slate-500">Mobile HUD keeps the map on by default.</p>
|
||||||
</section>
|
</CardFrame>
|
||||||
<section className="panel-section space-y-0.5 text-sm">
|
<CardFrame title="Mobile controls" bodyClassName="space-y-0.5 text-sm">
|
||||||
<p className="text-slate-400">Mobile controls</p>
|
|
||||||
<label className="flex items-center gap-0.5 text-slate-200">
|
<label className="flex items-center gap-0.5 text-slate-200">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -145,9 +144,8 @@ export default function SettingsPanel() {
|
|||||||
/>
|
/>
|
||||||
<span>Swap control columns (put joystick on the left)</span>
|
<span>Swap control columns (put joystick on the left)</span>
|
||||||
</label>
|
</label>
|
||||||
</section>
|
</CardFrame>
|
||||||
<section className="panel-section space-y-0.5 text-sm">
|
<CardFrame title="Macros" bodyClassName="space-y-0.5 text-sm">
|
||||||
<p className="text-slate-400">Macros</p>
|
|
||||||
<label className="flex items-center gap-0.5 text-slate-200">
|
<label className="flex items-center gap-0.5 text-slate-200">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -157,9 +155,8 @@ export default function SettingsPanel() {
|
|||||||
/>
|
/>
|
||||||
<span>Enable backward bump in drive macro</span>
|
<span>Enable backward bump in drive macro</span>
|
||||||
</label>
|
</label>
|
||||||
</section>
|
</CardFrame>
|
||||||
<section className="panel-section space-y-0.5 text-sm">
|
<CardFrame title="Audio" bodyClassName="space-y-0.5 text-sm">
|
||||||
<p className="text-slate-400">Audio</p>
|
|
||||||
<label className="grid gap-0.5 text-slate-200">
|
<label className="grid gap-0.5 text-slate-200">
|
||||||
<div className="flex items-center justify-between gap-0.5">
|
<div className="flex items-center justify-between gap-0.5">
|
||||||
<span>Master volume</span>
|
<span>Master volume</span>
|
||||||
@@ -233,9 +230,8 @@ export default function SettingsPanel() {
|
|||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-slate-500">
|
||||||
Lowers rover audio only while the main brush is running.
|
Lowers rover audio only while the main brush is running.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</CardFrame>
|
||||||
<section className="panel-section space-y-0.5 text-sm">
|
<CardFrame title="Connection" bodyClassName="space-y-0.5 text-sm">
|
||||||
<p className="text-slate-400">Connection</p>
|
|
||||||
<label className="flex items-center justify-between gap-0.5 text-slate-200">
|
<label className="flex items-center justify-between gap-0.5 text-slate-200">
|
||||||
<span>Transport</span>
|
<span>Transport</span>
|
||||||
<select
|
<select
|
||||||
@@ -248,14 +244,13 @@ export default function SettingsPanel() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-slate-500">Switching reconnects your session.</p>
|
<p className="text-xs text-slate-500">Switching reconnects your session.</p>
|
||||||
</section>
|
</CardFrame>
|
||||||
</div>
|
</div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="admin">
|
<TabPanel id="admin">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<section className="panel-section space-y-0.5 text-sm">
|
<CardFrame title="Manual OI commands" bodyClassName="space-y-0.5 text-sm">
|
||||||
<p className="text-slate-400">Manual OI commands</p>
|
<div className="flex flex-wrap gap-0.5">
|
||||||
<div className="surface flex flex-wrap gap-0.5">
|
|
||||||
{manualTabs.map((tab) => (
|
{manualTabs.map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
@@ -268,10 +263,9 @@ export default function SettingsPanel() {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
<section className="panel-section space-y-0.5 text-sm">
|
<CardFrame title="Sensor stream" bodyClassName="space-y-0.5 text-sm">
|
||||||
<p className="text-slate-400">Sensor stream</p>
|
<div className="flex gap-0.5">
|
||||||
<div className="surface flex gap-0.5">
|
|
||||||
{sensorButtons.map((btn) => (
|
{sensorButtons.map((btn) => (
|
||||||
<button
|
<button
|
||||||
key={btn.key}
|
key={btn.key}
|
||||||
@@ -285,7 +279,7 @@ export default function SettingsPanel() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{!canControl && <p className="text-xs text-slate-500">Assign a rover to toggle streams.</p>}
|
{!canControl && <p className="text-xs text-slate-500">Assign a rover to toggle streams.</p>}
|
||||||
</section>
|
</CardFrame>
|
||||||
<AuthPanel />
|
<AuthPanel />
|
||||||
<OvercurrentLimiterPanel />
|
<OvercurrentLimiterPanel />
|
||||||
<AdminPanel />
|
<AdminPanel />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useSocket } from '../../context/SocketContext.jsx';
|
import { useSocket } from '../../context/SocketContext.jsx';
|
||||||
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
|
|
||||||
const MAX_ENTRIES = 300;
|
const MAX_ENTRIES = 300;
|
||||||
const MAX_PAYLOAD_LENGTH = 240;
|
const MAX_PAYLOAD_LENGTH = 240;
|
||||||
@@ -113,19 +114,19 @@ export default function SocketLogPanel() {
|
|||||||
};
|
};
|
||||||
}, [socket]);
|
}, [socket]);
|
||||||
|
|
||||||
return (
|
const actions = (
|
||||||
<section className="panel-section space-y-0.5 text-base">
|
|
||||||
<div className="flex items-center justify-between text-sm text-slate-400">
|
|
||||||
<span>Socket activity</span>
|
|
||||||
<div className="flex items-center gap-0.5 text-xs text-slate-400">
|
<div className="flex items-center gap-0.5 text-xs text-slate-400">
|
||||||
<span className="surface-muted">in {counters.in}</span>
|
<span>in {counters.in}</span>
|
||||||
<span className="surface-muted">out {counters.out}</span>
|
<span>out {counters.out}</span>
|
||||||
<button type="button" className="button-dark text-xs" onClick={() => setEntries([])}>
|
<button type="button" className="button-dark text-xs" onClick={() => setEntries([])}>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
<div className="surface h-64 overflow-y-auto font-mono text-xs">
|
|
||||||
|
return (
|
||||||
|
<CardFrame title="Socket activity" actions={actions} bodyClassName="space-y-0.5 text-base">
|
||||||
|
<div className="h-64 overflow-y-auto font-mono text-xs">
|
||||||
{entries.length === 0 ? (
|
{entries.length === 0 ? (
|
||||||
<p>No socket activity yet.</p>
|
<p>No socket activity yet.</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -133,7 +134,7 @@ export default function SocketLogPanel() {
|
|||||||
.slice()
|
.slice()
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((entry) => (
|
.map((entry) => (
|
||||||
<div key={entry.id} className="surface">
|
<div key={entry.id}>
|
||||||
<span className="text-slate-400">{entry.timestamp}</span>{' '}
|
<span className="text-slate-400">{entry.timestamp}</span>{' '}
|
||||||
<span className={entry.direction === 'in' ? 'text-emerald-400' : 'text-amber-400'}>
|
<span className={entry.direction === 'in' ? 'text-emerald-400' : 'text-amber-400'}>
|
||||||
[{entry.direction.toUpperCase()}]
|
[{entry.direction.toUpperCase()}]
|
||||||
@@ -145,6 +146,6 @@ export default function SocketLogPanel() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-500">Logs are collected only while this panel is open.</p>
|
<p className="text-xs text-slate-500">Logs are collected only while this panel is open.</p>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ function TopDownMapContent({ sensors = {}, variant = 'full', size: overrideSize,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${overlay ? 'relative' : 'surface relative p-1'}`}
|
className={`${overlay ? 'relative' : 'relative p-1'}`}
|
||||||
style={overlay ? { width: `${size}px`, height: `${size}px` } : { height: '100%', width: '100%', aspectRatio: '1 / 1' }}
|
style={overlay ? { width: `${size}px`, height: `${size}px` } : { height: '100%', width: '100%', aspectRatio: '1 / 1' }}
|
||||||
>
|
>
|
||||||
<svg width="100%" height="100%" viewBox={`0 0 ${size} ${size}`} preserveAspectRatio="xMidYMid meet" className="mx-auto block">
|
<svg width="100%" height="100%" viewBox={`0 0 ${size} ${size}`} preserveAspectRatio="xMidYMid meet" className="mx-auto block">
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import CardFrame from '../CardFrame/index.jsx';
|
|||||||
|
|
||||||
export function NicknameEntryPanel({ compact = false }) {
|
export function NicknameEntryPanel({ compact = false }) {
|
||||||
return (
|
return (
|
||||||
<section className="panel-section flex h-full min-h-0 flex-col gap-0.5 text-base">
|
<CardFrame hideHeader className="h-full" bodyClassName="flex h-full min-h-0 flex-col gap-0.5 text-base">
|
||||||
<div className="surface flex w-full items-center px-0 py-0">
|
<div className="flex w-full items-center px-0 py-0">
|
||||||
<NicknameForm compact={compact} />
|
<NicknameForm compact={compact} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ export default function UserListPanel({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={user.socketId}
|
key={user.socketId}
|
||||||
className={`surface-muted flex items-center gap-0.5 ${compact ? 'py-0.25 text-[0.8rem]' : 'text-sm'}`}
|
className={`flex items-center gap-0.5 ${compact ? 'py-0.25 text-[0.8rem]' : 'text-sm'}`}
|
||||||
>
|
>
|
||||||
<p className={`font-semibold ${roleColors(user.role)}`}>{formatLabel(user, selfId)}</p>
|
<p className={`font-semibold ${roleColors(user.role)}`}>{formatLabel(user, selfId)}</p>
|
||||||
{user.roverId ? (
|
{user.roverId ? (
|
||||||
@@ -150,14 +150,19 @@ export default function UserListPanel({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<CardFrame
|
||||||
className={`panel-section space-y-0.5 text-base ${fillHeight ? 'flex h-full min-h-0 flex-col overflow-hidden' : ''} ${className}`}
|
title={!hideHeader ? (isTurnsMode ? (showQueuesSection ? 'Turn queues' : 'Users') : 'Users') : ''}
|
||||||
|
meta={!hideHeader ? (showQueuesSection && isTurnsMode ? Object.keys(turnQueues || {}).length : sorted.length) : null}
|
||||||
|
hideHeader={hideHeader}
|
||||||
|
fillHeight={fillHeight}
|
||||||
|
className={className}
|
||||||
|
bodyClassName="space-y-0.5 text-base"
|
||||||
>
|
>
|
||||||
{!hideNicknameForm && (
|
{!hideNicknameForm && (
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<div className="grid gap-0.5 md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
|
<div className="grid gap-0.5 md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="surface flex w-full items-center px-0 py-0">
|
<div className="flex w-full items-center px-0 py-0">
|
||||||
<NicknameForm compact={compact} />
|
<NicknameForm compact={compact} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,21 +173,8 @@ export default function UserListPanel({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`space-y-0.5 ${fillHeight ? 'flex flex-1 min-h-0 flex-col' : ''}`}>
|
<div className={`space-y-0.5 ${fillHeight ? 'flex flex-1 min-h-0 flex-col' : ''}`}>
|
||||||
{!hideHeader && (
|
{!hideHeader && showToggle ? (
|
||||||
<div className={`flex items-center justify-between text-sm text-slate-400 ${compact ? 'text-xs' : ''}`}>
|
<div className="flex justify-end">
|
||||||
<div className="flex items-center gap-0.5">
|
|
||||||
<span>
|
|
||||||
{isTurnsMode
|
|
||||||
? showQueuesSection
|
|
||||||
? 'Turn queues'
|
|
||||||
: 'Users'
|
|
||||||
: 'Users'}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-slate-500">
|
|
||||||
{showQueuesSection && isTurnsMode ? Object.keys(turnQueues || {}).length : sorted.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{showToggle ? (
|
|
||||||
<div className="inline-flex overflow-hidden rounded border border-slate-700 text-[0.7rem]">
|
<div className="inline-flex overflow-hidden rounded border border-slate-700 text-[0.7rem]">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -199,11 +191,10 @@ export default function UserListPanel({
|
|||||||
Users
|
Users
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
{showQueuesSection ? (
|
{showQueuesSection ? (
|
||||||
<div className={`surface space-y-0.5 px-0 pb-0 ${turnsListClass} ${compact ? 'text-[0.8rem]' : ''}`}>
|
<div className={`space-y-0.5 px-0 pb-0 ${turnsListClass} ${compact ? 'text-[0.8rem]' : ''}`}>
|
||||||
{Object.keys(turnQueues || {}).length === 0 ? (
|
{Object.keys(turnQueues || {}).length === 0 ? (
|
||||||
<p className="text-sm text-slate-500">No turn queues yet.</p>
|
<p className="text-sm text-slate-500">No turn queues yet.</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -220,7 +211,7 @@ export default function UserListPanel({
|
|||||||
: queue[0]
|
: queue[0]
|
||||||
: null;
|
: null;
|
||||||
return (
|
return (
|
||||||
<div key={roverId} className={`surface-muted flex flex-col gap-0.5 ${compact ? 'text-[0.8rem] py-0.25' : 'text-sm'}`}>
|
<div key={roverId} className={`flex flex-col gap-0.5 ${compact ? 'text-[0.8rem] py-0.25' : 'text-sm'}`}>
|
||||||
<div className="flex items-center gap-0.5">
|
<div className="flex items-center gap-0.5">
|
||||||
<p className="font-semibold text-slate-200">
|
<p className="font-semibold text-slate-200">
|
||||||
<span
|
<span
|
||||||
@@ -277,7 +268,7 @@ export default function UserListPanel({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{showUsersSection ? (
|
{showUsersSection ? (
|
||||||
<div className={`surface space-y-0.5 px-0 pb-0 ${usersListClass} ${compact ? 'text-[0.8rem]' : ''}`}>
|
<div className={`space-y-0.5 px-0 pb-0 ${usersListClass} ${compact ? 'text-[0.8rem]' : ''}`}>
|
||||||
{renderUserList()}
|
{renderUserList()}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -288,10 +279,10 @@ export default function UserListPanel({
|
|||||||
<span>Users</span>
|
<span>Users</span>
|
||||||
<span className="text-[0.7rem] text-slate-500">{sorted.length}</span>
|
<span className="text-[0.7rem] text-slate-500">{sorted.length}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`surface space-y-0.5 px-0 pb-0 ${usersListClass}`}>{renderUserList()}</div>
|
<div className={`space-y-0.5 px-0 pb-0 ${usersListClass}`}>{renderUserList()}</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
export const PRIDE_THEME_ENABLED = true;
|
export const PRIDE_THEME_ENABLED = true;
|
||||||
|
|
||||||
export const pageBackgroundClass = PRIDE_THEME_ENABLED ? 'pride-page-bg' : 'bg-black';
|
export const pageBackgroundClass = PRIDE_THEME_ENABLED ? 'pride-page-bg' : 'bg-black';
|
||||||
export const themeGapClass = PRIDE_THEME_ENABLED ? 'gap-1' : 'gap-0.5';
|
// export const themeGapClass = PRIDE_THEME_ENABLED ? 'gap-1' : 'gap-0.5';
|
||||||
export const themeStackClass = PRIDE_THEME_ENABLED ? 'space-y-1' : 'space-y-0.5';
|
// export const themeStackClass = PRIDE_THEME_ENABLED ? 'space-y-1' : 'space-y-0.5';
|
||||||
|
|
||||||
|
// permanent smaller gaps i think is better probably
|
||||||
|
export const themeGapClass = 'gap-0.5';
|
||||||
|
export const themeStackClass = 'space-y-0.5';
|
||||||
|
|||||||
Reference in New Issue
Block a user