SPB Git

spb/resto-ka

Public

Resto·Ka — tous les restaurants du Québec, menus complets et prix réels (famille ·Ka)

Barre de navigation mobile commune Groupe KA (KA Tabbar v1)

L ancienne tabbar 3 onglets bord-à-bord devient la pilule flottante du
standard ka-ui/nav : Découvrir · Recherche · Villes · Favoris, accent orange,
safe-area iOS. Recherche = focus du champ de l accueil (id f-q) ; le fab
Filtres remonte au-dessus de la pilule. Stats/Sources restent au menu.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Aug 25, 2026) parent 1cfcba6 Browse files

5 changed files +345 −39

modified frontend/src/App.tsx +25 −15
@@ -6,7 +6,7 @@
6 // ============================================================================== 6 // ==============================================================================
7 import { useEffect, useState } from "react"; 7 import { useEffect, useState } from "react";
8 import { createPortal } from "react-dom"; 8 import { createPortal } from "react-dom";
9−import { NavLink, Route, Routes, useLocation } from "react-router-dom"; 9+import { NavLink, Route, Routes, useLocation, useNavigate } from "react-router-dom";
10 import { AccountProvider, useAccount } from "./account"; 10 import { AccountProvider, useAccount } from "./account";
11 import { 11 import {
12 fetchAuthConfig, fetchFacets, fetchSources, fetchStats, logout, 12 fetchAuthConfig, fetchFacets, fetchSources, fetchStats, logout,
@@ -19,6 +19,7 @@ import { FavProvider } from "./favorites";
19 import { LogoIcon } from "./components/Logo"; 19 import { LogoIcon } from "./components/Logo";
20 import GroupeKaBadge from "./ka/GroupeKaBadge"; 20 import GroupeKaBadge from "./ka/GroupeKaBadge";
21 import KaFooter from "./ka/KaFooter"; 21 import KaFooter from "./ka/KaFooter";
22+import KaTabbar from "./ka/KaTabbar";
22 import ContactPage from "./pages/Contact"; 23 import ContactPage from "./pages/Contact";
23 import FavorisPage from "./pages/Favoris"; 24 import FavorisPage from "./pages/Favoris";
24 import Home from "./pages/Home"; 25 import Home from "./pages/Home";
@@ -242,23 +243,32 @@ function Footer() {
242 ); 243 );
243 } 244 }
244 245
245−/** Barre de navigation inférieure (mobile) — icône active en orange. */ 246+/** Barre de navigation mobile commune Groupe KA (ka-ui/nav — KA Tabbar v1) :
247+ Découvrir · Recherche · Villes · Favoris, accent orange signature. */
246 function MobileTabBar() { 248 function MobileTabBar() {
247 const location = useLocation(); 249 const location = useLocation();
248− const tabs = [ 250+ const navigate = useNavigate();
249− { to: "/", label: "Rechercher", icon: <IcoSearch size={20} />, on: location.pathname === "/" }, 251+ const p = location.pathname;
250− { to: "/stats", label: "Stats", icon: <IcoChart size={20} />, on: location.pathname.startsWith("/stats") }, 252+ const focusSearch = () => {
251− { to: "/sources", label: "Sources", icon: <IcoFolder size={20} />, on: location.pathname.startsWith("/sources") }, 253+ const go = !document.getElementById("f-q");
252− ]; 254+ if (go) navigate("/");
255+ window.setTimeout(() => {
256+ const el = document.getElementById("f-q") as HTMLInputElement | null;
257+ if (el) {
258+ el.scrollIntoView({ block: "center", behavior: "smooth" });
259+ el.focus({ preventScroll: true });
260+ }
261+ }, go ? 140 : 0);
262+ };
253 return ( 263 return (
254− <nav className="tabbar tabbar-3" aria-label="Navigation mobile"> 264+ <KaTabbar
255− {tabs.map((t) => ( 265+ tabs={[
256− <NavLink key={t.label} to={t.to} className={() => (t.on ? "active" : "")}> 266+ { label: "Découvrir", icon: "compass", to: "/", active: p === "/" || p.startsWith("/resto") },
257− {t.icon} 267+ { label: "Recherche", icon: "search", onPress: focusSearch },
258− {t.label} 268+ { label: "Villes", icon: "pin", to: "/villes", active: p.startsWith("/ville") },
259− </NavLink> 269+ { label: "Favoris", icon: "heart", to: "/favoris", active: p.startsWith("/favoris") },
260− ))} 270+ ]}
261− </nav> 271+ />
262 ); 272 );
263 } 273 }
264 274
added frontend/src/ka/KaTabbar.tsx +197 −0
@@ -0,0 +1,197 @@
1+// KA Tabbar v1 — canonique ka-ui.git/nav/KaTabbar.tsx
2+// Barre de navigation mobile commune Groupe KA (voir TABBAR-STANDARD.md).
3+// Copier dans src/ka/ avec ka-tabbar.css. Apps react-router uniquement ;
4+// pour Next.js (vrai-prix), adapter Link/usePathname — voir le standard.
5+import type { CSSProperties, ReactElement } from "react";
6+import { Link } from "react-router-dom";
7+import "./ka-tabbar.css";
8+
9+export type KaTabIcon =
10+ | "compass" | "search" | "heart" | "user" | "map" | "pin" | "tag"
11+ | "home" | "grid" | "store" | "basket" | "target" | "trendup"
12+ | "receipt" | "book" | "chart" | "calendar";
13+
14+const ICONS: Record<KaTabIcon, ReactElement> = {
15+ compass: (
16+ <>
17+ <circle cx="12" cy="12" r="9" />
18+ <path d="m15.5 8.5-2.2 5-4.8 2 2.2-5 4.8-2z" />
19+ </>
20+ ),
21+ search: (
22+ <>
23+ <circle cx="11" cy="11" r="7" />
24+ <path d="m20.5 20.5-4.2-4.2" />
25+ </>
26+ ),
27+ heart: (
28+ <path d="M12 20.5S4.5 15.8 2.6 11.4A5.3 5.3 0 0 1 12 6.6a5.3 5.3 0 0 1 9.4 4.8C19.5 15.8 12 20.5 12 20.5z" />
29+ ),
30+ user: (
31+ <>
32+ <circle cx="12" cy="8.2" r="3.7" />
33+ <path d="M4.5 20.2a7.5 7.5 0 0 1 15 0" />
34+ </>
35+ ),
36+ map: (
37+ <>
38+ <path d="M9 4 3.5 6v14L9 18l6 2 5.5-2V4L15 6 9 4z" />
39+ <path d="M9 4v14M15 6v14" />
40+ </>
41+ ),
42+ pin: (
43+ <>
44+ <path d="M12 21s-7-5.6-7-11a7 7 0 0 1 14 0c0 5.4-7 11-7 11z" />
45+ <circle cx="12" cy="10" r="2.6" />
46+ </>
47+ ),
48+ tag: (
49+ <>
50+ <path d="M20.6 13.4 12 22 2 12V2h10l8.6 8.6a2 2 0 0 1 0 2.8z" />
51+ <circle cx="7" cy="7" r="1.6" />
52+ </>
53+ ),
54+ home: (
55+ <>
56+ <path d="m3.5 10.5 8.5-7 8.5 7" />
57+ <path d="M5.5 9v11h13V9" />
58+ </>
59+ ),
60+ grid: (
61+ <>
62+ <rect x="4" y="4" width="7" height="7" rx="1.5" />
63+ <rect x="13" y="4" width="7" height="7" rx="1.5" />
64+ <rect x="4" y="13" width="7" height="7" rx="1.5" />
65+ <rect x="13" y="13" width="7" height="7" rx="1.5" />
66+ </>
67+ ),
68+ store: (
69+ <>
70+ <path d="M4.5 9.5 6 4.5h12l1.5 5" />
71+ <path d="M5 9.5h14V20H5V9.5z" />
72+ <path d="M10 20v-5h4v5" />
73+ </>
74+ ),
75+ basket: (
76+ <>
77+ <path d="M4 9h16l-1.6 10a2 2 0 0 1-2 1.7H7.6a2 2 0 0 1-2-1.7L4 9z" />
78+ <path d="M8.5 9 12 3l3.5 6" />
79+ </>
80+ ),
81+ target: (
82+ <>
83+ <circle cx="12" cy="12" r="8.5" />
84+ <circle cx="12" cy="12" r="4.5" />
85+ <circle cx="12" cy="12" r="0.8" />
86+ </>
87+ ),
88+ trendup: (
89+ <>
90+ <path d="m3.5 16.5 5.5-5.5 3.5 3.5 7.5-7.5" />
91+ <path d="M14.5 7H20v5.5" />
92+ </>
93+ ),
94+ receipt: (
95+ <>
96+ <path d="M6 3.5h12V21l-2-1.4-2 1.4-2-1.4-2 1.4-2-1.4L6 21V3.5z" />
97+ <path d="M9 8h6M9 11.5h6M9 15h4" />
98+ </>
99+ ),
100+ book: (
101+ <>
102+ <path d="M12 6.5C10.5 5 8.5 4.5 5.5 4.5H3.5V19h2c3 0 5 .5 6.5 2 1.5-1.5 3.5-2 6.5-2h2V4.5h-2c-3 0-5 .5-6.5 2z" />
103+ <path d="M12 6.5V21" />
104+ </>
105+ ),
106+ chart: (
107+ <>
108+ <path d="M4 20h16" />
109+ <path d="M7.5 20v-6M12 20V9M16.5 20v-4" />
110+ </>
111+ ),
112+ calendar: (
113+ <>
114+ <rect x="3.5" y="5" width="17" height="15.5" rx="2" />
115+ <path d="M8 3v4M16 3v4M3.5 10h17" />
116+ </>
117+ ),
118+};
119+
120+export function KaTabIco({ name, size = 21 }: { name: KaTabIcon; size?: number }) {
121+ return (
122+ <svg
123+ width={size}
124+ height={size}
125+ viewBox="0 0 24 24"
126+ fill="none"
127+ stroke="currentColor"
128+ strokeWidth={1.7}
129+ strokeLinecap="round"
130+ strokeLinejoin="round"
131+ aria-hidden="true"
132+ >
133+ {ICONS[name]}
134+ </svg>
135+ );
136+}
137+
138+export type KaTab = {
139+ label: string;
140+ icon: KaTabIcon;
141+ /** route interne (react-router Link) */
142+ to?: string;
143+ /** lien externe (ex. hub univers KA) */
144+ href?: string;
145+ /** action locale (ex. ouvrir la recherche) — rend un <button> */
146+ onPress?: () => void;
147+ active?: boolean;
148+};
149+
150+export default function KaTabbar({
151+ tabs,
152+ theme = "light",
153+ accent,
154+ className,
155+}: {
156+ tabs: KaTab[];
157+ theme?: "light" | "dark";
158+ /** couleur signature de la marque (défaut : var(--accent) du site) */
159+ accent?: string;
160+ className?: string;
161+}) {
162+ const cls =
163+ "ka-tabbar" +
164+ (theme === "dark" ? " ka-tabbar--dark" : "") +
165+ (className ? " " + className : "");
166+ const style = accent ? ({ "--ktb-accent": accent } as CSSProperties) : undefined;
167+ return (
168+ <nav className={cls} style={style} aria-label="Navigation mobile">
169+ {tabs.map((t) => {
170+ const inner = (
171+ <>
172+ <KaTabIco name={t.icon} />
173+ {t.label}
174+ </>
175+ );
176+ const c = t.active ? "active" : "";
177+ if (t.to)
178+ return (
179+ <Link key={t.label} to={t.to} className={c} aria-current={t.active ? "page" : undefined}>
180+ {inner}
181+ </Link>
182+ );
183+ if (t.href)
184+ return (
185+ <a key={t.label} href={t.href} className={c} target="_blank" rel="noreferrer">
186+ {inner}
187+ </a>
188+ );
189+ return (
190+ <button key={t.label} type="button" className={c} onClick={t.onPress}>
191+ {inner}
192+ </button>
193+ );
194+ })}
195+ </nav>
196+ );
197+}
added frontend/src/ka/ka-tabbar.css +118 −0
@@ -0,0 +1,118 @@
1+/* ============================================================
2+ KA Tabbar v1 — barre de navigation mobile commune Groupe KA
3+ Canonique : ka-ui.git/nav/ka-tabbar.css — copiée telle quelle
4+ dans chaque app (src/ka/ka-tabbar.css) et importée par le
5+ composant KaTabbar. Voir TABBAR-STANDARD.md.
6+
7+ Pilule flottante détachée du bas d'écran (langage Immo-Ka),
8+ 4-5 onglets, safe-area iOS, thème clair (défaut) ou sombre.
9+ Config par app :
10+ --ktb-accent couleur signature (trait actif + icône)
11+ --ktb-active-label couleur du label actif (défaut : encre)
12+ .ka-tabbar--dark variante sombre (fond encre/brand)
13+ --ktb-bg fond (surcharge libre, ex. vert profond)
14+ ============================================================ */
15+
16+.ka-tabbar { display: none; }
17+
18+@media (max-width: 760px) {
19+ .ka-tabbar {
20+ /* thème clair (défaut) — papier off-white légèrement translucide */
21+ --ktb-bg: rgba(250, 249, 245, 0.94);
22+ --ktb-ink: var(--ink, #16191a);
23+ --ktb-dim: var(--ink-3, #70756d);
24+ --ktb-accent: var(--accent, #d6336c);
25+ --ktb-line: var(--hairline-strong, rgba(20, 24, 20, 0.16));
26+ --ktb-shadow: 0 10px 30px rgba(20, 24, 20, 0.16), 0 2px 6px rgba(20, 24, 20, 0.07);
27+
28+ position: fixed;
29+ left: max(12px, env(safe-area-inset-left));
30+ right: max(12px, env(safe-area-inset-right));
31+ bottom: calc(10px + env(safe-area-inset-bottom));
32+ z-index: var(--z-bottombar, 600);
33+ display: grid;
34+ grid-auto-flow: column;
35+ grid-auto-columns: 1fr;
36+ max-width: 440px;
37+ margin-inline: auto;
38+ padding: 5px;
39+ background: var(--ktb-bg);
40+ -webkit-backdrop-filter: blur(14px) saturate(1.1);
41+ backdrop-filter: blur(14px) saturate(1.1);
42+ border: 1px solid var(--ktb-line);
43+ border-radius: 18px;
44+ box-shadow: var(--ktb-shadow);
45+ }
46+
47+ .ka-tabbar--dark {
48+ --ktb-bg: rgba(18, 22, 19, 0.93);
49+ --ktb-ink: #f5f3ee;
50+ --ktb-dim: rgba(245, 243, 238, 0.62);
51+ --ktb-line: rgba(245, 243, 238, 0.16);
52+ --ktb-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
53+ }
54+
55+ .ka-tabbar a,
56+ .ka-tabbar button {
57+ appearance: none;
58+ background: none;
59+ border: 0;
60+ cursor: pointer;
61+ display: flex;
62+ flex-direction: column;
63+ align-items: center;
64+ justify-content: center;
65+ gap: 3px;
66+ min-height: 50px;
67+ padding: 6px 2px 5px;
68+ border-radius: 13px;
69+ font-family: var(--font-display, inherit);
70+ font-size: 10px;
71+ font-weight: 650;
72+ letter-spacing: 0.01em;
73+ line-height: 1;
74+ color: var(--ktb-dim);
75+ text-decoration: none;
76+ -webkit-tap-highlight-color: transparent;
77+ transition: color 0.15s ease;
78+ }
79+ .ka-tabbar svg { display: block; transition: transform 0.15s ease; }
80+ .ka-tabbar a:active svg,
81+ .ka-tabbar button:active svg { transform: translateY(1px) scale(0.9); }
82+
83+ /* état actif : icône couleur signature, label encre, trait accent */
84+ .ka-tabbar .active { color: var(--ktb-active-label, var(--ktb-ink)); }
85+ .ka-tabbar .active svg { color: var(--ktb-accent); animation: ktb-pop 0.25s ease; }
86+ .ka-tabbar .active::after {
87+ content: "";
88+ width: 16px;
89+ height: 2.5px;
90+ border-radius: 2px;
91+ background: var(--ktb-accent);
92+ animation: ktb-under 0.22s ease;
93+ }
94+
95+ /* le contenu respire sous la pilule (footer compris) */
96+ body:has(.ka-tabbar) { padding-bottom: calc(84px + env(safe-area-inset-bottom)); }
97+
98+ /* un CTA sticky de fiche prend le bas d'écran : la pilule s'efface */
99+ body:has(.cta-sticky) .ka-tabbar { display: none; }
100+ body:has(.cta-sticky) { padding-bottom: 0; }
101+
102+ /* widget KA Agent : dégagé de la pilule (style injecté après le bundle
103+ → spécificité doublée pour gagner) */
104+ body:has(.ka-tabbar) .kaa-btn.kaa-btn { bottom: calc(88px + env(safe-area-inset-bottom, 0px)); right: 14px; }
105+ body:has(.ka-tabbar) .kaa-panel.kaa-panel:not(.kaa-full) { bottom: calc(154px + env(safe-area-inset-bottom, 0px)); }
106+}
107+
108+@keyframes ktb-under {
109+ from { transform: scaleX(0); opacity: 0; }
110+}
111+@keyframes ktb-pop {
112+ 40% { transform: translateY(-2px); }
113+}
114+
115+@media (prefers-reduced-motion: reduce) {
116+ .ka-tabbar svg { transition: none; }
117+ .ka-tabbar .active svg, .ka-tabbar .active::after { animation: none; }
118+}
modified frontend/src/pages/Home.tsx +1 −0
@@ -180,6 +180,7 @@ export default function Home() {
180 <label className="f-search"> 180 <label className="f-search">
181 <IcoSearch size={16} /> 181 <IcoSearch size={16} />
182 <input 182 <input
183+ id="f-q"
183 type="search" 184 type="search"
184 placeholder={mode === "plats" 185 placeholder={mode === "plats"
185 ? "Poutine, tartare, pad thaï, sushi…" 186 ? "Poutine, tartare, pad thaï, sushi…"
modified frontend/src/styles.css +4 −24
@@ -718,7 +718,7 @@ img { display: block; }
718 .fab { 718 .fab {
719 display: flex; align-items: center; gap: 6px; 719 display: flex; align-items: center; gap: 6px;
720 position: fixed; left: 50%; transform: translateX(-50%); 720 position: fixed; left: 50%; transform: translateX(-50%);
721− bottom: calc(74px + env(safe-area-inset-bottom)); z-index: 80; 721+ bottom: calc(88px + env(safe-area-inset-bottom)); z-index: 80;
722 background: var(--ink); color: var(--accent); border: 1.5px solid var(--ink); 722 background: var(--ink); color: var(--accent); border: 1.5px solid var(--ink);
723 border-radius: var(--r-ctl); padding: 13px 24px; font-family: var(--font-display); 723 border-radius: var(--r-ctl); padding: 13px 24px; font-family: var(--font-display);
724 font-weight: 700; font-size: 15px; cursor: pointer; 724 font-weight: 700; font-size: 15px; cursor: pointer;
@@ -737,29 +737,9 @@ img { display: block; }
737 .mi-thumb { width: 56px; height: 56px; } 737 .mi-thumb { width: 56px; height: 56px; }
738 } 738 }
739 739
740−/* ================= Barre de navigation inférieure (mobile) ================= */ 740+/* ============ Barre de navigation inférieure (mobile) ============
741−.tabbar { display: none; } 741+ KA Tabbar commune (src/ka/ka-tabbar.css) — pilule flottante,
742−@media (max-width: 760px) { 742+ accent orange du site via var(--accent). ============ */
743− .tabbar {
744− display: grid;
745− position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-bottombar, 600);
746− background: rgba(245, 243, 238, 0.94);
747− backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
748− border-top: 1.5px solid var(--ink);
749− padding: 6px 4px calc(6px + env(safe-area-inset-bottom));
750− }
751− .tabbar-3 { grid-template-columns: repeat(3, 1fr); }
752− .tabbar a {
753− display: flex; flex-direction: column; align-items: center; gap: 3px;
754− padding: 5px 2px; min-height: 48px; justify-content: center;
755− font-family: var(--font-mono); font-size: 10px; font-weight: 600; color: var(--ink-3);
756− border-radius: var(--r-ctl); transition: color 0.12s ease;
757− }
758− .tabbar a.active { color: var(--accent-deep); }
759− .tabbar a svg { display: block; }
760− body { padding-bottom: calc(64px + env(safe-area-inset-bottom)); }
761− .ka-footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); }
762−}
763 743
764 /* Anti-zoom iOS : les champs doivent faire >= 16px 744 /* Anti-zoom iOS : les champs doivent faire >= 16px
765 (filet global : tokens.css force aussi 16 px au doigt) */ 745 (filet global : tokens.css force aussi 16 px au doigt) */
766 746
Menu

Theme