Compare commits

...
2 Commits
Author SHA1 Message Date
legop3 a3c13f3dd3 fix title dark 2026-09-12 14:35:10 -04:00
legop3 b0389b5ddc ui tweak fling 2026-09-12 14:31:17 -04:00
12 changed files with 81 additions and 52 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -12,8 +12,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<!-- site-metadata:inject -->
<!-- analytics:inject -->
<script type="module" crossorigin src="/assets/index-DsdqxXyd.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-0J6fxLEg.css">
<script type="module" crossorigin src="/assets/index-CNVNbsvk.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B5TEaoXl.css">
</head>
<body>
<div id="root"></div>
@@ -16,22 +16,27 @@ export default function AccessoriesExpansion({ roverId }) {
return (
<div className="pointer-events-none absolute inset-y-0 left-0 z-20 flex items-center">
<AccessoriesToggle
label="Accessories"
ariaLabel={open ? 'Hide accessory controls' : 'Show accessory controls'}
onClick={() => setOpen(!open)}
hud
className="pointer-events-auto !h-28"
/>
{open ? (
<div className="pointer-events-auto w-64 overflow-hidden rounded-r-xl bg-black/60 p-0.5">
{/* This is exactly the renderer mounted by AuxColumn. The desktop
wrapper changes available dimensions, never control behavior.
Content determines the normal panel height; max-height becomes a
scrolling boundary only for genuinely long accessory lists. */}
<RoverAccessoryControls roverId={roverId} className="max-h-[70vh]" />
</div>
) : null}
{/* The tab remains attached to the video's left wall. When open, this
single shell grows around both the unchanged tab position and the
controls to its right, so the controls are not rendered as a second
disconnected panel. Its height follows content until the shared
renderer reaches the scrolling boundary. */}
<div className="pointer-events-auto flex max-h-[70vh] items-center overflow-hidden rounded-r-xl bg-black/60">
<AccessoriesToggle
label="Accessories"
ariaLabel={open ? 'Hide accessory controls' : 'Show accessory controls'}
onClick={() => setOpen(!open)}
hud
className="!h-28"
/>
{open ? (
<RoverAccessoryControls
roverId={roverId}
plainCenteredHeadings
className="w-64 max-h-[70vh]"
/>
) : null}
</div>
</div>
);
}
@@ -205,7 +205,7 @@ function RoverAuxColumn({ roverId, layout, className }) {
className="h-full"
headerAction={(
<AccessoriesToggle
label="Aux"
label="Back"
ariaLabel="Return to auxiliary controls"
compact
onClick={() => setShowAccessories(false)}
@@ -12,10 +12,17 @@ export default function AccessoriesToggle({
hud = false,
className = '',
}) {
const sizeClass = compact ? 'h-6 w-10' : 'h-full w-8';
// The Back action is still shorter than the vertical launcher, but it needs
// a normal touch target and a readable word instead of the previous tiny
// abbreviated control. The full launcher continues to fill the height its
// desktop or mobile parent assigns to it.
const sizeClass = compact ? 'h-8 w-14' : 'h-full w-8';
const toneClass = hud
? 'rounded-none border-0 bg-black/60 text-white/75 shadow-none hover:bg-black hover:text-white'
: 'rounded-xl border-2 border-cyan-300/70 bg-cyan-900 text-cyan-50 shadow-md hover:brightness-110 active:brightness-125';
// On desktop the tab sits inside the shared popout shell. Rounding only
// its exposed right edge preserves its left-wall attachment without
// introducing a separate accessory-specific panel treatment.
? 'rounded-r-xl border-0 bg-black/60 text-white shadow-none'
: 'rounded-xl border-2 border-cyan-300/70 bg-cyan-900 text-cyan-50 shadow-md';
return (
<button
@@ -25,10 +32,10 @@ export default function AccessoriesToggle({
triggerTouchHaptic('button');
onClick();
}}
className={`mobile-touch-control flex shrink-0 items-center justify-center text-sm font-semibold transition active:scale-[0.98] ${sizeClass} ${toneClass} ${className}`.trim()}
className={`mobile-touch-control flex shrink-0 items-center justify-center text-sm font-semibold ${sizeClass} ${toneClass} ${className}`.trim()}
>
{/* Full launchers use vertical writing in the narrow wall space. The
compact Aux return stays horizontal so it consumes only one heading. */}
compact Back action stays horizontal so it fits in the heading. */}
<span className={compact ? 'flex items-center' : 'flex items-center gap-1 [writing-mode:vertical-rl] rotate-180'}>
{!compact ? <FaPuzzlePiece className="shrink-0 text-sm" aria-hidden="true" /> : null}
<span>{label}</span>
@@ -21,6 +21,10 @@ function trimUnicode(value, maximumLength) {
}
const CARD_CLASS = 'mobile-touch-control rounded-xl border-2 px-2 py-1 text-slate-50';
// Sliders already contain a label row and a separate track. Giving that
// two-row control the same vertical padding as a one-row button made it much
// taller than its neighbors without improving its touch target.
const SLIDER_CARD_CLASS = 'mobile-touch-control rounded-xl border-2 px-1.5 py-0.5 text-slate-50';
const DISABLED_CLASS = 'disabled:cursor-not-allowed disabled:opacity-40';
function SliderControl({ peripheralId, control, disabled, send, value: storedValue }) {
@@ -97,7 +101,7 @@ function SliderControl({ peripheralId, control, disabled, send, value: storedVal
};
return (
<div className={`${CARD_CLASS} border-emerald-300/70 bg-emerald-900 ${disabled ? 'cursor-not-allowed opacity-40' : ''}`}>
<div className={`${SLIDER_CARD_CLASS} border-emerald-300/70 bg-emerald-900 ${disabled ? 'cursor-not-allowed opacity-40' : ''}`}>
<div className="flex items-center justify-between gap-1 text-sm font-semibold">
<span className="min-w-0 truncate">{control.name}</span>
<span className="shrink-0 font-mono text-emerald-100">{value}</span>
@@ -121,7 +125,7 @@ function SliderControl({ peripheralId, control, disabled, send, value: storedVal
onKeyDown={handleKeyDown}
onContextMenu={(event) => event.preventDefault()}
style={{ touchAction: 'none' }}
className="mobile-touch-control mobile-drag-control relative mt-1 h-7 w-full rounded-full border border-emerald-100/80 bg-emerald-950 shadow-inner focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-200"
className="mobile-touch-control mobile-drag-control relative mt-0.5 h-6 w-full rounded-full border border-emerald-100/80 bg-emerald-950 shadow-inner focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-200"
>
{/* An inset track gives the thumb room to remain entirely inside the
card at both endpoints without browser-specific range styling. */}
@@ -155,7 +159,7 @@ function ToggleControl({ peripheralId, control, disabled, send, value }) {
aria-pressed={enabled}
disabled={disabled}
onClick={toggle}
className={`${CARD_CLASS} ${DISABLED_CLASS} flex min-h-12 w-full items-center justify-between gap-1 font-semibold transition active:scale-[0.99] ${enabled ? 'border-emerald-300/70 bg-emerald-800 text-emerald-50' : 'border-amber-300/70 bg-amber-900 text-amber-50'}`}
className={`${CARD_CLASS} ${DISABLED_CLASS} flex min-h-12 w-full items-center justify-between gap-1 font-semibold ${enabled ? 'border-emerald-300/70 bg-emerald-800 text-emerald-50' : 'border-amber-300/70 bg-amber-900 text-amber-50'}`}
>
<span className="min-w-0 truncate">{control.name}</span>
<span className="shrink-0 text-xs">{enabled ? 'On' : 'Off'}</span>
@@ -221,7 +225,7 @@ function MomentaryControl({ peripheralId, control, disabled, send, value }) {
}
}}
onContextMenu={(event) => event.preventDefault()}
className={`${CARD_CLASS} ${DISABLED_CLASS} flex min-h-12 w-full items-center justify-center text-center font-semibold transition active:scale-[0.99] ${pressed ? 'border-fuchsia-200 bg-fuchsia-600 text-white' : 'border-fuchsia-300/70 bg-fuchsia-900 text-fuchsia-50'}`}
className={`${CARD_CLASS} ${DISABLED_CLASS} flex min-h-12 w-full items-center justify-center text-center font-semibold ${pressed ? 'border-fuchsia-200 bg-fuchsia-600 text-white' : 'border-fuchsia-300/70 bg-fuchsia-900 text-fuchsia-50'}`}
>
{control.name}
</button>
@@ -9,7 +9,12 @@ import useRoverAccessories from './useRoverAccessories.js';
const EMPTY_ACCESSORY_VALUES = Object.freeze({});
export default function RoverAccessoryControls({ roverId, headerAction = null, className = '' }) {
export default function RoverAccessoryControls({
roverId,
headerAction = null,
plainCenteredHeadings = false,
className = '',
}) {
const { peripherals } = useRoverAccessories(roverId);
const canControl = useCanControlRover(roverId);
const { setPeripheralControl } = useControlActions();
@@ -29,17 +34,25 @@ export default function RoverAccessoryControls({ roverId, headerAction = null, c
aria-label="Rover accessories"
>
{peripherals.map((peripheral, peripheralIndex) => {
const showHeading = peripherals.length > 1 || (peripheralIndex === 0 && headerAction);
return (
<section key={peripheral.id} className="mb-0.5 last:mb-0">
<section key={peripheral.id} className="mb-0.5 flex flex-col gap-0.5 last:mb-0">
{/* The firmware's array order is authoritative. Mapping directly over
it keeps physical authoring order intact across every UI host. */}
{showHeading ? (
<div className="mb-0.5 flex min-h-7 items-center gap-1 bg-black/60 px-1 text-xs font-semibold text-cyan-100">
<h3 className="min-w-0 flex-1 truncate">{peripheral.name}</h3>
{peripheralIndex === 0 ? headerAction : null}
</div>
) : null}
it keeps physical authoring order intact across every UI host.
Every peripheral keeps its heading even when it is the only
device, because its firmware-provided name identifies which
physical accessory owns the controls below it. */}
<div
className={`flex min-h-8 items-center gap-1 px-1 text-xs font-semibold text-white ${plainCenteredHeadings ? '' : 'bg-black/60'}`.trim()}
>
{/* Desktop already supplies one continuous HUD background, so
its title needs neither a second tone nor left alignment.
Mobile keeps the ordinary heading because it also carries
the Back action on the opposite side. */}
<h3 className={`min-w-0 flex-1 truncate ${plainCenteredHeadings ? 'text-center' : ''}`.trim()}>
{peripheral.name}
</h3>
{peripheralIndex === 0 ? headerAction : null}
</div>
<div className="flex flex-col gap-0.5">
{peripheral.controls.map((control) => (
<AccessoryControlField