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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user