// 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 (
{icon} {label}
{value}
{sub && {sub}
}
{children}
);
}
// ---- date field (native input, light) -------------------------------------
function GlassDateField({ label, value, onChange, accent = "var(--sun)" }) {
return (
);
}
// ---- 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 => (
onPick(a)} style={{ width: "100%", textAlign: "left", padding: "10px 14px", border: "none", background: "transparent", color: "var(--ink)", display: "flex", alignItems: "center", gap: 12, cursor: "pointer" }}
onMouseEnter={e => e.currentTarget.style.background = "rgba(11,26,43,.06)"} onMouseLeave={e => e.currentTarget.style.background = "transparent"}>
{a.city}
{a.country}
{a.code}
))}
{!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 (
setOpen(o => !o)} style={{ width: "100%", height: "100%", textAlign: "left", padding: "14px 18px", border: "none", background: open ? "rgba(11,26,43,.04)" : "transparent", color: "var(--ink)", cursor: "pointer" }}>
{label}
{summary}
{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 (
{children}
);
}
// ---- 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}
Back to search
{loading ? (
) : (
{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" : ""}
Reserve
);
}
// ---- 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" : ""}
Reserve
);
}
// ---- 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}.
>
)}
Done
) : (
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}
{state === "working" ? <> Booking…> : <>Confirm & book >}
)}
), document.body);
}
function BField({ label, value, onChange, span2, type = "text" }) {
return (
{label}
onChange(e.target.value)} style={{ width: "100%", padding: "12px 14px", borderRadius: 12, border: "1px solid var(--line)", background: "var(--paper)", fontSize: 15, outline: "none", color: "var(--ink)" }} />
);
}
Object.assign(window, { GlassField, GlassDateField, GlassPicker, GlassStepperField, ResultsOverlay, EmptyState, StayCard, CarCard, BookingOverlay });