SPB Git forge

spb/ka-maps

Public
6commits 1branches 0releases
448.0 KBsize
maindefault branch
29 days agolast push
TypeScript 87.7% CSS 12.3%
4.4 KB · 150 lines tsx
Raw Blame History
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