TypeScript 87.7%
CSS 12.3%
1/**2 * Author: Simon-Pierre Boucher3 * Contact: contact@spboucher.ai4 * Project: Groupe Ka / Ka Maps5 *6 * Selection preview shell: floating panel on desktop, bottom sheet on7 * mobile. The card CONTENT is app-owned (render prop) so each app keeps8 * its own design language; the framework owns placement, lifecycle,9 * dismissal and accessibility. React all the way — no setHTML, no XSS.10 */1112import { useEffect, useState, type ReactElement, type ReactNode } from "react";13import type { MapProperty } from "../types/index.js";14import { useKaMap } from "./KaMapView.js";1516export interface PropertyPreviewProps {17 /** App-owned card renderer for the selected property. */18 render: (property: MapProperty, close: () => void) => ReactNode;19 /** Viewport width (px) under which the bottom-sheet layout is used. */20 mobileBreakpoint?: number;21 closeLabel?: string;22}2324export function PropertyPreview(props: PropertyPreviewProps): ReactElement | null {25 const map = useKaMap();26 const [property, setProperty] = useState<MapProperty | null>(null);27 const [mobile, setMobile] = useState(false);28 const breakpoint = props.mobileBreakpoint ?? 780;2930 useEffect(() => {31 if (!map) return;32 return map.events.on("select", ({ propertyId }) => {33 setProperty(propertyId ? map.getProperty(propertyId) ?? null : null);34 });35 }, [map]);3637 useEffect(() => {38 const mq = window.matchMedia(`(max-width: ${breakpoint}px)`);39 const update = () => setMobile(mq.matches);40 update();41 mq.addEventListener("change", update);42 return () => mq.removeEventListener("change", update);43 }, [breakpoint]);4445 useEffect(() => {46 if (!property) return;47 const onKey = (e: KeyboardEvent) => {48 if (e.key === "Escape") close();49 };50 window.addEventListener("keydown", onKey);51 return () => window.removeEventListener("keydown", onKey);52 // eslint-disable-next-line react-hooks/exhaustive-deps53 }, [property]);5455 if (!map || !property) return null;5657 const close = () => map.select(null, "app");5859 return (60 <div61 className={mobile ? "ka-preview ka-preview-sheet" : "ka-preview ka-preview-panel"}62 role="dialog"63 aria-label={property.address ?? "Propriété sélectionnée"}64 >65 <button66 type="button"67 className="ka-preview-close"68 onClick={close}69 aria-label={props.closeLabel ?? "Fermer"}70 >71 ×72 </button>73 {props.render(property, close)}74 </div>75 );76}77