ADR-009 — Périmètre Storybook et exclusions présentationnelles¶
Date : 2026-04-23
Statut : Acceptée — implémentée
Implémentation : 0292e6e (bootstrap Storybook 10.3.5 + config preview/main + alias Vite + première story ABadge), f28f8d2 (5 atoms, 17 stories), 4b21877 (4 molecules, 16 stories), 7f5db44 (6 organisms présentationnels, 13 stories), 3252232 (5 pages MDX design system + storySort), ede869a (Dockerfile dédié + nginx + CSP adaptée, déploiement Coolify sur storybook.alpimonitor.fr).
Contexte¶
L'annonce du poste Développeur·se Front-End au CREALP cite explicitement Atomic Design dans les prérequis techniques. 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 au recruteur.
La session J15 (2026-04-23), posée dans une branche feature/storybook disjointe du livrable candidature, a cadré l'ajout d'un Storybook exhaustif comme signal front-end explicite. 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 de signal retournée dans la fenêtre de candidature.
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 post-candidature.
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
7f5db44annonce 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 le signal retourné : 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. Scope > ROI signal.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 recruteur 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.
Conséquences¶
Positives¶
- Signal front-end explicite. Atomic Design cité par l'annonce se matérialise en catalogue navigable. Patterns CSF3, ABEM, Pinia stub, MDX design system se lisent comme un vocabulaire d'équipe — plus rare en candidature qu'une maîtrise de framework.
- Vitrine découvrable. URL stable
storybook.alpimonitor.frconsultable sans cloner le repo, sans onboarding. Un recruteur CREALP avec 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 que le recruteur 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). Post-candidature, si AlpiMonitor évolue, la branche
mainporte le coût ; sinon, Storybook reste fixé sur v1.0.0-crealp + cette session. - Budget fenêtre candidature. J15 entier consommé sur Storybook au lieu d'une feature additive (brush/zoom D3, export CSV, comparateur de stations). Arbitrage ROI assumé : le signal design system + Atomic Design est spécifiquement adressé au poste visé ; une feature additive disperserait l'impact.
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 post-candidature 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 fenêtre candidature.
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 — signal dilué.
- Verdict : rejeté. L'écart de temps (~5 h) est inférieur à l'écart de signal retourné.
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 ROI signal 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 un livrable candidature, le rapport coût/signal est défavorable.
- Verdict : rejeté, restera backlog si Chromatic devient pertinent post-candidature.
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é à défendre en entretien ("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