Files
nav-carte/DESIGN-SYSTEM.md
T

10 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 :

  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).

<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 — 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

  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.