/* ============================================================ Homeowner mode — split screen ============================================================ */ /* global React, HomeMapView, Icon, LineChart20, BarChart */ const { useState: useStateH, useMemo: useMemoH, useEffect: useEffectH } = React; const { INR, NUM, suryaGharSubsidy, simplePayback, npv, emi, co2Tonnes, treesEquivalent, carKmEquivalent, dieselSavings, annualProduction, systemKwFromPanels, CO2_PER_KWH } = window.LEPTON_UTILS; const DEFAULT_LAYERS = { rgb: false, annualFlux: true, monthlyFlux: false, shaded: false, dsm: false, mask: false, segments: true, panels: true, }; const LAYER_DEFS = [ { id: "rgb", label: "RGB orthophoto", group: "imagery", swatch: "swatch-rgb", kbd: "1" }, { id: "mask", label: "Building mask", group: "imagery", swatch: "swatch-mask", kbd: "2" }, { id: "annualFlux", label: "Annual flux", group: "data", swatch: "swatch-flux", kbd: "3" }, { id: "monthlyFlux", label: "Monthly flux", group: "data", swatch: "swatch-monthly", kbd: "4" }, { id: "shaded", label: "Sun hours / yr", group: "data", swatch: "swatch-shade", kbd: "5" }, { id: "dsm", label: "DSM (heightmap)", group: "data", swatch: "swatch-dsm", kbd: "6" }, { id: "segments", label: "Roof segments", group: "vector", swatch: "swatch-segments", kbd: "7" }, { id: "panels", label: "Panel layout", group: "vector", swatch: "swatch-panels", kbd: "8" }, ]; function LayerRow({ def, on, onToggle }) { return (
onToggle(def.id)} style={{ gridTemplateColumns: "20px 14px 1fr auto" }} >
{def.label}
{def.kbd}
); } function LayerPanel({ layers, onToggle, building, monthIdx }) { const groups = [ { label: "Imagery", filter: "imagery" }, { label: "Solar API rasters", filter: "data" }, { label: "Vector", filter: "vector" }, ]; const onCount = LAYER_DEFS.filter(l => layers[l.id]).length; return (
Layers
{onCount} / {LAYER_DEFS.length}
{groups.map(g => (
{g.label}
{LAYER_DEFS.filter(l => l.group === g.filter).map(l => ( ))}
))}
imagery date{building.imageryDate}
quality{building.imageryQuality}
); } function MonthScrubber({ monthIdx, setMonthIdx, building, playing, setPlaying, visible }) { if (!visible) return null; const factors = building.monthFlux.map(m => m.factor); const maxF = Math.max(...factors); return (
{building.monthFlux.map((m, i) => ( ))}
{building.monthFlux[monthIdx].m}
×{building.monthFlux[monthIdx].factor.toFixed(2)}
); } function MapLegend({ activeLayers }) { // pick the primary "data" layer for the legend if (activeLayers.annualFlux) { return (
Annual flux
50011001750 kWh/m²
); } if (activeLayers.monthlyFlux) { return (
Monthly flux
lowhigh
); } if (activeLayers.shaded) { return (
Sun hours / yr
shadedpartialfull sun
Σ over 12 × 24 hourly-shade bitmasks · max ≈ 4380 hrs (daylight half)
); } if (activeLayers.dsm) { return (
DSM elevation
210m225m240m
); } if (activeLayers.segments && !activeLayers.annualFlux) { return (
Roof segments
{["#f59e0b","#3b82f6","#10b981","#ec4899"].map((c,i) => (
S{i+1}
))}
); } return null; } /* ============================================================ Side panel: Stats / Segments / System / Financials / Tweaks ============================================================ */ function StatsBlock({ building, systemKw, annualKwh }) { return (
Roof potential
Max array
{building.maxArrayPanelsCount}panels
≈ {building.maxArrayAreaMeters2.toFixed(1)} m² usable
Max system
{(building.maxArrayPanelsCount * building.panelCapacityWatts / 1000).toFixed(1)}kWp
@ {building.panelCapacityWatts}W panel
Max sunshine
{NUM(building.maxSunshineHoursPerYear)}hrs/yr
25th–95th pctile across roof
Annual production
{NUM(annualKwh)}kWh
{systemKw} kWp · {Math.round(annualKwh/systemKw)} kWh/kWp/yr
); } function SegmentTable({ building, hoveredSegment, onHoverSegment }) { const segColors = ["#f59e0b", "#3b82f6", "#10b981", "#ec4899"]; return (
Roof segments
{building.roofSegments.length}
{(() => { const totalKwh = building.roofSegments.reduce((a, s) => a + (s.yearlyKwhContribution || 0), 0); const bestSeg = totalKwh > 0 ? building.roofSegments.reduce((a, b) => (b.yearlyKwhContribution || 0) > (a.yearlyKwhContribution || 0) ? b : a) : null; return ( <> {building.roofSegments.map((seg, idx) => { const isBest = bestSeg && seg.id === bestSeg.id && totalKwh > 0; const sharePct = totalKwh > 0 ? Math.round((seg.yearlyKwhContribution || 0) / totalKwh * 100) : 0; return ( onHoverSegment(seg.id)} onMouseLeave={() => onHoverSegment(null)} > ); })}
idazimuth · pitch panels kWh/yr sun h/yr
{seg.id} {isBest && BEST} {seg.azimuthDeg}° / {seg.pitchDeg}° {seg.panelsAllocated || 0} {NUM(seg.yearlyKwhContribution || 0)} {sharePct > 0 && {sharePct}%} {NUM(seg.sunshineHours)}
{building.wholeRoofSunshineQuantiles && building.wholeRoofSunshineQuantiles.length >= 11 && (
Roof sunshine distribution
{building.wholeRoofSunshineQuantiles.map((q, i) => { const max = Math.max(...building.wholeRoofSunshineQuantiles); const h = max > 0 ? (q / max) * 100 : 0; const labels = ["0%", "10", "20", "30", "40", "50", "60", "70", "80", "90", "100%"]; return (
); })}
worst {NUM(building.wholeRoofSunshineQuantiles[0])} h median {NUM(building.wholeRoofSunshineQuantiles[5])} h best {NUM(building.wholeRoofSunshineQuantiles[10])} h
)} ); })()}
); } function SystemSlider({ panelCount, setPanelCount, building, systemKw, annualKwh }) { return (
System size
{systemKw} kWp
Panel count {panelCount} / {building.maxArrayPanelsCount}
setPanelCount(+e.target.value)} />
min 4 {Math.round(annualKwh / 12)} kWh / month avg max {building.maxArrayPanelsCount}
); } function TweaksBlock({ tweaks, setTweaks, discoms, monthlyBill, setMonthlyBill }) { const T = tweaks; const update = (k, v) => setTweaks({ ...T, [k]: v }); return (
Custom tweaks
India-specific
{/* Energy profile: DISCOM, bill or units, peak bill, sanctioned load */}
Your electricity
{discoms.find(d => d.id === T.discomId)?.name}
{(() => { const discom = discoms.find(d => d.id === T.discomId); const tariff = discom?.tariff || 7; const monthlyKwh = Math.round(monthlyBill / tariff); return ( <>
DISCOM{tariff.toFixed(2)} ₹/kWh
{/* Bill ↔ Units toggle */}
{T.billUnit === "inr" ? "Monthly bill" : "Monthly consumption"}
{T.billUnit === "inr" ? (
setMonthlyBill(Math.max(0, +e.target.value || 0))} style={{ paddingLeft: 22 }} />
) : (
setMonthlyBill(Math.round(Math.max(0, +e.target.value || 0) * tariff))} style={{ paddingRight: 36 }} /> kWh
)}
{INR(monthlyBill)} / mo · {NUM(monthlyKwh)} kWh / mo {NUM(monthlyKwh * 12)} kWh / yr
{/* Peak (summer) bill — optional */}
Peak summer bill (optional)
update("peakBill", Math.max(0, +e.target.value || 0))} style={{ paddingLeft: 22 }} />
{T.peakBill > 0 ? `Peak ≈ ${(T.peakBill / monthlyBill).toFixed(1)}× average. We weight 3 peak months into annual usage.` : "Leave blank if average bill is fine. Most Indian homes peak in May–Jul on AC."}
{/* Sanctioned load — optional */}
Sanctioned load (from DISCOM bill)
update("sanctionedLoadKw", Math.max(0, +e.target.value || 0))} style={{ paddingRight: 32 }} /> kW
Net-metering is capped at sanctioned load in most states. Leave blank if unsure.
); })()}
{/* EV charging plan */}
EV charging plan
{T.evEnabled ? `${T.evKmPerDay} km/d` : "off"}
{T.evEnabled && ( <>
Distance / day{T.evKmPerDay} km
update("evKmPerDay", +e.target.value)} />
Adds ≈ {NUM(Math.round(T.evKmPerDay * 365 * 0.15))} kWh / yr to your load ({Math.round(T.evKmPerDay * 0.15 * 30)} kWh / mo · ~ ₹{NUM(Math.round(T.evKmPerDay * 365 * 0.15 * (discoms.find(d=>d.id===T.discomId)?.tariff || 7)))} / yr at grid rate). Solar offsets this entirely if sized right.
)}
{/* Flat-roof tilt */}
Flat-roof tilt override
{T.tiltEnabled ? `${T.tiltDeg}°` : "off"}
{T.tiltEnabled && (
Tilt angle{T.tiltDeg}°
update("tiltDeg", +e.target.value)} />
Lat-optimal ≈ 24° in Gurugram. +{Math.round((1 - Math.abs(T.tiltDeg - 24) / 24) * 18)}% on flat segment.
)}
{/* Battery */}
Battery add-on
0 ? "on" : ""}`}>{T.batteryKwh > 0 ? `${T.batteryKwh} kWh` : "none"}
Capacity{T.batteryKwh} kWh
update("batteryKwh", +e.target.value)} />
+₹{NUM(T.batteryKwh * 32000)} CAPEX · {Math.min(95, 35 + T.batteryKwh * 4)}% self-consumption.
{/* Diesel offset */}
Diesel offset mode
{T.dieselEnabled ? `${T.dieselHrs} h/d` : "off"}
{T.dieselEnabled && ( <>
Diesel price₹{T.dieselPrice} / L
update("dieselPrice", +e.target.value)} />
Genset hrs / day{T.dieselHrs} h
update("dieselHrs", +e.target.value)} />
)}
); } function FinancialsBlock({ building, systemKw, annualKwh, tariff, tweaks, monthlyBill, discom }) { // CAPEX const baseCapex = Math.round(systemKw * 62000); const batteryCapex = tweaks.batteryKwh * 32000; const totalCapex = baseCapex + batteryCapex; // Subsidy const subRes = suryaGharSubsidy(systemKw); const netCapex = totalCapex - subRes.subsidy; // Annual baseline consumption (kWh). // If user provided a peak summer bill, weight 3 peak months at peak ₹ + 9 months at avg. // EV adds extra load if user planned for it. const effectiveAnnualBill = tweaks.peakBill > 0 ? tweaks.peakBill * 3 + monthlyBill * 9 : monthlyBill * 12; const evKwh = tweaks.evEnabled ? Math.round(tweaks.evKmPerDay * 365 * 0.15) : 0; const annualUsageKwh = Math.round(effectiveAnnualBill / tariff) + evKwh; // Savings — capped by usage const exported = Math.max(0, annualKwh - annualUsageKwh); const selfConsumed = annualKwh - exported; // self-consumed @ tariff, exported @ feed-in (₹2/kWh net metering avg) const exportRate = 2.0; const annualSavings = Math.round(selfConsumed * tariff + exported * exportRate); // CO2 — use Solar API's building-specific carbon factor when available // (CEA's grid factor varies by region; API reports ~0.93 kg/kWh for North India, // vs our generic 0.82 default). const co2KgPerKwh = (building?.carbonOffsetKgPerKwh && building.carbonOffsetKgPerKwh > 0) ? building.carbonOffsetKgPerKwh : CO2_PER_KWH; const annualCo2Kg = Math.round(annualKwh * co2KgPerKwh); // Lifetime — Solar API tells us panelLifetimeYears (typically 20). const lifetimeYears = building?.panelLifetimeYears || 20; // Payback / NPV — use the API-provided lifetime. const paybackYears = simplePayback(netCapex, annualSavings); const npv20 = npv(netCapex, annualSavings, lifetimeYears, 0.08, 0.04); // EMI (SBI Surya Ghar): 6.75%, 10y const emiAmount = Math.round(emi(netCapex, 0.0675, 10)); // Diesel const dieselSav = tweaks.dieselEnabled ? Math.round(annualKwh * 0.28 * (tweaks.dieselHrs / 8) * tweaks.dieselPrice * 0.6) : 0; const dieselPayback = tweaks.dieselEnabled && dieselSav > 0 ? +(netCapex / (annualSavings + dieselSav)).toFixed(1) : null; // Lifetime curve — Solar API tells us panelLifetimeYears (typically 20) const escalation = 0.04; const withSolar = []; const withoutSolar = []; { let solarCum = -netCapex; let nonCum = 0; let s = annualSavings; let g = annualUsageKwh * tariff; for (let y = 0; y < lifetimeYears; y++) { solarCum += s; nonCum -= g; withSolar.push(Math.round(solarCum)); withoutSolar.push(Math.round(nonCum)); s *= 1 + escalation; g *= 1 + escalation; } } const breakEvenYear = withSolar.findIndex(v => v >= 0) + 1; return (
Financials · India math
PM Surya Ghar
Net payback
{paybackYears < 100 ? paybackYears.toFixed(1) : "—"}years
NPV @ 8% · {lifetimeYears}y
{INR(npv20)}
System CAPEX ({systemKw} kWp @ ₹62k/kWp)
{INR(baseCapex)}
{tweaks.batteryKwh > 0 && <>
Battery ({tweaks.batteryKwh} kWh)
+ {INR(batteryCapex)}
}
PM Surya Ghar subsidy *
− {INR(subRes.subsidy)}
Net upfront
{INR(netCapex)}
{/* Sanctioned-load + EV usage callouts */} {(tweaks.sanctionedLoadKw > 0 || tweaks.evEnabled) && (
{tweaks.sanctionedLoadKw > 0 && systemKw > tweaks.sanctionedLoadKw && (
System ({systemKw} kWp) exceeds your {tweaks.sanctionedLoadKw} kW sanctioned load. Most DISCOMs cap net-metering at sanctioned load — you'd need a load enhancement first.
)} {tweaks.evEnabled && (
EV load: {NUM(evKwh)} kWh / yr included in consumption ({Math.round(evKwh / annualKwh * 100)}% of solar output goes to the EV).
)}
)} {/* Subsidy bar */}
Subsidized {INR(subRes.subsidy)} ({((subRes.subsidy/totalCapex)*100).toFixed(0)}%) Self-funded {INR(netCapex)}
*Residential only · empanelled vendor required · capped at 3 kW (₹78,000)
{/* Annual savings + CO2 */}
Annual savings
{INR(annualSavings)}
self-cons {NUM(selfConsumed)} · export {NUM(exported)} kWh
CO₂ offset
{(annualCo2Kg / 1000).toFixed(2)}t/yr
≡ {NUM(treesEquivalent(annualCo2Kg))} trees · {NUM(carKmEquivalent(annualCo2Kg))} car-km
{/* Lifetime curve (panelLifetimeYears from Solar API) */}
{lifetimeYears}-year cumulative
tariff esc 4%/yr
{/* SBI Surya Ghar EMI */}
SBI Surya Ghar loan
6.75% · 10 yr
Principal
{INR(netCapex)}
Monthly EMI
{INR(emiAmount)}
vs. monthly bill saved
−{INR(Math.round(annualSavings/12))}
Net monthly outlay
{INR(emiAmount - Math.round(annualSavings/12))}
{/* Diesel mode */} {tweaks.dieselEnabled && ( <>
Diesel offset · alternative payback
{tweaks.dieselHrs} hrs/d @ ₹{tweaks.dieselPrice}/L
Diesel saved per year
{INR(dieselSav)}
Combined annual savings
{INR(annualSavings + dieselSav)}
Diesel-adjusted payback
{dieselPayback} years
)}
); } /* ============================================================ Main HomeownerMode ============================================================ */ function HomeownerMode() { const data = window.useLeptonData(); const HOME_BUILDING = data.HOME_BUILDING; const DISCOMS = data.DISCOMS; const [layers, setLayers] = useStateH(DEFAULT_LAYERS); const [monthIdx, setMonthIdx] = useStateH(3); // April const [playing, setPlaying] = useStateH(false); const [panelCount, setPanelCount] = useStateH(28); const [hoveredSegment, setHoveredSegment] = useStateH(null); const [monthlyBill, setMonthlyBill] = useStateH(4200); const [tweaks, setTweaks] = useStateH({ discomId: "dhbvn", // Energy profile billUnit: "inr", // 'inr' | 'kwh' — drives which slider is shown peakBill: 0, // 0 = not set; if set, used for summer-aware sizing sanctionedLoadKw: 0, // 0 = not set; warns if proposed system exceeds // Roof + add-ons tiltEnabled: true, tiltDeg: 20, batteryKwh: 0, // Future load evEnabled: false, evKmPerDay: 30, // Backup dieselEnabled: false, dieselPrice: 95, dieselHrs: 3, }); // play month animation useEffectH(() => { if (!playing) return; const t = setInterval(() => setMonthIdx(m => (m + 1) % 12), 700); return () => clearInterval(t); }, [playing]); // keyboard shortcuts for layers useEffectH(() => { const handler = (e) => { if (e.target.tagName === "INPUT" || e.target.tagName === "SELECT") return; const def = LAYER_DEFS.find(l => l.kbd === e.key); if (def) { setLayers(L => ({ ...L, [def.id]: !L[def.id] })); } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, []); const toggleLayer = (id) => setLayers(L => ({ ...L, [id]: !L[id] })); if (!HOME_BUILDING) { return ; } const systemKw = systemKwFromPanels(panelCount, HOME_BUILDING.panelCapacityWatts); const annualKwh = annualProduction({ building: HOME_BUILDING, panels: panelCount, tiltOverride: tweaks.tiltEnabled ? tweaks.tiltDeg : null, }); const discom = DISCOMS.find(d => d.id === tweaks.discomId); const pendingLocation = data.pendingLocation; const pendingPolygon = data.pendingPolygon; const selectionMode = data.selectionMode; const lastSelection = data.lastSelection; const isComputing = data.loading.home; return (
{/* MAP */}
{pendingLocation && ( )} {!pendingLocation && lastSelection && lastSelection.offsetMeters >= 15 && ( )}
{/* SIDE PANEL */}
); } function AlignmentBanner({ offsetMeters }) { return (
Solar API resolved a building {Math.round(offsetMeters)} m from your selection (the red dot on the map). Click closer to the rooftop or drag the pin to recompute.
); } function LocationPanel({ pending, polygon, isLoading }) { const onCompute = () => { window.LEPTON_LOAD.commitPending(); }; const onCancel = () => { window.LEPTON_LOAD.clearPending(); }; return (
{polygon ? "Drawn polygon" : "Pending location"} {pending.source}
{pending.label && (
{pending.label}
)}
{pending.lat.toFixed(5)}°N · {pending.lng.toFixed(5)}°E {polygon && · {polygon.length} pts}
); } function LoadingScreen({ label, error }) { return (
L
Lepton Solar
{label || "Loading…"}
{error && (
{error}
)}
); } // Place-type → icon. Mirrors how Google Maps differentiates suggestion rows. function iconForPlaceTypes(types = []) { const t = new Set(types); if (t.has("establishment") || t.has("point_of_interest") || t.has("store") || t.has("hospital") || t.has("school")) return "warehouse"; if (t.has("premise") || t.has("subpremise") || t.has("street_address") || t.has("route")) return "home"; if (t.has("locality") || t.has("sublocality") || t.has("sublocality_level_1") || t.has("neighborhood")) return "people"; if (t.has("administrative_area_level_1") || t.has("administrative_area_level_2") || t.has("country")) return "compass"; return "pin"; } // Detect a raw "lat, lng" (or "lat lng" / "lat;lng") in the search input. // Accepts signed decimals; validates lat ∈ [-90, 90] and lng ∈ [-180, 180]. const LATLNG_RE = /^\s*(-?\d{1,2}(?:\.\d+)?)\s*[,\s;]\s*(-?\d{1,3}(?:\.\d+)?)\s*$/; function parseLatLng(s) { const m = LATLNG_RE.exec(s); if (!m) return null; const lat = parseFloat(m[1]); const lng = parseFloat(m[2]); if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null; if (lat < -90 || lat > 90 || lng < -180 || lng > 180) return null; return { lat, lng }; } function AddrBar({ building, placeholder: placeholderProp, skipSetPending, defaultZoom = 18 }) { const inputRef = React.useRef(null); const [query, setQuery] = React.useState(""); const [open, setOpen] = React.useState(false); const [suggestions, setSuggestions] = React.useState([]); const [highlight, setHighlight] = React.useState(0); const [focused, setFocused] = React.useState(false); // Building is optional (city mode has no current building). const placeholder = placeholderProp || (building && building.addressShort) || "Search place or lat, lng"; const fetchSuggestions = async (v) => { if (v.length < 2) { setSuggestions([]); setOpen(false); return; } // If the input parses as raw lat,lng, surface a synthetic suggestion ABOVE // the place results — lets the user jump straight to coordinates. const ll = parseLatLng(v); const synthetic = ll ? [{ synthetic: true, place_id: `_ll_${ll.lat}_${ll.lng}`, lat: ll.lat, lng: ll.lng, main: `${ll.lat.toFixed(5)}°, ${ll.lng.toFixed(5)}°`, secondary: "Jump to coordinates", description: `${ll.lat.toFixed(5)}, ${ll.lng.toFixed(5)}`, types: ["_latlng"], }] : []; try { const r = await fetch(`/api/places/autocomplete?q=${encodeURIComponent(v)}`); const j = await r.json(); const preds = (j.predictions || []).slice(0, 6).map(p => ({ place_id: p.place_id, main: p.structured_formatting?.main_text || p.description, secondary: p.structured_formatting?.secondary_text || "", description: p.description, types: p.types || [], })); setSuggestions([...synthetic, ...preds]); setOpen(true); setHighlight(0); } catch (e) { // If autocomplete fails (e.g. quota), still show the synthetic latlng row. if (synthetic.length) { setSuggestions(synthetic); setOpen(true); setHighlight(0); } else { console.error(e); } } }; const onChange = (e) => { const v = e.target.value; setQuery(v); fetchSuggestions(v); }; const clear = () => { setQuery(""); setSuggestions([]); setOpen(false); inputRef.current?.focus(); }; const panAndMaybePin = (lat, lng, label, types) => { const zoom = types.includes("street_address") || types.includes("premise") || types.includes("subpremise") || types.includes("establishment") ? 20 : types.includes("sublocality") || types.includes("neighborhood") || types.includes("sublocality_level_1") ? 18 : types.includes("locality") || types.includes("administrative_area_level_3") ? 17 : types.includes("administrative_area_level_2") ? 15 : types.includes("administrative_area_level_1") ? 12 : types.includes("_latlng") ? defaultZoom : 18; window.dispatchEvent(new CustomEvent("lepton:pan-to", { detail: { lat, lng, zoom, label }})); if (!skipSetPending) { window.LEPTON_LOAD.setPending({ lat, lng, label, source: "search" }); } }; const pickSuggestion = async (s) => { setOpen(false); setQuery(""); setSuggestions([]); if (s.synthetic) { // Direct lat-lng entry — no Places lookup needed. panAndMaybePin(s.lat, s.lng, s.description, ["_latlng"]); return; } try { const r = await fetch(`/api/places/details?place_id=${encodeURIComponent(s.place_id)}`); const j = await r.json(); const loc = j.result?.geometry?.location; if (loc) { const types = j.result?.types || s.types || []; panAndMaybePin(loc.lat, loc.lng, s.description, types); } } catch (e) { console.error(e); } }; const onKeyDown = (e) => { if (!open || !suggestions.length) { if (e.key === "Escape") inputRef.current?.blur(); return; } if (e.key === "ArrowDown") { e.preventDefault(); setHighlight(h => Math.min(suggestions.length - 1, h + 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setHighlight(h => Math.max(0, h - 1)); } else if (e.key === "Enter") { e.preventDefault(); pickSuggestion(suggestions[highlight]); } else if (e.key === "Escape") { e.preventDefault(); setOpen(false); inputRef.current?.blur(); } }; const hasQuery = query.length > 0; return (
{ setFocused(true); if (suggestions.length) setOpen(true); }} onBlur={() => { setFocused(false); setTimeout(() => setOpen(false), 150); }} onKeyDown={onKeyDown} placeholder={placeholder} autoComplete="off" spellCheck={false} style={{ border: 0, outline: 0, background: "transparent", fontSize: 13.5, color: "var(--ink-1)", flex: 1, minWidth: 0, fontFamily: "inherit", padding: 0, }} /> {hasQuery && ( )}
{open && suggestions.length > 0 && (
{suggestions.map((s, i) => (
{ e.preventDefault(); pickSuggestion(s); }} onMouseEnter={() => setHighlight(i)} style={{ padding: "10px 14px", cursor: "pointer", display: "flex", alignItems: "center", gap: 12, background: i === highlight ? "var(--bg-sunken)" : "transparent", transition: "background 0.08s", }} >
{s.main}
{s.secondary && (
{s.secondary}
)}
))}
)}
); } // Floating pill on the map for switching to polygon-draw mode. // Sits bottom-left so it doesn't compete with the search box at the top. function PolygonToggle({ selectionMode }) { const isDrawing = selectionMode === "polygon"; const toggle = () => { if (isDrawing) { window.LEPTON_LOAD.setSelectionMode("pin"); } else { window.LEPTON_LOAD.setSelectionMode("polygon"); window.LEPTON_LOAD.setPolygon(null); } }; return (
); } window.HomeownerMode = HomeownerMode; // Shared with CityMode so the polygon-draw control + search-bar are identical across tabs. window.PolygonToggle = PolygonToggle; window.AddrBar = AddrBar;