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
Python 51.9%
TypeScript 32.7%
JavaScript 4.8%
CSS 4.1%
Shell 2.9%
HTML 1.8%
SQL 1.5%
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