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 : 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 guard showSourcingBadge (ADR-008)
  • MStatusBadge — 4 stories (Live / Stale / Offline / Loading)

  • 6 organisms présentationnels, 13 stories (note : le message du commit 7f5db44 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)
  • OMapSection2 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 le signal retourné : 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. Scope > ROI signal.
  • 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 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-alpinenginx: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.

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.fr consultable 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. 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 que le recruteur 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). Post-candidature, si AlpiMonitor évolue, la branche main porte 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 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 post-candidature 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 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