SPB Git forge

spb/auto-ka

Public
61commits 1branches 0releases
14.4 MBsize
maindefault branch
12 days agolast push
Python 61.6% TypeScript 20.9% CSS 11.4% JavaScript 5.1% HTML 1.1%

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

Pilule flottante fixe en bas (standard ka-ui/nav) : Découvrir · Recherche ·
Favoris · Profil, accent orange signature, safe-area iOS, micro-interactions.
Recherche = focus du champ de l accueil ; Favoris = hub univers KA.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 25, 2026) parent 4cf78b4

3 changed files +349 −0

modified frontend/src/App.tsx +34 −0
@@ -11,6 +11,7 @@ import {
11 11 import { AccountProvider, useAccount } from "./account";
12 12 import GroupeKaBadge from "./ka/GroupeKaBadge";
13 13 import KaFooter from "./ka/KaFooter";
14 +import KaTabbar from "./ka/KaTabbar";
14 15 import Home from "./pages/Home";
15 16 import ContactPage from "./pages/Contact";
16 17 import VehiclePage from "./pages/Vehicle";
@@ -250,6 +251,38 @@ function Header() {
250 251 );
251 252 }
252 253
254 +/** Barre de navigation mobile commune Groupe KA (ka-ui/nav — KA Tabbar v1) :
255 + Découvrir · Recherche · Favoris · Profil, accent orange signature. */
256 +function MobileTabBar() {
257 + const location = useLocation();
258 + const navigate = useNavigate();
259 + const p = location.pathname;
260 + const discover =
261 + p === "/" ||
262 + ["/motos", "/scooters", "/usagees", "/region", "/ville", "/carrosserie", "/vehicule"].some((r) => p.startsWith(r));
263 + const focusSearch = () => {
264 + const go = !document.getElementById("f-q");
265 + if (go) navigate("/");
266 + window.setTimeout(() => {
267 + const el = document.getElementById("f-q") as HTMLInputElement | null;
268 + if (el) {
269 + el.scrollIntoView({ block: "center", behavior: "smooth" });
270 + el.focus({ preventScroll: true });
271 + }
272 + }, go ? 140 : 0);
273 + };
274 + return (
275 + <KaTabbar
276 + tabs={[
277 + { label: "Découvrir", icon: "compass", to: "/", active: discover },
278 + { label: "Recherche", icon: "search", onPress: focusSearch },
279 + { label: "Favoris", icon: "heart", href: "https://www.groupe-ka.com/compte" },
280 + { label: "Profil", icon: "user", to: "/profil", active: p.startsWith("/profil") },
281 + ]}
282 + />
283 + );
284 +}
285 +
253 286 export default function App() {
254 287 return (
255 288 <AccountProvider>
@@ -285,6 +318,7 @@ export default function App() {
285 318 />
286 319 </Routes>
287 320 </main>
321 + <MobileTabBar />
288 322 <KaFooter siteId="auto-ka" localLegal={[{ label: "Documentation", href: "/doc/" }]} />
289 323 </AccountProvider>
290 324 );
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 +}
119