SPB Git forge

spb/ka-ui

Public
30commits 1branches 0releases
145.7 MBsize
maindefault branch
27 days agolast push
Python 33.5% JavaScript 30.1% TypeScript 25% CSS 10% Shell 1.4%

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>
Simon-Pierre Boucher committed 29 days ago (Aug 25, 2026) parent 39e968d

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