mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
ui tweak fling
This commit is contained in:
@@ -16,22 +16,23 @@ 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} 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>
|
||||
|
||||
@@ -29,17 +29,17 @@ 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 bg-black/60 px-1 text-xs font-semibold text-white">
|
||||
<h3 className="min-w-0 flex-1 truncate">{peripheral.name}</h3>
|
||||
{peripheralIndex === 0 ? headerAction : null}
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{peripheral.controls.map((control) => (
|
||||
<AccessoryControlField
|
||||
|
||||
Reference in New Issue
Block a user