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
+68 -10
View File
@@ -1396,7 +1396,11 @@ function TopBar({
onDisconnectTesla: () => void; onDisconnectTesla: () => void;
inCar: boolean; 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 hideGpxChip = inCar;
const [locating, setLocating] = React.useState(false); const [locating, setLocating] = React.useState(false);
const handleLocate = async () => { const handleLocate = async () => {
@@ -1435,15 +1439,26 @@ 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="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)' }} /> <div className="w-1.5 h-1.5 rounded-full" style={{ background: 'var(--gd-text-2)' }} />
<input {showFromAsPill ? (
value={origin} <div
onChange={(e) => onOriginChange(e.target.value)} className="text-[12px] w-full flex items-center gap-1.5"
onKeyDown={(e) => { if (e.key === 'Enter') { e.currentTarget.blur(); onODCommit(); } }} style={{ color: 'var(--gd-text)' }}
onBlur={onODCommit} title="Auto-detected from your Tesla's GPS"
placeholder="From" >
className="bg-transparent border-none outline-none text-[13px] w-full" <Car className="w-3 h-3" style={{ color: 'var(--gd-red)' }} />
style={{ color: 'var(--gd-text)' }} <span className="truncate">{origin || 'Locating your car…'}</span>
/> </div>
) : (
<input
value={origin}
onChange={(e) => onOriginChange(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') { e.currentTarget.blur(); onODCommit(); } }}
onBlur={onODCommit}
placeholder="From"
className="bg-transparent border-none outline-none text-[13px] w-full"
style={{ color: 'var(--gd-text)' }}
/>
)}
<button <button
onClick={handleLocate} onClick={handleLocate}
disabled={locating} 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 && ( {!hideGpxChip && (
<ChipButton onClick={() => onOpenGpx()}> <ChipButton onClick={() => onOpenGpx()}>
<Download className="w-3.5 h-3.5" style={{ color: 'var(--gd-text-2)' }} /> <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() { function MockTeslaIndicator() {
const [scenario, setScenarioState] = React.useState(getMockScenario()); const [scenario, setScenarioState] = React.useState(getMockScenario());
if (!scenario) return null; if (!scenario) return null;