KA Tabbar v1 — barre de navigation mobile commune Groupe KA
Design system de navigation mobile (pilule flottante détachée du bas, safe-area iOS, thème clair/sombre, accent de marque configurable), généralisé depuis la barre Immo-Ka. ka-tabbar.css + KaTabbar.tsx (React) + ka-tabbar.js (vanilla) + TABBAR-STANDARD.md (specs, configs par app, recette d intégration). Déployé le 2026-08-25 sur auto-ka, food-ka, fabri-ka, resto-ka, sorti-ka, vrai-prix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 changed files +473 −0
added
nav/KaTabbar.tsx
+205 −0
@@ -0,0 +1,205 @@ | ||
| 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 | + /** complément d'un lien `to` (ex. focus d'un champ une fois arrivé) */ | |
| 148 | + onClick?: () => void; | |
| 149 | + active?: boolean; | |
| 150 | +}; | |
| 151 | + | |
| 152 | +export default function KaTabbar({ | |
| 153 | + tabs, | |
| 154 | + theme = "light", | |
| 155 | + accent, | |
| 156 | + className, | |
| 157 | +}: { | |
| 158 | + tabs: KaTab[]; | |
| 159 | + theme?: "light" | "dark"; | |
| 160 | + /** couleur signature de la marque (défaut : var(--accent) du site) */ | |
| 161 | + accent?: string; | |
| 162 | + className?: string; | |
| 163 | +}) { | |
| 164 | + const cls = | |
| 165 | + "ka-tabbar" + | |
| 166 | + (theme === "dark" ? " ka-tabbar--dark" : "") + | |
| 167 | + (className ? " " + className : ""); | |
| 168 | + const style = accent ? ({ "--ktb-accent": accent } as CSSProperties) : undefined; | |
| 169 | + return ( | |
| 170 | + <nav className={cls} style={style} aria-label="Navigation mobile"> | |
| 171 | + {tabs.map((t) => { | |
| 172 | + const inner = ( | |
| 173 | + <> | |
| 174 | + <KaTabIco name={t.icon} /> | |
| 175 | + {t.label} | |
| 176 | + </> | |
| 177 | + ); | |
| 178 | + const c = t.active ? "active" : ""; | |
| 179 | + if (t.to) | |
| 180 | + return ( | |
| 181 | + <Link | |
| 182 | + key={t.label} | |
| 183 | + to={t.to} | |
| 184 | + className={c} | |
| 185 | + onClick={t.onClick} | |
| 186 | + aria-current={t.active ? "page" : undefined} | |
| 187 | + > | |
| 188 | + {inner} | |
| 189 | + </Link> | |
| 190 | + ); | |
| 191 | + if (t.href) | |
| 192 | + return ( | |
| 193 | + <a key={t.label} href={t.href} className={c} target="_blank" rel="noreferrer"> | |
| 194 | + {inner} | |
| 195 | + </a> | |
| 196 | + ); | |
| 197 | + return ( | |
| 198 | + <button key={t.label} type="button" className={c} onClick={t.onPress}> | |
| 199 | + {inner} | |
| 200 | + </button> | |
| 201 | + ); | |
| 202 | + })} | |
| 203 | + </nav> | |
| 204 | + ); | |
| 205 | +} | |
added
nav/TABBAR-STANDARD.md
+69 −0
@@ -0,0 +1,69 @@ | ||
| 1 | +# KA Tabbar v1 — barre de navigation mobile commune Groupe KA (2026-08-25) | |
| 2 | + | |
| 3 | +Langage de navigation mobile commun à l'écosystème : **pilule flottante fixe | |
| 4 | +en bas d'écran**, détachée des bords (12 px + safe-area iOS), inspirée de la | |
| 5 | +barre Immo-Ka et généralisée en design system. C'est le moyen PRINCIPAL de | |
| 6 | +navigation sur mobile (≤ 760 px) ; le hamburger garde les fonctions | |
| 7 | +secondaires (stats, sources, contact, légal…). | |
| 8 | + | |
| 9 | +## Fichiers canoniques (ce dossier) | |
| 10 | + | |
| 11 | +| Fichier | Rôle | | |
| 12 | +|---|---| | |
| 13 | +| `ka-tabbar.css` | Styles complets — copie conforme dans `src/ka/ka-tabbar.css` de chaque app | | |
| 14 | +| `KaTabbar.tsx` | Composant React (react-router) + jeu d'icônes intégré — copie dans `src/ka/` | | |
| 15 | +| `ka-tabbar.js` | Version vanilla (`KaTabbar.mount({...})`) pour les sites sans React | | |
| 16 | +| `TABBAR-STANDARD.md` | Ce document | | |
| 17 | + | |
| 18 | +## Spécifications | |
| 19 | + | |
| 20 | +- **Dimensions** : onglets min-height 50 px, padding pilule 5 px, radius 18 px, | |
| 21 | + max-width 440 px centrée, icônes 21 px stroke 1.7, labels 10 px/650. | |
| 22 | +- **Position** : `position: fixed; left/right: max(12px, env(safe-area-inset-*)); | |
| 23 | + bottom: calc(10px + env(safe-area-inset-bottom)); z-index: var(--z-bottombar, 600)`. | |
| 24 | +- **Fond** : off-white translucide + blur 14 (clair) ou `.ka-tabbar--dark` | |
| 25 | + (encre) ; surcharger `--ktb-bg` pour un fond de marque (ex. vert profond | |
| 26 | + fabri-ka). Pas de glassmorphism appuyé. | |
| 27 | +- **État actif** : icône = couleur signature (`--ktb-accent`), label = encre, | |
| 28 | + trait 16×2.5 accent sous le label. Micro-interactions : pop d'icône 0.25 s, | |
| 29 | + apparition du trait 0.22 s, press = scale 0.9 (désactivées en | |
| 30 | + `prefers-reduced-motion`). | |
| 31 | +- **Cohabitation** : `body:has(.ka-tabbar)` reçoit un padding-bottom 84 px ; | |
| 32 | + `body:has(.cta-sticky)` masque la pilule (le CTA de fiche prend le bas) ; | |
| 33 | + le widget KA Agent (`.kaa-btn`) est remonté au-dessus de la pilule. | |
| 34 | +- **Onglets** : 4 (5 max). `to` (route), `href` (externe, ex. hub univers KA) | |
| 35 | + ou `onPress` (action — ex. ouvrir/focus la recherche). Le composant ne | |
| 36 | + devine PAS l'état actif : l'app le calcule (useLocation / routeur maison). | |
| 37 | + | |
| 38 | +## Configurations par app (déployé 2026-08-25) | |
| 39 | + | |
| 40 | +| App | Onglets | Accent actif | Thème | Notes | | |
| 41 | +|---|---|---|---|---| | |
| 42 | +| immo-ka | Découvrir · Carte · Taux · Profil | lime | clair | barre d'origine (base du standard), classes `.tabbar` historiques | | |
| 43 | +| auto-ka | Découvrir · Recherche · Favoris · Profil | `#ff5a2a` | clair | Recherche = focus recherche accueil ; Favoris = hub groupe-ka.com/compte | | |
| 44 | +| food-ka | Produits · Recherche · Soldes · Favoris | `#1f9d55` | clair | Soldes = /aubaines ; Favoris = hub groupe-ka.com/compte | | |
| 45 | +| fabri-ka | Accueil · Produits · Boutiques · Recherche | ambre sur vert profond | sombre (`--ktb-bg` #123f2e) | Recherche = overlay plein écran existant | | |
| 46 | +| resto-ka | Découvrir · Recherche · Villes · Favoris | `#f08c00` | clair | pas de carte dans l'app → Villes (icône pin) | | |
| 47 | +| sorti-ka | Découvrir · Recherche · Carte · Favoris | `#d6336c` | clair | vanilla ; Carte = vue résultats + bascule carte | | |
| 48 | +| vrai-prix | Estimer · Marché · Ventes · Méthode | `#d8372e` | clair | Next.js (variante `src/ka/KaTabbar.tsx` avec next/link) ; bilingue fr/en ; Estimer = focus recherche d'adresse (id `vp-adresse`) ; Ventes = ancre `/#ventes` | | |
| 49 | + | |
| 50 | +## Intégration (app React/Vite) | |
| 51 | + | |
| 52 | +1. Copier `ka-tabbar.css` + `KaTabbar.tsx` dans `frontend/src/ka/`. | |
| 53 | +2. Dans `App.tsx` : composer les onglets avec `useLocation` (état actif) et | |
| 54 | + monter `<KaTabbar tabs={…} />` après `<main>` ; retirer toute ancienne | |
| 55 | + barre basse et ses styles. | |
| 56 | +3. Onglet Recherche : `onPress` qui navigue vers l'accueil puis focus le champ | |
| 57 | + de recherche (`setTimeout` ~80 ms après navigation). | |
| 58 | +4. Vérifier : padding-bottom du body, footer non masqué, widget KA Agent | |
| 59 | + dégagé, aucun doublon header/tabbar sur mobile. | |
| 60 | +5. Build + `pm2 restart` + healthcheck + commit/push (spbgit). | |
| 61 | + | |
| 62 | +## Règles UX | |
| 63 | + | |
| 64 | +- La pilule ne doit JAMAIS masquer un CTA, un résultat ou un bouton de filtre | |
| 65 | + (le padding-bottom du body est là pour ça ; les feuilles/sheets passent | |
| 66 | + au-dessus via `--z-modal`). | |
| 67 | +- Jamais plus de 5 onglets ; labels d'un seul mot ; fr-CA. | |
| 68 | +- L'identité de marque passe par `--ktb-accent` (et `--ktb-bg` sombre au | |
| 69 | + besoin) — dimensions, spacing et animations restent IDENTIQUES partout. | |
added
nav/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 | +} | |
added
nav/ka-tabbar.js
+81 −0
@@ -0,0 +1,81 @@ | ||
| 1 | +/* KA Tabbar v1 — canonique ka-ui.git/nav/ka-tabbar.js | |
| 2 | + Version vanilla (sites sans React : sorti-ka, api-ka, ka-stats…). | |
| 3 | + Requiert ka-tabbar.css dans la page. Usage : | |
| 4 | + | |
| 5 | + const bar = KaTabbar.mount({ | |
| 6 | + theme: "light" | "dark", | |
| 7 | + accent: "#d6336c", // couleur signature | |
| 8 | + tabs: [ | |
| 9 | + { key: "decouvrir", label: "Découvrir", icon: "compass", href: "/" }, | |
| 10 | + { key: "recherche", label: "Recherche", icon: "search", onPress: () => {…} }, | |
| 11 | + ], | |
| 12 | + }); | |
| 13 | + bar.setActive("decouvrir"); // pilotage par le routeur du site | |
| 14 | + | |
| 15 | + Les <a> reçoivent data-nav pour être interceptés par les routeurs SPA | |
| 16 | + maison qui délèguent sur [data-nav]. */ | |
| 17 | +(function () { | |
| 18 | + "use strict"; | |
| 19 | + | |
| 20 | + var ICONS = { | |
| 21 | + compass: '<circle cx="12" cy="12" r="9"/><path d="m15.5 8.5-2.2 5-4.8 2 2.2-5 4.8-2z"/>', | |
| 22 | + search: '<circle cx="11" cy="11" r="7"/><path d="m20.5 20.5-4.2-4.2"/>', | |
| 23 | + heart: '<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"/>', | |
| 24 | + user: '<circle cx="12" cy="8.2" r="3.7"/><path d="M4.5 20.2a7.5 7.5 0 0 1 15 0"/>', | |
| 25 | + map: '<path d="M9 4 3.5 6v14L9 18l6 2 5.5-2V4L15 6 9 4z"/><path d="M9 4v14M15 6v14"/>', | |
| 26 | + pin: '<path d="M12 21s-7-5.6-7-11a7 7 0 0 1 14 0c0 5.4-7 11-7 11z"/><circle cx="12" cy="10" r="2.6"/>', | |
| 27 | + tag: '<path d="M20.6 13.4 12 22 2 12V2h10l8.6 8.6a2 2 0 0 1 0 2.8z"/><circle cx="7" cy="7" r="1.6"/>', | |
| 28 | + home: '<path d="m3.5 10.5 8.5-7 8.5 7"/><path d="M5.5 9v11h13V9"/>', | |
| 29 | + grid: '<rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/>', | |
| 30 | + store: '<path d="M4.5 9.5 6 4.5h12l1.5 5"/><path d="M5 9.5h14V20H5V9.5z"/><path d="M10 20v-5h4v5"/>', | |
| 31 | + basket: '<path d="M4 9h16l-1.6 10a2 2 0 0 1-2 1.7H7.6a2 2 0 0 1-2-1.7L4 9z"/><path d="M8.5 9 12 3l3.5 6"/>', | |
| 32 | + target: '<circle cx="12" cy="12" r="8.5"/><circle cx="12" cy="12" r="4.5"/><circle cx="12" cy="12" r="0.8"/>', | |
| 33 | + trendup: '<path d="m3.5 16.5 5.5-5.5 3.5 3.5 7.5-7.5"/><path d="M14.5 7H20v5.5"/>', | |
| 34 | + receipt: '<path d="M6 3.5h12V21l-2-1.4-2 1.4-2-1.4-2 1.4-2-1.4L6 21V3.5z"/><path d="M9 8h6M9 11.5h6M9 15h4"/>', | |
| 35 | + book: '<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"/><path d="M12 6.5V21"/>', | |
| 36 | + chart: '<path d="M4 20h16"/><path d="M7.5 20v-6M12 20V9M16.5 20v-4"/>', | |
| 37 | + calendar: '<rect x="3.5" y="5" width="17" height="15.5" rx="2"/><path d="M8 3v4M16 3v4M3.5 10h17"/>', | |
| 38 | + }; | |
| 39 | + | |
| 40 | + function svg(name) { | |
| 41 | + return ( | |
| 42 | + '<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor"' + | |
| 43 | + ' stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + | |
| 44 | + (ICONS[name] || "") + | |
| 45 | + "</svg>" | |
| 46 | + ); | |
| 47 | + } | |
| 48 | + | |
| 49 | + window.KaTabbar = { | |
| 50 | + mount: function (cfg) { | |
| 51 | + var nav = document.createElement("nav"); | |
| 52 | + nav.className = "ka-tabbar" + (cfg.theme === "dark" ? " ka-tabbar--dark" : ""); | |
| 53 | + nav.setAttribute("aria-label", "Navigation mobile"); | |
| 54 | + if (cfg.accent) nav.style.setProperty("--ktb-accent", cfg.accent); | |
| 55 | + (cfg.tabs || []).forEach(function (t) { | |
| 56 | + var el; | |
| 57 | + if (t.onPress) { | |
| 58 | + el = document.createElement("button"); | |
| 59 | + el.type = "button"; | |
| 60 | + el.addEventListener("click", t.onPress); | |
| 61 | + } else { | |
| 62 | + el = document.createElement("a"); | |
| 63 | + el.href = t.href || "#"; | |
| 64 | + el.setAttribute("data-nav", ""); | |
| 65 | + } | |
| 66 | + el.dataset.tab = t.key || t.label; | |
| 67 | + el.innerHTML = svg(t.icon) + t.label; | |
| 68 | + nav.appendChild(el); | |
| 69 | + }); | |
| 70 | + document.body.appendChild(nav); | |
| 71 | + return { | |
| 72 | + el: nav, | |
| 73 | + setActive: function (key) { | |
| 74 | + nav.querySelectorAll("a,button").forEach(function (b) { | |
| 75 | + b.classList.toggle("active", b.dataset.tab === key); | |
| 76 | + }); | |
| 77 | + }, | |
| 78 | + }; | |
| 79 | + }, | |
| 80 | + }; | |
| 81 | +})(); | |
| 82 | ||