SPB Git forge

spb/resto-ka

Public

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

52commits 1branches 0releases
11.6 MBsize
maindefault branch
19 days agolast push
Python 69.3% TypeScript 16.7% CSS 7.9% JavaScript 4.7% HTML 1.4%

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 29 days ago (Aug 25, 2026) parent 1cfcba6

5 changed files +345 −39

modified frontend/src/App.tsx +25 −15
@@ -6,7 +6,7 @@
6 6 // ==============================================================================
7 7 import { useEffect, useState } from "react";
8 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 10 import { AccountProvider, useAccount } from "./account";
11 11 import {
12 12 fetchAuthConfig, fetchFacets, fetchSources, fetchStats, logout,
@@ -19,6 +19,7 @@ import { FavProvider } from "./favorites";
19 19 import { LogoIcon } from "./components/Logo";
20 20 import GroupeKaBadge from "./ka/GroupeKaBadge";
21 21 import KaFooter from "./ka/KaFooter";
22 +import KaTabbar from "./ka/KaTabbar";
22 23 import ContactPage from "./pages/Contact";
23 24 import FavorisPage from "./pages/Favoris";
24 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 248 function MobileTabBar() {
247 249 const location = useLocation();
248 − const tabs = [
249 − { to: "/", label: "Rechercher", icon: <IcoSearch size={20} />, on: location.pathname === "/" },
250 − { to: "/stats", label: "Stats", icon: <IcoChart size={20} />, on: location.pathname.startsWith("/stats") },
251 − { to: "/sources", label: "Sources", icon: <IcoFolder size={20} />, on: location.pathname.startsWith("/sources") },
252 − ];
250 + const navigate = useNavigate();
251 + const p = location.pathname;
252 + const focusSearch = () => {
253 + const go = !document.getElementById("f-q");
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 263 return (
254 − <nav className="tabbar tabbar-3" aria-label="Navigation mobile">
255 − {tabs.map((t) => (
256 − <NavLink key={t.label} to={t.to} className={() => (t.on ? "active" : "")}>
257 − {t.icon}
258 − {t.label}
259 − </NavLink>
260 − ))}
261 − </nav>
264 + <KaTabbar
265 + tabs={[
266 + { label: "Découvrir", icon: "compass", to: "/", active: p === "/" || p.startsWith("/resto") },
267 + { label: "Recherche", icon: "search", onPress: focusSearch },
268 + { label: "Villes", icon: "pin", to: "/villes", active: p.startsWith("/ville") },
269 + { label: "Favoris", icon: "heart", to: "/favoris", active: p.startsWith("/favoris") },
270 + ]}
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 180 <label className="f-search">
181 181 <IcoSearch size={16} />
182 182 <input
183 + id="f-q"
183 184 type="search"
184 185 placeholder={mode === "plats"
185 186 ? "Poutine, tartare, pad thaï, sushi…"
modified frontend/src/styles.css +4 −24
@@ -718,7 +718,7 @@ img { display: block; }
718 718 .fab {
719 719 display: flex; align-items: center; gap: 6px;
720 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 722 background: var(--ink); color: var(--accent); border: 1.5px solid var(--ink);
723 723 border-radius: var(--r-ctl); padding: 13px 24px; font-family: var(--font-display);
724 724 font-weight: 700; font-size: 15px; cursor: pointer;
@@ -737,29 +737,9 @@ img { display: block; }
737 737 .mi-thumb { width: 56px; height: 56px; }
738 738 }
739 739
740 −/* ================= Barre de navigation inférieure (mobile) ================= */
741 −.tabbar { display: none; }
742 −@media (max-width: 760px) {
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 −}
740 +/* ============ Barre de navigation inférieure (mobile) ============
741 + KA Tabbar commune (src/ka/ka-tabbar.css) — pilule flottante,
742 + accent orange du site via var(--accent). ============ */
763 743
764 744 /* Anti-zoom iOS : les champs doivent faire >= 16px
765 745 (filet global : tokens.css force aussi 16 px au doigt) */
766 746