SPB Git

spb/vrai-prix

Public

Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.

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

Pilule flottante fixe en bas (standard ka-ui/nav, variante Next.js) :
Estimer · Marché · Ventes · Méthode, accent rouge signature, bilingue fr/en,
safe-area iOS. Estimer mène immédiatement à la recherche d adresse (focus du
champ héros, id vp-adresse) ; Ventes ancre le flux des dernières ventes
réelles de l accueil (id ventes).

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

6 changed files +400 −1

modified src/app/HomeClient.tsx +1 −1
@@ -284,7 +284,7 @@ export default function HomeClient({
284 284
285 {/* ================= VENTES RÉELLES — flux prix-d'abord ================= */} 285 {/* ================= VENTES RÉELLES — flux prix-d'abord ================= */}
286 {recent.length > 0 && ( 286 {recent.length > 0 && (
287− <section className="mt-14"> 287+ <section id="ventes" className="mt-14 scroll-mt-20">
288 <SectionHead 288 <SectionHead
289 num="02" 289 num="02"
290 kicker={fr ? "Flux en continu" : "Live feed"} 290 kicker={fr ? "Flux en continu" : "Live feed"}
modified src/app/layout.tsx +3 −0
@@ -2,10 +2,12 @@
2 import type { Metadata, Viewport } from "next"; 2 import type { Metadata, Viewport } from "next";
3 import { Inter, JetBrains_Mono, Space_Grotesk } from "next/font/google"; 3 import { Inter, JetBrains_Mono, Space_Grotesk } from "next/font/google";
4 import "./globals.css"; 4 import "./globals.css";
5+import "../ka/ka-tabbar.css";
5 import { LangProvider, LangToggle } from "@/components/LangContext"; 6 import { LangProvider, LangToggle } from "@/components/LangContext";
6 import AccountButton from "@/components/AccountButton"; 7 import AccountButton from "@/components/AccountButton";
7 import CookieConsent from "@/components/CookieConsent"; 8 import CookieConsent from "@/components/CookieConsent";
8 import FooterNav from "@/components/FooterNav"; 9 import FooterNav from "@/components/FooterNav";
10+import MobileTabBar from "@/components/MobileTabBar";
9 import HeaderNav from "@/components/HeaderNav"; 11 import HeaderNav from "@/components/HeaderNav";
10 import GroupeKaBadge from "@/ka/GroupeKaBadge"; 12 import GroupeKaBadge from "@/ka/GroupeKaBadge";
11 import KaFooter from "@/ka/KaFooter"; 13 import KaFooter from "@/ka/KaFooter";
@@ -176,6 +178,7 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
176 { label: "Méthodologie", href: "/methodologie" }, 178 { label: "Méthodologie", href: "/methodologie" },
177 ]} 179 ]}
178 /> 180 />
181+ <MobileTabBar />
179 <CookieConsent /> 182 <CookieConsent />
180 <script 183 <script
181 dangerouslySetInnerHTML={{ 184 dangerouslySetInnerHTML={{
added src/components/MobileTabBar.tsx +74 −0
@@ -0,0 +1,74 @@
1+// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2+/**
3+ * Barre de navigation mobile commune Groupe KA (ka-ui/nav — KA Tabbar v1) :
4+ * Estimer · Marché · Ventes · Méthode, accent rouge signature, bilingue.
5+ * Estimer mène immédiatement à la recherche d'adresse (focus du champ héros) ;
6+ * Ventes ancre le flux « dernières ventes réelles » de l'accueil.
7+ */
8+"use client";
9+import { useEffect, useState } from "react";
10+import { usePathname } from "next/navigation";
11+import KaTabbar from "../ka/KaTabbar";
12+import { useLang } from "./LangContext";
13+
14+/** focus du champ d'adresse (réessaie le temps que l'accueil se monte) */
15+function focusAdresse(tries = 10) {
16+ const el = document.getElementById("vp-adresse") as HTMLInputElement | null;
17+ if (el) {
18+ el.scrollIntoView({ block: "center", behavior: "smooth" });
19+ el.focus({ preventScroll: true });
20+ } else if (tries > 0) {
21+ window.setTimeout(() => focusAdresse(tries - 1), 100);
22+ }
23+}
24+
25+export default function MobileTabBar() {
26+ const path = usePathname();
27+ const { lang } = useLang();
28+ const fr = lang === "fr";
29+ const [hash, setHash] = useState("");
30+
31+ useEffect(() => {
32+ const sync = () => setHash(window.location.hash);
33+ sync();
34+ window.addEventListener("hashchange", sync);
35+ return () => window.removeEventListener("hashchange", sync);
36+ }, [path]);
37+
38+ const ventes = path === "/" && hash === "#ventes";
39+ return (
40+ <KaTabbar
41+ tabs={[
42+ {
43+ label: fr ? "Estimer" : "Estimate",
44+ icon: "target",
45+ to: "/",
46+ onClick: () => {
47+ setHash("");
48+ focusAdresse();
49+ },
50+ active: (path === "/" && !ventes) || path.startsWith("/estimation"),
51+ },
52+ {
53+ label: fr ? "Marché" : "Market",
54+ icon: "trendup",
55+ to: "/stats",
56+ active: path.startsWith("/stats") || path.startsWith("/municipalite"),
57+ },
58+ {
59+ label: fr ? "Ventes" : "Sales",
60+ icon: "receipt",
61+ to: "/#ventes",
62+ onClick: () => setHash("#ventes"),
63+ active: ventes,
64+ },
65+ {
66+ label: fr ? "Méthode" : "Method",
67+ icon: "book",
68+ to: "/methodologie",
69+ active: path.startsWith("/methodologie"),
70+ },
71+ ]}
72+ />
73+ );
74+}
modified src/components/SearchBox.tsx +1 −0
@@ -67,6 +67,7 @@ export default function SearchBox({
67 <div className="relative w-full"> 67 <div className="relative w-full">
68 <div className={hero ? "flex items-stretch gap-0" : undefined}> 68 <div className={hero ? "flex items-stretch gap-0" : undefined}>
69 <input 69 <input
70+ id={variant === "hero" ? "vp-adresse" : undefined}
70 ref={input} 71 ref={input}
71 value={q} 72 value={q}
72 onChange={(e) => onChange(e.target.value)} 73 onChange={(e) => onChange(e.target.value)}
added src/ka/KaTabbar.tsx +203 −0
@@ -0,0 +1,203 @@
1+"use client";
2+// KA Tabbar v1 — variante Next.js du canonique ka-ui.git/nav/KaTabbar.tsx
3+// (Link next/link ; le CSS global ka-tabbar.css est importé par layout.tsx).
4+import type { CSSProperties, ReactElement } from "react";
5+import Link from "next/link";
6+
7+export type KaTabIcon =
8+ | "compass" | "search" | "heart" | "user" | "map" | "pin" | "tag"
9+ | "home" | "grid" | "store" | "basket" | "target" | "trendup"
10+ | "receipt" | "book" | "chart" | "calendar";
11+
12+const ICONS: Record<KaTabIcon, ReactElement> = {
13+ compass: (
14+ <>
15+ <circle cx="12" cy="12" r="9" />
16+ <path d="m15.5 8.5-2.2 5-4.8 2 2.2-5 4.8-2z" />
17+ </>
18+ ),
19+ search: (
20+ <>
21+ <circle cx="11" cy="11" r="7" />
22+ <path d="m20.5 20.5-4.2-4.2" />
23+ </>
24+ ),
25+ heart: (
26+ <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" />
27+ ),
28+ user: (
29+ <>
30+ <circle cx="12" cy="8.2" r="3.7" />
31+ <path d="M4.5 20.2a7.5 7.5 0 0 1 15 0" />
32+ </>
33+ ),
34+ map: (
35+ <>
36+ <path d="M9 4 3.5 6v14L9 18l6 2 5.5-2V4L15 6 9 4z" />
37+ <path d="M9 4v14M15 6v14" />
38+ </>
39+ ),
40+ pin: (
41+ <>
42+ <path d="M12 21s-7-5.6-7-11a7 7 0 0 1 14 0c0 5.4-7 11-7 11z" />
43+ <circle cx="12" cy="10" r="2.6" />
44+ </>
45+ ),
46+ tag: (
47+ <>
48+ <path d="M20.6 13.4 12 22 2 12V2h10l8.6 8.6a2 2 0 0 1 0 2.8z" />
49+ <circle cx="7" cy="7" r="1.6" />
50+ </>
51+ ),
52+ home: (
53+ <>
54+ <path d="m3.5 10.5 8.5-7 8.5 7" />
55+ <path d="M5.5 9v11h13V9" />
56+ </>
57+ ),
58+ grid: (
59+ <>
60+ <rect x="4" y="4" width="7" height="7" rx="1.5" />
61+ <rect x="13" y="4" width="7" height="7" rx="1.5" />
62+ <rect x="4" y="13" width="7" height="7" rx="1.5" />
63+ <rect x="13" y="13" width="7" height="7" rx="1.5" />
64+ </>
65+ ),
66+ store: (
67+ <>
68+ <path d="M4.5 9.5 6 4.5h12l1.5 5" />
69+ <path d="M5 9.5h14V20H5V9.5z" />
70+ <path d="M10 20v-5h4v5" />
71+ </>
72+ ),
73+ basket: (
74+ <>
75+ <path d="M4 9h16l-1.6 10a2 2 0 0 1-2 1.7H7.6a2 2 0 0 1-2-1.7L4 9z" />
76+ <path d="M8.5 9 12 3l3.5 6" />
77+ </>
78+ ),
79+ target: (
80+ <>
81+ <circle cx="12" cy="12" r="8.5" />
82+ <circle cx="12" cy="12" r="4.5" />
83+ <circle cx="12" cy="12" r="0.8" />
84+ </>
85+ ),
86+ trendup: (
87+ <>
88+ <path d="m3.5 16.5 5.5-5.5 3.5 3.5 7.5-7.5" />
89+ <path d="M14.5 7H20v5.5" />
90+ </>
91+ ),
92+ receipt: (
93+ <>
94+ <path d="M6 3.5h12V21l-2-1.4-2 1.4-2-1.4-2 1.4-2-1.4L6 21V3.5z" />
95+ <path d="M9 8h6M9 11.5h6M9 15h4" />
96+ </>
97+ ),
98+ book: (
99+ <>
100+ <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" />
101+ <path d="M12 6.5V21" />
102+ </>
103+ ),
104+ chart: (
105+ <>
106+ <path d="M4 20h16" />
107+ <path d="M7.5 20v-6M12 20V9M16.5 20v-4" />
108+ </>
109+ ),
110+ calendar: (
111+ <>
112+ <rect x="3.5" y="5" width="17" height="15.5" rx="2" />
113+ <path d="M8 3v4M16 3v4M3.5 10h17" />
114+ </>
115+ ),
116+};
117+
118+export function KaTabIco({ name, size = 21 }: { name: KaTabIcon; size?: number }) {
119+ return (
120+ <svg
121+ width={size}
122+ height={size}
123+ viewBox="0 0 24 24"
124+ fill="none"
125+ stroke="currentColor"
126+ strokeWidth={1.7}
127+ strokeLinecap="round"
128+ strokeLinejoin="round"
129+ aria-hidden="true"
130+ >
131+ {ICONS[name]}
132+ </svg>
133+ );
134+}
135+
136+export type KaTab = {
137+ label: string;
138+ icon: KaTabIcon;
139+ /** route interne (react-router Link) */
140+ to?: string;
141+ /** lien externe (ex. hub univers KA) */
142+ href?: string;
143+ /** action locale (ex. ouvrir la recherche) — rend un <button> */
144+ onPress?: () => void;
145+ /** complément d'un lien `to` (ex. focus d'un champ une fois arrivé) */
146+ onClick?: () => 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
180+ key={t.label}
181+ href={t.to}
182+ className={c}
183+ onClick={t.onClick}
184+ aria-current={t.active ? "page" : undefined}
185+ >
186+ {inner}
187+ </Link>
188+ );
189+ if (t.href)
190+ return (
191+ <a key={t.label} href={t.href} className={c} target="_blank" rel="noreferrer">
192+ {inner}
193+ </a>
194+ );
195+ return (
196+ <button key={t.label} type="button" className={c} onClick={t.onPress}>
197+ {inner}
198+ </button>
199+ );
200+ })}
201+ </nav>
202+ );
203+}
added 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
Menu

Theme