Aller au contenu

ADR-009 — Périmètre Storybook et exclusions présentationnelles

Date : 2026-04-23 Statut : Acceptée — implémentée Implémentation : 67928c2 (bootstrap Storybook 10.3.5 + config preview/main + alias Vite + première story ABadge), b8a545d (5 atoms, 17 stories), f4a6106 (4 molecules, 16 stories), 75c5004 (6 organisms présentationnels, 13 stories), a8fb7bb (5 pages MDX design system + storySort), 7eb402a (Dockerfile dédié + nginx + CSP adaptée, déploiement Coolify sur storybook.alpimonitor.fr).

Contexte

Atomic Design est la méthodologie structurante du front (ADR-002). Jusqu'au tag v1.0.0-crealp (2026-04-22), l'application incarnait la méthodologie sans la documenter visuellement : les préfixes ABEM (a-, m-, o-) et la convention de nommage étaient consignés dans ADR-002, mais aucun catalogue consultable n'exposait le design system à un lecteur extérieur.

La session J15 (2026-04-23), posée dans une branche feature/storybook disjointe de la version publiée v1.0.0-crealp, a cadré l'ajout d'un Storybook exhaustif comme documentation vivante du design system. L'exhaustivité est à borner : certains organisms couplent store Pinia + routeur + bibliothèque externe, et leur story-ification demanderait un outillage de mock disproportionné face à la valeur documentaire retournée dans la fenêtre de la première phase.

Enjeu central : livrer un catalogue riche et autoportant sans sacrifier la frontière d'exclusion avec du mock infrastructure qui pollue le code produit ou allonge le chemin de maintenance.

Décision

Couverture

Storyiser 15 composants présentationnels — 46 stories au total, chacune typée CSF3 strict (Meta<typeof Component> + StoryObj<typeof meta> + satisfies) et marquée tags: ['autodocs'] pour générer une page Docs complémentaire :

  • 5 atoms, 17 stories :
  • ABadge — 4 stories (Live / Research / Neutral / Long text wrapping)
  • AButton — 4 stories (Primary / Secondary / Ghost / Sizes)
  • AIcon — 3 stories (Default / Gallery des 11 icônes en grille / Sizes 16-48)
  • ANumericValue — 4 stories (Default / Without unit / Large KPI / Extra-large Hero)
  • ASourcingBadge — 2 stories (Confirmed / Illustrative sur fond graphite dark)

  • 4 molecules, 16 stories :

  • MSectionHeader — 4 stories (TitleOnly / WithEyebrow / Full / Dark)
  • MStatCard — 4 stories (Discharge / StationCount / LastRefresh / TrendChart)
  • MStationCard — 4 stories (Federal·light / Research·light / Research·dark·CONFIRMED Bramois / Research·dark·ILLUSTRATIVE Les Haudères), calquées 1:1 sur la production pour démontrer le guard showSourcingBadge (ADR-008)
  • MStatusBadge — 4 stories (Live / Stale / Offline / Loading)

  • 6 organisms présentationnels, 13 stories (note : le message du commit 75c5004 annonce 12 stories par erreur de comptage ; le fichier en contient 13 — ce chiffre est celui qui fait foi) :

  • OHeroSection — 4 stories (Live / Stale / Offline / Loading), chacune seede le store via un decorator factory
  • OHydroChart — 4 stories (Default 24h discharge / Single point / Empty / Narrow mobile < 420px)
  • OMapSection — 2 stories seulement (Loading / Error). L'état Ready est volontairement absent : il déclencherait le rendu de OStationMap, lui-même exclu (voir ci-dessous). Un JSDoc en tête du fichier trace la décision.
  • OResearchZonesSection — 1 story (Default), rend les 3 stations research câblées via useI18nList sur fr.json
  • OSiteFooter — 1 story (Default)
  • OWhyLindasSection — 1 story (Default, avec snippet SPARQL highlighté localement)

Exclusions

Exclure 3 organisms explicitement, chacun pour une raison technique précise :

  • OKeyMetricsSection — lit useStationsStore et useStatusStore, appelle fetchStations() + fetchStatus() dans onMounted. Une story représentative demanderait soit de mocker le backend API entier (MSW ou équivalent), soit de reproduire en décorateur le pattern seedStations pour chaque combinaison store × status (minimum 4 scénarios). L'investissement en outillage dépasse la valeur documentaire retournée : les trois métriques rendues sont déjà couvertes à l'échelle moléculaire par les 4 stories de MStatCard.
  • OStationDrawer — composition de useStationsStore (sélection + fetch measurements), vue-router (URL sync de l'ID sélectionné), OHydroChart enfant, et gestion cycle focus trap + backdrop blur. Le récit "drawer ouvert sur une station" n'a de sens qu'avec une station sélectionnée, ce qui demande de seeder items + selectedStationId + measurementsByStation et un routeur Vue minimal. Coût > valeur documentaire.
  • OStationMap — dépend de Leaflet (ADR-005), d'un ResizeObserver, d'un cleanup map.remove() sur unmount, et du CSS externe leaflet/dist/leaflet.css. Un boot en canvas Storybook isolé n'a pas de valeur pédagogique au-delà de ce que la carte fait déjà en prod — et introduit une dépendance runtime (icônes marqueurs, z-index Traefik vs popup) que le reste du catalogue n'exige pas.

Documentation

Ajouter 5 pages MDX dans apps/web/src/stories/design-system/ : Introduction (philosophie Atomic Design + exclusions justifiées), Colors (5 tokens custom + emerald Tailwind default justifié par [ADR-008]), Typography (Inter + JetBrains Mono, 5 niveaux de hiérarchie), Spacing (échelle Tailwind + containers + breakpoints réellement utilisés), Icons (<Canvas of={...}> réutilisant la story AIcon Gallery).

Imposer l'ordre de la sidebar via parameters.options.storySort dans .storybook/preview.ts : Design System → Atoms → Molecules → Organisms. Le lecteur rencontre la grammaire avant le code.

Déploiement

Servir la sortie storybook-static/ sur un sous-domaine dédié storybook.alpimonitor.fr, sur le même VPS Hetzner (95.216.196.69) que alpimonitor.fr et api.alpimonitor.fr, via une nouvelle application Coolify indépendante. Dockerfile multi-stage node:20-alpine → nginx:1.27-alpine, CSP dédiée (voir arbitrages ci-dessous), TLS Let's Encrypt auto-provisionné par Traefik.

Arbitrages d'implémentation

  • Storybook 10.3.5 via @storybook/vue3-vite. @storybook/addon-essentials est devenu un empty metapackage en v9/v10 ; les addons docs, controls, viewport, backgrounds sont consolidés dans le core. Installation individuelle de @storybook/addon-docs et @storybook/addon-a11y uniquement — addon-onboarding ajouté par storybook init est retiré au setup.
  • CSF3 strict avec Meta<typeof Component> satisfies ... + StoryObj<typeof meta>. Pas de defineMeta/defineStory (qui n'existent pas dans l'API v10). tags: ['autodocs'] sur chaque Meta génère une page Docs sans boilerplate.
  • Pattern slot-via-arg pour les composants à slot par défaut (ABadge, AButton) : un arg label typé text est piped dans le slot via le template render, permettant au Controls panel d'éditer le contenu en live.
  • Pattern globals.backgrounds.value: 'graphite' pour les stories à fond sombre (ASourcingBadge, MSectionHeader Dark, MStationCard dark variants, OResearchZonesSection). C'est l'API Storybook 10. L'ancien parameters.backgrounds.default reste accepté silencieusement comme no-op — il fallait détecter le changement d'API pour que le background s'applique réellement.
  • Pattern seedStatus / seedStations factory pour les organisms Pinia-couplés (OHeroSection 4 états, OMapSection 2 états). Un decorator factory retourne un composant Vue dont le setup() (a) obtient l'instance store via useStatusStore() / useStationsStore(), (b) applique store.$patch(...) avec l'état seeded, (c) réassigne store.fetchStatus = async () => {} pour court-circuiter le polling onMounted qui pollurait sinon l'état seedé avec un échec réseau. Fonctionne parce que les méthodes des stores Pinia composition-API sont des propriétés réassignables sur l'instance proxy.
  • Liens ADR en URL GitHub absolues dans les 5 MDX (https://github.com/sodigitaljeremy/alpimonitor/blob/main/docs/architecture/adr/...). Les chemins relatifs ../../../docs/... fonctionnent en dev (Vite résout), mais cassent en storybook-static/ servi par nginx — le dossier docs/ n'est pas copié dans l'image.
  • CSP Storybook-spécifique (nginx-storybook-security-headers.conf). Deux dérogations vs nginx-security-headers.conf servant l'app principale, chacune load-bearing :
  • frame-ancestors 'self' (vs 'none') — le manager Storybook embarque le preview comme iframe same-origin ; 'none' casserait tout rendu.
  • script-src 'unsafe-eval' (vs absent) — le bundle preview compilé évalue MDX et docgen data via new Function(...).

Tout le reste (HSTS, X-Content-Type-Options, X-Frame-Options SAMEORIGIN, Referrer-Policy, COOP) matche la posture du vhost principal.

Mise à jour du 2026-09-28 — plateforme à deux domaines (ADR-014)

La décision ci-dessus est inchangée ; ce qui suit l'applique aux composants de la plateforme.

  • Quatrième exclusion : OSiteHeader. La barre de navigation lit la route courante (useRoute) pour marquer le domaine actif, y compris sur une sous-page (/energie/regroupement). Une story représentative demanderait un routeur en mémoire en décorateur, avec une route par état : un outillage disproportionné pour trois liens. Même justification que OStationDrawer : dépendance au routeur, coût de mock supérieur à la valeur documentaire. Le comportement est couvert par OSiteHeader.test.ts, monté avec un routeur en mémoire.
  • MDomainCard storyisée (6 stories : trois états datés, chargement, erreur, donnée absente). Elle contient un RouterLink mais ne lit pas la route. .storybook/preview.ts enregistre donc RouterLink comme un simple <a> — ce n'est pas un routeur. La ligne de partage : un composant qui contient un lien peut être catalogué, un composant qui lit la route ne l'est pas.
  • OSiteFooter : deux stories ajoutées pour ses variantes energy et platform.
  • Liens ADR des pages MDX : ils pointent désormais vers docs.alpimonitor.fr. Les URL GitHub absolues décrites plus haut renvoient une 404 publique depuis que le dépôt est privé.
  • Catalogue : 16 composants, 54 stories, 5 pages MDX. Les composants apparus avec la couche IA (ADR-012 : alertes, chat), le domaine énergie (ADR-013) et l'accueil (OHomeMethod, OHomeDomains) ne sont pas storyisés : cette mise à jour ne tranche pas leur cas.

Conséquences

Positives

  • Design system explicite. La méthodologie Atomic Design (ADR-002) se matérialise en catalogue navigable. Patterns CSF3, ABEM, Pinia stub, MDX design system se lisent comme un vocabulaire d'équipe.
  • Vitrine découvrable. URL stable storybook.alpimonitor.fr consultable sans cloner le repo, sans onboarding. Un relecteur qui dispose de 30 secondes voit la sidebar et saisit la profondeur.
  • Documentation vivante. Les 5 MDX ne sont pas un README figé : ils référencent les stories (<Canvas of={...}>), les ADR (liens absolus traversant le déploiement), et les usages réels dans le code. Le design system devient sa propre doc de référence.
  • Patterns techniques réutilisables. seedStatus factory, slot-via-arg, globals.backgrounds sont immédiatement réemployables pour toute feature future demandant une story sur un composant Pinia-couplé ou dark-theme — zéro apprentissage à refaire.
  • Cohérence self-hosting. storybook.alpimonitor.fr tourne sur la même stack que le reste (Coolify + Traefik + Let's Encrypt + Hetzner). Même posture sécurité (6 headers), même pipeline auto-deploy sur push. Aucune dette d'infra externalisée.

Négatives

  • 3 organisms cœur-produit non-storyisés. La carte (OStationMap), le drawer (OStationDrawer) et la section KPI (OKeyMetricsSection) sont précisément les interactions qu'un visiteur attend. Mitigation : le JSDoc en tête d'OMapSection.stories.ts nomme la décision, l'Introduction.mdx la trace, cette ADR la formalise. La frontière d'exclusion est tenue par la cohérence du discours, pas par l'occultation.
  • Coût de maintenance. Chaque évolution d'un des 15 composants impose la mise à jour de sa story associée, sinon la vitrine dérive. Acceptable sur un projet actif (refactors = opportunité de réarbitrer les stories), pénalisant sur un projet abandonné (Storybook périmé nuit plus qu'il n'aide). Si AlpiMonitor évolue, la branche main porte le coût ; sinon, Storybook reste fixé sur v1.0.0-crealp + cette session.
  • Budget de la première phase. J15 entier consommé sur Storybook au lieu d'une feature additive (brush/zoom D3, export CSV, comparateur de stations). Arbitrage assumé : documenter et consolider le design system existant plutôt qu'étendre le périmètre ; une feature additive aurait dispersé l'effort.

Trade-offs assumés

  • Pas de Chromatic (visual regression testing). Payant au-delà d'un seuil de snapshots, overkill pour un portfolio mono-développeur. Les régressions visuelles sont détectables en inspection manuelle sur un catalogue de 46 stories.
  • Pas de play functions / interactions testing. Vitest couvre déjà le comportement côté app (9 fichiers de test existants sur stations, status, chart-model, station-map-mapping, OKeyMetricsSection, OStationDrawer, OSiteFooter, MStationCard, ASourcingBadge). Dédoubler en play functions ajouterait une surface de test sans couvrir un chemin manquant.
  • Pas de addon-onboarding. Ajouté par défaut par storybook init, retiré au setup (Phase 1). Utile pour un catalogue vide qu'on démarre, bruit pour un design system mûr livré en une session.
  • Tailwind defaults (emerald-300 / emerald-400) pour ASourcingBadge variant CONFIRMED. Respect du parti-pris ADR-002 de parcimonie des tokens : introduire un sixième token custom pour un usage unique serait disproportionné. Explicité dans la page Colors.mdx.
  • Research stations dans fr.json, pas dans le store Pinia. Dette déjà assumée par ADR-008. Les stories d'OResearchZonesSection reflètent fidèlement ce trade-off (elles consomment useI18nList, comme le composant en prod).
  • Warning deprecation Storybook 11. La console navigateur affiche PopoverProvider ariaLabel will become mandatory in Storybook 11 — deprecation interne de Storybook, pas du code applicatif. À résoudre lors d'une mise à jour majeure ultérieure, hors scope de la première phase.

Alternatives écartées

Scope minimal — atoms + molecules + 2-3 organisms exemplaires

  • Bénéfice : 4-6 h de travail vs 9-13 h. Moins de surface à maintenir.
  • Coût : "exhaustif" explicitement cadré avec Jérémy en début de session impliquait la couverture de tous les organisms présentationnels. Un catalogue partiel lit comme un proof-of-concept, pas comme un design system — valeur documentaire diluée.
  • Verdict : rejeté. L'écart de temps (~5 h) est inférieur à l'écart de valeur documentaire.

Scope étendu — tout inclus, y compris conteneurs (OKeyMetrics / OStationDrawer / OStationMap) avec mocks store + API

  • Bénéfice : catalogue complet, aucune exclusion à défendre.
  • Coût : introduire msw-storybook-addon (ou équivalent) pour simuler le backend, instrumenter un router Vue minimal pour OStationDrawer, gérer le cycle de vie Leaflet dans un canvas isolé (ResizeObserver, cleanup). 3-5 h d'outillage additionnel, risque de régression sur le code produit (imports conditionnels, fallbacks spécifiques au contexte Storybook).
  • Verdict : rejeté. La complexité du mock d'infra dépasse le gain par rapport à un catalogue plus petit mais techniquement propre.

Chromatic visual regression

  • Bénéfice : détection automatique des régressions visuelles, diff pixel dans les PR.
  • Coût : payant au-delà de 5k snapshots/mois, nécessite un compte externe. Pour une démonstration sans équipe de revue visuelle, le rapport coût/bénéfice est défavorable.
  • Verdict : rejeté, restera backlog si Chromatic devient pertinent.

Vercel / Netlify / GitHub Pages pour le déploiement Storybook

  • Bénéfice : déploiement Storybook en une commande, CDN global, zéro config serveur.
  • Coût : rupture de la cohérence self-hosting du projet. Le reste d'AlpiMonitor tourne sur Coolify + Hetzner, avec des patterns Dockerfile + nginx réutilisables. Externaliser uniquement Storybook créerait une singularité à justifier ("pourquoi cette surface est gérée autrement ?") pour un gain opérationnel marginal.
  • Verdict : rejeté. Le Dockerfile + vhost nginx + CSP dédiée capitalisent sur les snippets déjà éprouvés sur alpimonitor.fr.

Storybook 7.x / 8.x au lieu de 10.3

  • Bénéfice : documentation plus abondante en ligne (v7 cumule 2+ ans de contenu), quelques patterns stabilisés.
  • Coût : versions en fin de cycle, API defineMeta / doc blocks sur le point de changer. Un projet démarré début 2026 démarre en v10.
  • Verdict : rejeté. Context7 a confirmé 10.3.5 comme version stable au 2026-04, sans régression bloquante sur @storybook/vue3-vite.

Références