coloooors

This commit is contained in:
legop3
2025-11-18 15:36:56 -05:00
parent 457ad42565
commit fb5b166a37
31 changed files with 292 additions and 327 deletions
+8 -12
View File
@@ -57,14 +57,10 @@ export default function AdminPanel() {
if (!isAdmin) return null;
return (
<section className="rounded-sm bg-[#242a32] p-1 text-base text-slate-100">
<div className="flex items-center justify-between gap-1 text-sm">
<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="rounded-sm bg-black/40 px-1 py-1 text-sm text-slate-100"
>
<select value={currentMode} onChange={handleModeChange} className="field-input text-sm">
{MODES.map((mode) => (
<option key={mode.key} value={mode.key}>
{mode.label}
@@ -73,25 +69,25 @@ export default function AdminPanel() {
</select>
</div>
<div className="mt-1 space-y-1 text-sm">
<div className="space-y-0.5 text-sm">
{roster.map((rover) => (
<div key={rover.id} className="flex flex-wrap items-center justify-between gap-1 rounded-sm bg-black/30 px-1 py-1">
<div key={rover.id} className="surface flex flex-wrap items-center justify-between gap-0.5">
<div>
<p className="text-slate-100">{rover.name}</p>
<p className="text-xs text-slate-400">{lockMap[rover.id] ? 'locked' : 'unlocked'}</p>
</div>
<div className="flex flex-wrap gap-1 text-xs">
<div className="flex flex-wrap gap-0.5 text-xs">
<button
type="button"
onClick={() => handleLockToggle(rover.id, !lockMap[rover.id])}
className="rounded-sm bg-black/50 px-1 py-1 text-slate-100"
className="button-dark"
>
{lockMap[rover.id] ? 'Unlock' : 'Lock'}
</button>
<button
type="button"
onClick={() => handleForceControl(rover.id)}
className="rounded-sm bg-black/50 px-1 py-1 text-slate-100"
className="button-dark"
>
Force
</button>
+3 -3
View File
@@ -30,7 +30,7 @@ export default function AlertFeed() {
if (!visible.length) return null;
return (
<div className="pointer-events-none fixed top-1 left-1/2 z-50 flex -translate-x-1/2 flex-col gap-1">
<div className="pointer-events-none fixed top-0.5 left-1/2 z-50 flex -translate-x-1/2 flex-col gap-0.5">
{visible.map((toast) => (
<AlertToast key={toast.key} alert={toast.alert} />
))}
@@ -46,8 +46,8 @@ function AlertToast({ alert }) {
? 'bg-amber-500/30 text-amber-100'
: 'bg-emerald-500/30 text-emerald-100';
return (
<div className={`pointer-events-auto rounded-sm px-1 py-1 text-[0.75rem] ${colorClasses}`}>
<p className="text-[0.65rem] text-slate-300">{alert.title || 'Alert'}</p>
<div className={`pointer-events-auto px-0.5 py-0.5 text-[0.85rem] ${colorClasses}`}>
<p className="text-xs text-slate-200">{alert.title || 'Alert'}</p>
<p className="text-sm text-white">{alert.message}</p>
</div>
);
+8 -20
View File
@@ -22,43 +22,31 @@ export default function AuthPanel() {
}
return (
<div className="rounded-sm bg-[#242a32] p-1 text-base text-slate-100">
<div className="panel-section space-y-0.5 text-base">
<p className="text-sm text-slate-400">Admin login</p>
<form className="mt-1 flex flex-col gap-1" onSubmit={handleLogin}>
<form className="flex flex-col gap-0.5" onSubmit={handleLogin}>
<input
className="rounded-sm bg-black/50 px-1 py-1 text-slate-100"
className="field-input"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
className="rounded-sm bg-black/50 px-1 py-1 text-slate-100"
className="field-input"
placeholder="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
type="submit"
disabled={loading}
className="rounded-sm bg-slate-200 px-1 py-1 text-sm font-semibold text-black disabled:opacity-50"
>
<button type="submit" disabled={loading} className="button-dark w-full disabled:opacity-50">
{loading ? 'Logging in…' : 'Login'}
</button>
</form>
<div className="mt-1 flex gap-1 text-sm">
<button
type="button"
onClick={() => setRole('user')}
className="flex-1 rounded-sm bg-black/40 px-1 py-1 text-slate-200"
>
<div className="flex gap-0.5 text-sm">
<button type="button" onClick={() => setRole('user')} className="flex-1 button-dark">
Driver
</button>
<button
type="button"
onClick={() => setRole('spectator')}
className="flex-1 rounded-sm bg-black/40 px-1 py-1 text-slate-200"
>
<button type="button" onClick={() => setRole('spectator')} className="flex-1 button-dark">
Spectator
</button>
</div>
+8 -20
View File
@@ -81,12 +81,12 @@ export default function CameraServoPanel() {
};
return (
<section className="rounded-sm bg-[#242a32] p-1 text-base text-slate-100">
<section className="panel-section space-y-0.5 text-base">
<div className="flex items-center justify-between text-sm text-slate-300">
<span>Camera Tilt</span>
<span className="font-mono text-sm text-slate-100">{formatDegrees(value)}</span>
</div>
<div className="mt-2">
<div>
<input
type="range"
className="w-full accent-emerald-400"
@@ -99,35 +99,23 @@ export default function CameraServoPanel() {
onTouchEnd={commitSlider}
onPointerUp={commitSlider}
/>
<div className="mt-1 flex justify-between text-xs text-slate-400">
<div className="mt-0.5 flex justify-between text-xs text-slate-400">
<span>{formatDegrees(min)}</span>
<span>{formatDegrees(max)}</span>
</div>
</div>
<div className="mt-2 flex gap-1 text-sm">
<button
type="button"
className="flex-1 rounded bg-slate-800 px-1 py-0.5 text-slate-100 hover:bg-slate-700"
onClick={() => handleNudge(-1)}
>
<div className="flex gap-0.5 text-sm">
<button type="button" className="flex-1 button-dark" onClick={() => handleNudge(-1)}>
Tilt Down
</button>
<button
type="button"
className="flex-1 rounded bg-slate-700 px-1 py-0.5 text-slate-100 hover:bg-slate-600"
onClick={() => goServoHome()}
>
<button type="button" className="flex-1 button-dark" onClick={() => goServoHome()}>
Center
</button>
<button
type="button"
className="flex-1 rounded bg-slate-800 px-1 py-0.5 text-slate-100 hover:bg-slate-700"
onClick={() => handleNudge(1)}
>
<button type="button" className="flex-1 button-dark" onClick={() => handleNudge(1)}>
Tilt Up
</button>
</div>
<p className="mt-1 text-xs text-slate-400">
<p className="text-xs text-slate-400">
Step: {formatDegrees(step)} · Pin GPIO {config?.pin}
</p>
</section>
+11 -8
View File
@@ -27,12 +27,12 @@ export default function DrivePanel() {
};
return (
<section className="rounded-sm bg-[#242a32] p-1 text-base text-slate-100">
<section className="panel-section space-y-0.5 text-base">
<div className="flex items-center justify-between text-sm text-slate-300">
<span>Drive control</span>
<span>{roverId ? `Rover ${roverId}` : 'unassigned'}</span>
<span>{roverId ? 'Ready to drive' : 'No rover assigned'}</span>
</div>
<div className="mt-1 space-y-1">
<div className="space-y-0.5">
<ActionCard
title="Start Driving"
description="Press to enable driving mode, then start moving. The headlamps should illuminate."
@@ -58,21 +58,24 @@ export default function DrivePanel() {
}
function ActionCard({ title, description, statuses, tone, onClick, disabled, footnote }) {
const baseColor = tone === 'indigo' ? 'bg-indigo-600' : 'bg-emerald-600';
const colors =
tone === 'indigo'
? { base: 'bg-indigo-600', hover: 'hover:bg-indigo-500' }
: { base: 'bg-emerald-600', hover: 'hover:bg-emerald-500' };
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
className={`w-full rounded-lg ${baseColor} px-1 py-1 text-left text-white disabled:opacity-40`}
className={`w-full ${colors.base} ${colors.hover} px-0.5 py-0.5 text-left text-white transition-colors disabled:opacity-40`}
>
<p className="text-base font-semibold">{title}</p>
<p className="text-sm text-white/90">{description}</p>
<div className="mt-1 flex flex-wrap gap-1">
<div className="mt-0.5 flex flex-wrap gap-0.5">
{statuses.map((status) => (
<span
key={status.label}
className={`rounded-full px-1 py-0.5 text-xs font-semibold ${
className={`px-0.5 py-0.5 text-xs font-semibold ${
status.active ? 'bg-lime-300 text-emerald-900' : 'bg-emerald-900 text-emerald-100'
}`}
>
@@ -80,7 +83,7 @@ function ActionCard({ title, description, statuses, tone, onClick, disabled, foo
</span>
))}
</div>
{footnote && <p className="mt-1 text-xs text-emerald-50/80">{footnote}</p>}
{footnote && <p className="mt-0.5 text-xs text-emerald-50/80">{footnote}</p>}
</button>
);
}
+5 -11
View File
@@ -15,20 +15,14 @@ export default function DriverVideoPanel() {
: null;
const batteryConfig = batteryRecord?.battery ?? null;
const roverLabel = batteryRecord?.name || (roverId ? `Rover ${roverId}` : '');
return (
<section className="rounded-sm bg-[#242a32] p-1">
<section className="panel">
{roverId ? (
<div className="w-full">
<VideoTile
sessionInfo={info}
label={roverId}
muted={false}
telemetryFrame={frame}
batteryConfig={batteryConfig}
/>
</div>
<VideoTile sessionInfo={info} label={roverLabel} telemetryFrame={frame} batteryConfig={batteryConfig} />
) : (
<p className="text-sm text-slate-400">Assign a rover to view the video feed.</p>
<p className="panel-muted">Assign a rover to view the video feed.</p>
)}
</section>
);
+17 -25
View File
@@ -42,7 +42,7 @@ const ACTIONS = [
function SliderField({ label, description, min, max, step, value, onChange }) {
return (
<label className="block rounded border border-white/5 p-1">
<label className="surface block">
<div className="flex items-center justify-between text-xs text-slate-300">
<span className="font-semibold text-slate-100">{label}</span>
<span className="font-mono text-slate-400">{NUMBER_FORMAT.format(value)}</span>
@@ -55,7 +55,7 @@ function SliderField({ label, description, min, max, step, value, onChange }) {
step={step}
value={value}
onChange={(event) => onChange(Number(event.target.value))}
className="mt-1 w-full accent-emerald-400"
className="mt-0.5 w-full accent-emerald-400"
/>
</label>
);
@@ -162,31 +162,27 @@ export default function GamepadMappingSettings() {
};
return (
<section className="rounded-sm bg-[#1d232b] p-1 text-sm text-slate-100">
<section className="panel-section space-y-0.5 text-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">Gamepad mapping</p>
<p className="text-xs text-slate-400">Gamepad mapping</p>
<p className="text-[0.65rem] text-slate-500">
{gamepadConnected
? 'Move sticks/axes with a big sweep or press buttons when capturing.'
: 'Connect a controller to configure.'}
</p>
{capture && (
<p className="mt-1 text-[0.7rem] text-emerald-400">Capturing {capture.label}</p>
<p className="mt-0.5 text-[0.7rem] text-emerald-400">Capturing {capture.label}</p>
)}
</div>
<button
type="button"
onClick={() => reset()}
className="rounded-sm bg-black/40 px-2 py-1 text-xs uppercase tracking-wide text-slate-200 hover:bg-black/60"
>
<button type="button" onClick={() => reset()} className="button-dark text-xs">
Clear all
</button>
</div>
<div className="mt-2 space-y-2">
<div className="rounded border border-white/5 p-1">
<p className="text-[0.7rem] uppercase tracking-wide text-slate-500">Sensitivity &amp; feel</p>
<div className="mt-1 space-y-1">
<div className="space-y-0.5">
<div className="space-y-0.5 surface">
<p className="text-[0.7rem] text-slate-500">Sensitivity &amp; feel</p>
<div className="space-y-0.5">
<SliderField
label="Drive deadzone"
description="Ignore small drive stick movement"
@@ -226,27 +222,23 @@ export default function GamepadMappingSettings() {
</div>
</div>
{Object.entries(grouped).map(([section, actions]) => (
<div key={section} className="rounded border border-white/5 p-1">
<p className="text-[0.7rem] uppercase tracking-wide text-slate-500">{section}</p>
<div className="mt-1 space-y-1">
<div key={section} className="space-y-0.5 surface">
<p className="text-[0.7rem] text-slate-500">{section}</p>
<div className="space-y-0.5">
{actions.map((action) => (
<div key={action.id} className="flex items-center justify-between rounded bg-black/30 px-1 py-1 text-xs">
<div key={action.id} className="surface-muted flex items-center justify-between text-xs">
<div>
<p className="font-semibold text-slate-100">{action.label}</p>
<p className="text-[0.65rem] text-slate-400">{getValueLabel(action)}</p>
</div>
<div className="flex items-center gap-1">
<button
type="button"
onClick={() => handleClear(action)}
className="rounded bg-slate-800 px-2 py-1 text-[0.65rem] uppercase tracking-wide"
>
<div className="flex items-center gap-0.5">
<button type="button" onClick={() => handleClear(action)} className="button-dark text-[0.7rem]">
Clear
</button>
<button
type="button"
onClick={() => setCapture(action)}
className={`rounded px-2 py-1 text-[0.65rem] uppercase tracking-wide ${capture?.id === action.id ? 'bg-emerald-500 text-emerald-950' : 'bg-slate-700 text-slate-100'}`}
className={`${capture?.id === action.id ? 'px-0.5 py-0.5 bg-emerald-500 text-emerald-950 hover:bg-emerald-400' : 'button-dark'} text-[0.7rem] font-medium`}
>
{capture?.id === action.id ? 'Waiting…' : 'Capture'}
</button>
+3 -3
View File
@@ -18,11 +18,11 @@ export default function HelpPanel({ layout }) {
};
const tips = presets[layout] || presets.desktop;
return (
<section className="rounded-sm bg-[#242a32] p-1 text-base text-slate-100">
<section className="panel-section space-y-0.5 text-base">
<p className="text-sm text-slate-400">Help</p>
<ul className="mt-1 space-y-1 text-sm">
<ul className="space-y-0.5 text-sm">
{tips.map((tip) => (
<li key={tip} className="rounded-sm bg-black/30 px-1 py-1 text-slate-200">
<li key={tip} className="surface text-slate-200">
{tip}
</li>
))}
+12 -23
View File
@@ -71,51 +71,40 @@ export default function KeymapSettings() {
);
return (
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
<section className="panel-section space-y-0.5 text-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">Keyboard Layout</p>
<p className="text-xs text-slate-400">Keyboard layout</p>
<p className="text-[0.7rem] text-slate-500">Per-browser · click to change a binding</p>
</div>
<button
type="button"
onClick={() => resetKeyBindings()}
className="rounded-sm bg-black/40 px-2 py-1 text-xs uppercase tracking-wide text-slate-200 hover:bg-black/60"
>
Reset Defaults
<button type="button" onClick={() => resetKeyBindings()} className="button-dark text-xs">
Reset defaults
</button>
</div>
<div className="mt-2 space-y-2">
<div className="space-y-0.5">
{Object.entries(grouped).map(([group, actions]) => (
<div key={group} className="rounded border border-white/5 p-1">
<p className="text-[0.7rem] uppercase tracking-wide text-slate-400">{group}</p>
<div className="mt-1 space-y-1">
<div key={group} className="space-y-0.5 surface">
<p className="text-[0.75rem] text-slate-400">{group}</p>
<div className="space-y-0.5">
{actions.map((action) => {
const value = currentKey(action.id);
const isActive = active === action.id;
return (
<div
key={action.id}
className="flex items-center justify-between rounded bg-black/30 px-1 py-1 text-xs"
>
<div key={action.id} className="surface-muted flex items-center justify-between text-xs">
<div>
<p className="font-semibold text-slate-100">{action.label}</p>
<p className="text-[0.65rem] text-slate-400">{formatKeyLabel(value)}</p>
</div>
<div className="flex items-center gap-1">
<div className="flex items-center gap-0.5">
{isActive && (
<button
type="button"
onClick={() => cancel()}
className="rounded bg-red-600 px-2 py-1 text-[0.65rem] uppercase tracking-wide text-red-100"
>
<button type="button" onClick={() => cancel()} className="button-danger text-[0.7rem]">
Cancel
</button>
)}
<button
type="button"
onClick={() => startCapture(action.id)}
className={`rounded px-2 py-1 text-[0.65rem] uppercase tracking-wide ${isActive ? 'bg-emerald-500 text-emerald-950' : 'bg-slate-700 text-slate-100'}`}
className={`${isActive ? 'px-0.5 py-0.5 bg-emerald-500 text-emerald-950 hover:bg-emerald-400' : 'button-dark'} text-[0.7rem] font-medium transition-colors`}
>
{isActive ? 'Press a key…' : 'Change'}
</button>
+3 -3
View File
@@ -3,12 +3,12 @@ import { useSession } from '../context/SessionContext.jsx';
export default function LogPanel() {
const { logs } = useSession();
return (
<div className="rounded-sm bg-[#242a32] p-1 text-base text-slate-100">
<div className="panel-section space-y-0.5 text-base">
<div className="flex items-center justify-between text-sm text-slate-400">
<span>Server logs</span>
<span>{logs.length}</span>
</div>
<div className="mt-1 h-64 overflow-y-auto rounded-sm bg-black/40 p-1 font-mono text-xs">
<div className="surface h-64 overflow-y-auto space-y-0.5 font-mono text-xs">
{logs.length === 0 ? (
<p>No logs yet.</p>
) : (
@@ -16,7 +16,7 @@ export default function LogPanel() {
.slice()
.reverse()
.map((entry) => (
<div key={entry.id} className="mb-1">
<div key={entry.id} className="surface-muted">
<span className="text-amber-400">{entry.timestamp}</span>{' '}
<span className="text-lime-400">[{entry.level}]</span>{' '}
{entry.label && <span className="text-teal-400">[{entry.label}]</span>}{' '}
+16 -21
View File
@@ -96,7 +96,7 @@ function FloatingJoystick({ disabled, layout, radius, onMove, onStop }) {
<div
ref={containerRef}
role="presentation"
className={`relative w-full ${heightClass} select-none overflow-hidden rounded-lg border border-slate-800 bg-slate-900/70`}
className={`relative w-full ${heightClass} select-none overflow-hidden bg-zinc-950`}
style={{ touchAction: 'none' }}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
@@ -114,11 +114,11 @@ function FloatingJoystick({ disabled, layout, radius, onMove, onStop }) {
{visual.active && (
<>
<div
className="pointer-events-none absolute h-28 w-28 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-cyan-400/60 bg-cyan-400/10"
className="pointer-events-none absolute h-28 w-28 -translate-x-1/2 -translate-y-1/2 bg-cyan-400/10 outline outline-2 outline-cyan-400/60 [clip-path:circle(50%)]"
style={{ left: visual.base.x, top: visual.base.y }}
/>
<div
className="pointer-events-none absolute h-12 w-12 -translate-x-1/2 -translate-y-1/2 rounded-full bg-cyan-300/80 shadow-lg"
className="pointer-events-none absolute h-12 w-12 -translate-x-1/2 -translate-y-1/2 bg-cyan-300/80 shadow-lg [clip-path:circle(50%)]"
style={{
left: visual.base.x + visual.knob.x,
top: visual.base.y + visual.knob.y,
@@ -195,7 +195,7 @@ function MobileJoystickPanel({ layout }) {
};
return (
<div className="flex flex-col gap-1 text-slate-100">
<div className="flex flex-col gap-0.5 text-slate-100">
<DriveModeToggle size="compact" />
<FloatingJoystick
disabled={disabled}
@@ -204,17 +204,12 @@ function MobileJoystickPanel({ layout }) {
onMove={handleMove}
onStop={handleStop}
/>
<button
type="button"
onClick={stopAllMotion}
disabled={disabled}
className="rounded bg-red-600 px-2 py-2 text-xs font-semibold uppercase tracking-wide text-red-100 disabled:opacity-40"
>
<button type="button" onClick={stopAllMotion} disabled={disabled} className="button-danger w-full disabled:opacity-40">
Panic Stop
</button>
{cameraEnabled && (
<div className="rounded-lg bg-slate-900/70 p-2 text-xs">
<div className="flex items-center justify-between text-[0.7rem] uppercase tracking-wide text-slate-400">
<div className="bg-zinc-950 p-0.5 text-xs">
<div className="flex items-center justify-between text-[0.75rem] text-slate-400">
<span>Camera Tilt</span>
<span className="font-mono text-slate-200">{cameraValue.toFixed(1)}°</span>
</div>
@@ -225,7 +220,7 @@ function MobileJoystickPanel({ layout }) {
step={0.5}
value={cameraValue}
onChange={handleCameraSlider}
className="mt-1 w-full accent-cyan-400"
className="mt-0.5 w-full accent-cyan-400"
/>
</div>
)}
@@ -269,9 +264,9 @@ function AuxMotorPanel({ orientation }) {
const gridCols = orientation === 'landscape' ? 'grid-cols-1' : 'grid-cols-1 sm:grid-cols-2';
return (
<div className="flex flex-col gap-1 text-slate-100">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">Aux controls</p>
<div className={`grid ${gridCols} gap-1`}>
<div className="flex flex-col gap-0.5 text-slate-100">
<p className="text-xs text-slate-400">Aux controls</p>
<div className={`grid ${gridCols} gap-0.5`}>
{AUX_BUTTONS.map((button) => (
<button
key={button.id}
@@ -285,7 +280,7 @@ function AuxMotorPanel({ orientation }) {
onPointerLeave={() => handleRelease(button)}
onPointerCancel={() => handleRelease(button)}
onContextMenu={(event) => event.preventDefault()}
className={`rounded-lg px-2 py-2 text-left text-sm font-semibold uppercase tracking-wide text-white transition ${button.color} disabled:opacity-30`}
className={`px-0.5 py-0.5 text-left text-sm font-semibold text-white transition ${button.color} hover:brightness-110 disabled:opacity-30`}
>
{button.label}
</button>
@@ -297,7 +292,7 @@ function AuxMotorPanel({ orientation }) {
export function MobileLandscapeAuxColumn() {
return (
<div className="flex h-full flex-col gap-1">
<div className="flex h-full flex-col gap-0.5">
<AuxMotorPanel orientation="landscape" />
</div>
);
@@ -305,7 +300,7 @@ export function MobileLandscapeAuxColumn() {
export function MobileLandscapeControlColumn() {
return (
<div className="flex h-full flex-col gap-1">
<div className="flex h-full flex-col gap-0.5">
<MobileJoystickPanel layout="landscape" />
</div>
);
@@ -313,8 +308,8 @@ export function MobileLandscapeControlColumn() {
export default function MobilePortraitControls() {
return (
<section className="w-full">
<div className="grid grid-cols-2 gap-1">
<section className="panel">
<div className="grid grid-cols-2 gap-0.5">
<AuxMotorPanel orientation="portrait" />
<MobileJoystickPanel layout="portrait" />
</div>
+4 -4
View File
@@ -33,13 +33,13 @@ export default function ModeGateOverlay() {
const details = getModeDetails(mode);
return (
<div className="pointer-events-auto fixed inset-0 z-50 flex items-center justify-center bg-black/85 px-2 py-2">
<div className="w-full max-w-md space-y-2 rounded-lg bg-[#141821] p-2 text-slate-100 shadow-2xl">
<div className="space-y-1">
<div className="pointer-events-auto fixed inset-0 z-50 flex items-center justify-center bg-black/85 px-0.5 py-0.5">
<div className="surface w-full max-w-md space-y-0.5 text-slate-100 shadow-2xl">
<div className="space-y-0.5">
<p className="text-lg font-semibold">{details.title}</p>
<p className="text-sm text-slate-300">{details.description}</p>
</div>
<div className="rounded-md bg-black/40 p-1">
<div className="surface-muted">
<AuthPanel />
</div>
<p className="text-xs text-slate-500">
+6 -6
View File
@@ -8,16 +8,16 @@ import Tabs, { Tab, TabList, TabPanel, TabPanels } from './Tabs.jsx';
function RoomControlsPlaceholder() {
return (
<div className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
<p className="text-xs text-slate-400">Room controls</p>
<p className="text-sm text-slate-200">Coming soon: lighting, docks, and environmental toggles.</p>
<div className="panel-section space-y-0.5 text-sm">
<p className="text-slate-400">Room controls</p>
<p className="text-slate-200">Coming soon: lighting, docks, and environmental toggles.</p>
</div>
);
}
export default function RightPaneTabs({ layout }) {
return (
<section className="rounded-sm bg-[#1a1d23] p-1 text-base text-slate-100">
<section className="panel text-base">
<Tabs defaultTab="telemetry">
<TabList>
<Tab id="telemetry">Rover Controls</Tab>
@@ -27,14 +27,14 @@ export default function RightPaneTabs({ layout }) {
</TabList>
<TabPanels>
<TabPanel id="telemetry">
<div className="space-y-1">
<div className="space-y-0.5">
<DrivePanel />
<CameraServoPanel />
<TelemetryPanel />
</div>
</TabPanel>
<TabPanel id="room">
<div className="space-y-1">
<div className="space-y-0.5">
<RoomCameraPanel />
<RoomControlsPlaceholder />
</div>
+2 -2
View File
@@ -1,8 +1,8 @@
export default function RoomCameraPanel() {
return (
<div className="min-h-[8rem] rounded-sm bg-[#242a32] p-1 text-base text-slate-100">
<div className="panel-section min-h-[8rem] space-y-0.5 text-base">
<p className="text-center text-sm text-slate-400">Room camera</p>
<p className="mt-1 text-center">Feed placeholder. Wire upcoming room cam here.</p>
<p className="text-center text-slate-200">Feed placeholder. Wire upcoming room cam here.</p>
</div>
);
}
+2 -2
View File
@@ -5,9 +5,9 @@ export default function SessionSnapshot() {
const { session } = useSession();
const payload = useMemo(() => JSON.stringify(session ?? {}, null, 2), [session]);
return (
<div className="rounded-sm bg-[#242a32] p-1 text-xs text-slate-100">
<div className="panel-section space-y-0.5 text-xs">
<p className="text-sm text-slate-400">Session snapshot</p>
<pre className="mt-1 h-64 overflow-y-auto bg-black/40 p-1 text-[0.7rem] text-lime-300">{payload}</pre>
<pre className="surface h-64 overflow-y-auto font-mono text-[0.7rem] text-lime-300">{payload}</pre>
</div>
);
}
+12 -12
View File
@@ -42,49 +42,49 @@ export default function SettingsPanel() {
</TabList>
<TabPanels>
<TabPanel id="keybindings">
<div className="space-y-1">
<div className="space-y-0.5">
<KeymapSettings />
</div>
</TabPanel>
<TabPanel id="controller">
<div className="space-y-1">
<div className="space-y-0.5">
<GamepadMappingSettings />
</div>
</TabPanel>
<TabPanel id="admin">
<div className="space-y-1">
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
<p className="text-xs text-slate-400">Manual OI commands</p>
<div className="mt-1 flex flex-wrap gap-1">
<div className="space-y-0.5">
<section className="panel-section space-y-0.5 text-sm">
<p className="text-slate-400">Manual OI commands</p>
<div className="surface flex flex-wrap gap-0.5">
{manualTabs.map((tab) => (
<button
key={tab.key}
type="button"
onClick={() => sendOiCommand(tab.key)}
disabled={!canControl}
className="rounded-sm bg-black/40 px-1 py-0.5 text-xs text-slate-200 disabled:opacity-30"
className="button-dark text-xs disabled:opacity-30"
>
{tab.label}
</button>
))}
</div>
</section>
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
<p className="text-xs text-slate-400">Sensor stream</p>
<div className="mt-1 flex gap-1">
<section className="panel-section space-y-0.5 text-sm">
<p className="text-slate-400">Sensor stream</p>
<div className="surface flex gap-0.5">
{sensorButtons.map((btn) => (
<button
key={btn.key}
type="button"
onClick={() => handleSensorToggle(btn.enable)}
disabled={!canControl}
className="flex-1 rounded-sm bg-black/40 px-1 py-1 text-xs text-slate-200 disabled:opacity-30"
className="flex-1 button-dark text-xs disabled:opacity-30"
>
{btn.label}
</button>
))}
</div>
{!canControl && <p className="mt-1 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>
<AuthPanel />
<AdminPanel />
+5 -5
View File
@@ -12,9 +12,9 @@ function useTabsContext() {
const TAB_VARIANTS = {
primary: {
base: 'flex-1 rounded-sm px-1 py-1 text-xs font-semibold transition-colors focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-slate-500',
active: 'bg-slate-100 text-slate-900',
inactive: 'bg-black/30 text-slate-300 hover:text-slate-100',
base: 'flex-1 px-0.5 py-0.5 text-sm font-medium transition-colors focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-slate-500',
active: 'bg-sky-600 text-white',
inactive: 'bg-zinc-800 text-slate-200 hover:bg-sky-500 hover:text-white',
},
};
@@ -86,7 +86,7 @@ export default function Tabs({ children, defaultTab, currentTab, onTabChange, va
}
export function TabList({ children, className = '' }) {
return <div className={classNames('flex gap-1', className)}>{children}</div>;
return <div className={classNames('flex gap-0.5', className)}>{children}</div>;
}
export function Tab({ id, children, className = '', disabled = false }) {
@@ -118,7 +118,7 @@ export function Tab({ id, children, className = '', disabled = false }) {
}
export function TabPanels({ children, className = '' }) {
return <div className={classNames('mt-1 space-y-1', className)}>{children}</div>;
return <div className={classNames('mt-0.5 space-y-0.5', className)}>{children}</div>;
}
export function TabPanel({ id, children, keepMounted = false }) {
+14 -14
View File
@@ -47,7 +47,7 @@ export default function TelemetryPanel() {
}
return (
<section className="rounded-sm bg-[#242a32] p-1 text-base text-slate-100">
<section className="panel-section space-y-0.5 text-base text-slate-100">
<div className="text-sm text-slate-400">
<span>{connected ? 'online' : 'offline'}</span>
<span> · role {session?.role || 'unknown'}</span>
@@ -56,12 +56,12 @@ export default function TelemetryPanel() {
<span> · driver {driverLabel}</span>
</div>
{!roverId ? (
<p className="mt-1 text-[0.75rem] text-slate-400">Assign a rover to view sensors.</p>
<p className="text-sm text-slate-500">Assign a rover to view sensors.</p>
) : !frame ? (
<p className="mt-1 text-[0.75rem] text-slate-400">Waiting for sensor frames</p>
<p className="text-sm text-slate-500">Waiting for sensor frames</p>
) : (
<>
<div className="flex flex-wrap gap-0.5">
<div className="flex flex-wrap gap-0.5 text-sm">
<Metric label="Charge" value={formatMetric(charge != null && capacity != null ? `${charge}/${capacity}` : null)} />
<Metric label="Charging" value={formatMetric(sensors.chargingState?.label)} />
<Metric label="OI mode" value={formatMetric(sensors.oiMode?.label)} />
@@ -70,7 +70,7 @@ export default function TelemetryPanel() {
<Metric label="Left Encoder" value={formatMetric(sensors.encoderCountsLeft)} />
<Metric label="Right Encoder" value={formatMetric(sensors.encoderCountsRight)} />
</div>
<div className="mt-1 flex gap-0.5">
<div className="flex gap-0.5">
<CliffBar value={sensors.cliffLeftSignal} />
<CliffBar value={sensors.cliffFrontLeftSignal} />
<CliffBar value={sensors.cliffFrontRightSignal} />
@@ -80,24 +80,24 @@ export default function TelemetryPanel() {
)}
{rawSnippet && (
// console.log('rawSnippet:', rawSnippet),
<pre className="mt-1 text-wrap break-words p-1 text-xs text-lime-300 bg-gray-700">
<pre className="surface whitespace-pre-wrap break-words text-xs text-lime-300">
{rawSnippet}
</pre>
)}
<div className="mt-1">
<div className="space-y-0.5">
<p className="text-sm text-slate-400">Rovers</p>
{roster.length === 0 ? (
// if there ARE NO rovers
<p className="text-sm text-slate-500">No roster data.</p>
) : (
// if there ARE rovers
<ul className="mt-1 space-y-1 text-sm">
<ul className="space-y-0.5 text-sm">
{roster.map((rover) => (
<li key={rover.id} className="flex items-center justify-between gap-1">
<li key={rover.id} className="surface flex items-center justify-between gap-0.5">
<div>
<p className="text-slate-200">{rover.name}</p>
<p className="text-xs text-slate-500">
@@ -109,7 +109,7 @@ export default function TelemetryPanel() {
type="button"
onClick={() => handleRequest(rover.id)}
disabled={pending[rover.id]}
className="rounded-sm bg-black/40 px-1 py-0.5 text-xs text-slate-200 disabled:opacity-40"
className="button-dark disabled:opacity-40"
>
{pending[rover.id] ? '...' : 'request'}
</button>
@@ -130,15 +130,15 @@ function CliffBar({ value }) {
const pct = value == null ? 0 : Math.max(0, Math.min(100, (value / 4095) * 100));
const height = `${pct}%`;
return (
<div className="w-1/4 h-6 bg-gray-700 rounded-sm overflow-hidden">
<div className="w-full bg-amber-400" style={{ height }} />
<div className="surface-muted h-6 w-1/4">
<div className="w-full bg-amber-500" style={{ height }} />
</div>
);
}
function Metric({ label, value }) {
return (
<div className="bg-gray-700 rounded-sm p-0.5 text-sm">
<div className="surface text-sm">
{label}: {value}
</div>
);
+32 -67
View File
@@ -34,6 +34,17 @@ export default function VideoTile({ sessionInfo, label, forceMute = false, telem
restartTimer.current = setTimeout(() => setRestartToken(Date.now()), RESTART_DELAY_MS);
}, []);
const ensurePlayback = useCallback(async () => {
const video = videoRef.current;
if (!video) return;
try {
video.muted = true;
await video.play();
} catch {
// Autoplay might still be blocked; retry logic elsewhere will handle it.
}
}, []);
const attemptUnmute = useCallback(
(delay = 0) => {
if (forceMute) return;
@@ -52,6 +63,7 @@ export default function VideoTile({ sessionInfo, label, forceMute = false, telem
const video = videoRef.current;
if (!video) return;
try {
await ensurePlayback();
video.muted = false;
await video.play();
setMuted(false);
@@ -64,7 +76,7 @@ export default function VideoTile({ sessionInfo, label, forceMute = false, telem
unmuteTimer.current = setTimeout(tryPlay, delay);
},
[forceMute],
[ensurePlayback, forceMute],
);
useEffect(
@@ -92,6 +104,9 @@ export default function VideoTile({ sessionInfo, label, forceMute = false, telem
if (!active) return;
setStatus(nextStatus);
setDetail(info || null);
if (nextStatus === 'playing') {
ensurePlayback();
}
if (['error', 'failed', 'disconnected', 'closed'].includes(nextStatus)) {
scheduleRestart();
}
@@ -116,7 +131,7 @@ export default function VideoTile({ sessionInfo, label, forceMute = false, telem
clearTimeout(resetMuteId);
player?.stop();
};
}, [sessionInfo?.url, sessionInfo?.token, restartToken, scheduleRestart]);
}, [sessionInfo?.url, sessionInfo?.token, restartToken, scheduleRestart, ensurePlayback]);
useEffect(() => {
if (status === 'stopped' && sessionInfo?.url) {
@@ -133,8 +148,8 @@ export default function VideoTile({ sessionInfo, label, forceMute = false, telem
: status;
return (
<div className="flex flex-col gap-1">
<div className="relative w-full overflow-hidden rounded-sm bg-black aspect-video">
<div className="flex flex-col gap-0.5">
<div className="relative w-full overflow-hidden bg-black aspect-video">
<video
ref={videoRef}
muted={forceMute || muted}
@@ -157,12 +172,11 @@ function BatteryBar({ charge, config, label, status }) {
const urgent = config?.Urgent ?? null;
if (charge == null || full == null || warn == null) {
return (
<div className="rounded-sm bg-[#1e1e1e] px-1 py-1 text-sm text-slate-200">
<div className="panel-section space-y-0.5 text-sm">
<div className="flex items-center justify-between text-xs text-slate-400">
{/* <span>{label}</span> */}
<span>{status}</span>
</div>
<p className="mt-1 text-xs text-slate-400">Battery telemetry unavailable</p>
<p className="text-xs text-slate-500">Battery telemetry unavailable</p>
</div>
);
}
@@ -177,13 +191,12 @@ function BatteryBar({ charge, config, label, status }) {
const warnTriggered = urgent != null && charge <= urgent;
const barClass = depleted ? 'bg-red-500 animate-pulse' : warnTriggered ? 'bg-amber-400' : 'bg-emerald-500';
return (
<div className="space-y-2 rounded-sm bg-[#1e1e1e] px-1 py-1 text-sm text-slate-200">
<div className="panel-section space-y-0.5 text-sm">
<div className="flex items-center justify-between text-xs text-slate-400">
{/* <span>{label}</span> */}
<span>{status}</span>
</div>
<div className="relative h-4 w-full rounded-full bg-slate-800">
<div className={`h-full rounded-full transition-[width] ${barClass}`} style={{ width: `${percentDisplay}%` }} />
<div className="relative h-4 w-full bg-zinc-900">
<div className={`h-full transition-[width] ${barClass}`} style={{ width: `${percentDisplay}%` }} />
<span className="absolute inset-0 flex items-center justify-center text-xs font-semibold text-black/80">
Battery {percentText}
</span>
@@ -204,73 +217,25 @@ function HudOverlay({ frame, label }) {
const pulse = frame?.receivedAt ? now - frame.receivedAt < 200 : false;
// const bumperBadges = [
// { label: 'B-L', active: bumps.bumpLeft },
// { label: 'B-R', active: bumps.bumpRight },
// ];
// const wheelBadges = [
// { label: 'Drop L', active: bumps.wheelDropLeft },
// { label: 'Drop R', active: bumps.wheelDropRight },
// ];
// console.log('session', session);
return (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
{/* <div className="flex items-center justify-between gap-1">
<div className="flex items-center gap-1 text-[0.55rem] text-slate-400">
<span className={`h-2 w-2 rounded-full ${pulse ? 'bg-emerald-300 shadow-[0_0_4px_rgba(16,185,129,0.8)]' : 'bg-slate-700'}`} />
<span>sensor</span>
</div>
<div className="flex gap-1">
{bumperBadges.map((badge) => (
<HudBadge key={badge.label} label={badge.label} active={badge.active} />
))}
</div>
</div>
<div className="flex justify-end gap-1">
{wheelBadges.map((badge) => (
<HudBadge key={badge.label} label={badge.label} active={badge.active} />
))}
</div> */}
{/* status that tells you the name of your rover (label) */}
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-2 rounded-full bg-gray-900/40 px-2 py-0.5 text-[0.65rem] font-medium text-slate-200">
<div className="absolute bottom-0.5 left-1/2 flex -translate-x-1/2 gap-0.5 bg-black/80 px-0.5 py-0.5 text-xs text-slate-100">
<span>You are driving {label || 'Unnamed Rover'}</span>
<span>|</span>
<span>{pulse ? 'Sensors Active' : 'No Recent Sensor Data'}</span>
<span>{pulse ? 'Sensors active' : 'No recent sensors'}</span>
</div>
{/* bump and wheel drops bar */}
<div className="absolute top-3 left-1/2 -translate-x-1/2 flex gap-1 rounded-full bg-gray-900/40 text-[0.55rem] font-medium">
<div className={`px-0.5 rounded-full ${bumps.bumpLeft ? 'bg-red-500 text-white animate-pulse' : 'bg-black/40 text-slate-500'}`}>
Left Bump
</div>
{/* left wheel drop */}
<div className={`px-0.5 rounded-full ${bumps.wheelDropLeft ? 'bg-red-500 text-white animate-pulse' : 'bg-black/40 text-slate-500'}`}>
Left Wheel Drop
</div>
{/* right wheel drop */}
<div className={`px-0.5 rounded-full ${bumps.wheelDropRight ? 'bg-red-500 text-white animate-pulse' : 'bg-black/40 text-slate-500'}`}>
Right Wheel Drop
</div>
{/* right bump */}
<div className={`px-0.5 rounded-full ${bumps.bumpRight ? 'bg-red-500 text-white animate-pulse' : 'bg-black/40 text-slate-500'}`}>
Right Bump
</div>
<div className="absolute top-0.5 left-1/2 flex -translate-x-1/2 gap-0.5 bg-black/70 px-0.5 py-0.5 text-[0.6rem] font-medium text-slate-200">
<div className={`px-0.5 py-0.5 ${bumps.bumpLeft ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Left bump</div>
<div className={`px-0.5 py-0.5 ${bumps.wheelDropLeft ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Left wheel drop</div>
<div className={`px-0.5 py-0.5 ${bumps.wheelDropRight ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Right wheel drop</div>
<div className={`px-0.5 py-0.5 ${bumps.bumpRight ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Right bump</div>
</div>
</div>
);
}
// function HudBadge({ label, active }) {
// return (
// <span
// className={`rounded-sm px-1 py-0.5 text-[0.55rem] ${active ? 'bg-emerald-500/30 text-emerald-100' : 'bg-black/40 text-slate-500'}`}
// >
// {label}
// </span>
// );
// }
const OVERCURRENT_LABELS = {
leftWheel: 'Left wheel',
rightWheel: 'Right wheel',
@@ -285,7 +250,7 @@ function OvercurrentOverlay({ motors }) {
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-red-900/60">
<div className="text-center text-2xl font-semibold text-white animate-pulse">
<div>Overcurrent</div>
<div className="mt-1 text-xl font-medium text-white">{labels.join(', ')}</div>
<div className="mt-0.5 text-xl font-medium text-white">{labels.join(', ')}</div>
</div>
</div>
);
@@ -37,25 +37,21 @@ export default function DriveModeToggle({ size = 'default' }) {
}
};
const pillClass =
size === 'compact'
? 'text-xs px-1 py-0.5'
: 'text-sm px-1.5 py-0.5';
const pillClass = size === 'compact' ? 'text-xs px-0.5 py-0.5' : 'text-sm px-0.5 py-0.5';
return (
<div className="rounded-sm bg-black/30 p-1 text-slate-100">
<div className="flex items-center justify-between">
<span className={`rounded-full ${driveReady ? 'bg-emerald-700' : 'bg-indigo-700'} ${pillClass}`}>
<div className="bg-black/30 p-0.5 text-slate-100">
<div className="flex items-center">
<span className={`${driveReady ? 'bg-emerald-700' : 'bg-indigo-700'} ${pillClass}`}>
{currentLabel}
</span>
<span className="text-[0.65rem] text-slate-400">Rover {roverId ?? '—'}</span>
</div>
<div className="mt-1 grid grid-cols-2 gap-1 text-xs">
<div className="mt-0.5 grid grid-cols-2 gap-0.5 text-xs">
<button
type="button"
onClick={handleDrive}
disabled={disabled}
className={`rounded-sm bg-emerald-600 px-1 py-1 font-semibold uppercase tracking-wide text-emerald-50 disabled:opacity-40 ${size === 'compact' ? 'text-xs' : 'text-sm'}`}
className={`bg-emerald-600 px-0.5 py-0.5 font-semibold text-emerald-50 transition-colors hover:bg-emerald-500 disabled:opacity-40 ${size === 'compact' ? 'text-xs' : 'text-sm'}`}
>
Drive
</button>
@@ -63,7 +59,7 @@ export default function DriveModeToggle({ size = 'default' }) {
type="button"
onClick={handleDock}
disabled={disabled}
className={`rounded-sm bg-indigo-600 px-1 py-1 font-semibold uppercase tracking-wide text-indigo-50 disabled:opacity-40 ${size === 'compact' ? 'text-xs' : 'text-sm'}`}
className={`bg-indigo-600 px-0.5 py-0.5 font-semibold text-indigo-50 transition-colors hover:bg-indigo-500 disabled:opacity-40 ${size === 'compact' ? 'text-xs' : 'text-sm'}`}
>
Dock
</button>