From b61e3510b9337c0c0a66e595e95a8506c78030db Mon Sep 17 00:00:00 2001 From: Tony James Date: Wed, 20 May 2026 14:05:48 +0100 Subject: [PATCH] =?UTF-8?q?feat(ui):=20Phase=203c=20=E2=80=94=20vehicle=20?= =?UTF-8?q?selector=20panel=20with=20all=20Tesla=20trims?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the simple onVehicleChange(VEHICLES.find(v => v.name === e.target.value)!)} - className="appearance-none h-[38px] pl-9 pr-7 rounded-[10px] text-[12px] cursor-pointer" - style={{ - background: 'var(--gd-panel)', - border: '1px solid var(--gd-border)', - color: 'var(--gd-text)', - fontFamily: 'inherit', - }} - > - {VEHICLES.map(v => ( - - ))} - - - - + {/* Vehicle chip — opens trim panel */} + onOpenGpx()}> @@ -1190,7 +1260,9 @@ export default function TeslaTripPlanner() { const [chips, setChips] = useState([]); const [thinking, setThinking] = useState(false); const [itinerary, setItinerary] = useState(EMPTY_ITINERARY); - const [vehicle, setVehicle] = useState(VEHICLES[0]); + const [vehicle, setVehicle] = useState(DEFAULT_VEHICLE); + const [vehiclePanelOpen, setVehiclePanelOpen] = useState(false); + const [vehicleAnchor, setVehicleAnchor] = useState(null); const [grokStatus, setGrokStatus] = useState<{ label?: string }>({ label: 'Local Heavy' }); const [legs, setLegs] = useState([]); const [activeStopId, setActiveStopId] = useState(null); @@ -1467,7 +1539,8 @@ export default function TeslaTripPlanner() { chatInput={chatInput} setChatInput={setChatInput} onChatSubmit={() => sendMessage(chatInput)} chips={chips} onRemoveChip={(i) => setChips(chips.filter((_, idx) => idx !== i))} - vehicle={vehicle} onVehicleChange={setVehicle} + vehicle={vehicle} + onOpenVehiclePanel={(rect) => { setVehicleAnchor(rect); setVehiclePanelOpen(true); }} grokStatus={grokStatus} onOpenGpx={() => setModal({ kind: 'gpx' })} /> @@ -1752,6 +1825,14 @@ export default function TeslaTripPlanner() { {modal?.kind === 'gpx' && ( setModal(null)} /> )} + + setVehiclePanelOpen(false)} + /> ); } @@ -2510,6 +2591,157 @@ function GpxExportModal({ itinerary, onClose }: { itinerary: Itinerary; onClose: ); } +// ─── Vehicle selector panel ────────────────────────────────────────────────── +function VehicleSelectorPanel({ + open, anchorRect, selected, onSelect, onClose, +}: { + open: boolean; + anchorRect: DOMRect | null; + selected: Vehicle; + onSelect: (v: Vehicle) => void; + onClose: () => void; +}) { + const [chargePct, setChargePct] = React.useState(80); + React.useEffect(() => { + const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [onClose]); + if (!open || !anchorRect) return null; + const panelWidth = 460; + const left = Math.max(12, Math.min(window.innerWidth - panelWidth - 12, anchorRect.right - panelWidth)); + const top = anchorRect.bottom + 8; + + const pick = (m: VehicleModel, t: VehicleTrim) => { + onSelect({ + modelId: m.id, trimId: t.id, name: m.name, trim: t.name, + rangeKm: t.rangeKm, kw: t.kw, sec0to60: t.sec0to60, topKmh: t.topKmh, badge: t.badge, + }); + onClose(); + }; + + return ( + <> +
+
+ {/* Header with starting-charge slider */} +
+
+
+ Starting charge +
+
+ {chargePct}% · + {Math.round(selected.rangeKm * chargePct / 100)} km + +
+
+
+ setChargePct(parseInt(e.target.value))} + className="absolute inset-0 w-full opacity-0 cursor-pointer z-10" + /> +
+
+
+
+
+ + {/* Model + trim list */} +
+ {TESLA_MODELS.map(m => ( + pick(m, t)} + /> + ))} +
+
+ + ); +} + +function ModelGroup({ model, selected, chargePct, onPick }: { + model: VehicleModel; selected: Vehicle; chargePct: number; onPick: (t: VehicleTrim) => void; +}) { + const isExpanded = model.id === selected.modelId; + const [open, setOpen] = React.useState(isExpanded); + return ( +
+ + {open && ( +
+ {model.trims.map(t => { + const isSel = model.id === selected.modelId && t.id === selected.trimId; + const kmNow = Math.round(t.rangeKm * chargePct / 100); + return ( + + ); + })} +
+ )} +
+ ); +} + function CheckRow({ label, value, onChange }: { label: string; value: boolean; onChange: (v: boolean) => void }) { return (