mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
new battery bars!!!
This commit is contained in:
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
@@ -11,8 +11,8 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||
<title>Multi Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-DLm82-hG.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CnYxiRu2.css">
|
||||
<script type="module" crossorigin src="/assets/index-Ds3QBek7.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bz1ixYh7.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -196,6 +196,14 @@ function computeBatteryState(record, sensors) {
|
||||
if (percent != null) {
|
||||
percent = Math.max(0, Math.min(1, percent));
|
||||
}
|
||||
const percentDisplay = computeBatteryDisplayPercent({
|
||||
charge,
|
||||
full,
|
||||
warn,
|
||||
urgent,
|
||||
percent,
|
||||
capacity,
|
||||
});
|
||||
return {
|
||||
charge,
|
||||
capacity,
|
||||
@@ -203,13 +211,41 @@ function computeBatteryState(record, sensors) {
|
||||
warn,
|
||||
urgent,
|
||||
percent,
|
||||
percentDisplay: percent == null ? null : Math.round(percent * 100),
|
||||
percentDisplay,
|
||||
warnActive: Boolean(warn != null && charge != null && charge <= warn),
|
||||
urgentActive: Boolean(urgent != null && charge != null && charge <= urgent),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function computeBatteryDisplayPercent({ charge, full, warn, urgent, percent, capacity }) {
|
||||
if (
|
||||
charge != null &&
|
||||
full != null &&
|
||||
warn != null &&
|
||||
urgent != null &&
|
||||
full > warn &&
|
||||
warn > urgent
|
||||
) {
|
||||
if (charge <= urgent) return 0;
|
||||
if (charge <= warn) {
|
||||
const t = (charge - urgent) / (warn - urgent);
|
||||
return Math.round(Math.max(0, Math.min(1, t)) * 10);
|
||||
}
|
||||
if (charge >= full) return 100;
|
||||
const t = (charge - warn) / (full - warn);
|
||||
return Math.round((0.1 + Math.max(0, Math.min(1, t)) * 0.9) * 100);
|
||||
}
|
||||
if (percent != null && Number.isFinite(percent)) {
|
||||
return Math.round(Math.max(0, Math.min(1, percent)) * 100);
|
||||
}
|
||||
if (charge != null && capacity != null && capacity > 0) {
|
||||
const fallback = charge / capacity;
|
||||
return Math.round(Math.max(0, Math.min(1, fallback)) * 100);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleSensorFrame(roverId, frame) {
|
||||
const record = rovers.get(roverId);
|
||||
if (!record) return;
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { WARN_DISPLAY_PERCENT } from '../lib/battery.js';
|
||||
|
||||
const WARN_FLASH_MS = 1600;
|
||||
const URGENT_FLASH_MS = 800;
|
||||
|
||||
function classNames(...values) {
|
||||
return values.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
export default function BatteryBar({
|
||||
visual,
|
||||
orientation = 'horizontal',
|
||||
variant = 'inline',
|
||||
compact = false,
|
||||
showLabel,
|
||||
className = '',
|
||||
}) {
|
||||
const isBackground = variant === 'background';
|
||||
const isVertical = orientation === 'vertical';
|
||||
const resolvedShowLabel = showLabel ?? !isBackground;
|
||||
|
||||
if (!visual?.available) {
|
||||
if (isBackground) {
|
||||
const warnPercent = WARN_DISPLAY_PERCENT;
|
||||
const markerClass = classNames(
|
||||
'absolute z-10',
|
||||
isVertical ? 'left-0 right-0 h-[2px]' : 'top-0 bottom-0 w-[2px]',
|
||||
'bg-black/70',
|
||||
isWarn && !isUrgent && 'battery-tick-warn',
|
||||
);
|
||||
const markerStyle = isVertical ? { bottom: `${warnPercent}%` } : { left: `${warnPercent}%` };
|
||||
const containerClass = classNames('absolute inset-0 pointer-events-none', className);
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
<div className="absolute inset-0 bg-slate-900/35" />
|
||||
<div className={markerClass} style={markerStyle} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isVertical) return null;
|
||||
return <p className="text-xs text-slate-500">Battery telemetry unavailable</p>;
|
||||
}
|
||||
|
||||
const percentDisplay = visual.percentDisplay ?? 0;
|
||||
const percent = Math.max(0, Math.min(100, percentDisplay));
|
||||
const warnPercent = visual.warnDisplayPercent ?? WARN_DISPLAY_PERCENT;
|
||||
|
||||
const isUrgent = Boolean(visual.urgentActive);
|
||||
const isWarn = Boolean(visual.warnActive);
|
||||
const shouldFlash = isUrgent || isWarn;
|
||||
const animationDuration = isUrgent ? `${URGENT_FLASH_MS}ms` : isWarn ? `${WARN_FLASH_MS}ms` : undefined;
|
||||
|
||||
const fillColor = isUrgent || isWarn ? 'bg-red-500' : 'bg-emerald-500';
|
||||
const fillTone = isBackground
|
||||
? isUrgent || isWarn
|
||||
? 'bg-red-500/45'
|
||||
: 'bg-emerald-500/45'
|
||||
: fillColor;
|
||||
const baseTone = isUrgent || isWarn ? 'bg-red-900/35' : 'bg-slate-900/35';
|
||||
const baseToneStrong = isUrgent || isWarn ? 'bg-red-900/45' : 'bg-slate-900/35';
|
||||
|
||||
const containerClass = classNames(
|
||||
isBackground ? 'absolute inset-0 pointer-events-none' : 'relative pointer-events-auto',
|
||||
isVertical && !isBackground ? 'flex flex-col items-center justify-center' : 'w-full',
|
||||
className,
|
||||
);
|
||||
|
||||
if (isBackground) {
|
||||
const fillStyle = isVertical
|
||||
? { height: `${percent}%` }
|
||||
: { width: `${percent}%` };
|
||||
const fillClass = classNames(
|
||||
'absolute',
|
||||
isVertical ? 'bottom-0 left-0 right-0' : 'left-0 top-0 bottom-0',
|
||||
fillTone,
|
||||
'transition-[width,height]',
|
||||
isUrgent && 'battery-urgent-flash',
|
||||
);
|
||||
const markerClass = classNames(
|
||||
'absolute z-10',
|
||||
isVertical ? 'left-0 right-0 h-[2px]' : 'top-0 bottom-0 w-[2px]',
|
||||
'bg-black/70',
|
||||
isWarn && !isUrgent && 'battery-tick-warn',
|
||||
);
|
||||
const markerStyle = isVertical ? { bottom: `${warnPercent}%` } : { left: `${warnPercent}%` };
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
<div className={classNames('absolute inset-0', baseTone, isUrgent && 'battery-urgent-flash')} />
|
||||
<div className={fillClass} style={{ ...fillStyle, animationDuration }} />
|
||||
<div className={markerClass} style={markerStyle} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const barShellClass = classNames(
|
||||
'relative overflow-hidden rounded-sm border border-slate-800',
|
||||
isUrgent || isWarn ? baseToneStrong : 'bg-zinc-900/90',
|
||||
isUrgent && 'battery-urgent-flash',
|
||||
isVertical
|
||||
? 'w-full flex-1'
|
||||
: compact
|
||||
? 'h-4'
|
||||
: 'h-5',
|
||||
);
|
||||
|
||||
const fillStyle = isVertical
|
||||
? { height: `${percent}%` }
|
||||
: { width: `${percent}%` };
|
||||
|
||||
const fillClass = classNames(
|
||||
'absolute',
|
||||
isVertical ? 'bottom-0 left-0 right-0' : 'left-0 top-0 bottom-0',
|
||||
fillTone,
|
||||
'transition-[width,height]',
|
||||
isUrgent && 'battery-urgent-flash',
|
||||
);
|
||||
|
||||
const markerClass = classNames(
|
||||
'absolute z-10',
|
||||
isVertical ? 'left-0 right-0 h-[2px]' : 'top-0 bottom-0 w-[2px]',
|
||||
'bg-black/70',
|
||||
isWarn && !isUrgent && 'battery-tick-warn',
|
||||
);
|
||||
const markerStyle = isVertical ? { bottom: `${warnPercent}%` } : { left: `${warnPercent}%` };
|
||||
|
||||
const labelText = `Battery ${percentDisplay}%`;
|
||||
const labelClass = classNames(
|
||||
'pointer-events-none absolute inset-0 flex items-center justify-center text-center font-semibold',
|
||||
isVertical ? (compact ? 'text-[0.55rem]' : 'text-[0.65rem]') : 'text-xs',
|
||||
);
|
||||
const labelPillClass = classNames(
|
||||
'rounded px-1.5 py-0.5',
|
||||
isBackground ? 'bg-black/30 text-slate-100' : 'bg-black/50 text-slate-100',
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
<div className={barShellClass}>
|
||||
<div className={fillClass} style={{ ...fillStyle, animationDuration }} />
|
||||
<div className={markerClass} style={markerStyle} />
|
||||
</div>
|
||||
{resolvedShowLabel && !isVertical ? (
|
||||
<div className={labelClass}>
|
||||
<span className={labelPillClass}>{labelText}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{resolvedShowLabel && isVertical ? (
|
||||
<div className="mt-0.5 text-[0.65rem] font-semibold text-slate-100">
|
||||
<span className={labelPillClass}>{percentDisplay}%</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,37 +6,13 @@ import { useChat } from '../context/ChatContext.jsx';
|
||||
import { useSession } from '../context/SessionContext.jsx';
|
||||
import { useSettingsNamespace } from '../settings/index.js';
|
||||
import SocialButton from './SocialButton.jsx';
|
||||
import BatteryBar from './BatteryBar.jsx';
|
||||
import { buildBatteryVisual } from '../lib/battery.js';
|
||||
|
||||
const RESTART_DELAY_MS = 2000;
|
||||
const UNMUTE_RETRY_MS = 3000;
|
||||
const AUDIO_RETRY_MS = 3000;
|
||||
|
||||
function buildBatteryVisual(charge, config) {
|
||||
const full = config?.Full;
|
||||
const warn = config?.Warn;
|
||||
const urgent = config?.Urgent ?? null;
|
||||
if (charge == null || full == null || warn == null) {
|
||||
return { available: false };
|
||||
}
|
||||
|
||||
const span = full - warn;
|
||||
if (span <= 0) return { available: false };
|
||||
const normalized = (charge - warn) / span;
|
||||
const percent = Math.min(1, Math.max(0, normalized));
|
||||
const percentDisplay = Math.round(percent * 100);
|
||||
const depleted = normalized <= 0;
|
||||
const warnTriggered = urgent != null && charge <= urgent;
|
||||
const barClass = depleted ? 'bg-red-500 animate-pulse' : warnTriggered ? 'bg-amber-400' : 'bg-emerald-500';
|
||||
|
||||
return {
|
||||
available: true,
|
||||
percentDisplay,
|
||||
depleted,
|
||||
warnTriggered,
|
||||
barClass,
|
||||
};
|
||||
}
|
||||
|
||||
export default function VideoTile({
|
||||
sessionInfo,
|
||||
audioSessionInfo,
|
||||
@@ -83,7 +59,7 @@ export default function VideoTile({
|
||||
const effectiveHudMapPosition = mobileHud ? 'top-right' : hudMapPosition;
|
||||
const [showHudMapDesktop, setShowHudMapDesktop] = useHudMapSetting();
|
||||
const showHudMap = hudForceMap ? true : mobileHud ? true : showHudMapDesktop;
|
||||
const batteryVisual = buildBatteryVisual(batteryCharge, batteryConfig);
|
||||
const batteryVisual = buildBatteryVisual({ charge: batteryCharge, config: batteryConfig });
|
||||
// console.log('[BatteryBarDebug]', {
|
||||
// frameSensors: sensors,
|
||||
// batteryCharge,
|
||||
@@ -466,9 +442,17 @@ export default function VideoTile({
|
||||
</div>
|
||||
) : null}
|
||||
{!noHud ? <OvercurrentOverlay motors={overlayMotors} fill={overlayFill} compact={mobileHud} /> : null}
|
||||
{!noHud ? <LowBatteryOverlay charge={batteryCharge} config={batteryConfig} compact={mobileHud} /> : null}
|
||||
{!noHud ? <LowBatteryOverlay battery={batteryVisual} compact={mobileHud} /> : null}
|
||||
{!noHud && showVerticalBattery && batteryVisual.available ? (
|
||||
<BatteryBarVertical visual={batteryVisual} />
|
||||
<div className="pointer-events-none absolute right-1 top-1/2 flex h-[70%] -translate-y-1/2 flex-col items-center justify-center rounded bg-black/60 px-0.5 pb-1 pt-1">
|
||||
<BatteryBar
|
||||
visual={batteryVisual}
|
||||
orientation="vertical"
|
||||
variant="inline"
|
||||
compact={mobileHud}
|
||||
className="h-full w-4"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{!noHud && qualityNotice ? (
|
||||
<div className="pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2">
|
||||
@@ -492,52 +476,15 @@ export default function VideoTile({
|
||||
{!noHud && !showVerticalBattery && (
|
||||
<div className="space-y-0.5">
|
||||
<LightBumpBars sensors={sensors} />
|
||||
<BatteryBar visual={batteryVisual} />
|
||||
<div className="panel-section space-y-0.5 text-sm">
|
||||
<BatteryBar visual={batteryVisual} compact={mobileHud} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BatteryBar({ visual }) {
|
||||
if (!visual?.available) {
|
||||
return (
|
||||
<div className="panel-section space-y-0.5 text-sm">
|
||||
<p className="text-xs text-slate-500">Battery telemetry unavailable</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const percentText = `${visual.percentDisplay}%`;
|
||||
const barClass = visual.barClass;
|
||||
return (
|
||||
<div className="panel-section space-y-0.5 text-sm">
|
||||
<div className="relative h-4 w-full bg-zinc-900 flex">
|
||||
<div className={`h-full transition-[width] ${barClass}`} style={{ width: `${visual.percentDisplay}%` }}>
|
||||
<span className="inset-0 flex items-center justify-center text-xs font-semibold text-black/80">
|
||||
Battery {percentText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BatteryBarVertical({ visual }) {
|
||||
if (!visual?.available) return null;
|
||||
const percentText = `${visual.percentDisplay}%`;
|
||||
return (
|
||||
<div className="pointer-events-none absolute right-1 top-1/2 flex h-[70%] -translate-y-1/2 flex-col items-center justify-end rounded bg-black/60 px-0.5 pb-1 pt-1">
|
||||
<div className="flex h-full w-4 items-end overflow-hidden rounded bg-zinc-900">
|
||||
<div
|
||||
className={`${visual.barClass} w-full transition-[height]`}
|
||||
style={{ height: `${visual.percentDisplay}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="mt-0 text-[0.65rem] font-semibold text-slate-100">{percentText}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LightBumpBars({ sensors }) {
|
||||
const values = [
|
||||
sensors?.lightBumpLeftSignal,
|
||||
@@ -828,20 +775,13 @@ function OvercurrentOverlay({ motors, fill = 0, compact = false }) {
|
||||
}
|
||||
|
||||
// low battery overlay, change text based on warn / urgent. use percentage calculated same as BatteryBar. change text based on warn or urgent.
|
||||
function LowBatteryOverlay({ charge, config, compact = false }) {
|
||||
if (charge == null || config == null) return null;
|
||||
const full = config.Full;
|
||||
const warn = config.Warn;
|
||||
const urgent = config.Urgent ?? null;
|
||||
const span = full - warn;
|
||||
if (span <= 0) return null;
|
||||
const normalized = (charge - warn) / span;
|
||||
const percent = Math.min(1, Math.max(0, normalized));
|
||||
const depleted = normalized <= 0;
|
||||
const warnTriggered = urgent != null && charge <= urgent;
|
||||
if (!warnTriggered && !depleted) return null;
|
||||
function LowBatteryOverlay({ battery, compact = false }) {
|
||||
if (!battery?.available) return null;
|
||||
if (!battery.warnActive && !battery.urgentActive) return null;
|
||||
|
||||
const message = depleted ? 'Battery low! please dock and charge the rover soon.' : 'BATTERY VERY LOW, PLEASE DOCK THE ROVER AND CHARGE IMMEDIATELY!!';
|
||||
const message = battery.urgentActive
|
||||
? 'BATTERY VERY LOW, DOCK THE ROVER AND CHARGE IMMEDIATELY!!'
|
||||
: 'Battery low! please dock and charge the rover soon.';
|
||||
|
||||
const containerClass = compact ? 'p-2 top-6' : 'p-4 top-10';
|
||||
const textClass = compact ? 'text-sm' : 'text-2xl';
|
||||
|
||||
@@ -78,6 +78,36 @@ body {
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@keyframes batteryTickWarn {
|
||||
0%,
|
||||
49% {
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
background-color: rgba(239, 68, 68, 0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.battery-tick-warn {
|
||||
animation: batteryTickWarn 0.4s steps(2, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes batteryUrgentFlash {
|
||||
0%,
|
||||
49% {
|
||||
opacity: 1;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
.battery-urgent-flash {
|
||||
animation: batteryUrgentFlash 0.4s steps(2, end) infinite;
|
||||
}
|
||||
|
||||
.no-touch-select {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
export const WARN_DISPLAY_PERCENT = 10;
|
||||
|
||||
export function buildBatteryVisual({ batteryState = null, charge = null, config = null }) {
|
||||
const percentDisplayFromState =
|
||||
batteryState?.percentDisplay != null && Number.isFinite(batteryState.percentDisplay)
|
||||
? Math.round(batteryState.percentDisplay)
|
||||
: null;
|
||||
const warnActiveFromState = batteryState?.warnActive ?? null;
|
||||
const urgentActiveFromState = batteryState?.urgentActive ?? null;
|
||||
|
||||
const full = config?.Full ?? null;
|
||||
const warn = config?.Warn ?? null;
|
||||
const urgent = config?.Urgent ?? null;
|
||||
const chargeValue = charge ?? batteryState?.charge ?? null;
|
||||
|
||||
let percentDisplay = percentDisplayFromState;
|
||||
if (percentDisplay == null) {
|
||||
percentDisplay = computeDisplayPercent({ charge: chargeValue, full, warn, urgent });
|
||||
}
|
||||
if (percentDisplay == null) {
|
||||
return { available: false };
|
||||
}
|
||||
|
||||
const percent = Math.max(0, Math.min(1, percentDisplay / 100));
|
||||
const warnActive =
|
||||
warnActiveFromState != null
|
||||
? warnActiveFromState
|
||||
: Boolean(warn != null && chargeValue != null && chargeValue <= warn);
|
||||
const urgentActive =
|
||||
urgentActiveFromState != null
|
||||
? urgentActiveFromState
|
||||
: Boolean(urgent != null && chargeValue != null && chargeValue <= urgent);
|
||||
|
||||
return {
|
||||
available: true,
|
||||
percent,
|
||||
percentDisplay,
|
||||
warnActive,
|
||||
urgentActive,
|
||||
warnDisplayPercent: WARN_DISPLAY_PERCENT,
|
||||
};
|
||||
}
|
||||
|
||||
function computeDisplayPercent({ charge, full, warn, urgent }) {
|
||||
if (
|
||||
charge != null &&
|
||||
full != null &&
|
||||
warn != null &&
|
||||
urgent != null &&
|
||||
full > warn &&
|
||||
warn > urgent
|
||||
) {
|
||||
const warnOffset = WARN_DISPLAY_PERCENT / 100;
|
||||
const upperSpan = 1 - warnOffset;
|
||||
if (charge <= urgent) return 0;
|
||||
if (charge <= warn) {
|
||||
const t = (charge - urgent) / (warn - urgent);
|
||||
return Math.round(Math.max(0, Math.min(1, t)) * WARN_DISPLAY_PERCENT);
|
||||
}
|
||||
if (charge >= full) return 100;
|
||||
const t = (charge - warn) / (full - warn);
|
||||
return Math.round((warnOffset + Math.max(0, Math.min(1, t)) * upperSpan) * 100);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import VideoTile from '../components/VideoTile.jsx';
|
||||
import TopDownMap from '../components/TopDownMap.jsx';
|
||||
import AlertFeed from '../components/AlertFeed.jsx';
|
||||
import useDefaultNickname from '../hooks/useDefaultNickname.js';
|
||||
import BatteryBar from '../components/BatteryBar.jsx';
|
||||
import { buildBatteryVisual } from '../lib/battery.js';
|
||||
|
||||
const ROTATE_MS = 20000;
|
||||
const HARD_REFRESH_MS = 1 * 60 * 60 * 1000;
|
||||
@@ -22,28 +24,10 @@ function formatDriverLabel({ roverId, session }) {
|
||||
return mode === 'turns' && turnInfo?.current ? `${label}` : label;
|
||||
}
|
||||
|
||||
function getBatteryPercent({ rover, frame }) {
|
||||
const percentDisplay = rover?.batteryState?.percentDisplay;
|
||||
if (percentDisplay != null && Number.isFinite(percentDisplay)) {
|
||||
return Math.round(percentDisplay);
|
||||
}
|
||||
const config = rover?.battery ?? null;
|
||||
function getBatteryVisual({ rover, frame }) {
|
||||
const charge = frame?.sensors?.batteryChargeMah ?? null;
|
||||
const full = config?.Full ?? null;
|
||||
const warn = config?.Warn ?? null;
|
||||
if (charge == null || full == null || warn == null) return null;
|
||||
const span = full - warn;
|
||||
if (span <= 0) return null;
|
||||
const normalized = (charge - warn) / span;
|
||||
return Math.round(Math.max(0, Math.min(1, normalized)) * 100);
|
||||
}
|
||||
|
||||
function getBatteryFillClass({ rover }) {
|
||||
const urgentActive = rover?.batteryState?.urgentActive ?? false;
|
||||
const warnActive = rover?.batteryState?.warnActive ?? false;
|
||||
if (urgentActive) return 'bg-red-500/40';
|
||||
if (warnActive) return 'bg-amber-400/40';
|
||||
return 'bg-emerald-400/40';
|
||||
const config = rover?.battery ?? null;
|
||||
return buildBatteryVisual({ batteryState: rover?.batteryState ?? null, charge, config });
|
||||
}
|
||||
|
||||
function InfoColumn({
|
||||
@@ -57,22 +41,20 @@ function InfoColumn({
|
||||
showPreview = true,
|
||||
variant = 'stacked',
|
||||
}) {
|
||||
const batteryPercent = getBatteryPercent({ rover, frame });
|
||||
const batteryFillClass = getBatteryFillClass({ rover });
|
||||
const batteryVisual = getBatteryVisual({ rover, frame });
|
||||
const batteryPercent = batteryVisual?.available ? batteryVisual.percentDisplay : null;
|
||||
const isActiveView = variant === 'active';
|
||||
const batteryFillStyle = isActiveView
|
||||
? { height: `${batteryPercent == null ? 0 : batteryPercent}%`, bottom: 0, top: 'auto' }
|
||||
: { width: `${batteryPercent == null ? 0 : batteryPercent}%` };
|
||||
const batteryFillClassName = isActiveView
|
||||
? `pointer-events-none absolute bottom-0 left-0 right-0 ${batteryFillClass}`
|
||||
: `pointer-events-none absolute left-0 top-0 bottom-0 ${batteryFillClass}`;
|
||||
return (
|
||||
<div
|
||||
className={`relative flex min-w-0 flex-1 flex-col gap-4 overflow-hidden bg-black px-0 py-0 ${
|
||||
withDivider ? 'border-r border-slate-700/60' : ''
|
||||
}`}
|
||||
>
|
||||
<div className={batteryFillClassName} style={batteryFillStyle} />
|
||||
<BatteryBar
|
||||
visual={batteryVisual}
|
||||
orientation="vertical"
|
||||
variant="background"
|
||||
/>
|
||||
{isActiveView ? (
|
||||
<div className="relative z-10 flex min-w-0 flex-1 flex-col justify-between text-center">
|
||||
<div className="min-w-0 bg-transparent px-0 py-0 leading-none">
|
||||
|
||||
Reference in New Issue
Block a user