feat(ui): Phase 3c — vehicle selector panel with all Tesla trims
Replace the simple <select> vehicle chip with a proper anchored
glass panel matching Direction B.
- Full Tesla model catalogue (S / 3 / Y / X / Cybertruck) with all
trims (Standard / Long Range RWD / Long Range AWD / Performance /
Plaid / Cyberbeast) and per-trim range, peak kW, 0-100, top speed,
and badges (Most popular, Best range, Performance).
- Vehicle chip in the top bar now shows the model + abbreviated trim
("Model Y LR AWD") with range underneath, opens the panel anchored
to the chip's bounding rect.
- Panel header has a starting-charge slider (10-100%, snaps to 5).
Each trim shows "X km now" (= range × charge%) in green so the
user sees the live impact of picking a different trim or charge
level.
- Model groups collapse / expand independently; the currently-
selected model expands by default.
- Picking a trim updates the chip everywhere; closes the panel.
- Backdrop click and Esc both dismiss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -118,13 +118,89 @@ const EMPTY_ITINERARY: Itinerary = {
|
|||||||
|
|
||||||
const STOP_TYPES: StopType[] = ['supercharger', 'destination-charger', 'hotel', 'attraction', 'restaurant', 'cafe', 'viewpoint', 'custom', 'origin', 'destination', 'tunnel'];
|
const STOP_TYPES: StopType[] = ['supercharger', 'destination-charger', 'hotel', 'attraction', 'restaurant', 'cafe', 'viewpoint', 'custom', 'origin', 'destination', 'tunnel'];
|
||||||
|
|
||||||
const VEHICLES = [
|
interface VehicleTrim {
|
||||||
{ name: 'Model Y Long Range', trim: 'Long Range AWD', rangeKm: 514, efficiency: 165 },
|
id: string;
|
||||||
{ name: 'Model 3 Highland LR', trim: 'Long Range', rangeKm: 549, efficiency: 155 },
|
name: string; // e.g. "Long Range AWD"
|
||||||
{ name: 'Model S Long Range', trim: 'Long Range', rangeKm: 634, efficiency: 175 },
|
rangeKm: number;
|
||||||
{ name: 'Model Y RWD (EU)', trim: 'Standard Range', rangeKm: 455, efficiency: 158 },
|
kw: number;
|
||||||
|
sec0to60: number;
|
||||||
|
topKmh: number;
|
||||||
|
badge?: string;
|
||||||
|
}
|
||||||
|
interface VehicleModel {
|
||||||
|
id: string;
|
||||||
|
name: string; // e.g. "Model Y"
|
||||||
|
description: string;
|
||||||
|
trims: VehicleTrim[];
|
||||||
|
}
|
||||||
|
interface Vehicle {
|
||||||
|
modelId: string;
|
||||||
|
trimId: string;
|
||||||
|
name: string; // model name
|
||||||
|
trim: string; // trim name
|
||||||
|
rangeKm: number;
|
||||||
|
kw: number;
|
||||||
|
sec0to60: number;
|
||||||
|
topKmh: number;
|
||||||
|
badge?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TESLA_MODELS: VehicleModel[] = [
|
||||||
|
{
|
||||||
|
id: 'model-s', name: 'Model S', description: 'Fastback sedan',
|
||||||
|
trims: [
|
||||||
|
{ id: 'lr', name: 'Long Range', rangeKm: 634, kw: 250, sec0to60: 3.1, topKmh: 240 },
|
||||||
|
{ id: 'plaid', name: 'Plaid', rangeKm: 600, kw: 250, sec0to60: 1.99, topKmh: 322, badge: 'Performance' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'model-3', name: 'Model 3', description: 'Compact sedan',
|
||||||
|
trims: [
|
||||||
|
{ id: 'std', name: 'Standard Range', rangeKm: 438, kw: 175, sec0to60: 5.6, topKmh: 201 },
|
||||||
|
{ id: 'lr-rwd', name: 'Long Range RWD', rangeKm: 553, kw: 250, sec0to60: 4.9, topKmh: 201, badge: 'Best range' },
|
||||||
|
{ id: 'lr-awd', name: 'Long Range AWD', rangeKm: 528, kw: 250, sec0to60: 4.2, topKmh: 201 },
|
||||||
|
{ id: 'perf', name: 'Performance', rangeKm: 528, kw: 250, sec0to60: 2.9, topKmh: 261, badge: 'Performance' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'model-y', name: 'Model Y', description: 'Crossover · best-seller',
|
||||||
|
trims: [
|
||||||
|
{ id: 'std', name: 'Standard Range', rangeKm: 460, kw: 175, sec0to60: 5.6, topKmh: 217 },
|
||||||
|
{ id: 'lr-rwd', name: 'Long Range RWD', rangeKm: 531, kw: 250, sec0to60: 5.9, topKmh: 217 },
|
||||||
|
{ id: 'lr-awd', name: 'Long Range AWD', rangeKm: 514, kw: 250, sec0to60: 4.8, topKmh: 217, badge: 'Most popular' },
|
||||||
|
{ id: 'perf', name: 'Performance', rangeKm: 488, kw: 250, sec0to60: 3.5, topKmh: 250, badge: 'Performance' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'model-x', name: 'Model X', description: 'Three-row SUV · falcon doors',
|
||||||
|
trims: [
|
||||||
|
{ id: 'lr', name: 'Long Range', rangeKm: 543, kw: 250, sec0to60: 3.8, topKmh: 250 },
|
||||||
|
{ id: 'plaid', name: 'Plaid', rangeKm: 528, kw: 250, sec0to60: 2.5, topKmh: 262, badge: 'Performance' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cybertruck', name: 'Cybertruck', description: 'Angular pickup',
|
||||||
|
trims: [
|
||||||
|
{ id: 'rwd', name: 'Long Range RWD', rangeKm: 563, kw: 350, sec0to60: 6.5, topKmh: 180 },
|
||||||
|
{ id: 'awd', name: 'AWD', rangeKm: 547, kw: 350, sec0to60: 4.1, topKmh: 180 },
|
||||||
|
{ id: 'beast', name: 'Cyberbeast', rangeKm: 515, kw: 350, sec0to60: 2.6, topKmh: 209, badge: 'Performance' },
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const DEFAULT_VEHICLE: Vehicle = (() => {
|
||||||
|
const m = TESLA_MODELS.find(x => x.id === 'model-y')!;
|
||||||
|
const t = m.trims.find(tr => tr.id === 'lr-awd')!;
|
||||||
|
return { 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 };
|
||||||
|
})();
|
||||||
|
|
||||||
|
function abbrevTrim(trim: string): string {
|
||||||
|
return trim
|
||||||
|
.replace('Long Range', 'LR')
|
||||||
|
.replace('Standard Range', 'Std')
|
||||||
|
.replace('Performance', 'Perf');
|
||||||
|
}
|
||||||
|
|
||||||
const QUICK_PROMPTS = [
|
const QUICK_PROMPTS = [
|
||||||
'Plan a 2-day trip from London to Edinburgh in my Model Y',
|
'Plan a 2-day trip from London to Edinburgh in my Model Y',
|
||||||
'I want to drive from Amsterdam to Munich',
|
'I want to drive from Amsterdam to Munich',
|
||||||
@@ -1031,7 +1107,7 @@ function makePinIcon(color: string, active: boolean, hover: boolean): L.DivIcon
|
|||||||
function TopBar({
|
function TopBar({
|
||||||
origin, destination, onOriginChange, onDestinationChange, onODCommit,
|
origin, destination, onOriginChange, onDestinationChange, onODCommit,
|
||||||
chatInput, setChatInput, onChatSubmit, chips, onRemoveChip,
|
chatInput, setChatInput, onChatSubmit, chips, onRemoveChip,
|
||||||
vehicle, onVehicleChange, grokStatus, onOpenGpx,
|
vehicle, onOpenVehiclePanel, grokStatus, onOpenGpx,
|
||||||
}: {
|
}: {
|
||||||
origin: string; destination: string;
|
origin: string; destination: string;
|
||||||
onOriginChange: (v: string) => void;
|
onOriginChange: (v: string) => void;
|
||||||
@@ -1040,7 +1116,7 @@ function TopBar({
|
|||||||
chatInput: string; setChatInput: (v: string) => void;
|
chatInput: string; setChatInput: (v: string) => void;
|
||||||
onChatSubmit: () => void;
|
onChatSubmit: () => void;
|
||||||
chips: string[]; onRemoveChip: (i: number) => void;
|
chips: string[]; onRemoveChip: (i: number) => void;
|
||||||
vehicle: typeof VEHICLES[number]; onVehicleChange: (v: typeof VEHICLES[number]) => void;
|
vehicle: Vehicle; onOpenVehiclePanel: (rect: DOMRect) => void;
|
||||||
grokStatus: { label?: string };
|
grokStatus: { label?: string };
|
||||||
onOpenGpx: () => void;
|
onOpenGpx: () => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -1141,28 +1217,22 @@ function TopBar({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Vehicle chip */}
|
{/* Vehicle chip — opens trim panel */}
|
||||||
<div className="relative">
|
<button
|
||||||
<select
|
onClick={(e) => onOpenVehiclePanel(e.currentTarget.getBoundingClientRect())}
|
||||||
value={vehicle.name}
|
className="h-[38px] px-3 inline-flex items-center gap-2 rounded-[10px] cursor-pointer"
|
||||||
onChange={(e) => onVehicleChange(VEHICLES.find(v => v.name === e.target.value)!)}
|
style={{ background: 'var(--gd-panel)', border: '1px solid var(--gd-border)' }}
|
||||||
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 => (
|
<Gauge className="w-3.5 h-3.5" style={{ color: 'var(--gd-text-2)' }} />
|
||||||
<option key={v.name} value={v.name}>
|
<div className="flex flex-col items-start leading-[1.15]">
|
||||||
{v.name} — {v.rangeKm} km
|
<div className="text-[12px] font-medium" style={{ letterSpacing: '-0.005em' }}>
|
||||||
</option>
|
{vehicle.name}{' '}
|
||||||
))}
|
<span style={{ color: 'var(--gd-text-3)', fontWeight: 400 }}>{abbrevTrim(vehicle.trim)}</span>
|
||||||
</select>
|
|
||||||
<Gauge className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 pointer-events-none" style={{ color: 'var(--gd-text-2)' }} />
|
|
||||||
<ChevronDown className="absolute right-2.5 top-1/2 -translate-y-1/2 w-3 h-3 pointer-events-none" style={{ color: 'var(--gd-text-3)' }} />
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-[10px] num" style={{ color: 'var(--gd-text-3)' }}>{vehicle.rangeKm} km</div>
|
||||||
|
</div>
|
||||||
|
<ChevronDown className="w-3 h-3" style={{ color: 'var(--gd-text-3)' }} />
|
||||||
|
</button>
|
||||||
|
|
||||||
<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)' }} />
|
||||||
@@ -1190,7 +1260,9 @@ export default function TeslaTripPlanner() {
|
|||||||
const [chips, setChips] = useState<string[]>([]);
|
const [chips, setChips] = useState<string[]>([]);
|
||||||
const [thinking, setThinking] = useState(false);
|
const [thinking, setThinking] = useState(false);
|
||||||
const [itinerary, setItinerary] = useState<Itinerary>(EMPTY_ITINERARY);
|
const [itinerary, setItinerary] = useState<Itinerary>(EMPTY_ITINERARY);
|
||||||
const [vehicle, setVehicle] = useState(VEHICLES[0]);
|
const [vehicle, setVehicle] = useState<Vehicle>(DEFAULT_VEHICLE);
|
||||||
|
const [vehiclePanelOpen, setVehiclePanelOpen] = useState(false);
|
||||||
|
const [vehicleAnchor, setVehicleAnchor] = useState<DOMRect | null>(null);
|
||||||
const [grokStatus, setGrokStatus] = useState<{ label?: string }>({ label: 'Local Heavy' });
|
const [grokStatus, setGrokStatus] = useState<{ label?: string }>({ label: 'Local Heavy' });
|
||||||
const [legs, setLegs] = useState<Leg[]>([]);
|
const [legs, setLegs] = useState<Leg[]>([]);
|
||||||
const [activeStopId, setActiveStopId] = useState<string | null>(null);
|
const [activeStopId, setActiveStopId] = useState<string | null>(null);
|
||||||
@@ -1467,7 +1539,8 @@ export default function TeslaTripPlanner() {
|
|||||||
chatInput={chatInput} setChatInput={setChatInput}
|
chatInput={chatInput} setChatInput={setChatInput}
|
||||||
onChatSubmit={() => sendMessage(chatInput)}
|
onChatSubmit={() => sendMessage(chatInput)}
|
||||||
chips={chips} onRemoveChip={(i) => setChips(chips.filter((_, idx) => idx !== i))}
|
chips={chips} onRemoveChip={(i) => setChips(chips.filter((_, idx) => idx !== i))}
|
||||||
vehicle={vehicle} onVehicleChange={setVehicle}
|
vehicle={vehicle}
|
||||||
|
onOpenVehiclePanel={(rect) => { setVehicleAnchor(rect); setVehiclePanelOpen(true); }}
|
||||||
grokStatus={grokStatus}
|
grokStatus={grokStatus}
|
||||||
onOpenGpx={() => setModal({ kind: 'gpx' })}
|
onOpenGpx={() => setModal({ kind: 'gpx' })}
|
||||||
/>
|
/>
|
||||||
@@ -1752,6 +1825,14 @@ export default function TeslaTripPlanner() {
|
|||||||
{modal?.kind === 'gpx' && (
|
{modal?.kind === 'gpx' && (
|
||||||
<GpxExportModal itinerary={itinerary} onClose={() => setModal(null)} />
|
<GpxExportModal itinerary={itinerary} onClose={() => setModal(null)} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<VehicleSelectorPanel
|
||||||
|
open={vehiclePanelOpen}
|
||||||
|
anchorRect={vehicleAnchor}
|
||||||
|
selected={vehicle}
|
||||||
|
onSelect={setVehicle}
|
||||||
|
onClose={() => setVehiclePanelOpen(false)}
|
||||||
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<div className="fixed inset-0 z-40" onClick={onClose} />
|
||||||
|
<div
|
||||||
|
className="fixed z-50 overflow-hidden"
|
||||||
|
style={{
|
||||||
|
left, top, width: panelWidth, maxHeight: '78vh',
|
||||||
|
background: 'rgba(20,20,24,0.96)',
|
||||||
|
backdropFilter: 'blur(18px)',
|
||||||
|
border: '1px solid var(--gd-border-2)',
|
||||||
|
borderRadius: 14,
|
||||||
|
boxShadow: '0 24px 60px rgba(0,0,0,0.5)',
|
||||||
|
display: 'flex', flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header with starting-charge slider */}
|
||||||
|
<div className="px-4 py-3.5 flex-shrink-0" style={{ borderBottom: '1px solid var(--gd-border)' }}>
|
||||||
|
<div className="flex items-center justify-between mb-2.5">
|
||||||
|
<div className="text-[11px] uppercase tracking-wider" style={{ color: 'var(--gd-text-3)' }}>
|
||||||
|
Starting charge
|
||||||
|
</div>
|
||||||
|
<div className="text-[12px] num" style={{ color: 'var(--gd-text)' }}>
|
||||||
|
{chargePct}% · <span className="num" style={{ color: 'var(--gd-green)' }}>
|
||||||
|
{Math.round(selected.rangeKm * chargePct / 100)} km
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative h-4">
|
||||||
|
<input
|
||||||
|
type="range" min={10} max={100} step={5} value={chargePct}
|
||||||
|
onChange={(e) => setChargePct(parseInt(e.target.value))}
|
||||||
|
className="absolute inset-0 w-full opacity-0 cursor-pointer z-10"
|
||||||
|
/>
|
||||||
|
<div className="absolute top-1.5 left-0 right-0 h-1 rounded" style={{ background: 'var(--gd-border)' }} />
|
||||||
|
<div className="absolute top-1.5 left-0 h-1 rounded" style={{ width: `${chargePct}%`, background: 'var(--gd-green)' }} />
|
||||||
|
<div
|
||||||
|
className="absolute top-0 w-4 h-4 rounded-full"
|
||||||
|
style={{ left: `calc(${chargePct}% - 8px)`, background: '#fff', border: '3px solid var(--gd-green)' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Model + trim list */}
|
||||||
|
<div className="overflow-y-auto px-2 py-2 flex-1 min-h-0">
|
||||||
|
{TESLA_MODELS.map(m => (
|
||||||
|
<ModelGroup
|
||||||
|
key={m.id}
|
||||||
|
model={m}
|
||||||
|
selected={selected}
|
||||||
|
chargePct={chargePct}
|
||||||
|
onPick={(t) => pick(m, t)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="mb-1.5">
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen(o => !o)}
|
||||||
|
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-white/[0.04] transition"
|
||||||
|
>
|
||||||
|
<div className="text-[14px] font-medium flex-1 text-left">{model.name}</div>
|
||||||
|
<div className="text-[11px]" style={{ color: 'var(--gd-text-3)' }}>{model.description}</div>
|
||||||
|
{open ? <ChevronUp className="w-3.5 h-3.5" style={{ color: 'var(--gd-text-3)' }} /> : <ChevronDown className="w-3.5 h-3.5" style={{ color: 'var(--gd-text-3)' }} />}
|
||||||
|
</button>
|
||||||
|
{open && (
|
||||||
|
<div className="pl-3 pr-1 pb-1 space-y-1">
|
||||||
|
{model.trims.map(t => {
|
||||||
|
const isSel = model.id === selected.modelId && t.id === selected.trimId;
|
||||||
|
const kmNow = Math.round(t.rangeKm * chargePct / 100);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={t.id}
|
||||||
|
onClick={() => onPick(t)}
|
||||||
|
className="w-full text-left p-2.5 rounded-lg transition"
|
||||||
|
style={{
|
||||||
|
background: isSel ? 'var(--gd-red-soft)' : 'transparent',
|
||||||
|
border: `1px solid ${isSel ? 'var(--gd-red-line)' : 'var(--gd-border)'}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<div className="text-[13px] font-medium">{t.name}</div>
|
||||||
|
{t.badge && (
|
||||||
|
<span
|
||||||
|
className="text-[9px] font-semibold tracking-wider px-1.5 py-0.5 rounded-full"
|
||||||
|
style={{
|
||||||
|
color: t.badge === 'Performance' ? 'var(--gd-amber)' : 'var(--gd-blue)',
|
||||||
|
border: `1px solid ${t.badge === 'Performance' ? 'rgba(251,191,36,0.4)' : 'rgba(96,165,250,0.4)'}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t.badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="flex-1" />
|
||||||
|
{isSel && (
|
||||||
|
<span className="text-[9.5px] font-semibold tracking-wider" style={{ color: 'var(--gd-red)' }}>SELECTED</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3.5 mt-1.5 text-[10.5px] num" style={{ color: 'var(--gd-text-3)' }}>
|
||||||
|
<span>{t.rangeKm} km full</span>
|
||||||
|
<span style={{ color: 'var(--gd-green)' }}>{kmNow} km now</span>
|
||||||
|
<span>0–100 in {t.sec0to60}s</span>
|
||||||
|
<span>{t.kw} kW peak</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function CheckRow({ label, value, onChange }: { label: string; value: boolean; onChange: (v: boolean) => void }) {
|
function CheckRow({ label, value, onChange }: { label: string; value: boolean; onChange: (v: boolean) => void }) {
|
||||||
return (
|
return (
|
||||||
<label className="flex items-center gap-2 py-1.5 text-[12px] cursor-pointer" style={{ color: 'var(--gd-text-2)' }}>
|
<label className="flex items-center gap-2 py-1.5 text-[12px] cursor-pointer" style={{ color: 'var(--gd-text-2)' }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user