TypeScript 87.7%
CSS 12.3%
1/**2 * Author: Simon-Pierre Boucher3 * Contact: contact@spboucher.ai4 * Project: Groupe Ka / Ka Maps5 *6 * React bindings. <KaMapView> owns the KaMap lifecycle (StrictMode-safe),7 * exposes it through context, and renders app-provided overlays (previews,8 * legends, controls) as normal React children — no HTML-string popups.9 */1011import {12 createContext,13 useContext,14 useEffect,15 useRef,16 useState,17 type CSSProperties,18 type ReactElement,19 type ReactNode,20} from "react";21import { KaMap, type KaMapOptions } from "../core/KaMap.js";22import type { KaMapMode } from "../theming/tokens.js";23import type { MapProperty } from "../types/index.js";2425const KaMapContext = createContext<KaMap | null>(null);2627/** Access the KaMap engine from any child of <KaMapView>. */28export function useKaMap(): KaMap | null {29 return useContext(KaMapContext);30}3132export interface KaMapViewProps33 extends Omit<KaMapOptions, "container" | "mode"> {34 mode?: KaMapMode;35 className?: string;36 style?: CSSProperties;37 /** Static data path (no adapter): render these properties directly. */38 properties?: MapProperty[];39 /** App-owned filter payload forwarded to the adapter. */40 filters?: Record<string, unknown>;41 onSelect?: (property: MapProperty | null, origin: "map" | "app") => void;42 onHover?: (property: MapProperty | null) => void;43 onData?: (count: number, totalCount?: number) => void;44 onLoading?: (loading: boolean) => void;45 onSearchAreaDirty?: (dirty: boolean) => void;46 onMoveEnd?: (47 center: { lat: number; lng: number },48 zoom: number,49 byUser: boolean,50 ) => void;51 /** Outil de dessin : polygone posé/effacé, ou tracé en cours. */52 onDraw?: (polygon: [number, number][] | null, drawing: boolean) => void;53 /** Survol d'un cluster (fourchette de prix) — null à la sortie. */54 onClusterHover?: (55 info: { count: number; min: number | null; max: number | null; x: number; y: number } | null,56 ) => void;57 children?: ReactNode;58}5960export function KaMapView(props: KaMapViewProps): ReactElement {61 const containerRef = useRef<HTMLDivElement | null>(null);62 const [engine, setEngine] = useState<KaMap | null>(null);6364 // Latest callbacks in refs so the engine effect never re-runs for them.65 const callbacks = useRef(props);66 callbacks.current = props;6768 useEffect(() => {69 const container = containerRef.current;70 if (!container) return;7172 const p = callbacks.current;73 const map = new KaMap({74 container,75 theme: p.theme,76 mapboxToken: p.mapboxToken,77 mode: p.mode,78 adapter: p.adapter,79 basemap: p.basemap,80 center: p.center,81 zoom: p.zoom,82 pitch: p.pitch,83 minZoom: p.minZoom,84 maxZoom: p.maxZoom,85 searchMode: p.searchMode,86 query: p.query,87 cooperativeGestures: p.cooperativeGestures,88 navControl: p.navControl,89 cluster: p.cluster,90 });9192 const offs = [93 map.events.on("select", ({ propertyId, origin }) => {94 callbacks.current.onSelect?.(95 propertyId ? map.getProperty(propertyId) ?? null : null,96 origin,97 );98 }),99 map.events.on("hover", ({ propertyId }) => {100 callbacks.current.onHover?.(101 propertyId ? map.getProperty(propertyId) ?? null : null,102 );103 }),104 map.events.on("data", ({ count, totalCount }) =>105 callbacks.current.onData?.(count, totalCount),106 ),107 map.events.on("loading", ({ loading }) =>108 callbacks.current.onLoading?.(loading),109 ),110 map.events.on("searchAreaDirty", ({ dirty }) =>111 callbacks.current.onSearchAreaDirty?.(dirty),112 ),113 map.events.on("moveend", ({ center, zoom, byUser }) =>114 callbacks.current.onMoveEnd?.(center, zoom, byUser),115 ),116 map.events.on("draw", ({ polygon, drawing }) =>117 callbacks.current.onDraw?.(polygon, drawing),118 ),119 map.events.on("clusterHover", ({ info }) =>120 callbacks.current.onClusterHover?.(info),121 ),122 ];123124 setEngine(map);125 return () => {126 for (const off of offs) off();127 setEngine(null);128 map.destroy();129 };130 // The engine is created once per mount; theme/adapter swaps remount.131 // eslint-disable-next-line react-hooks/exhaustive-deps132 }, [props.theme.id, props.adapter?.id]);133134 // Static data path.135 useEffect(() => {136 if (engine && props.properties) engine.setProperties(props.properties);137 }, [engine, props.properties]);138139 // Filters → adapter refetch.140 const filtersKey = props.filters ? JSON.stringify(props.filters) : "";141 useEffect(() => {142 if (engine && props.adapter) engine.setFilters(props.filters);143 // eslint-disable-next-line react-hooks/exhaustive-deps144 }, [engine, filtersKey]);145146 // Light/dark swaps restyle in place.147 useEffect(() => {148 if (engine && props.mode) engine.setMode(props.mode);149 }, [engine, props.mode]);150151 return (152 <div153 className={`ka-map ${props.className ?? ""}`}154 style={{ position: "relative", width: "100%", height: "100%", ...props.style }}155 >156 <div ref={containerRef} className="ka-map-canvas" style={{ position: "absolute", inset: 0 }} />157 <KaMapContext.Provider value={engine}>158 {engine ? props.children : null}159 </KaMapContext.Provider>160 </div>161 );162}163