// Shared light-themed search primitives for the Hotels + Cars panels. // Matches the flights panel style: white fields, var(--ink) text, var(--line) dividers. // Styled from ui-tokens.json / index.html :root — no new design system. Attaches to window. // Loaded BEFORE screen-search-hotels.jsx and screen-search-cars.jsx. // ---- frosted field (matches the flight Field look, with a mode accent) ------ function GlassField({ label, icon, accent = "var(--sun)", value, sub, onClick, open, children }) { return (
{children}
); } // ---- date field (native input, light) ------------------------------------- function GlassDateField({ label, value, onChange, accent = "var(--sun)" }) { return (
{label}
onChange(e.target.value)} style={{ marginTop: 4, fontSize: 16, fontWeight: 500, background: "transparent", border: "none", outline: "none", color: "var(--ink)", colorScheme: "light", width: "100%", padding: 0 }} />
); } // ---- searchable picker dropdown (light theme) -------------------------------------------- function GlassPicker({ items, onPick, placeholder = "Search" }) { const [q, setQ] = useState(""); const filtered = useMemo(() => { const ql = q.trim().toLowerCase(); return items.filter(a => !ql || (a.city || "").toLowerCase().includes(ql) || (a.code || "").toLowerCase().includes(ql) || (a.country || "").toLowerCase().includes(ql)); }, [q, items]); return (
e.stopPropagation()} style={{ position: "absolute", top: "calc(100% + 6px)", left: 0, width: 360, zIndex: 30, background: "white", border: "1px solid var(--line)", borderRadius: 14, boxShadow: "0 24px 60px -20px rgba(0,0,0,.4)", overflow: "hidden", color: "var(--ink)" }}>
setQ(e.target.value)} placeholder={placeholder} style={{ flex: 1, border: "none", outline: "none", fontSize: 14, background: "transparent", color: "var(--ink)" }} />
{filtered.map(a => ( ))} {!filtered.length &&
No matches.
}
); } // ---- stepper field (guests / rooms / driver age, light theme) ---------------------------- function GlassStepperField({ label, summary, steppers, accent = "var(--sun)" }) { const [open, setOpen] = useState(false); return (
{open && (
e.stopPropagation()} style={{ position: "absolute", top: "calc(100% + 6px)", right: 0, width: 260, zIndex: 30, background: "white", border: "1px solid var(--line)", borderRadius: 14, padding: 8, color: "var(--ink)", boxShadow: "0 24px 60px -20px rgba(0,0,0,.4)" }}> {steppers.map((s) => (
{s.label} s.set(Math.max(s.min, s.value - 1))} disabled={s.value <= s.min}>− {s.value} s.set(Math.min(s.max, s.value + 1))} disabled={s.value >= s.max}>+
))}
)}
); } function StepBtn({ children, onClick, disabled }) { return ( ); } // ---- results overlay (full-screen, cream) ----------------------------------- function ResultsOverlay({ title, onClose, loading, count, accent = "var(--sun)", children }) { // Portal to so the glass panel's backdrop-filter doesn't trap position:fixed. return ReactDOM.createPortal((
{loading ? "Searching" : `${count} result${count === 1 ? "" : "s"}`}
{title}
{loading ? (
Finding the best options
) : (
{children}
)}
), document.body); } function EmptyState({ text }) { return
{text}
; } function Stars({ n = 0 }) { return {Array.from({ length: 5 }, (_, i) => )}; } // ---- hotel result card ------------------------------------------------------ function StayCard({ stay, onReserve }) { const a = stay.accommodation || {}; const price = Math.round(parseFloat(stay.cheapestRateTotalAmount || "0")); const photo = a.photos?.[0]?.url; const addr = a.location?.address; return (
{a.name}
{a.rating ? : null}
{addr &&
{[addr.lineOne, addr.city].filter(Boolean).join(", ")}
} {a.reviewScore ?
★ {a.reviewScore} guest score
: null}
{fmtMoney(price)}
total{stay._mock ? " · demo" : ""}
); } // ---- car result card -------------------------------------------------------- function CarCard({ car, onReserve }) { const price = Math.round(parseFloat(car.totalAmount || "0")); return (
{car.vehicleClass}
{car.vehicleName}
{car.transmission}· {car.seats} seats· {car.bags} bags
{car.vendor}
{fmtMoney(price)}
total{car._mock ? " · demo" : ""}
); } // ---- booking overlay (guest form → confirmation) ---------------------------- function BookingOverlay({ kind, title, priceLabel, onClose, onConfirm }) { const [guest, setGuest] = useState({ first: "", last: "", email: "", phone: "" }); const [state, setState] = useState("form"); // form | working | done const [conf, setConf] = useState(null); const valid = guest.first && guest.last && guest.email; async function confirm() { setState("working"); try { const r = await onConfirm(guest); setConf(r); setState("done"); } catch (e) { setConf({ error: e.message }); setState("done"); } } return ReactDOM.createPortal((
e.stopPropagation()} style={{ width: "min(520px, 100%)", background: "var(--cream)", color: "var(--ink)", borderRadius: 22, overflow: "hidden", boxShadow: "0 40px 90px -30px rgba(0,0,0,.6)" }}>
{kind === "hotel" ? "Confirm stay" : "Confirm rental"}
{title}
{state === "done" ? (
{conf?.error ? ( <>
Couldn't complete booking
{conf.error}
) : ( <>
Booked{conf?._mock ? " (demo)" : ""}
Reference {conf?.reference}
A confirmation was sent to {guest.email}.
)}
) : (
setGuest(g => ({ ...g, first: v }))} /> setGuest(g => ({ ...g, last: v }))} /> setGuest(g => ({ ...g, email: v }))} span2 type="email" /> setGuest(g => ({ ...g, phone: v }))} span2 type="tel" />
{priceLabel}
)}
), document.body); } function BField({ label, value, onChange, span2, type = "text" }) { return ( ); } Object.assign(window, { GlassField, GlassDateField, GlassPicker, GlassStepperField, ResultsOverlay, EmptyState, StayCard, CarCard, BookingOverlay });