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%
5.3 KB · 155 lines tsx
Raw Blame History
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