SPB Git forge

spb/vrai-prix

Public

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

60commits 1branches 0releases
12.3 MBsize
maindefault branch
17 days agolast push
TypeScript 90.2% JavaScript 3.5% Python 3.4% CSS 1.9% HTML 0.6%

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 1 mo ago (Aug 25, 2026) parent 33f17c0

6 changed files +400 −1

modified src/app/HomeClient.tsx +1 −1
@@ -284,7 +284,7 @@ export default function HomeClient({
284 284
285 285 {/* ================= VENTES RÉELLES — flux prix-d'abord ================= */}
286 286 {recent.length > 0 && (
287 − <section className="mt-14">
287 + <section id="ventes" className="mt-14 scroll-mt-20">
288 288 <SectionHead
289 289 num="02"
290 290 kicker={fr ? "Flux en continu" : "Live feed"}
modified src/app/layout.tsx +3 −0
@@ -2,10 +2,12 @@
2 2 import type { Metadata, Viewport } from "next";
3 3 import { Inter, JetBrains_Mono, Space_Grotesk } from "next/font/google";
4 4 import "./globals.css";
5 +import "../ka/ka-tabbar.css";
5 6 import { LangProvider, LangToggle } from "@/components/LangContext";
6 7 import AccountButton from "@/components/AccountButton";
7 8 import CookieConsent from "@/components/CookieConsent";
8 9 import FooterNav from "@/components/FooterNav";
10 +import MobileTabBar from "@/components/MobileTabBar";
9 11 import HeaderNav from "@/components/HeaderNav";
10 12 import GroupeKaBadge from "@/ka/GroupeKaBadge";
11 13 import KaFooter from "@/ka/KaFooter";
@@ -176,6 +178,7 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
176 178 { label: "Méthodologie", href: "/methodologie" },
177 179 ]}
178 180 />
181 + <MobileTabBar />
179 182 <CookieConsent />
180 183 <script
181 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 67 <div className="relative w-full">
68 68 <div className={hero ? "flex items-stretch gap-0" : undefined}>
69 69 <input
70 + id={variant === "hero" ? "vp-adresse" : undefined}
70 71 ref={input}
71 72 value={q}
72 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