Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBu7CSLy7PJgL4HJaV5oHP
13 KiB
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 :
@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.@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,--bgetc. 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 desrgba(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 (#1a2238clair,#c8d2f0sombre). Différent de--primarydu DS, qui est le bleu nuit à 60 % d'opacité (utilisé pour les surfaces translucides).--nav-primary-solidsert 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 (#f8f6f1clair /#111520sombre) plutôt qu'aliasé sur--on-primarydu 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#c8d2f0est un lavande clair). Le rôle--on-primarydu 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).
<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) :
- Ajoute son type à l'union
Org | StructureV2 | TonNouveauTypedans la signature detoMini. - Ajoute une garde de type (comme
isStructure) basée sur un champ qui n'existe QUE dans ton nouveau type — ne réutilise pasfamille_principaleou un champ ambigu. - 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. chips= 3 étiquettes maximum (fonctions, hashtags, ou l'équivalent pour ton type).geoloc=latitude != null && longitude != nullsi ton type a des coordonnées, sinontrue(pas de fiche à masquer côté carte).
components/FicheMiniCard.vue consomme uniquement MiniFiche — 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.
FichesPanel — le panneau « toutes les fiches »
components/FichesPanel.vue : props fiches: MiniFiche[] (déjà filtrée par la page appelante — le panneau ne refiltre pas, ne crée pas de second champ de recherche), hashtags: string[] (options de filtre), selected: string[], query: string (sert seulement à nuancer le message d'état vide), intention: string. Émet update:selected et open(fiche).
Le panneau ne possède pas son propre état de filtre : ses chips pilotent la variable réactive de la page appelante via update:selected (ex. fonctions sur /, selectedHashtags sur /agences). C'est ce qui garantit qu'une chip cochée dans le panneau et la même chip dans la sidebar (si elle est visible) affichent toujours le même état — une seule source de vérité, jamais deux.
Hashtags repliés (B3-M4) : au-delà de 14 chips (COLLAPSE_THRESHOLD), seules les premières s'affichent + un chip-bouton « voir les N autres » qui déplie. Les chips déjà actives restent visibles même repliées (union entre les 14 premières et les actives hors de ce lot). État déplié non persistant — ref locale, reset à chaque montage. / (10 fonctions) ne dépasse jamais le seuil ; /agences (~60 hashtags) se replie par défaut.
Accès : ?vue=fiches dans l'URL, en plus des paramètres de filtre existants. Sur /, desktopMapView/mobileMapView gagnent une valeur 'fiches' à côté de 'metropole'/'outremer' ; sur /agences, à côté de 'metropole'/'outremer'/'graphe'. Recharger l'URL avec ?vue=fiches doit rouvrir directement le panneau, filtres compris.
Mobile : le panneau ne se monte JAMAIS dans un MobileSheet (le composant sheet demi-hauteur pensé pour flotter au-dessus d'une carte) — il n'y a pas de carte derrière la vue fiches, donc pas de sheet à demi-hauteur : le panneau occupe toute la zone sous la barre d'onglets, avec son propre scroll interne. Piège vécu en B3 : monter le panneau dans un MobileSheet par réflexe (comme les autres onglets) laisse ~45 % de l'écran vide au-dessus, puisque la sheet démarre repliée à mi-hauteur et qu'il n'y a rien à voir en dessous.
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 FichesPanel, l'ordre de l'en-tête est fixe : intention → compteur → chips → 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
- 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-accentest toujours une couleur sombre (#1a2238clair,#111520sombre). Vérifié au pixel sur la varianteposturedeChip.vue. --nav-text-on-primaryreste 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.