mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
adjusting stylings
This commit is contained in:
@@ -1131,13 +1131,13 @@ Generic peripheral controls are rover controls, so they follow the new driver's
|
|||||||
|
|
||||||
The standardized replacements do not create any new UI. `cameraServo`, `headlight`, and `laser` continue to use their current camera-tilt, headlight, and laser HUD controls. Only entries in the generic `controls` arrays appear in a new surface named `Accessories`.
|
The standardized replacements do not create any new UI. `cameraServo`, `headlight`, and `laser` continue to use their current camera-tilt, headlight, and laser HUD controls. Only entries in the generic `controls` arrays appear in a new surface named `Accessories`.
|
||||||
|
|
||||||
On desktop, `Accessories` is a vertical button centered on the left wall of the video. It opens a height-limited, vertically scrollable panel toward the right. The panel uses the same large control cards as mobile and is independent of the bottom-left horn, headlight, and laser pod.
|
On desktop, `Accessories` is a vertical button centered on the left wall of the video. It uses the existing translucent black HUD treatment and opens a height-limited, vertically scrollable panel toward the right. The panel uses the same compact control renderer as mobile and is independent of the bottom-left horn, headlight, and laser pod.
|
||||||
|
|
||||||
On mobile, a vertical `Accessories` button sits directly to the right of the vacuum-forward and vacuum-backward buttons. Activating it replaces the complete `AuxColumn` contents with the ordered, vertically scrollable accessory list. A much smaller vertical `Aux` tab returns to the normal vacuum, camera, light, laser, and horn controls without reserving a large empty rail beside the accessory list.
|
On mobile, a vertical `Accessories` button sits directly to the right of the vacuum-forward and vacuum-backward buttons. Activating it replaces the complete `AuxColumn` contents with the ordered, vertically scrollable accessory list. A small `Aux` button shares the first compact device heading and returns to the normal vacuum, camera, light, laser, and horn controls without creating a separate rail or overlay border.
|
||||||
|
|
||||||
Desktop and mobile reuse one placement-independent `RoverAccessoryControls` renderer inside their different containers. Device-specific React components are not created for individual peripherals. The renderer sends actions through `ControlSystemProvider`, `ControlContext`, and the existing command pipeline so assignment gating, input cancellation, and command behavior remain consistent with other rover HUD controls. Both parents and the renderer disappear completely when the assigned rover has no generic controls; no launcher, empty shell, or reserved space remains.
|
Desktop and mobile reuse one placement-independent `RoverAccessoryControls` renderer inside their different containers. Device-specific React components are not created for individual peripherals. The renderer sends actions through `ControlSystemProvider`, `ControlContext`, and the existing command pipeline so assignment gating, input cancellation, and command behavior remain consistent with other rover HUD controls. Both parents and the renderer disappear completely when the assigned rover has no generic controls; no launcher, empty shell, or reserved space remains.
|
||||||
|
|
||||||
Control values are local UI values in the first implementation. Slider and toggle changes update the displayed value immediately and are then sent to the server. Restarting `roverd` recreates controls from the new hello rather than persisting peripheral values in `roverSettings`.
|
Control values are local UI values in the first implementation. Slider and toggle changes update the displayed value immediately and are then sent to the server. Generic sliders use the same custom pointer-capture approach as mobile camera tilt rather than a browser-native range control, which keeps touch behavior and appearance consistent while driving. Restarting `roverd` recreates controls from the new hello rather than persisting peripheral values in `roverSettings`.
|
||||||
|
|
||||||
## Permissions
|
## Permissions
|
||||||
|
|
||||||
|
|||||||
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
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
@@ -12,8 +12,8 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<!-- site-metadata:inject -->
|
<!-- site-metadata:inject -->
|
||||||
<!-- analytics:inject -->
|
<!-- analytics:inject -->
|
||||||
<script type="module" crossorigin src="/assets/index-C6QLPM0P.js"></script>
|
<script type="module" crossorigin src="/assets/index-DVBMljJU.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BF6Z3hOA.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CDWVlrvF.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -20,10 +20,11 @@ export default function AccessoriesExpansion({ roverId }) {
|
|||||||
label="Accessories"
|
label="Accessories"
|
||||||
ariaLabel={open ? 'Hide accessory controls' : 'Show accessory controls'}
|
ariaLabel={open ? 'Hide accessory controls' : 'Show accessory controls'}
|
||||||
onClick={() => setOpen(!open)}
|
onClick={() => setOpen(!open)}
|
||||||
className={`pointer-events-auto !h-28 rounded-l-none ${open ? 'rounded-r-none' : ''}`}
|
hud
|
||||||
|
className="pointer-events-auto !h-28"
|
||||||
/>
|
/>
|
||||||
{open ? (
|
{open ? (
|
||||||
<div className="pointer-events-auto h-[70%] min-h-48 max-h-[32rem] w-72 overflow-hidden rounded-r-xl border-2 border-l-0 border-cyan-300/70 bg-slate-950/95 shadow-2xl">
|
<div className="pointer-events-auto h-[70%] min-h-48 max-h-[32rem] w-64 overflow-hidden rounded-r-xl bg-black/60 p-0.5">
|
||||||
{/* This is exactly the renderer mounted by AuxColumn. The desktop
|
{/* This is exactly the renderer mounted by AuxColumn. The desktop
|
||||||
wrapper changes available dimensions, never control behavior. */}
|
wrapper changes available dimensions, never control behavior. */}
|
||||||
<RoverAccessoryControls roverId={roverId} className="h-full" />
|
<RoverAccessoryControls roverId={roverId} className="h-full" />
|
||||||
|
|||||||
@@ -199,17 +199,19 @@ function RoverAuxColumn({ roverId, layout, className }) {
|
|||||||
return (
|
return (
|
||||||
<div className={`mobile-touch-control flex flex-col gap-0.5 ${className}`.trim()} data-mobile-layout={layout}>
|
<div className={`mobile-touch-control flex flex-col gap-0.5 ${className}`.trim()} data-mobile-layout={layout}>
|
||||||
{showAccessories && hasAccessories ? (
|
{showAccessories && hasAccessories ? (
|
||||||
<div className="mobile-touch-control relative h-full min-h-0 overflow-hidden rounded-xl border-2 border-cyan-300/70 bg-slate-950/95">
|
<div className="mobile-touch-control h-full min-h-0 overflow-hidden">
|
||||||
{/* The compact return tab overlays only the first heading corner. It
|
<RoverAccessoryControls
|
||||||
does not reserve an otherwise empty rail down the full column. */}
|
roverId={roverId}
|
||||||
<AccessoriesToggle
|
className="h-full"
|
||||||
label="Aux"
|
headerAction={(
|
||||||
ariaLabel="Return to auxiliary controls"
|
<AccessoriesToggle
|
||||||
compact
|
label="Aux"
|
||||||
onClick={() => setShowAccessories(false)}
|
ariaLabel="Return to auxiliary controls"
|
||||||
className="absolute right-1 top-1 z-10"
|
compact
|
||||||
|
onClick={() => setShowAccessories(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<RoverAccessoryControls roverId={roverId} className="h-full pr-1" />
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<AuxColumnContent
|
<AuxColumnContent
|
||||||
|
|||||||
@@ -9,8 +9,14 @@ export default function AccessoriesToggle({
|
|||||||
ariaLabel,
|
ariaLabel,
|
||||||
onClick,
|
onClick,
|
||||||
compact = false,
|
compact = false,
|
||||||
|
hud = false,
|
||||||
className = '',
|
className = '',
|
||||||
}) {
|
}) {
|
||||||
|
const sizeClass = compact ? 'h-6 w-10' : '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';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -19,11 +25,11 @@ export default function AccessoriesToggle({
|
|||||||
triggerTouchHaptic('button');
|
triggerTouchHaptic('button');
|
||||||
onClick();
|
onClick();
|
||||||
}}
|
}}
|
||||||
className={`mobile-touch-control flex shrink-0 items-center justify-center rounded-xl border-2 border-cyan-300/70 bg-cyan-900 text-sm font-semibold text-cyan-50 shadow-md transition hover:brightness-110 active:scale-[0.98] active:brightness-125 ${compact ? 'h-14 w-7' : 'h-full w-8'} ${className}`.trim()}
|
className={`mobile-touch-control flex shrink-0 items-center justify-center text-sm font-semibold transition active:scale-[0.98] ${sizeClass} ${toneClass} ${className}`.trim()}
|
||||||
>
|
>
|
||||||
{/* Vertical writing keeps the launcher readable in the narrow wall space
|
{/* Full launchers use vertical writing in the narrow wall space. The
|
||||||
without rotating the glyph itself away from its natural orientation. */}
|
compact Aux return stays horizontal so it consumes only one heading. */}
|
||||||
<span className="flex items-center gap-1 [writing-mode:vertical-rl] rotate-180">
|
<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}
|
{!compact ? <FaPuzzlePiece className="shrink-0 text-sm" aria-hidden="true" /> : null}
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Accessory Control Field
|
// Accessory Control Field
|
||||||
// Purpose: Maps one firmware-advertised generic control to a large rover-control surface.
|
// Purpose: Maps one firmware-advertised generic control to a compact rover-control surface.
|
||||||
// Scope: Owns browser-local values and input semantics; transport and device-specific behavior stay outside this file.
|
// Scope: Owns browser-local input semantics; transport and device-specific behavior stay outside this file.
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { triggerTouchHaptic } from '../../lib/touchHaptics.js';
|
import { triggerTouchHaptic } from '../../lib/touchHaptics.js';
|
||||||
|
|
||||||
function integerBound(value, fallback) {
|
function integerBound(value, fallback) {
|
||||||
@@ -20,7 +20,7 @@ function trimUnicode(value, maximumLength) {
|
|||||||
return Array.from(String(value ?? '')).slice(0, maximumLength).join('');
|
return Array.from(String(value ?? '')).slice(0, maximumLength).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
const CARD_CLASS = 'mobile-touch-control rounded-xl border-2 px-2.5 py-2 text-slate-50 shadow-md';
|
const CARD_CLASS = 'mobile-touch-control rounded-xl border-2 px-2 py-1 text-slate-50';
|
||||||
const DISABLED_CLASS = 'disabled:cursor-not-allowed disabled:opacity-40';
|
const DISABLED_CLASS = 'disabled:cursor-not-allowed disabled:opacity-40';
|
||||||
|
|
||||||
function SliderControl({ peripheralId, control, disabled, send, value: storedValue }) {
|
function SliderControl({ peripheralId, control, disabled, send, value: storedValue }) {
|
||||||
@@ -29,33 +29,114 @@ function SliderControl({ peripheralId, control, disabled, send, value: storedVal
|
|||||||
const value = Number.isInteger(storedValue)
|
const value = Number.isInteger(storedValue)
|
||||||
? clampInteger(storedValue, minimum, maximum)
|
? clampInteger(storedValue, minimum, maximum)
|
||||||
: minimum;
|
: minimum;
|
||||||
|
const trackRef = useRef(null);
|
||||||
|
const pointerIdRef = useRef(null);
|
||||||
|
const lastHapticValueRef = useRef(value);
|
||||||
|
|
||||||
const updateValue = (event) => {
|
const valuePercent = useMemo(() => {
|
||||||
const next = clampInteger(event.target.value, minimum, maximum);
|
if (maximum === minimum) return 50;
|
||||||
|
return ((value - minimum) / (maximum - minimum)) * 100;
|
||||||
|
}, [maximum, minimum, value]);
|
||||||
|
|
||||||
|
const sendValue = useCallback((nextValue) => {
|
||||||
|
const next = clampInteger(nextValue, minimum, maximum);
|
||||||
|
const hapticStep = Math.max(1, Math.round((maximum - minimum) / 20));
|
||||||
|
if (Math.abs(next - lastHapticValueRef.current) >= hapticStep) {
|
||||||
|
triggerTouchHaptic('camera');
|
||||||
|
lastHapticValueRef.current = next;
|
||||||
|
}
|
||||||
send(peripheralId, control.id, next);
|
send(peripheralId, control.id, next);
|
||||||
|
}, [control.id, maximum, minimum, peripheralId, send]);
|
||||||
|
|
||||||
|
const valueFromPointer = useCallback((event) => {
|
||||||
|
const track = trackRef.current;
|
||||||
|
if (!track) return value;
|
||||||
|
const bounds = track.getBoundingClientRect();
|
||||||
|
const rawPercent = (event.clientX - bounds.left) / Math.max(1, bounds.width);
|
||||||
|
return minimum + Math.max(0, Math.min(1, rawPercent)) * (maximum - minimum);
|
||||||
|
}, [maximum, minimum, value]);
|
||||||
|
|
||||||
|
const updateFromPointer = useCallback((event) => {
|
||||||
|
sendValue(valueFromPointer(event));
|
||||||
|
}, [sendValue, valueFromPointer]);
|
||||||
|
|
||||||
|
const handlePointerDown = useCallback((event) => {
|
||||||
|
if (disabled || pointerIdRef.current !== null) return;
|
||||||
|
// This follows VerticalCameraTilt's custom pointer-capture path so a range
|
||||||
|
// drag remains reliable while another finger is operating the drive pad.
|
||||||
|
event.preventDefault();
|
||||||
|
pointerIdRef.current = event.pointerId;
|
||||||
|
lastHapticValueRef.current = value;
|
||||||
|
trackRef.current?.setPointerCapture?.(event.pointerId);
|
||||||
|
updateFromPointer(event);
|
||||||
|
}, [disabled, updateFromPointer, value]);
|
||||||
|
|
||||||
|
const handlePointerMove = useCallback((event) => {
|
||||||
|
if (pointerIdRef.current !== event.pointerId) return;
|
||||||
|
event.preventDefault();
|
||||||
|
updateFromPointer(event);
|
||||||
|
}, [updateFromPointer]);
|
||||||
|
|
||||||
|
const handlePointerEnd = useCallback((event) => {
|
||||||
|
if (pointerIdRef.current !== event.pointerId) return;
|
||||||
|
event.preventDefault();
|
||||||
|
pointerIdRef.current = null;
|
||||||
|
trackRef.current?.releasePointerCapture?.(event.pointerId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
|
if (disabled) return;
|
||||||
|
let next = null;
|
||||||
|
if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') next = value - 1;
|
||||||
|
if (event.key === 'ArrowRight' || event.key === 'ArrowUp') next = value + 1;
|
||||||
|
if (event.key === 'Home') next = minimum;
|
||||||
|
if (event.key === 'End') next = maximum;
|
||||||
|
if (next == null) return;
|
||||||
|
event.preventDefault();
|
||||||
|
sendValue(next);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className={`${CARD_CLASS} block border-emerald-300/70 bg-emerald-900`}>
|
<div className={`${CARD_CLASS} border-emerald-300/70 bg-emerald-900 ${disabled ? 'cursor-not-allowed opacity-40' : ''}`}>
|
||||||
<span className="flex items-center justify-between gap-2 text-sm font-semibold">
|
<div className="flex items-center justify-between gap-1 text-sm font-semibold">
|
||||||
<span>{control.name}</span>
|
<span className="min-w-0 truncate">{control.name}</span>
|
||||||
<span className="font-mono text-emerald-100">{value}</span>
|
<span className="shrink-0 font-mono text-emerald-100">{value}</span>
|
||||||
</span>
|
</div>
|
||||||
<input
|
<div
|
||||||
type="range"
|
ref={trackRef}
|
||||||
min={minimum}
|
role="slider"
|
||||||
max={maximum}
|
aria-label={control.name}
|
||||||
step="1"
|
aria-valuemin={minimum}
|
||||||
value={value}
|
aria-valuemax={maximum}
|
||||||
disabled={disabled}
|
aria-valuenow={value}
|
||||||
onChange={updateValue}
|
aria-disabled={disabled}
|
||||||
className={`mobile-touch-control mt-2 h-8 w-full cursor-pointer accent-emerald-300 ${DISABLED_CLASS}`}
|
tabIndex={disabled ? -1 : 0}
|
||||||
/>
|
onPointerDown={handlePointerDown}
|
||||||
<span className="flex justify-between text-xs text-emerald-100/80" aria-hidden="true">
|
onPointerMove={handlePointerMove}
|
||||||
<span>{minimum}</span>
|
onPointerUp={handlePointerEnd}
|
||||||
<span>{maximum}</span>
|
onPointerCancel={handlePointerEnd}
|
||||||
</span>
|
onLostPointerCapture={(event) => {
|
||||||
</label>
|
if (pointerIdRef.current === event.pointerId) pointerIdRef.current = null;
|
||||||
|
}}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{/* An inset track gives the thumb room to remain entirely inside the
|
||||||
|
card at both endpoints without browser-specific range styling. */}
|
||||||
|
<div className="pointer-events-none absolute inset-1">
|
||||||
|
<div
|
||||||
|
className="absolute inset-y-0 left-0 rounded-full bg-emerald-400"
|
||||||
|
style={{ width: `${valuePercent}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute top-1/2 h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full border border-emerald-950 bg-emerald-200 shadow"
|
||||||
|
style={{ left: `clamp(0.4375rem, ${valuePercent}%, calc(100% - 0.4375rem))` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,8 +145,7 @@ function ToggleControl({ peripheralId, control, disabled, send, value }) {
|
|||||||
|
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
const next = !enabled;
|
send(peripheralId, control.id, !enabled);
|
||||||
send(peripheralId, control.id, next);
|
|
||||||
triggerTouchHaptic('button');
|
triggerTouchHaptic('button');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,10 +155,10 @@ function ToggleControl({ peripheralId, control, disabled, send, value }) {
|
|||||||
aria-pressed={enabled}
|
aria-pressed={enabled}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
className={`${CARD_CLASS} ${DISABLED_CLASS} flex min-h-[4.5rem] w-full items-center justify-between gap-2 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 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'}`}
|
||||||
>
|
>
|
||||||
<span>{control.name}</span>
|
<span className="min-w-0 truncate">{control.name}</span>
|
||||||
<span className="text-sm">{enabled ? 'On' : 'Off'}</span>
|
<span className="shrink-0 text-xs">{enabled ? 'On' : 'Off'}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -92,8 +172,8 @@ function MomentaryControl({ peripheralId, control, disabled, send, value }) {
|
|||||||
if (!pressedRef.current) return;
|
if (!pressedRef.current) return;
|
||||||
pressedRef.current = false;
|
pressedRef.current = false;
|
||||||
pointerIdRef.current = null;
|
pointerIdRef.current = null;
|
||||||
// Always pair a successful press with false. In particular, this cleanup
|
// Always pair a successful press with false, including cancellation,
|
||||||
// path runs when a touch is cancelled or the Accessories view is replaced.
|
// permission loss, and replacement of the Accessories view.
|
||||||
send(peripheralId, control.id, false);
|
send(peripheralId, control.id, false);
|
||||||
}, [control.id, peripheralId, send]);
|
}, [control.id, peripheralId, send]);
|
||||||
|
|
||||||
@@ -105,8 +185,6 @@ function MomentaryControl({ peripheralId, control, disabled, send, value }) {
|
|||||||
|
|
||||||
useEffect(() => release, [release]);
|
useEffect(() => release, [release]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Browsers do not guarantee pointer-up after a held button becomes
|
|
||||||
// disabled. Proactively emit the neutral edge at the permission boundary.
|
|
||||||
if (disabled) release();
|
if (disabled) release();
|
||||||
}, [disabled, release]);
|
}, [disabled, release]);
|
||||||
|
|
||||||
@@ -143,7 +221,7 @@ function MomentaryControl({ peripheralId, control, disabled, send, value }) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onContextMenu={(event) => event.preventDefault()}
|
onContextMenu={(event) => event.preventDefault()}
|
||||||
className={`${CARD_CLASS} ${DISABLED_CLASS} flex min-h-[4.5rem] 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 transition active:scale-[0.99] ${pressed ? 'border-fuchsia-200 bg-fuchsia-600 text-white' : 'border-fuchsia-300/70 bg-fuchsia-900 text-fuchsia-50'}`}
|
||||||
>
|
>
|
||||||
{control.name}
|
{control.name}
|
||||||
</button>
|
</button>
|
||||||
@@ -169,11 +247,8 @@ function NumberControl({ peripheralId, control, disabled, send, value: storedVal
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className={`${CARD_CLASS} block border-indigo-300/70 bg-indigo-900`}>
|
<label className={`${CARD_CLASS} flex min-h-12 items-center gap-1 border-indigo-300/70 bg-indigo-900`}>
|
||||||
<span className="flex items-center justify-between gap-2 text-sm font-semibold">
|
<span className="min-w-0 flex-1 truncate text-sm font-semibold">{control.name}</span>
|
||||||
<span>{control.name}</span>
|
|
||||||
<span className="text-xs text-indigo-100/80">{minimum}–{maximum}</span>
|
|
||||||
</span>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
@@ -182,6 +257,7 @@ function NumberControl({ peripheralId, control, disabled, send, value: storedVal
|
|||||||
step="1"
|
step="1"
|
||||||
value={value}
|
value={value}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
aria-label={`${control.name}, ${minimum} to ${maximum}`}
|
||||||
onChange={(event) => setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
onBlur={commit}
|
onBlur={commit}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
@@ -191,7 +267,7 @@ function NumberControl({ peripheralId, control, disabled, send, value: storedVal
|
|||||||
event.currentTarget.blur();
|
event.currentTarget.blur();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`mobile-touch-control mt-2 min-h-11 w-full rounded-lg border border-indigo-200/70 bg-indigo-950 px-3 text-base text-white outline-none focus-visible:ring-2 focus-visible:ring-indigo-200 ${DISABLED_CLASS}`}
|
className={`mobile-touch-control h-9 w-[45%] min-w-16 rounded-lg border border-indigo-200/70 bg-indigo-950 px-1.5 text-right text-base text-white outline-none focus-visible:ring-2 focus-visible:ring-indigo-200 ${DISABLED_CLASS}`}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
@@ -215,15 +291,13 @@ function TextControl({ peripheralId, control, disabled, send, value: storedValue
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className={`${CARD_CLASS} block border-sky-300/70 bg-sky-900`}>
|
<label className={`${CARD_CLASS} flex min-h-12 items-center gap-1 border-sky-300/70 bg-sky-900`}>
|
||||||
<span className="flex items-center justify-between gap-2 text-sm font-semibold">
|
<span className="min-w-0 flex-1 truncate text-sm font-semibold">{control.name}</span>
|
||||||
<span>{control.name}</span>
|
|
||||||
<span className="text-xs text-sky-100/80">{Array.from(value).length}/{maximumLength}</span>
|
|
||||||
</span>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
aria-label={`${control.name}, maximum ${maximumLength} characters`}
|
||||||
onChange={(event) => setValue(trimUnicode(event.target.value, maximumLength))}
|
onChange={(event) => setValue(trimUnicode(event.target.value, maximumLength))}
|
||||||
onBlur={commit}
|
onBlur={commit}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
@@ -233,7 +307,7 @@ function TextControl({ peripheralId, control, disabled, send, value: storedValue
|
|||||||
event.currentTarget.blur();
|
event.currentTarget.blur();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`mobile-touch-control mt-2 min-h-11 w-full rounded-lg border border-sky-200/70 bg-sky-950 px-3 text-base text-white outline-none focus-visible:ring-2 focus-visible:ring-sky-200 ${DISABLED_CLASS}`}
|
className={`mobile-touch-control h-9 w-[55%] min-w-20 rounded-lg border border-sky-200/70 bg-sky-950 px-1.5 text-base text-white outline-none focus-visible:ring-2 focus-visible:ring-sky-200 ${DISABLED_CLASS}`}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import useRoverAccessories from './useRoverAccessories.js';
|
|||||||
|
|
||||||
const EMPTY_ACCESSORY_VALUES = Object.freeze({});
|
const EMPTY_ACCESSORY_VALUES = Object.freeze({});
|
||||||
|
|
||||||
export default function RoverAccessoryControls({ roverId, className = '' }) {
|
export default function RoverAccessoryControls({ roverId, headerAction = null, className = '' }) {
|
||||||
const { peripherals } = useRoverAccessories(roverId);
|
const { peripherals } = useRoverAccessories(roverId);
|
||||||
const canControl = useCanControlRover(roverId);
|
const canControl = useCanControlRover(roverId);
|
||||||
const { setPeripheralControl } = useControlActions();
|
const { setPeripheralControl } = useControlActions();
|
||||||
@@ -25,30 +25,36 @@ export default function RoverAccessoryControls({ roverId, className = '' }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`mobile-touch-control min-h-0 overflow-y-auto overscroll-contain p-1.5 text-slate-100 ${className}`.trim()}
|
className={`mobile-touch-control min-h-0 overflow-y-auto overscroll-contain text-slate-100 ${className}`.trim()}
|
||||||
aria-label="Rover accessories"
|
aria-label="Rover accessories"
|
||||||
>
|
>
|
||||||
{peripherals.map((peripheral) => (
|
{peripherals.map((peripheral, peripheralIndex) => {
|
||||||
<section key={peripheral.id} className="mb-2 last:mb-0">
|
const showHeading = peripherals.length > 1 || (peripheralIndex === 0 && headerAction);
|
||||||
{/* The firmware's array order is authoritative. Mapping directly over
|
return (
|
||||||
it keeps physical authoring order intact across every UI host. */}
|
<section key={peripheral.id} className="mb-0.5 last:mb-0">
|
||||||
<h3 className="mb-1.5 border-b border-cyan-300/40 px-1 pr-8 pb-1 text-sm font-semibold text-cyan-100">
|
{/* The firmware's array order is authoritative. Mapping directly over
|
||||||
{peripheral.name}
|
it keeps physical authoring order intact across every UI host. */}
|
||||||
</h3>
|
{showHeading ? (
|
||||||
<div className="flex flex-col gap-1.5">
|
<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">
|
||||||
{peripheral.controls.map((control) => (
|
<h3 className="min-w-0 flex-1 truncate">{peripheral.name}</h3>
|
||||||
<AccessoryControlField
|
{peripheralIndex === 0 ? headerAction : null}
|
||||||
key={control.id}
|
</div>
|
||||||
peripheralId={peripheral.id}
|
) : null}
|
||||||
control={control}
|
<div className="flex flex-col gap-0.5">
|
||||||
value={values[peripheral.id]?.[control.id]}
|
{peripheral.controls.map((control) => (
|
||||||
disabled={!roverId || !canControl}
|
<AccessoryControlField
|
||||||
send={send}
|
key={control.id}
|
||||||
/>
|
peripheralId={peripheral.id}
|
||||||
))}
|
control={control}
|
||||||
</div>
|
value={values[peripheral.id]?.[control.id]}
|
||||||
</section>
|
disabled={!roverId || !canControl}
|
||||||
))}
|
send={send}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user