/** * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * Project: Groupe Ka / Ka Maps * * KaMapShell — le « mode carte » du Ka Map System v2. Prise de contrôle du * viewport : la carte devient LA surface de navigation, plus une vignette * dans une page. Barre supérieure ultra compacte, puis : * · mobile : carte edge-to-edge + résultats en bottom sheet à 3 crans ; * · desktop : split résultats|carte redimensionnable (KaMapDesktopSplit), * la carte pouvant passer quasi plein écran. * Le shell verrouille le scroll d'arrière-plan tant que le mode est actif. */ import { createContext, useContext, useEffect, useMemo, useState, type ReactElement, type ReactNode, } from "react"; import { KaMapBottomSheet, type KaSheetPosition } from "./KaMapBottomSheet.js"; import { KaMapDesktopSplit, type KaSplitLayout } from "./KaMapDesktopSplit.js"; export interface KaShellState { /** Point de rupture atteint : agencement mobile (sheet) ou desktop (split). */ isMobile: boolean; /** Position courante du bottom sheet mobile. */ sheet: KaSheetPosition; setSheet: (p: KaSheetPosition) => void; /** Agencement desktop courant (split | carte quasi plein écran). */ layout: KaSplitLayout; setLayout: (l: KaSplitLayout) => void; } const KaShellContext = createContext(null); /** État du mode carte (sheet, layout) depuis n'importe quel descendant. */ export function useKaShell(): KaShellState | null { return useContext(KaShellContext); } export interface KaMapShellProps { /** Marque compacte affichée à gauche de la barre (ex. logo + « Carte »). */ brand?: ReactNode; /** Sortie du mode carte (retour à la vue liste de l'app). */ onExit?: () => void; exitLabel?: string; /** Zone droite de la barre : compteur, tri, bouton Filtres… */ topExtras?: ReactNode; /** Volet résultats : colonne desktop ET contenu du sheet mobile. */ list?: ReactNode; /** En-tête collant du volet résultats (compteur + tri) — aussi la partie * visible du sheet en position mini. */ listHeader?: ReactNode; /** Largeur max (px) de l'agencement mobile. Défaut 780. */ mobileBreakpoint?: number; /** Identifiant de persistance du ratio de split (localStorage). */ storageKey?: string; /** La carte (KaMapView) — occupe toute la surface restante. */ children: ReactNode; className?: string; } export function KaMapShell(props: KaMapShellProps): ReactElement { const breakpoint = props.mobileBreakpoint ?? 780; const [isMobile, setIsMobile] = useState( () => window.matchMedia(`(max-width: ${breakpoint}px)`).matches, ); const [sheet, setSheet] = useState("half"); const [layout, setLayout] = useState("split"); useEffect(() => { const mq = window.matchMedia(`(max-width: ${breakpoint}px)`); const update = () => setIsMobile(mq.matches); mq.addEventListener("change", update); return () => mq.removeEventListener("change", update); }, [breakpoint]); // Mode carte = prise de contrôle : pas de scroll de page en dessous. useEffect(() => { const previous = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = previous; }; }, []); const state = useMemo( () => ({ isMobile, sheet, setSheet, layout, setLayout }), [isMobile, sheet, layout], ); return (
{props.onExit ? ( ) : null} {props.brand ?
{props.brand}
: null}
{props.topExtras}
{isMobile ? ( <>
{props.children}
{props.list ? ( {props.list} ) : null} ) : ( {props.listHeader ? (
{props.listHeader}
) : null}
{props.list}
} > {props.children}
)}
); }