SPB Git forge

spb/fabri-ka

Public

Agrégateur de produits québécois — www.fabri-ka.com

217commits 1branches 0releases
66.1 MBsize
maindefault branch
5 h agolast push
Python 38.4% HTML 30.3% TypeScript 17.2% CSS 11% JavaScript 3.2%

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

La tab bar maison migre vers la pilule flottante du standard ka-ui/nav :
Accueil · Produits · Boutiques · Recherche, thème sombre vert masthead,
actif ambre, safe-area iOS. L overlay de recherche plein écran est conservé.
Sur les fiches produit/boutique, le CTA sticky prend le bas (pilule effacée).

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

4 changed files +345 −101

modified frontend/src/components/BottomNav.tsx +17 −27
@@ -1,6 +1,7 @@
1 1 import { FormEvent, useEffect, useRef, useState } from 'react'
2 −import { NavLink, useLocation, useNavigate } from 'react-router-dom'
3 −import { IconClose, IconGrid, IconHome, IconSearch, IconStore } from './Icons'
2 +import { useLocation, useNavigate } from 'react-router-dom'
3 +import { IconClose, IconSearch } from './Icons'
4 +import KaTabbar from '../ka/KaTabbar'
4 5
5 6 /**
6 7 * Mobile bottom tab bar (<768px) — Accueil, Produits, Boutiques, Recherche.
@@ -81,31 +82,20 @@ export default function BottomNav() {
81 82 </div>
82 83 )}
83 84
84 − <nav className="bottom-nav" aria-label="Navigation mobile">
85 − <NavLink to="/" end className="bottom-nav-item">
86 − <IconHome size={22} />
87 − <span>Accueil</span>
88 − </NavLink>
89 − <NavLink to="/produits" className="bottom-nav-item">
90 − <IconGrid size={22} />
91 − <span>Produits</span>
92 − </NavLink>
93 − <NavLink to="/boutiques" className="bottom-nav-item">
94 − <IconStore size={22} />
95 − <span>Boutiques</span>
96 − </NavLink>
97 − <button
98 − type="button"
99 − className={
100 − searchOpen ? 'bottom-nav-item active' : 'bottom-nav-item'
101 − }
102 − onClick={() => setSearchOpen(true)}
103 − aria-label="Ouvrir la recherche"
104 − >
105 − <IconSearch size={22} />
106 − <span>Recherche</span>
107 − </button>
108 − </nav>
85 + <KaTabbar
86 + theme="dark"
87 + tabs={[
88 + { label: 'Accueil', icon: 'home', to: '/', active: location.pathname === '/' },
89 + {
90 + label: 'Produits',
91 + icon: 'grid',
92 + to: '/produits',
93 + active: ['/produits', '/categorie', '/region'].some((r) => location.pathname.startsWith(r)),
94 + },
95 + { label: 'Boutiques', icon: 'store', to: '/boutiques', active: location.pathname.startsWith('/boutiques') },
96 + { label: 'Recherche', icon: 'search', onPress: () => setSearchOpen(true), active: searchOpen },
97 + ]}
98 + />
109 99 </>
110 100 )
111 101 }
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/styles.css +13 −74
@@ -42,7 +42,7 @@
42 42 --maxw: 1280px;
43 43
44 44 --header-h: 56px;
45 − --tabbar-h: 58px;
45 + --tabbar-h: 74px;
46 46 --safe-b: env(safe-area-inset-bottom, 0px);
47 47
48 48 --focus-ring: 0 0 0 3px rgba(196, 83, 46, 0.28);
@@ -352,67 +352,22 @@ button:focus-visible,
352 352 }
353 353
354 354 /* --------------------------------------------------------------------------
355 − Bottom navigation (mobile)
355 + Bottom navigation (mobile) — KA Tabbar commune (src/ka/ka-tabbar.css),
356 + assortie au masthead : pilule vert profond, actif ambre.
356 357 -------------------------------------------------------------------------- */
357 358
358 −.bottom-nav {
359 − position: fixed;
360 − left: 0;
361 − right: 0;
362 − bottom: 0;
363 − z-index: var(--z-bottombar, 600);
364 − display: grid;
365 − grid-template-columns: repeat(4, 1fr);
366 − background: var(--paper);
367 − border-top: 1px solid var(--border);
368 − padding-bottom: var(--safe-b);
369 − height: calc(var(--tabbar-h) + var(--safe-b));
370 −}
371 −
372 −.bottom-nav-item {
373 − appearance: none;
374 − background: none;
375 − border: none;
376 − display: flex;
377 − flex-direction: column;
378 − align-items: center;
379 − justify-content: center;
380 − gap: 2px;
381 − min-height: 44px;
382 − padding: 6px 0 4px;
383 − color: var(--muted);
384 − font-size: 0.66rem;
385 − font-weight: 600;
386 − letter-spacing: 0.03em;
387 − text-transform: uppercase;
388 − cursor: pointer;
389 − transition: color 0.15s ease;
390 −}
391 −
392 −.bottom-nav-item svg {
393 − transition: transform 0.15s ease;
394 −}
395 −
396 −.bottom-nav-item:active svg {
397 − transform: scale(0.9);
398 −}
399 −
400 −.bottom-nav-item.active {
401 − color: var(--accent);
402 −}
403 −
404 −@media (hover: hover) {
405 − .bottom-nav-item:hover {
406 − text-decoration: none;
407 − color: var(--accent);
408 − }
359 +.ka-tabbar--dark {
360 + --ktb-bg: rgba(18, 63, 46, 0.95); /* --mast-green */
361 + --ktb-ink: var(--mast-fg, #f5f3ee);
362 + --ktb-dim: var(--mast-dim, rgba(245, 243, 238, 0.62));
363 + --ktb-accent: var(--amber, #e8a33d);
364 + --ktb-line: rgba(245, 243, 238, 0.18);
409 365 }
410 366
411 −@media (min-width: 768px) {
412 − .bottom-nav {
413 − display: none;
414 − }
415 −}
367 +/* fiches produit/boutique : le CTA sticky prend le bas, la pilule s efface */
368 +body:has(.pd-cta-bar) .ka-tabbar,
369 +body:has(.store-cta-bar) .ka-tabbar { display: none; }
370 +.pd-cta-bar, .store-cta-bar { bottom: 0; padding-bottom: calc(0.6rem + var(--safe-b)); }
416 371
417 372 /* Mobile search overlay (Recherche tab) */
418 373 .search-overlay {
@@ -4739,12 +4694,6 @@ a.card:active { border-color: var(--ink); }
4739 4694 box-shadow: 1px 1px 0 var(--ink);
4740 4695 }
4741 4696
4742 −/* ---- barre d'onglets mobile ---------------------------------------------- */
4743 −.bottom-nav {
4744 − background: rgba(245, 243, 238, 0.96);
4745 − border-top: 2px solid var(--ink);
4746 −}
4747 −
4748 4697 /* ---- contenu SSR (seo.py) aligné sur le thème ---------------------------- */
4749 4698 .seo-page h1, .seo-page h2 { font-family: var(--font-display); }
4750 4699 .seo-stats b { color: var(--accent); font-family: var(--font-mono); }
@@ -5058,16 +5007,6 @@ a.card:active { border-color: var(--ink); }
5058 5007 .signup-link { color: var(--mast-dim); }
5059 5008 @media (hover: hover) { .signup-link:hover { color: var(--mast-fg); } }
5060 5009
5061 −/* ---- tab bar mobile assortie au masthead --------------------------------- */
5062 −.bottom-nav {
5063 − background: var(--mast-green);
5064 − border-top: 0;
5065 − box-shadow: 0 -1px 0 rgba(245, 243, 238, 0.12);
5066 −}
5067 −.bottom-nav-item { color: var(--mast-dim); }
5068 −.bottom-nav-item.active { color: var(--amber); }
5069 −@media (hover: hover) { .bottom-nav-item:hover { color: var(--mast-fg); } }
5070 −
5071 5010 /* ---- héro : filet fort + chiffres serif ---------------------------------- */
5072 5011 .hero-stats { border-top: 2.5px solid var(--ink); padding-top: 1.1rem; gap: 0 2.25rem; }
5073 5012 .stat { padding-top: 0.15rem; }
5074 5013