TypeScript 87.7%
CSS 12.3%
1/**2 * Author: Simon-Pierre Boucher3 * Contact: contact@spboucher.ai4 * Project: Groupe Ka / Ka Maps5 *6 * KaMapShell — le « mode carte » du Ka Map System v2. Prise de contrôle du7 * viewport : la carte devient LA surface de navigation, plus une vignette8 * dans une page. Barre supérieure ultra compacte, puis :9 * · mobile : carte edge-to-edge + résultats en bottom sheet à 3 crans ;10 * · desktop : split résultats|carte redimensionnable (KaMapDesktopSplit),11 * la carte pouvant passer quasi plein écran.12 * Le shell verrouille le scroll d'arrière-plan tant que le mode est actif.13 */1415import {16 createContext,17 useContext,18 useEffect,19 useMemo,20 useState,21 type ReactElement,22 type ReactNode,23} from "react";24import { KaMapBottomSheet, type KaSheetPosition } from "./KaMapBottomSheet.js";25import { KaMapDesktopSplit, type KaSplitLayout } from "./KaMapDesktopSplit.js";2627export interface KaShellState {28 /** Point de rupture atteint : agencement mobile (sheet) ou desktop (split). */29 isMobile: boolean;30 /** Position courante du bottom sheet mobile. */31 sheet: KaSheetPosition;32 setSheet: (p: KaSheetPosition) => void;33 /** Agencement desktop courant (split | carte quasi plein écran). */34 layout: KaSplitLayout;35 setLayout: (l: KaSplitLayout) => void;36}3738const KaShellContext = createContext<KaShellState | null>(null);3940/** État du mode carte (sheet, layout) depuis n'importe quel descendant. */41export function useKaShell(): KaShellState | null {42 return useContext(KaShellContext);43}4445export interface KaMapShellProps {46 /** Marque compacte affichée à gauche de la barre (ex. logo + « Carte »). */47 brand?: ReactNode;48 /** Sortie du mode carte (retour à la vue liste de l'app). */49 onExit?: () => void;50 exitLabel?: string;51 /** Zone droite de la barre : compteur, tri, bouton Filtres… */52 topExtras?: ReactNode;53 /** Volet résultats : colonne desktop ET contenu du sheet mobile. */54 list?: ReactNode;55 /** En-tête collant du volet résultats (compteur + tri) — aussi la partie56 * visible du sheet en position mini. */57 listHeader?: ReactNode;58 /** Largeur max (px) de l'agencement mobile. Défaut 780. */59 mobileBreakpoint?: number;60 /** Identifiant de persistance du ratio de split (localStorage). */61 storageKey?: string;62 /** La carte (KaMapView) — occupe toute la surface restante. */63 children: ReactNode;64 className?: string;65}6667export function KaMapShell(props: KaMapShellProps): ReactElement {68 const breakpoint = props.mobileBreakpoint ?? 780;69 const [isMobile, setIsMobile] = useState(70 () => window.matchMedia(`(max-width: ${breakpoint}px)`).matches,71 );72 const [sheet, setSheet] = useState<KaSheetPosition>("half");73 const [layout, setLayout] = useState<KaSplitLayout>("split");7475 useEffect(() => {76 const mq = window.matchMedia(`(max-width: ${breakpoint}px)`);77 const update = () => setIsMobile(mq.matches);78 mq.addEventListener("change", update);79 return () => mq.removeEventListener("change", update);80 }, [breakpoint]);8182 // Mode carte = prise de contrôle : pas de scroll de page en dessous.83 useEffect(() => {84 const previous = document.body.style.overflow;85 document.body.style.overflow = "hidden";86 return () => {87 document.body.style.overflow = previous;88 };89 }, []);9091 const state = useMemo<KaShellState>(92 () => ({ isMobile, sheet, setSheet, layout, setLayout }),93 [isMobile, sheet, layout],94 );9596 return (97 <KaShellContext.Provider value={state}>98 <div99 className={`ka-shell ${isMobile ? "ka-shell-mobile" : "ka-shell-desktop"} ${props.className ?? ""}`}100 role="region"101 aria-label="Mode carte"102 >103 <div className="ka-shell-top">104 {props.onExit ? (105 <button106 type="button"107 className="ka-shell-exit"108 onClick={props.onExit}109 aria-label={props.exitLabel ?? "Revenir à la liste"}110 >111 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">112 <path d="M15 18l-6-6 6-6" />113 </svg>114 <span>{props.exitLabel ?? "Liste"}</span>115 </button>116 ) : null}117 {props.brand ? <div className="ka-shell-brand">{props.brand}</div> : null}118 <div className="ka-shell-extras">{props.topExtras}</div>119 </div>120121 {isMobile ? (122 <>123 <div className="ka-shell-map">{props.children}</div>124 {props.list ? (125 <KaMapBottomSheet126 position={sheet}127 onPosition={setSheet}128 header={props.listHeader}129 >130 {props.list}131 </KaMapBottomSheet>132 ) : null}133 </>134 ) : (135 <KaMapDesktopSplit136 layout={layout}137 onLayout={setLayout}138 storageKey={props.storageKey}139 list={140 <>141 {props.listHeader ? (142 <div className="ka-pane-head">{props.listHeader}</div>143 ) : null}144 <div className="ka-pane-body">{props.list}</div>145 </>146 }147 >148 {props.children}149 </KaMapDesktopSplit>150 )}151 </div>152 </KaShellContext.Provider>153 );154}155