feat(tesla): charging widget + in-car From pill + hide vehicle picker

- ChargingWidget: green topbar pill with live kW + battery + minutes-to-target,
  shown only when state.chargingState === 'Charging'. Animated bolt icon.
- Vehicle picker chip is hidden whenever Tesla is connected (anywhere, not
  just in-car) — we already know the car from vehicle_config.
- When in-car AND Tesla connected, the From input collapses to a static
  pill with a Tesla badge so the driver doesn't have to type on the touch
  keyboard. Destination input remains editable.
This commit is contained in:
2026-05-31 22:52:15 +01:00
parent 25d2779c39
commit 7265103573
+59 -1
View File
@@ -1396,7 +1396,11 @@ function TopBar({
onDisconnectTesla: () => void;
inCar: boolean;
}) {
const hideVehicleChip = inCar && !!teslaStatus?.connected;
// Tesla connected → we know the car, so the manual vehicle picker is dead weight.
const hideVehicleChip = !!teslaStatus?.connected;
// In-car AND connected → origin is the car's actual GPS, so collapse the
// From input to a static pill (typing on a touchscreen sucks).
const showFromAsPill = inCar && !!teslaStatus?.connected;
const hideGpxChip = inCar;
const [locating, setLocating] = React.useState(false);
const handleLocate = async () => {
@@ -1435,6 +1439,16 @@ function TopBar({
>
<div className="px-3.5 flex items-center gap-2 h-full flex-1" style={{ borderRight: '1px solid var(--gd-border)' }}>
<div className="w-1.5 h-1.5 rounded-full" style={{ background: 'var(--gd-text-2)' }} />
{showFromAsPill ? (
<div
className="text-[12px] w-full flex items-center gap-1.5"
style={{ color: 'var(--gd-text)' }}
title="Auto-detected from your Tesla's GPS"
>
<Car className="w-3 h-3" style={{ color: 'var(--gd-red)' }} />
<span className="truncate">{origin || 'Locating your car…'}</span>
</div>
) : (
<input
value={origin}
onChange={(e) => onOriginChange(e.target.value)}
@@ -1444,6 +1458,7 @@ function TopBar({
className="bg-transparent border-none outline-none text-[13px] w-full"
style={{ color: 'var(--gd-text)' }}
/>
)}
<button
onClick={handleLocate}
disabled={locating}
@@ -1581,6 +1596,15 @@ function TopBar({
)
)}
{/* Live charging widget — only when plugged in. */}
{teslaState?.chargingState === 'Charging' && (
<ChargingWidget
kw={teslaState.chargerPowerKw}
minutesToTarget={teslaState.timeToFullCharge != null ? Math.round(teslaState.timeToFullCharge * 60) : null}
battery={teslaState.battery}
/>
)}
{!hideGpxChip && (
<ChipButton onClick={() => onOpenGpx()}>
<Download className="w-3.5 h-3.5" style={{ color: 'var(--gd-text-2)' }} />
@@ -1603,6 +1627,40 @@ function TopBar({
);
}
function ChargingWidget({ kw, minutesToTarget, battery }: {
kw: number | null;
minutesToTarget: number | null;
battery: number | null;
}) {
return (
<div
className="h-[38px] px-3 inline-flex items-center gap-2 rounded-[10px]"
style={{
background: 'rgba(74,222,128,0.10)',
border: '1px solid rgba(74,222,128,0.4)',
color: 'var(--gd-green)',
}}
title="Live charging from your Tesla"
>
<Zap className="w-3.5 h-3.5 animate-pulse" />
<div className="flex flex-col items-start leading-[1.15]">
<div className="text-[11.5px] font-semibold num">
{kw != null ? `${Math.round(kw)} kW` : 'Charging'}
</div>
<div className="text-[9.5px] num" style={{ color: 'var(--gd-text-3)' }}>
{battery != null && minutesToTarget != null
? `${battery}% · ${minutesToTarget}m left`
: minutesToTarget != null
? `${minutesToTarget}m left`
: battery != null
? `${battery}%`
: '—'}
</div>
</div>
</div>
);
}
function MockTeslaIndicator() {
const [scenario, setScenarioState] = React.useState(getMockScenario());
if (!scenario) return null;