/** * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * Project: Groupe Ka / Ka Maps * * Selection preview shell: floating panel on desktop, bottom sheet on * mobile. The card CONTENT is app-owned (render prop) so each app keeps * its own design language; the framework owns placement, lifecycle, * dismissal and accessibility. React all the way — no setHTML, no XSS. */ import { useEffect, useState, type ReactElement, type ReactNode } from "react"; import type { MapProperty } from "../types/index.js"; import { useKaMap } from "./KaMapView.js"; export interface PropertyPreviewProps { /** App-owned card renderer for the selected property. */ render: (property: MapProperty, close: () => void) => ReactNode; /** Viewport width (px) under which the bottom-sheet layout is used. */ mobileBreakpoint?: number; closeLabel?: string; } export function PropertyPreview(props: PropertyPreviewProps): ReactElement | null { const map = useKaMap(); const [property, setProperty] = useState(null); const [mobile, setMobile] = useState(false); const breakpoint = props.mobileBreakpoint ?? 780; useEffect(() => { if (!map) return; return map.events.on("select", ({ propertyId }) => { setProperty(propertyId ? map.getProperty(propertyId) ?? null : null); }); }, [map]); useEffect(() => { const mq = window.matchMedia(`(max-width: ${breakpoint}px)`); const update = () => setMobile(mq.matches); update(); mq.addEventListener("change", update); return () => mq.removeEventListener("change", update); }, [breakpoint]); useEffect(() => { if (!property) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") close(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); // eslint-disable-next-line react-hooks/exhaustive-deps }, [property]); if (!map || !property) return null; const close = () => map.select(null, "app"); return (
{props.render(property, close)}
); }