TypeScript 87.7%
CSS 12.3%
1/**2 * Author: Simon-Pierre Boucher3 * Contact: contact@spboucher.ai4 * Project: Groupe Ka / Ka Maps5 *6 * KaMapToolbar — LA grappe de contrôles du Ka Map System v2. Fini les7 * pilules indépendantes qui flottent partout : une seule colonne compacte8 * de boutons iconographiques (zoom, 3D, dessiner, me localiser, filtres…),9 * regroupés par segments hairline. Chaque bouton expose son libellé en10 * infobulle/aria — le chrome reste minimal, la carte respire.11 */1213import {14 useEffect,15 useState,16 type ReactElement,17 type ReactNode,18} from "react";19import { useKaMap } from "./KaMapView.js";2021/* ------------------------------------------------------------------ icônes */2223function Icon({ d, filled }: { d: string; filled?: boolean }): ReactElement {24 return (25 <svg26 width="17"27 height="17"28 viewBox="0 0 24 24"29 fill={filled ? "currentColor" : "none"}30 stroke="currentColor"31 strokeWidth="2"32 strokeLinecap="round"33 strokeLinejoin="round"34 aria-hidden="true"35 >36 <path d={d} />37 </svg>38 );39}4041const ICONS = {42 plus: "M12 5v14M5 12h14",43 minus: "M5 12h14",44 locate: "M12 2v3M12 19v3M2 12h3M19 12h3M12 8a4 4 0 100 8 4 4 0 000-8z",45 draw: "M12 19l7-7 3 3-7 7-3-3zM18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5zM2 2l7.586 7.586M11 13a2 2 0 100-4 2 2 0 000 4z",46 layers: "M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5",47 filters: "M21 4h-7M10 4H3M21 12h-9M8 12H3M21 20h-5M12 20H3M14 2v4M8 10v4M16 18v4",48 close: "M18 6L6 18M6 6l12 12",49};5051/* ------------------------------------------------------------- primitives */5253export interface KaToolbarButtonProps {54 label: string;55 onClick: () => void;56 active?: boolean;57 disabled?: boolean;58 icon?: keyof typeof ICONS;59 /** Icône/contenu custom (prime sur `icon`). */60 children?: ReactNode;61 badge?: string | number;62}6364export function KaToolbarButton(props: KaToolbarButtonProps): ReactElement {65 return (66 <button67 type="button"68 className={`ka-tb-btn${props.active ? " on" : ""}`}69 onClick={props.onClick}70 disabled={props.disabled}71 aria-label={props.label}72 aria-pressed={props.active}73 title={props.label}74 >75 {props.children ?? (props.icon ? <Icon d={ICONS[props.icon]} /> : null)}76 {props.badge != null && props.badge !== 0 ? (77 <span className="ka-tb-badge">{props.badge}</span>78 ) : null}79 </button>80 );81}8283/** Conteneur : colonne de segments (chaque enfant direct = un groupe). */84export function KaMapToolbar(props: {85 children: ReactNode;86 className?: string;87}): ReactElement {88 return (89 <div className={`ka-toolbar ${props.className ?? ""}`} role="toolbar" aria-label="Outils de carte">90 {props.children}91 </div>92 );93}9495export function KaToolbarGroup(props: { children: ReactNode }): ReactElement {96 return <div className="ka-tb-group">{props.children}</div>;97}9899/* ------------------------------------------------------- boutons intégrés */100101/** Zoom +/− (remplace le NavigationControl natif — passer navControl:false). */102export function KaToolbarZoom(): ReactElement | null {103 const map = useKaMap();104 if (!map) return null;105 return (106 <>107 <KaToolbarButton label="Zoom avant" icon="plus" onClick={() => map.map.zoomIn()} />108 <KaToolbarButton label="Zoom arrière" icon="minus" onClick={() => map.map.zoomOut()} />109 </>110 );111}112113/** Bascule 3D — libellé texte court, état visuel accentué. */114export function KaToolbar3D(): ReactElement | null {115 const map = useKaMap();116 const [tilted, setTilted] = useState(() => map?.isTilted() ?? false);117118 useEffect(() => {119 if (!map) return;120 return map.events.on("moveend", () => setTilted(map.isTilted()));121 }, [map]);122123 if (!map) return null;124 return (125 <KaToolbarButton126 label={tilted ? "Vue à plat (2D)" : "Vue en relief (3D)"}127 active={tilted}128 onClick={() => map.setTilt(!tilted)}129 >130 <span className="ka-tb-txt">{tilted ? "2D" : "3D"}</span>131 </KaToolbarButton>132 );133}134135/** Dessiner une zone — icône crayon ; état actif pendant le tracé et tant136 * qu'une zone est posée (re-clic : annule ou efface). */137export function KaToolbarDraw(): ReactElement | null {138 const map = useKaMap();139 const [drawing, setDrawing] = useState(false);140 const [hasZone, setHasZone] = useState(false);141142 useEffect(() => {143 if (!map) return;144 setDrawing(map.isDrawing());145 setHasZone(map.getDrawnPolygon() !== null);146 return map.events.on("draw", ({ polygon, drawing: d }) => {147 setDrawing(d);148 setHasZone(polygon !== null);149 });150 }, [map]);151152 if (!map) return null;153 return (154 <KaToolbarButton155 label={156 drawing157 ? "Annuler le tracé"158 : hasZone159 ? "Effacer la zone dessinée"160 : "Dessiner une zone"161 }162 active={drawing || hasZone}163 icon="draw"164 onClick={() => {165 if (drawing) map.cancelDraw();166 else if (hasZone) map.clearDrawnPolygon();167 else map.startDraw();168 }}169 />170 );171}172173/** Me localiser — géolocalisation sur geste explicite uniquement. */174export function KaToolbarLocate(props: {175 /** Recentrage effectué — l'app peut synchroniser sa liste. */176 onLocated?: (pos: { lat: number; lng: number }) => void;177}): ReactElement | null {178 const map = useKaMap();179 const [state, setState] = useState<"idle" | "busy" | "denied">("idle");180181 if (!map) return null;182183 const locate = () => {184 if (!("geolocation" in navigator)) {185 setState("denied");186 return;187 }188 setState("busy");189 navigator.geolocation.getCurrentPosition(190 (pos) => {191 setState("idle");192 const at = { lat: pos.coords.latitude, lng: pos.coords.longitude };193 map.flyTo(at, { zoom: Math.max(map.map.getZoom(), 13.5), duration: 700 });194 props.onLocated?.(at);195 },196 () => setState("denied"),197 { enableHighAccuracy: true, timeout: 10_000 },198 );199 };200201 return (202 <KaToolbarButton203 label={state === "denied" ? "Géolocalisation indisponible" : "Me localiser"}204 icon="locate"205 disabled={state === "busy"}206 onClick={locate}207 />208 );209}210