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;
|
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,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="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)' }} />
|
||||||
|
{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
|
<input
|
||||||
value={origin}
|
value={origin}
|
||||||
onChange={(e) => onOriginChange(e.target.value)}
|
onChange={(e) => onOriginChange(e.target.value)}
|
||||||
@@ -1444,6 +1458,7 @@ function TopBar({
|
|||||||
className="bg-transparent border-none outline-none text-[13px] w-full"
|
className="bg-transparent border-none outline-none text-[13px] w-full"
|
||||||
style={{ color: 'var(--gd-text)' }}
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user