Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGNSEFZdoKvbdsGyfSnu5S
144 lines
18 KiB
Markdown
144 lines
18 KiB
Markdown
# Design system nav-carte (AEP)
|
||
|
||
Ce fichier existe pour que B6 à B9 n'aient pas à relire les récaps de B3 (`1 PROJETS/DEV/AEP/Cascade-Refonte/recaps/B3-*.md` dans le vault Dropbox) pour comprendre ce qui a été posé. Si tu es sur B6+ et que tu touches au visuel de nav-carte, lis ceci d'abord.
|
||
|
||
## D'où viennent les tokens
|
||
|
||
Les couleurs, espacements, rayons, ombres, etc. ne sont **jamais** des valeurs en dur dans les composants nav-carte. Ils viennent de deux fichiers, chargés dans cet ordre dans `nuxt.config.ts` :
|
||
|
||
1. `@transformer/ui/tokens.css` — package npm partagé (`git+https://git.trans-former.fr/jules/design-system.git`), le schéma canonique de tokens par **rôle** (`--bg`, `--text`, `--primary`, `--focus-ring`, `--space-*`, `--radius-*`, `--fs-*`, etc.). Jamais cloné en local, jamais modifié depuis ce repo.
|
||
2. `@transformer/ui/theme/aep.css` — la palette AEP spécifique (« Sobre institutionnel », validée par Jules), qui surcharge les rôles du point 1 avec les vraies couleurs : bleu nuit `#1a2238`, accent safran `#f5b342`, fond crème `#f8f6f1`. C'est ce fichier qui rend `--primary`, `--accent`, `--bg` etc. concrets pour AEP.
|
||
|
||
`assets/css/main.css` définit ensuite des variables `--nav-*` (héritées de l'ancienne palette V1, avant l'arrivée du DS). Elles sont **alias** des rôles ci-dessus : `--nav-bg: var(--bg)`, `--nav-text: var(--text)`, etc. Elles existent uniquement parce que tout le code existant (avant B3) les consomme — ne pas les supprimer sans grep préalable, ne pas en créer de nouvelles : pour du code neuf, consommer directement les tokens `--bg`/`--text`/`--primary`/... du DS.
|
||
|
||
**Trois variables restent volontairement non-aliasées**, propres à nav-carte :
|
||
|
||
- `--nav-primary-raw` — triplet RGB (`26, 34, 56`) utilisé dans des `rgba(var(--nav-primary-raw), x)`. Le DS n'expose pas de triplet RGB séparé, seulement des couleurs finales.
|
||
- `--nav-primary-solid` — bleu nuit plein (`#1a2238` clair, `#c8d2f0` sombre). Différent de `--primary` du DS, qui est le bleu nuit à 60 % d'opacité (utilisé pour les surfaces translucides). `--nav-primary-solid` sert au texte et aux éléments qui doivent rester lisibles en plein (logo, soulignement d'onglet actif).
|
||
- `--nav-text-on-primary` — texte de contraste, gardé en valeur littérale (`#f8f6f1` clair / `#111520` sombre) plutôt qu'aliasé sur `--on-primary` du DS. **Raison** : cette variable sert de texte de contraste sur DEUX fonds différents dans le code existant — `--nav-primary` (translucide) ET `--nav-primary-solid` (opaque) — qui ont une légèreté **inversée** en mode sombre (le translucide rend sombre une fois mélangé au fond, le solid `#c8d2f0` est un lavande clair). Le rôle `--on-primary` du DS ne colle qu'au premier cas ; l'aliaser cassait le contraste à une dizaine d'endroits (logo « AEP », boutons, bandeaux) en sombre. Si tu ajoutes un nouvel usage de cette variable, vérifie d'abord sur QUEL fond (translucide ou solid) le texte doit être lisible.
|
||
|
||
Dark mode : classe `.dark` posée sur `<html>` (`document.documentElement.classList.add('dark')`, persisté dans `localStorage.aep_theme`). `theme/aep.css` définit ses valeurs sombres sous le sélecteur `:root.dark, :root[data-theme="dark"]` — les deux formes sont acceptées, mais nav-carte n'utilise que `.dark`. Pas de bascule automatique par `prefers-color-scheme` (décision explicite : le site n'en a jamais eu, ça changerait ce que voient les visiteurs par défaut).
|
||
|
||
## Grammaire des chips par dimension
|
||
|
||
Trois dimensions, trois traitements visuels — posés en B3-M1, pensés pour rester stables jusqu'à B4+ (nouvelles dimensions à ajouter, pas à réinventer) :
|
||
|
||
| Dimension | Variant | Rendu inactif | Rendu actif |
|
||
|---|---|---|---|
|
||
| Échelle (national/régional/local) | `echelle` | outline, fond transparent | fond `--primary` plein, texte `--on-primary` |
|
||
| Fonction / hashtag | `fonction` (défaut) | fond `--bg-alt` | fond `--primary` plein, texte `--on-primary` |
|
||
| Posture (préparée pour B4, pas encore consommée) | `posture` | outline safran (`--accent`) | fond `--accent` plein, texte `--on-accent` **sombre** — jamais blanc sur safran (voir règle de contraste plus bas) |
|
||
|
||
Composants : `components/Chip.vue` (une chip, `<button>` ou `<span>` selon `as`) et `components/ChipGroup.vue` (conteneur `role="group"`, `flex-wrap`, gap). Toujours les deux ensemble — jamais une chip nue hors groupe (accessibilité : le groupe porte l'`aria-label` qui dit ce qu'on filtre).
|
||
|
||
```vue
|
||
<ChipGroup aria-label="Filtrer par fonction">
|
||
<Chip
|
||
v-for="fn in fonctions"
|
||
:key="fn"
|
||
:label="fn"
|
||
variant="fonction"
|
||
:active="selected.includes(fn)"
|
||
@toggle="toggleFonction(fn)"
|
||
/>
|
||
</ChipGroup>
|
||
```
|
||
|
||
Pour une chip non cliquable (affichage seul, ex. liste de résultats) : `as="span"` — pas de `type="button"`, pas d'`aria-pressed`, pas de handler `@toggle` nécessaire.
|
||
|
||
## FicheMiniCard + useFicheAdapter — brancher une nouvelle source
|
||
|
||
`composables/useFicheAdapter.ts` expose `toMini(source): MiniFiche`, où `MiniFiche = { id, nom, resume, chips, echelle?, geoloc, kind }`. Aujourd'hui il distingue `Org` (NocoDB, page `/`) et `StructureV2` (JSON statique `reseaux-bifurcation.json`, page `/agences`) via `'famille_principale' in source` (seul champ propre à `StructureV2`).
|
||
|
||
**Pour brancher une 3e source** (B6+, ex. un nouveau type de fiche) :
|
||
1. Ajoute son type à l'union `Org | StructureV2 | TonNouveauType` dans la signature de `toMini`.
|
||
2. Ajoute une garde de type (comme `isStructure`) basée sur un champ qui n'existe QUE dans ton nouveau type — ne réutilise pas `famille_principale` ou un champ ambigu.
|
||
3. Choisis l'ordre de repli du résumé : `buildResume([champA, champB, ...])` prend le premier champ non vide, en garde la première phrase, tronque à 110 caractères sur un mot. Vérifie quels champs de ton type contiennent du texte descriptif avant de les ordonner.
|
||
4. `chips` = 3 étiquettes maximum (fonctions, hashtags, ou l'équivalent pour ton type). `geoloc` = `latitude != null && longitude != null` si ton type a des coordonnées, sinon `true` (pas de fiche à masquer côté carte).
|
||
|
||
`components/FicheMiniCard.vue` consomme uniquement `MiniFiche` (depuis B11, `MiniFiche` porte aussi `etiquettes`, `chipsDimension`, `coords`, `date`, `ville`, `texte` : voir §Vue fiches) — il n'a jamais besoin de connaître `Org` ou `StructureV2`. C'est ce découplage qui permet d'ajouter une source sans toucher au composant d'affichage.
|
||
|
||
## Vue fiches (B11 02/10/2026, détail B12 03/10/2026) — `VueFiches` + `useVueFiches`
|
||
|
||
Remplace `FichesPanel`, `NavSidebar`, `MobileSheet` et les onglets Métropolitain / Outre-mer / Toutes les fiches de `/` et `/agences` (tous retirés). Spécification : `1 PROJETS/DEV/AEP/Cadrage/SPEC-vue-fiches.md` (vault).
|
||
|
||
**Trois fichiers, trois rôles.**
|
||
|
||
| Fichier | Rôle | Testé par |
|
||
|---|---|---|
|
||
| `utils/vueFiches.ts` | logique pure : `lireQuery` / `ecrireQuery`, `filtrerFiches`, `compterEtiquettes`, `trierFiches`, `regrouper`, `ordreFiches` ; B12 : `lireFiche`, `queryAvecFiche`, `positionFiche`, `modeOuverture` / `modeFermeture`, `empreinteListe` | `node scripts/test-vue-fiches.mjs` |
|
||
| `composables/useVueFiches.ts` | branche la logique sur la route ; rend un objet `reactive` (`vf`) | navigateur |
|
||
| `components/VueFiches.vue` (+ `VueFichesFiltres.vue`) | barre d'outils, trois états, filtres, grille, poignée, feuille « Filtrer », panneau de détail (B12) | navigateur |
|
||
|
||
**L'URL est la source de vérité.** L'état se lit dans `route.query` (surveillée) et s'écrit par `router.replace`. Paramètres : `vue` (`carte` · `mixte` · `fiches`, absent = défaut par largeur), `q`, un paramètre par dimension (valeurs séparées par des virgules), `adresse=sans`, `tri`, `groupe`, et celui des sous-vues de carte (`mode` sur `/`, `carte` sur `/agences`). Un paramètre inconnu ou invalide est ignoré ; un paramètre que la vue ne gère pas (`territoire`, `random`) est recopié tel quel, `fiche=` aussi (un filtre changé garde la fiche ouverte). La recherche part dans l'URL 250 ms après la dernière touche.
|
||
|
||
**États.** Sans `vue=` : Mixte à partir de 1024 px, Fiches en dessous. Mixte n'existe pas sous 1024 px (il se lit Fiches). Avant le montage, la racine porte `vf--auto` et la CSS tranche seule, pour qu'un lien s'affiche sans saut. La poignée ‹ › bascule Mixte ↔ Carte ; le repli est retenu en `sessionStorage` (`aep_vue_fiches_liste_repliee`) et ne compte que si l'URL n'a pas de `vue=`. En état Fiches, la carte reste montée, invisible (Leaflet garde une taille) ; les cartes appellent `invalidateSize` par `ResizeObserver`.
|
||
|
||
**Filtres : OU dans une dimension, ET entre dimensions.** Compteur d'une chip = fiches qui portent l'étiquette parmi celles que laissent passer la recherche et les AUTRES dimensions ; 0 = chip grisée (`disabled`). Repli au-delà de 14 valeurs, dans l'ordre de la dimension (stable). Les filtres sont à UN endroit : panneau de gauche (Mixte), tête de grille (Fiches, ordinateur et tablette), feuille du bas (téléphone) ; en Carte, seules les pastilles actives, dans la barre. Une chip cliquée dans une carte-fiche ajoute le filtre (`MiniFiche.chipsDimension`).
|
||
|
||
**Tri** : `pertinence` (nombre d'étiquettes cochées, puis nom ; défaut dès qu'un filtre est actif), `nom` (défaut sinon), `recent` (date puis Id décroissant ; à n'offrir que si la source a une date). **Regroupement** : une fiche va sous sa PREMIÈRE étiquette de la dimension, jamais en double.
|
||
|
||
**Détail et fiche à fiche (B12).** Cliquer une carte-fiche (ou une épingle : la page appelle `vf.ouvrirFiche(id)`) écrit `fiche=<id>` par `router.push` ; passer d'une fiche à l'autre (Précédente / Suivante, ← →, balayage au téléphone) par `router.replace`. Fermer (✕, « ‹ N fiches », Échap) revient en arrière si l'entrée vient de la page et que la liste n'a pas changé depuis, sinon retire `fiche=` sur place (lien partagé, filtre changé fiche ouverte) : le retour du téléphone ferme la fiche, jamais la page. La fiche est cherchée dans TOUTES les fiches : hors du résultat, le panneau dit « Hors de vos filtres » et n'a pas de Précédente / Suivante. Panneau de 380 px en troisième colonne (ordinateur, dans les trois états ; la grille Fiches passe à 2 colonnes), 340 px sur tablette, plein écran par-dessus la vue au téléphone (barre, liste et carte en `inert`, fiche à fiche en pied). Le focus entre dans le panneau à l'ouverture et revient, à la fermeture, sur la carte-fiche de la dernière fiche lue ; la position de défilement de la liste est rendue. Actions du panneau : Ouvrir la page de la fiche (prop `lien-page`), Voir sur la carte (si la fiche a une épingle dans le résultat : Mixte sur ordinateur, Carte au téléphone ; la page lit `vf.centrage` et le passe à sa carte, prop `centrage` de `NavMap` / `NavMapV2`), Copier le lien (l'URL courante dit tout). Deux écritures d'URL dans le même tick s'enchaînent (la seconde part de la query en attente), elles ne s'écrasent plus.
|
||
|
||
**Contrat.**
|
||
|
||
```vue
|
||
<VueFiches :vf="vf" intention="Une ligne." libelle-recherche="Rechercher…" :pending="pending"
|
||
:lien-page="(f) => `/fiche/${f.id}`" @survol="…">
|
||
<template #carte> <!-- carte(s) de la page, une par sous-vue, v-show sur vf.etat.sousVue --> </template>
|
||
<template #detail="{ fiche, filtrer }"> <!-- contenu propre à la source ; filtrer(dimension, valeur) au clic d'une étiquette --> </template>
|
||
<template #vide> <!-- optionnel --> </template>
|
||
</VueFiches>
|
||
```
|
||
|
||
Contenus de détail en place : `/` → `FicheDetail` (`compact`, `etiquettes-cliquables`) + commentaires, l'`Org` venant de la liste (même enregistrement NocoDB que `/api/fiche/:id`) ; `/agences` → `FicheReseauContenu` (corps de l'ancienne `FicheModalV2`, une structure liée s'ouvre dans le même panneau). `FicheModal` et `FicheModalV2` sont retirées ; la page `/fiche/:id` reste.
|
||
|
||
`const vf = useVueFiches(fiches, config)` : `fiches` = `MiniFiche[]` déjà adaptées, non filtrées (ref, computed ou getter) ; `config: ConfigVueFiches` = `{ dimensions, tris, groupes, etats, sousVues? }` (types dans `utils/vueFiches.ts`). Le composant ne charge rien et ne connaît pas la carte. La page lit `vf.idsResultat` pour ne montrer sur la carte que le résultat courant, `vf.survolId` pour allumer l'épingle (et l'écrit au survol d'une épingle), `vf.ficheId` / `vf.ficheOuverte` / `vf.position` pour la fiche ouverte (B12), `vf.largeur` et `vf.vueAffichee` pour placer ses boutons flottants.
|
||
|
||
**Brancher une troisième page** (Codev en B12, Outils, vue Œuvres) :
|
||
1. Adaptateur dans `useFicheAdapter.ts` : remplir `etiquettes` avec une clé par dimension (= nom du paramètre d'URL), `chipsDimension`, `coords` si la source en a (sinon `geoloc: true`).
|
||
2. Une `ConfigVueFiches` dans la page : dimensions (variante `fonction` pour la principale, `echelle` pour la secondaire), tris (pas de `recent` sans date), groupes, `etats` (`['carte', 'fiches']` si la page n'a pas la place pour Mixte), `sousVues` si la carte en a.
|
||
3. Le slot `#carte`, et le slot `#detail` (contenu de la fiche ; une page sans contenu de détail affiche seulement les actions). Les épingles appellent `vf.ouvrirFiche(id)`.
|
||
4. Ajouter des cas à `scripts/test-vue-fiches.mjs` si la page introduit une règle nouvelle.
|
||
|
||
**`FicheMiniCard`** : racine `<article>`, toute la carte ouvre la fiche par un bouton étiré (`::after`) ; avec `etiquettes-cliquables`, les chips deviennent des boutons (impossible dans l'ancienne racine `<button>`). Props `actif` (liseré safran au survol d'épingle), `etiquettes-actives`. Résumé et « Sans localisation » en `color-mix(--text 78 %)` : `--text-muted` et `--text-subtle` échouaient au contraste.
|
||
|
||
## Intention d'abord
|
||
|
||
Principe de design validé par Jules pour B3 : un panneau (ou une carte) s'ouvre sur **une ligne d'intention** avant l'outillage (filtres, compteur, grille). La ligne d'intention dit en une phrase ce que montre l'écran et le premier geste possible (« Toutes les structures d'entraide, y compris celles sans adresse sur la carte. Cochez une fonction pour filtrer. »). Dans `VueFiches`, l'intention ouvre la liste, avant les filtres et la grille. Ne pas remonter le compteur ou les chips au-dessus de l'intention, même pour gagner de la place.
|
||
|
||
## Motif maillage (favicon)
|
||
|
||
Le favicon (`public/favicon.svg`, décliné en `favicon-32.png`, `favicon-16.png`, `apple-touch-icon.png`) dessine un petit graphe : un nœud central relié à cinq nœuds périphériques par des arêtes, sur fond bleu nuit avec des nœuds/arêtes safran. C'est un **motif à décliner**, pas une icône isolée — il préfigure les liens de graphe et les connexions entre fiches attendus en B4 (postures), B8 et B9 (structures liées, graphe de réseau déjà présent dans `FicheModalV2` via `structuresVoisines`). Si tu dessines un composant de graphe ou de connexions entre fiches dans ces batchs, reprends ce vocabulaire visuel (nœuds pleins, arêtes fines, un nœud central mis en avant) plutôt que d'en inventer un nouveau.
|
||
|
||
## Deux règles de contraste
|
||
|
||
1. **Texte sombre sur l'accent safran, jamais blanc.** `--accent`/`--on-accent` : le blanc sur `#f5b342` échoue le contrôle AA (décision DS du 20/07/2026). `--on-accent` est toujours une couleur sombre (`#1a2238` clair, `#111520` sombre). Vérifié au pixel sur la variante `posture` de `Chip.vue`.
|
||
2. **`--nav-text-on-primary` reste une valeur littérale**, pas un alias — voir l'explication détaillée dans la section tokens ci-dessus. Si un nouveau composant a besoin de texte de contraste sur un fond bleu nuit, vérifie d'abord si ce fond est `--nav-primary` (translucide) ou `--nav-primary-solid` (opaque) avant de choisir la variable de texte : elles n'ont pas la même réponse en mode sombre.
|
||
|
||
## Navigation par échelles S / M / XL (B10, 02/10/2026)
|
||
|
||
**Source unique : `utils/echelles.ts`.** Chaque échelle porte ses `onglets` (`libelle`, `to`, `actif(route)`) ; `ORDRE_ECHELLES` fixe l'ordre S → M → XL ; `echelleDeRoute(route)` rend le groupe et l'onglet courants, ou `null` hors groupe (Manifeste, À propos, Proposer, Signaler, fiche, `/rag`). Aucun libellé d'échelle ni d'onglet ne se recopie dans un composant : ajouter un onglet = une ligne dans ce fichier, et `scripts/test-echelles-nav.mjs` (une ligne de cas).
|
||
|
||
| Échelle | Accroche | Onglets |
|
||
|---|---|---|
|
||
| S | Partir de soi | Jobs (`/trouver-du-taf`) · Outils (`/outils`) |
|
||
| M | La communauté | Écosystème d'entraide (`/`) · Réseaux AEP (`/agences`) · Codev (`/codev*`) |
|
||
| XL | Contribuer | Recherche média (`/media`) · Publications (`/media?tab=projets`) |
|
||
|
||
**Seuils** (CSS des composants, pas de breakpoint Tailwind : 1280 et 1440 n'en sont pas) :
|
||
|
||
| Largeur | En-tête | Sous l'en-tête |
|
||
|---|---|---|
|
||
| ≥ 1440 | logo complet · trois groupes · Manifeste · À propos · Soutenir (contour safran) · + Proposer (plein safran) · sombre | — |
|
||
| 1280-1439 | idem, logo réduit au bloc « AEP » | — |
|
||
| 1024-1279 | logo complet · groupe courant seul · Manifeste · + Proposer · sombre · « Menu » | — |
|
||
| 768-1023 | logo réduit · groupe courant seul · Manifeste · + Proposer · sombre · « Menu » | — |
|
||
| < 768 | logo complet · sombre · + (icône) · ☰ | `FilEchelle` (28 px), sauf hors groupe |
|
||
|
||
**Composants** : `NavEchelles.vue` (barre ; chaque groupe est un `<ul>` étiqueté par son accroche via `aria-labelledby`, onglet courant en `aria-current="page"`), `NavTiroir.vue` (menu < 1280 : `role="dialog"`, voile, Échap, focus piégé et rendu à l'élément d'ouverture, fermeture au changement de route et au passage à ≥ 1280), `FilEchelle.vue` (< 768, monté dans `app.vue` entre `<header>` et le conteneur de page, **jamais dedans** : `/` y est en `overflow-hidden`). Le lien de don vit dans `utils/liens.ts` (`SOUTENIR_URL`).
|
||
|
||
**Texte atténué** : `color-mix(in srgb, var(--text) 78 %, transparent)`, pas `--text-muted` (3,7:1 sur blanc, sous AA à 0,8 rem) ni `opacity` (qui éteindrait aussi l'anneau de focus).
|
||
|
||
**Signaler** n'est plus dans la barre d'ordinateur : il reste dans le tiroir, dans chaque fiche, et en pied d'À propos.
|