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.5 KB · 142 lines tsx
Raw Blame History
1/**2 * Author: Simon-Pierre Boucher3 * Contact: contact@spboucher.ai4 * Project: Groupe Ka / Ka Maps5 *6 * KaMapBottomSheet — le volet de résultats mobile du Ka Map System v2.7 * Trois crans : mini (poignée + en-tête), half (mi-écran), full (quasi8 * plein écran). Glissement au doigt sur la poignée/l'en-tête, snap au cran9 * le plus proche à la relâche, tap sur la poignée pour passer au cran10 * suivant. Le contenu ne défile qu'en position full — sinon le geste11 * vertical appartient au sheet.12 */1314import {15  useCallback,16  useEffect,17  useRef,18  useState,19  type ReactElement,20  type ReactNode,21} from "react";2223export type KaSheetPosition = "mini" | "half" | "full";2425const POSITIONS: KaSheetPosition[] = ["mini", "half", "full"];2627/** Hauteur cible (px) d'un cran pour un viewport donné. */28function snapHeight(pos: KaSheetPosition, viewport: number): number {29  switch (pos) {30    case "mini":31      return 96;32    case "half":33      return Math.round(viewport * 0.44);34    case "full":35      return Math.round(viewport - 108);36  }37}3839export interface KaMapBottomSheetProps {40  position: KaSheetPosition;41  onPosition: (p: KaSheetPosition) => void;42  /** En-tête toujours visible (compteur, tri) — zone de glissement. */43  header?: ReactNode;44  children: ReactNode;45}4647export function KaMapBottomSheet(props: KaMapBottomSheetProps): ReactElement {48  const { position, onPosition } = props;49  const [dragHeight, setDragHeight] = useState<number | null>(null);50  const dragRef = useRef<{ startY: number; startH: number; moved: boolean } | null>(null);51  const sheetRef = useRef<HTMLDivElement | null>(null);5253  const viewport = () => window.innerHeight;5455  const onPointerDown = useCallback((e: React.PointerEvent) => {56    // Ne pas capturer les gestes commencés sur un élément interactif.57    const target = e.target as HTMLElement;58    if (target.closest("button, a, select, input, label")) return;59    const h = sheetRef.current?.getBoundingClientRect().height ?? 0;60    dragRef.current = { startY: e.clientY, startH: h, moved: false };61    (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);62  }, []);6364  const onPointerMove = useCallback((e: React.PointerEvent) => {65    const drag = dragRef.current;66    if (!drag) return;67    const delta = drag.startY - e.clientY;68    if (Math.abs(delta) > 4) drag.moved = true;69    const max = snapHeight("full", viewport());70    const next = Math.min(max, Math.max(64, drag.startH + delta));71    setDragHeight(next);72  }, []);7374  const settle = useCallback(75    (e: React.PointerEvent) => {76      const drag = dragRef.current;77      dragRef.current = null;78      if (!drag) return;79      try {80        (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);81      } catch {82        // capture déjà relâchée83      }84      setDragHeight(null);85      if (!drag.moved) {86        // Tap : cran suivant (mini → half → full → mini).87        const i = POSITIONS.indexOf(position);88        onPosition(POSITIONS[(i + 1) % POSITIONS.length] as KaSheetPosition);89        return;90      }91      const delta = drag.startY - e.clientY;92      const h = drag.startH + delta;93      const vp = viewport();94      let best: KaSheetPosition = "mini";95      let bestD = Infinity;96      for (const p of POSITIONS) {97        const d = Math.abs(snapHeight(p, vp) - h);98        if (d < bestD) {99          bestD = d;100          best = p;101        }102      }103      onPosition(best);104    },105    [position, onPosition],106  );107108  // Recalage à l'orientation/redimensionnement (hauteur en px inline).109  const [, forceRender] = useState(0);110  useEffect(() => {111    const onResize = () => forceRender((x) => x + 1);112    window.addEventListener("resize", onResize);113    return () => window.removeEventListener("resize", onResize);114  }, []);115116  const height = dragHeight ?? snapHeight(position, viewport());117118  return (119    <div120      ref={sheetRef}121      className={`ka-sheet ka-sheet-${position}${dragHeight !== null ? " dragging" : ""}`}122      style={{ height }}123      role="region"124      aria-label="Résultats"125    >126      <div127        className="ka-sheet-grip"128        onPointerDown={onPointerDown}129        onPointerMove={onPointerMove}130        onPointerUp={settle}131        onPointerCancel={settle}132      >133        <span className="ka-sheet-handle" aria-hidden="true" />134        {props.header ? <div className="ka-sheet-head">{props.header}</div> : null}135      </div>136      <div className="ka-sheet-body" aria-hidden={position === "mini"}>137        {props.children}138      </div>139    </div>140  );141}142