SPB Git forge

spb/trouve-ka

Public ★ Pinned

Trouve-KA — moteur de recherche web indépendant, Québec-first. Crawler distribué, index OpenSearch, ranking bilingue, galerie d'images. En prod : www.trouve-ka.com

40commits 1branches 0releases
9.2 MBsize
maindefault branch
23 days agolast push
Python 51.9% TypeScript 32.7% JavaScript 4.8% CSS 4.1% Shell 2.9% HTML 1.8% SQL 1.5%
5.4 KB · 162 lines tsx
Raw Blame History
1/**2 * Trouve-KA — navigation mobile (hamburger + panneau plein écran)3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 *6 * Standard KA Nav v2 (2026-08-25) :7 * · bouton 44×44 (var(--touch)), visible uniquement < 768 px (md:hidden) ;8 * · panneau `fixed inset-0` (plein écran, z-modal) avec sa PROPRE barre de9 *   fermeture ✕ — le burger du header ne sert qu'à ouvrir : si le header10 *   n'est pas épinglé (bas de page), le ✕ interne reste visible ;11 * · le header n'a PAS de backdrop-filter, le fixed n'est pas re-parenté ;12 * · verrou de scroll via la classe ka-scroll-lock sur <html> ;13 * · fermeture : ✕ interne, Escape (focus rendu au bouton), navigation ;14 * · liens géants numérotés (gk-display), safe-area iPhone en bas ;15 * · z-index : échelle --z-* des tokens (900 = --z-modal).16 */1718"use client";1920import { useEffect, useRef, useState } from "react";21import Link from "next/link";22import { usePathname } from "next/navigation";2324interface MobileNavProps {25  links: ReadonlyArray<{ href: string; label: string }>;26}2728export function MobileNav({ links }: MobileNavProps) {29  const [open, setOpen] = useState(false);30  const rootRef = useRef<HTMLDivElement>(null);31  const buttonRef = useRef<HTMLButtonElement>(null);32  const pathname = usePathname();3334  // Fermeture à la navigation (le lien tapé a déjà déclenché le routage).35  useEffect(() => {36    setOpen(false);37  }, [pathname]);3839  // Panneau ouvert : verrou de scroll + Escape + tap extérieur.40  useEffect(() => {41    if (!open) return;42    document.documentElement.classList.add("ka-scroll-lock");4344    const onKeyDown = (event: KeyboardEvent) => {45      if (event.key === "Escape") {46        setOpen(false);47        buttonRef.current?.focus();48      }49    };50    const onPointerDown = (event: PointerEvent) => {51      if (rootRef.current && !rootRef.current.contains(event.target as Node)) {52        setOpen(false);53      }54    };5556    document.addEventListener("keydown", onKeyDown);57    document.addEventListener("pointerdown", onPointerDown);58    return () => {59      document.documentElement.classList.remove("ka-scroll-lock");60      document.removeEventListener("keydown", onKeyDown);61      document.removeEventListener("pointerdown", onPointerDown);62    };63  }, [open]);6465  return (66    <div ref={rootRef} className="md:hidden">67      <button68        ref={buttonRef}69        type="button"70        aria-expanded={open}71        aria-controls="menu-mobile"72        aria-label={open ? "Fermer le menu" : "Ouvrir le menu"}73        onClick={() => setOpen((v) => !v)}74        className="flex h-11 w-11 items-center justify-center rounded-ctl border-[1.5px] border-transparent text-ink transition-colors hover:border-ink"75      >76        {open ? (77          <svg78            width="20"79            height="20"80            viewBox="0 0 20 20"81            fill="none"82            stroke="currentColor"83            strokeWidth="2"84            strokeLinecap="round"85            aria-hidden="true"86          >87            <path d="M4 4l12 12M16 4L4 16" />88          </svg>89        ) : (90          <svg91            width="20"92            height="20"93            viewBox="0 0 20 20"94            fill="none"95            stroke="currentColor"96            strokeWidth="2"97            strokeLinecap="round"98            aria-hidden="true"99          >100            <path d="M3 5h14M3 10h14M3 15h14" />101          </svg>102        )}103      </button>104105      {open && (106        <div107          id="menu-mobile"108          role="dialog"109          aria-modal="true"110          aria-label="Menu"111          className="fixed inset-0 z-[var(--z-modal,900)] flex flex-col overflow-y-auto overscroll-contain bg-paper"112          style={{ paddingBottom: "max(20px, env(safe-area-inset-bottom, 0px))" }}113        >114          <div className="flex h-14 shrink-0 items-center justify-end border-b-[1.5px] border-[var(--line-strong)] px-4">115            <button116              type="button"117              onClick={() => setOpen(false)}118              aria-label="Fermer le menu"119              className="flex h-11 w-11 items-center justify-center rounded-full border-[1.5px] border-ink"120              style={{ background: "var(--ink)", color: "var(--paper)" }}121            >122              <svg123                width="20"124                height="20"125                viewBox="0 0 20 20"126                fill="none"127                stroke="currentColor"128                strokeWidth="2"129                strokeLinecap="round"130                aria-hidden="true"131              >132                <path d="M4 4l12 12M16 4L4 16" />133              </svg>134            </button>135          </div>136          <ul className="flex flex-1 flex-col justify-center px-6 py-8">137            {links.map((link, i) => (138              <li139                key={link.href}140                className="border-b-[1.5px] border-dashed border-[var(--line-strong)] last:border-0"141              >142                <Link143                  href={link.href}144                  onClick={() => setOpen(false)}145                  className="gk-display flex items-baseline gap-3.5 py-5 text-ink"146                >147                  <span className="font-mono text-[12px] font-bold text-ink-2">148                    0{i + 1}149                  </span>150                  <span className="text-[26px] font-bold uppercase leading-none tracking-[-0.03em]">151                    {link.label}152                  </span>153                </Link>154              </li>155            ))}156          </ul>157        </div>158      )}159    </div>160  );161}162