TypeScript 87.7%
CSS 12.3%
1/**2 * Author: Simon-Pierre Boucher3 * Contact: contact@spboucher.ai4 * Project: Groupe Ka / Ka Maps5 *6 * KaMapDesktopSplit — l'agencement grand écran du Ka Map System v2 :7 * résultats à gauche, carte à droite, séparés par une poignée glissable8 * (24 % à 60 % pour la liste, ratio persisté par app). Un bouton sur la9 * poignée bascule en mode « carte quasi plein écran » (liste repliée) et10 * inversement. Double-clic sur la poignée : ratio par défaut.11 */1213import {14 useCallback,15 useEffect,16 useRef,17 useState,18 type ReactElement,19 type ReactNode,20} from "react";2122export type KaSplitLayout = "split" | "map";2324const DEFAULT_RATIO = 0.40;25const MIN_RATIO = 0.24;26const MAX_RATIO = 0.60;2728export interface KaMapDesktopSplitProps {29 layout: KaSplitLayout;30 onLayout: (l: KaSplitLayout) => void;31 /** Clé localStorage du ratio (ex. "ka-split-lou-ka"). */32 storageKey?: string;33 list: ReactNode;34 /** La carte. */35 children: ReactNode;36}3738export function KaMapDesktopSplit(props: KaMapDesktopSplitProps): ReactElement {39 const { layout, onLayout } = props;40 const rootRef = useRef<HTMLDivElement | null>(null);41 const [ratio, setRatio] = useState<number>(() => {42 if (props.storageKey) {43 const saved = Number(localStorage.getItem(props.storageKey));44 if (Number.isFinite(saved) && saved >= MIN_RATIO && saved <= MAX_RATIO) {45 return saved;46 }47 }48 return DEFAULT_RATIO;49 });50 const [dragging, setDragging] = useState(false);5152 const persist = useCallback(53 (r: number) => {54 if (props.storageKey) {55 try {56 localStorage.setItem(props.storageKey, String(r));57 } catch {58 // stockage plein/privé : le ratio reste pour la session59 }60 }61 },62 [props.storageKey],63 );6465 const onPointerDown = useCallback(66 (e: React.PointerEvent) => {67 if (layout !== "split") return;68 e.preventDefault();69 setDragging(true);70 (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);71 },72 [layout],73 );7475 const onPointerMove = useCallback(76 (e: React.PointerEvent) => {77 if (!dragging || !rootRef.current) return;78 const rect = rootRef.current.getBoundingClientRect();79 const r = (e.clientX - rect.left) / rect.width;80 setRatio(Math.min(MAX_RATIO, Math.max(MIN_RATIO, r)));81 },82 [dragging],83 );8485 const onPointerUp = useCallback(86 (e: React.PointerEvent) => {87 if (!dragging) return;88 setDragging(false);89 try {90 (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);91 } catch {92 // capture déjà relâchée93 }94 setRatio((r) => {95 persist(r);96 return r;97 });98 },99 [dragging, persist],100 );101102 // La carte doit se recaler quand la colonne change de largeur.103 useEffect(() => {104 const t = setTimeout(() => window.dispatchEvent(new Event("resize")), 220);105 return () => clearTimeout(t);106 }, [layout, ratio]);107108 const mapFull = layout === "map";109110 return (111 <div112 ref={rootRef}113 className={`ka-split ka-mode-${layout}${dragging ? " dragging" : ""}`}114 style={{ "--ka-split": `${(ratio * 100).toFixed(2)}%` } as React.CSSProperties}115 >116 <div className="ka-split-list" aria-hidden={mapFull}>117 {props.list}118 </div>119 <div120 className="ka-split-handle"121 role="separator"122 aria-orientation="vertical"123 aria-label="Redimensionner les résultats et la carte"124 onPointerDown={onPointerDown}125 onPointerMove={onPointerMove}126 onPointerUp={onPointerUp}127 onPointerCancel={onPointerUp}128 onDoubleClick={() => {129 setRatio(DEFAULT_RATIO);130 persist(DEFAULT_RATIO);131 }}132 >133 <button134 type="button"135 className="ka-split-toggle"136 onClick={() => onLayout(mapFull ? "split" : "map")}137 onPointerDown={(e) => e.stopPropagation()}138 aria-label={mapFull ? "Réafficher la liste" : "Agrandir la carte"}139 title={mapFull ? "Réafficher la liste" : "Agrandir la carte"}140 >141 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">142 {mapFull ? <path d="M9 6l6 6-6 6" /> : <path d="M15 6l-6 6 6 6" />}143 </svg>144 </button>145 </div>146 <div className="ka-split-map">{props.children}</div>147 </div>148 );149}150