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 guardshowSourcingBadge(ADR-008)-
MStatusBadge— 4 stories (Live / Stale / Offline / Loading) -
6 organisms présentationnels, 13 stories (note : le message du commit
75c5004annonce 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 factoryOHydroChart— 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 deOStationMap, 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 viauseI18nListsurfr.jsonOSiteFooter— 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— lituseStationsStoreetuseStatusStore, appellefetchStations()+fetchStatus()dansonMounted. Une story représentative demanderait soit de mocker le backend API entier (MSW ou équivalent), soit de reproduire en décorateur le patternseedStationspour 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 deMStatCard.OStationDrawer— composition deuseStationsStore(sélection + fetch measurements),vue-router(URL sync de l'ID sélectionné),OHydroChartenfant, 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 seederitems+selectedStationId+measurementsByStationet un routeur Vue minimal. Coût > valeur documentaire.OStationMap— dépend de Leaflet (ADR-005), d'unResizeObserver, d'un cleanupmap.remove()sur unmount, et du CSS externeleaflet/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-essentialsest devenu un empty metapackage en v9/v10 ; les addons docs, controls, viewport, backgrounds sont consolidés dans le core. Installation individuelle de@storybook/addon-docset@storybook/addon-a11yuniquement —addon-onboardingajouté parstorybook initest retiré au setup. - CSF3 strict avec
Meta<typeof Component> satisfies ...+StoryObj<typeof meta>. Pas dedefineMeta/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 arglabeltypétextest piped dans le slot via le templaterender, permettant au Controls panel d'éditer le contenu en live. - Pattern
globals.backgrounds.value: 'graphite'pour les stories à fond sombre (ASourcingBadge,MSectionHeader Dark,MStationCarddark variants,OResearchZonesSection). C'est l'API Storybook 10. L'ancienparameters.backgrounds.defaultreste accepté silencieusement comme no-op — il fallait détecter le changement d'API pour que le background s'applique réellement. - Pattern
seedStatus/seedStationsfactory pour les organisms Pinia-couplés (OHeroSection4 états,OMapSection2 états). Un decorator factory retourne un composant Vue dont lesetup()(a) obtient l'instance store viauseStatusStore()/useStationsStore(), (b) appliquestore.$patch(...)avec l'état seeded, (c) réassignestore.fetchStatus = async () => {}pour court-circuiter le pollingonMountedqui 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 enstorybook-static/servi par nginx — le dossierdocs/n'est pas copié dans l'image. - CSP Storybook-spécifique (
nginx-storybook-security-headers.conf). Deux dérogations vsnginx-security-headers.confservant 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 vianew 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 queOStationDrawer: dépendance au routeur, coût de mock supérieur à la valeur documentaire. Le comportement est couvert parOSiteHeader.test.ts, monté avec un routeur en mémoire. MDomainCardstoryisée (6 stories : trois états datés, chargement, erreur, donnée absente). Elle contient unRouterLinkmais ne lit pas la route..storybook/preview.tsenregistre doncRouterLinkcomme 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 variantesenergyetplatform.- 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.frconsultable 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.
seedStatusfactory,slot-via-arg,globals.backgroundssont 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.frtourne 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.tsnomme la décision, l'Introduction.mdxla 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
mainporte 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
playfunctions / interactions testing. Vitest couvre déjà le comportement côté app (9 fichiers de test existants surstations,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 parstorybook 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) pourASourcingBadgevariant 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 pageColors.mdx. - Research stations dans
fr.json, pas dans le store Pinia. Dette déjà assumée par ADR-008. Les stories d'OResearchZonesSectionreflètent fidèlement ce trade-off (elles consommentuseI18nList, 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¶
- storybook.alpimonitor.fr — déploiement live, branche
feature/storybookpuismainpost-merge - apps/web/src/stories/design-system/ — 5 pages MDX (Introduction, Colors, Typography, Spacing, Icons)
- apps/web/Dockerfile.storybook — build multi-stage node:20-alpine → nginx:1.27-alpine
- apps/web/nginx-storybook.conf — vhost avec cache immutable sur
/assets/,/sb-*/, no-cache surindex.html+iframe.html - apps/web/nginx-storybook-security-headers.conf — CSP dédiée (frame-ancestors 'self' + script-src 'unsafe-eval')
- ADR-002 — parti-pris ABEM et parcimonie des tokens Tailwind
- ADR-005 — Leaflet pour la cartographie (cadre l'exclusion d'
OStationMap) - ADR-008 — sourcing transparency, origine de
ASourcingBadgeet vocabulaire CONFIRMED / ILLUSTRATIVE repris dans les storiesMStationCard