Archive

Journal, du 23 au 30 octobre 2026 (archive)

Ce fichier est l'historique du journal de reprise jusqu'au 30/10/2026, gardé tel quel le 02/11/2026 : ses tableaux d'avancement et ses « reste à faire » décrivent l'état de l'époque. L'état actuel et la façon de reprendre sont dans le journal de reprise, les décisions dans le registre des décisions.

1. Règles de travail

  • Branche recit/setup. Rien n'est commité ni poussé : l'utilisateur commite et pousse lui-même. Ne jamais lancer git commit, git push ni git add.
  • Une seule stack, pas de dépendance en double (AGENTS.md).
  • Tout Markdown a un jumeau JSON typé, testé dans les deux sens.
  • Un seul système de types : défini une fois dans @recit/core, dérivé des schémas zod.
  • Les sources de récits (examples/**/*.md|mdx, sites/*/stories/**) ne passent jamais par oxfmt.

2. Avancement

PhaseÉtatDétail
P0 Fondations et nettoyage✅fork réduit au cœur, CI et release Récit, brand/, skills, SETUP.md, CONTRIBUTING.md
P1 Import du deck✅@recit/core, theme, components, deck, cli, @recit/site, recit ; exemples deck-*, site-embed ; e2e Playwright avec captures
P2 Sites✅sites/{www,docs,presentation,components} sur @recit/site-kit ; correctifs de packages et 3 patchs du framework de sites (voir UPSTREAM.md)
P3 Story, config, JSON, i18n✅1 841 tests, 139 e2e deux fois ; restent mineurs : packs d'interface par langue, sélecteur de langue plein écran, RTL complet, bandeau « non traduit »
P4-V1 Import d'emaki✅ rapatriédépôt réuni vérifié : pnpm check (règle des extensions .js désormais appliquée), build et typecheck de 23 projets, 1 940 tests (24/10, après le renommage publish:), 16 goldens, 139 e2e deux fois
Plans du backlog (10 à 14)✅ écrits et intégrésformulaires, images, glossaire et annuaire, cartes, IA et scénario ; phases P13 à P16 et lots en P6 à P11 dans 06
Plans 10 à 15 et cohérence✅ 24/10retouches de 02, 09, NOTICE.md ; P4 reporté dans 03, 00, 05 ; section du site renommée publish: dans le code (sans alias, erreur explicite) ; pnpm check, 23 projets, 1 940 tests, 139 e2e
P4 V2 à V7✅ 25/10chargeur unique, Markdown motion et jumeau JSON typé, CLI fusionné (render, dur, extract, schema, mcp serve, dev), sites/motion, StoryEmbed format=motion, MCP (describe_format, guides), thèmes nommés partagés ; correction finale : pnpm check, 37 tâches build/typecheck, 2 182 tests, 32 goldens, 158 e2e deux fois. Restent : réécriture chirurgicale v2 (V5), studio, CI MP4 et Chrome, Vercel recit-motion, site npm en double (voir §4)
Audit✅ 25/10audit.md : 163 constats (3 critiques, 67 majeurs, 93 mineurs), plan d'action en 20 lots ; lot 1 (verrou de release : ST-1, ST-2, ST-10, ST-19, BP-15, DO-19) fait le 25/10 : release manuelle depuis main, alpha par défaut, build.yaml rejoué, dry run, environnement npm (relecteurs à cocher par le mainteneur, SETUP.md §3), contrôle des version plans dans pnpm check (dont preminor obligatoire au début d'une phase), plans passés en preminor, release.projects réduit aux 11 packages publiés, actions épinglées par SHA et npm 11.21.1 exact, Dependabot sans packages/site ni auto-approbation (dependabot.yaml et .github/release.yaml supprimés), test scripts/__tests__/release-workflow.spec.ts (commandes passées à nx, SHA, liste des packages) ; corrigés au passage : nx release refusait la config git de nx.json, nx décalait les bumps en 0.x, --yes et --skip-publish sont incompatibles dans nx 23 ; reste à faire sur GitHub : relecteurs de l'environnement, premier run réel (le dry run local s'arrête à la GitHub Release, faute de remote github.com)
Nettoyage de la racine✅ 25/10supprimés : patches/, llm/, graphql.config.yml, .nxignore, .cspell.json, .vscode/launch.json (ajoutés aux chemins supprimés d'UPSTREAM.md) ; .tool-versions aligné sur pnpm 11.6.0 ; BACKLOG_IDEA.md déplacé dans sites/plans/pages/archive/ (chemins corrigés dans 06, 11, 14) ; pets.md exclu d'oxfmt
Broc, couleurs, SETUP✅ 25/10workflow wf_a2e51a50-b24 : pets.md intégré aux plans 11, 14, 06 (hatch-pet, Broc), planches de couleurs Q07 dans brand/boards/colors/ (10 teintes, générateurs par défaut identiques octet par octet), SETUP.md réécrit ; vérifié par un relecteur indépendant
Reprise sur machine Linux✅ 25/10 (soir)dépôt repris depuis origin/recit/setup sur une machine Linux (la précédente était un Mac) ; pnpm check remis au vert (mise en forme de 8 fichiers, scripts/e2e-linux.mjs renommé en .js pour check:imports) ; 37 tâches build/typecheck, 2 219 tests ; lot 2 de l'audit terminé (voir §6) ; décisions du 25/10 reportées dans 01, 06, 07, 11, 14, context.md, NOTICE.md, pets.md et sites/plans/pages/annexes/broc/ ; goldens rouges pour cause de polices système, voir §6

3. Reprendre un workflow interrompu

Les workflows s'arrêtent quand la session Claude Code se ferme. Pour reprendre au même point (les étapes finies ne sont pas refaites) :

Code
Workflow({ scriptPath: "<script>", resumeFromRunId: "<run id>" })
RunScript
wf_9981dbe3-61f~/.claude/projects/-Users-vguerand-Work-presentation-recit/7e3cb9c1-144a-474f-b065-4e64c876acf6/workflows/scripts/recit-p3-story-wf_9981dbe3-61f.js
wf_17230d86-10e…/workflows/scripts/recit-p4-v1-import-emaki-wf_17230d86-10e.js
wf_97154b86-d4a…/workflows/scripts/recit-backlog-plans-wf_97154b86-d4a.js
wf_c5726349-758…/workflows/scripts/recit-plans-integration-wf_c5726349-758.js
wf_256acca2-bbf…/workflows/scripts/recit-plans-final-consistency-wf_256acca2-bbf.js
wf_361552bf-7dc…/workflows/scripts/recit-p4-motion-wf_361552bf-7dc.js
wf_5542962e-d0b…/workflows/scripts/recit-audit-wf_5542962e-d0b.js (audit en lecture seule, écrit audit.md)
wf_a2e51a50-b24…/workflows/scripts/recit-broc-brand-setup-wf_a2e51a50-b24.js
wf_ef5aeaad-5ab…/workflows/scripts/recit-audit-lots-wf_ef5aeaad-5ab.js (20 lots de l'audit)
wf_d6a804b5-88e…/workflows/scripts/recit-broc-research-wf_d6a804b5-88e.js (recherche atelier de personnages, bible et 5 scénarios de Broc, décisions du 25/10)

Une reprise ne fonctionne que dans la même session. Depuis une nouvelle session, relire ce journal, regarder l'état réel (git status, pnpm check, pnpm test) et relancer la suite à la main.

Historique (rapatrié le 24/10, voir §4) — résultat de P4-V1, sauvegardé hors de /tmp : /Users/vguerand/Work/presentation/recit-p4-staging (copie complète du dépôt avec l'import d'emaki, sans node_modules). Tout y est vert : 97 tests d'emaki sur 97, 16 goldens, build, typecheck et lint des 4 packages. À rapatrier dans le dépôt après la fin de P3 : packages/formats/{motion,motion-blocks,motion-render}, packages/mcp, la suppression de packages/tools/extract, .nx/version-plans/version-plan-* de motion, puis réconcilier à la main pnpm-workspace.yaml, NOTICE.md et sites/plans/pages/03-packaging.md, et régénérer le lockfile avec pnpm install. Le CLI d'emaki, mis de côté pour la fusion avec @recit/cli, est dans scratch-emaki-cli/index.ts. Restent ouverts : une seconde version de zod (4.4.3) imposée par Remotion dans l'arbre de motion-render, et des goldens qui dépendent des polices de la machine. À vérifier ensuite dans tout le dépôt : biome n'applique pas useImportExtensions lors d'un simple biome lint, il faut lancer pnpm biome lint --only=correctness/useImportExtensions.

4. Suite prévue, dans l'ordre

  1. Rapatrier P4-V1 et tout revérifier. Fait le 24/10.
  2. Intégrer les plans 10 à 15 en une seule passe et appliquer les décisions du §5. Fait le 24/10.
  3. P4 V2 à V7. Fait le 25/10 (voir sites/plans/pages/05-video.md). Ouverts : la copie npm de framework@0.93.0 (et un zod 4.4.3 de plus) tirée par le pair optionnel de @recit/site-stories ; relier le fork casse son typecheck, à traiter avec TE-3. MotionTheme reste écrit à la main dans @recit/theme (sans zod), gardé par un test de types contre ThemeContract. 3b. Après P4 : appliquer les lots de audit.md §6 (workflow), en commençant par TE-1 (captures Linux), CG-1 (renommer le modèle vidéo « deck » d'emaki), BP-1 à BP-4 (sécurité), ST-5/ST-6 (frontières de packages), TE-3 (typecheck des sites).
  4. P5 : alpha (release.projects déjà rempli dans nx.json, publint, size-limit, create-recit). Aucune publication npm.
  5. P6 et suivants : report et paged (avec PDF0 et PDF1), toon, book, roadmap, calendar, stories, puis les formats des plans 10 à 14.

5. Décisions prises sans l'utilisateur (à confirmer)

L'utilisateur a demandé de trancher seul les questions ouvertes. Chaque décision suit la recommandation du plan concerné et peut être revue.

SujetDécision
Webtoon en PDFpages de hauteur fixe coupées entre les cases ; une case trop haute a sa propre page ; CBZ hors périmètre
Table des matières du reportsans numéros en P6 (Chromium), numérotée en P8 avec Paged.js
Lancement de Chromiumsous-export @recit/paged/runner, pas de nouveau package pour l'instant
Réglages des formatsune seule convention : à plat, settings = section du format ; tailles en valeurs littérales (A4, 6x9in)
PDF de storiesnon : stories produit des images et des vidéos
PDF des pages de docoptionnel, après P8
PDF baliséactivé par défaut, désactivable ; un alt manquant donne un avertissement
Studio d'emakinon importé tant qu'il est simulé ; le squelette packages/tools/studio reste
extract d'emakifusionné dans @recit/motion
Plans 10 à 14 (produit)tranchées, voir 06 « Questions ouvertes » et le §8 de chaque plan
Section du site dans recit.configrenommée stories: → publish: ; stories: est réservé aux réglages du format stories
@recit/pagedoutil Node utilisé par la CLI seule ; les formats et fondations n'exposent que des vues imprimables
Mot kindréservé au discriminant des documents JSON ; l'annuaire utilise entries, la carte mode
Manifeste exports.jsontypes ajoutés : webp, gif, svg, jsonld, fountain, skill ; format facultatif, source block ou tool
Plan 15accepté (questions 1 à 6), questions 7 à 9 ouvertes
Règle des extensions .jsappliquée par pnpm check (check:imports), coupée pour sites/** et site-kit (imports d'assets)
Releasemanuelle uniquement (workflow_dispatch sur main), alpha par défaut, version stable refusée en 0.x ; job d'écriture derrière l'environnement npm ; en 0.x, version plans en preminor (nouvelle phase) ou prerelease, jamais patch/minor/major (scripts/check-version-plans.js) ; release.projects liste les packages publiés un par un ; actions épinglées par SHA ; PR Dependabot relues à la main
Modèle vidéo d'emaki (CG-1)renommé Film (film.json) pour libérer « deck » au seul format diaporama — à faire après P4
« Pet » et skill du brocolirépondu par l'utilisateur le 25/10 (pets.md) : skill hatch-pet d'OpenAI (Apache-2.0, vérifié), avec imagegen ; outils externes, jamais une dépendance
Kit de Broc (plan 14 D12)character.md + jumeau character.json (bible à une entrée, BibleSource) ; champ sheet typé par SpriteSheet dans @recit/core/schema ; image de référence = source de vérité ; layout codex-pet-v1 (9 lignes), v2 à 11 lignes refusé sans spec publique
Import d'une planche hatch-petétape manuelle documentée (copie, recit assets register, ::sheet, recit validate) ; recit import pet seulement sur besoin répété
Rendu des poses (plan 11 D9, IM4)bloc sprite (fonction pure de la frame, décalage CSS) et SpriteImage statique ; pas de Broc en pixel art ; IM4 débloqué, en P16
Couleur de la marque (25/10)une teinte par format, elle remplace la teinte unique du 24/10 : recit.dev en noir et blanc, graphite par défaut (docs, components, outils, @recit/theme), deck bleu, motion violet, report sarcelle, toon orange, book ambre, roadmap vert, calendar rouge, stories magenta, atlas indigo. Valeurs dans brand/boards/colors/palette.json ; mise en œuvre après les lots de l'audit (01 §1, 07 §4)
Apparence de Broc (25/10)l'image fournie fait foi : brocoli vert souriant, 3D doux, T-pose, sans smoking ni casquette. pets.md, 11 §2.4 et 14 D12 corrigés d'après elle (note datée en tête de pets.md, qui reste une trace d'échange)
Licence de l'image de Broc (25/10)© vguerand, tous droits réservés, hors licence MIT : ligne ajoutée à NOTICE.md (le fichier était déjà commité sans elle) ; la planche dérivée hérite de la restriction (14 D13)
Police de titre (25/10)Unbounded (NT Type, SIL OFL 1.1) remplace Monument Extended, exclue car non libre en usage commercial
lebrocoli.fr (25/10)seul nom réel autorisé du dépôt, lieux fictifs
Voix et scénarios de Broc (25/10)délégués à l'assistant : bible et 5 scénarios dans sites/plans/pages/annexes/broc/, tout ce qui n'est pas visible sur l'image reste inferred jusqu'à relecture

Questions qui restent à l'utilisateur (faits que lui seul connaît). Toutes celles du 24/10 ont été répondues le 25/10 : « Pet » et le skill du brocoli (hatch-pet), le nom et la description du personnage (Broc), son image de référence et sa licence, la police de titre (Unbounded), le « projet basic » (atelier de personnages 2D/pixel, sites/plans/pages/16), lebrocoli.fr comme seul exemple réel avec des lieux fictifs, et maps.recit.dev avec cartes.recit.dev en alias. Il reste une relecture, pas une question : valider les traits inferred de Broc (voix, personnalité, palette, personnages secondaires) dans sites/plans/pages/annexes/broc/.

6. Reprise du 25/10 au soir, sur une machine Linux

La session précédente travaillait sur un Mac. Le dépôt a été repris depuis origin/recit/setup (commit de sauvegarde 770a2775) sur une machine Linux x86_64, Node 22.20 (.tool-versions demande 22.22, simple avertissement de pnpm), Docker vide. Ce changement de machine explique deux des trois rouges rencontrés.

Vérification complète

ÉtapeRésultat
pnpm install✅
pnpm check✅ après deux corrections (ci-dessous)
nx run-many -t build typecheck✅ 37 tâches
pnpm test✅ 199 fichiers, 2 219 tests
motion-render goldens:check❌ polices système, voir ci-dessous
pnpm test:e2e:linux (conteneur)✅ après le lot 2 terminé

Deux corrections pour remettre pnpm check au vert, toutes deux héritées du lot 2 interrompu :

  • 8 fichiers jamais passés à oxfmt (le skill e2e-baselines, les six annexes de Broc) : mise en forme seule, aucun contenu touché ;
  • check:imports refusait import … from "../e2e-linux.mjs" : le script est renommé scripts/e2e-linux.js (la racine est type: module, .mjs n'apportait rien) et ses références suivent. scripts/check-admonitions.mjs garde son extension : il vient d'upstream.

Goldens : différence de machine, pas une régression

goldens:check échoue sur 14 des 32 images, de 0,27 % à 1,63 % de pixels (seuil 0,2 %). Les rendus JSON et Markdown donnent exactement le même écart, donc le jumeau typé n'est pas en cause. La raison est dans motion-blocks/src/themes/build.ts : les thèmes utilisent des piles de polices système (system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif). Sur le Mac elles résolvaient vers SF Pro, ici vers DejaVu / Liberation / Noto — le texte ne tombe pas sur les mêmes pixels. Les goldens du dépôt sont des références macOS ; elles n'ont pas été régénérées ici, ce qui remplacerait des références Mac par des références Linux sans rien prouver.

C'est exactement le lot 12 de l'audit (TE-5 « goldens en conteneur », BP-14 « polices embarquées ») : tant qu'il n'est pas fait, goldens:check ne peut pas être vert sur deux machines différentes. À traiter avec le lot 12, en réutilisant le harnais Docker du lot 2.

Lot 2 de l'audit (captures e2e Linux) — terminé

Le lot était déjà écrit pour l'essentiel (playwright.config.ts, build.yaml, les scripts, le skill). Restaient trois défauts, trouvés en l'exécutant pour de bon :

  1. Le conteneur ne démarrait pas. La copie du dépôt n'a pas de .git (exclu du tar), or le script prepare de la racine est lefthook install, qui en exige un : exit status 128. Puis, une fois git init ajouté, git refusait un dépôt appartenant à un autre utilisateur (« dubious ownership »), le conteneur tournant en root. scripts/e2e-linux.sh déclare maintenant safe.directory et initialise un dépôt vide, avant pnpm install.

  2. Les références revenaient en root:root. Les 206 -linux.png écrits par le conteneur étaient illisibles en écriture pour l'utilisateur, alors qu'ils sont faits pour être relus et commités par lui. Le script les rend maintenant à l'utilisateur de /src (chown d'après stat -c '%u:%g' /src).

  3. Un test instable, et c'en était un vrai. /documentation/reference/architecture prenait sa capture pleine page avant que Mermaid n'arrive : 4 615 px sans le diagramme, 4 861 px avec (mesuré, pas supposé). Mermaid est un chunk paresseux, chargé après document.fonts.ready. e2e/support.ts a un mermaidReady() sur le modèle de filmsReady(), et les specs listent les pages concernées (DIAGRAM_PAGES dans site-docs, le cas mermaid dans site-components). Les deux formes du DOM sont couvertes : pre.mermaid svg (pipeline Markdown) et le SVG du composant MDX, reconnu par son aria-roledescription.

  4. Deux autres captures instables, du même genre, trouvées en relançant. Une page de doc qui embarque un deck était capturée pendant la transition d'entrée (/theme/aspect-ratio-and- transitions, environ 5 % de pixels d'écart) : decksReady() attend maintenant les decks embarqués, sans liste de pages à tenir à jour, puisqu'il ne fait rien quand la page n'en a pas. Et /formats/motion, page de 8 860 px sans contenu dynamique, oscillait de 2 pixels de hauteur d'une capture à l'autre ; sur l'hôte elle est parfaitement stable (8 860 px six fois de suite), l'écart n'apparaît que dans le conteneur sous charge parallèle, quand une ressource tardive arrive après document.fonts.ready. heightStable() attend que la hauteur ne bouge plus avant de capturer.

  5. Un dernier reste, sur /theme/dark-mode. La page embarque <StoryEmbed slug="course" slide={7} /> : le deck se monte sur la diapo 1 puis saute à la 7. L'attente plate de 700 ms de decksReady() était un pari sur le temps ; elle attend maintenant que les compteurs de diapo (.slide-footer-right) cessent de bouger, puis laisse 700 ms à la transition.

Les tests couvrant ces correctifs sont dans scripts/__tests__/e2e-linux.spec.ts (12 tests).

Vérification finale : pnpm test:e2e:linux lancé deux fois de suite, machine libre, les deux fois 158 tests passés, 0 échec, 0 instable (1,6 min et 1,5 min). C'est la double passe que demande le skill e2e-baselines. À noter pour la prochaine fois : ne rien lancer d'autre pendant une passe conteneur. Avec pnpm test en parallèle sur l'hôte, la passe tombe à 4,3 min et fait apparaître des échecs qui n'ont rien à voir (un page.goto qui expire au bout de 60 s). Les 206 références -linux sont générées et appartiennent à l'utilisateur ; CONTRIBUTING.md et le skill e2e-baselines disent maintenant qu'une capture instable est un temps d'attente manquant, pas une référence à réécrire.

Vu une fois, non reproduit : lors de la première génération, /demos/mdx a signalé une erreur React #418 (texte rendu côté serveur différent du client) dans la console, sur les deux essais. Ni la passe de comparaison suivante ni un essai sur l'hôte ne l'ont reproduite. À surveiller : si elle revient, c'est une vraie erreur d'hydratation, pas une instabilité de capture.

Lots de l'audit faits le 25/10 au soir

Trois analyses en parallèle (lots 3, 5, 6 et 7) ont précédé l'écriture. Elles ont corrigé l'audit sur plusieurs points, chiffre en main — c'est le principal apport de la méthode :

PointCe que disait l'auditCe que la mesure montre
BP-12 clobberPrefixrelever le préfixe et adapter slideAnchorle préfixe serait doublé (mdast-util-to-hast le pose déjà des deux côtés) et casserait tous les liens de notes ; slideAnchor est hors pipeline
BP-3 jeton de dev« jeton lisible »le jeton est servi en clair dans le HTML, donc les deux contrôles existants ne protégeaient rien
TY-11« utiliser BrandInputSchema »ce schéma n'existe pas, il faut l'écrire
ST-10release.projects à corrigerdéjà corrigé
MCPlignes 391-402, 522…lignes décalées d'environ +50

Lot 10 (couverture des récits). TE-2 était déjà fait pour l'essentiel : le glob existe. Ce qui manquait est le garde-fou — un test qui parcourt examples/ et sites/ et échoue sur tout récit hors du glob (mesuré : 29 récits couverts, 63 pages de doc hors périmètre, 0 oublié). TE-6 : le plugin Vite remonte enfin les diagnostics, et a immédiatement trouvé un diagramme Mermaid sans titre accessible dans deck-tech-talk. PL-1 : la règle du jumeau était écrite trois fois et se contredisait ; le code avait raison, AGENTS.md la dit maintenant une seule fois.

Lot 3 (sécurité du rendu). Le XSS était exploitable de bout en bout : :::component ou du HTML brut forgeant .slide-component → CodeBlock avec un langage inconnu → Shiki lève → le repli concaténait <pre><code>${code}</code></pre>. Repli en JSX, liste blanche des 11 data-* du pipeline à la place de data*, id/name restreints, parseProps filtré, props de confiance appliquées après l'étalement, et security.allowHtml devenu la seule source, à false par défaut. Aucun récit du dépôt n'utilisait de HTML brut.

Lot 5 (sécurité réseau et MCP). Les outils MCP lisaient et écrivaient n'importe où, et formatOf traite toute extension non-Markdown comme du JSON : un out non vérifié écrasait sa cible. Ils sont confinés à une racine (--root, RECIT_MCP_ROOT, défaut : le dossier courant). @recit/motion-render passe en pair optionnelle. Le serveur de dev exige un jeton sur toutes ses routes hors localhost, avec deux jetons séparés (écran / téléphone) et un cookie HttpOnly. /__assets ne sert plus que des médias.

Lot 6, début. @recit/site-stories était typé contre le @recit/site de npm au lieu du fork (TE-4). Le doublon a disparu du lockfile. Les 28 erreurs que cela révèle viennent toutes des sources du fork, compilées avec des options plus strictes que les siennes : tsconfig.site.json réaligne les trois qui divergent, et le package est à zéro erreur.

Non appliqué, et pourquoi. L'annexe ST-8 demandait platform: "node" pour motion-render : mesuré, tsdown émet alors .mjs/.d.mts alors que les exports pointent sur dist/index.js — plus aucun consommateur ne résout le package. La raison est écrite à côté du réglage.

Dette connue, non corrigée ici. Le diagramme Mermaid de examples/deck-tech-talk/slides.md:54 n'a pas de titre accessible. Le corriger affiche un <h4>, donc déplace 4 captures sur deux plateformes ; seules les -linux sont régénérables ici. À faire quand un Mac est disponible.

Doc : audit d'uniformité et vision.recit.dev (25/10 au soir)

Un audit d'uniformité a mesuré les 69 pages des sites plus la doc racine. Il a trouvé une contradiction qu'aucun lecteur ne pouvait démêler : le statut du format motion était déclaré de douze façons, dont trois incompatibles — « available » sur une page et, vingt-cinq lignes plus bas, sous le titre « Planned formats » ; « Planned » en bas des 15 pages de components ; « planned » dans le README ; « In progress » dans le sidebar. Et cinq vocabulaires pour trois états.

Corrigé à la racine : un manifeste FORMATS unique dans @recit/site-kit (id, nom, « une scène est… », statut, phase, site), lu par la grille de recit.dev, le tableau « In other formats » de components, et désormais par le site vision. Deux états publics seulement : Available et Planned · P<n> — « coming soon », « in progress », « shipped » et « specified » sont bannis. Un test e2e vérifie la cohérence.

Autres corrections grave :

  • README.md donnait npm create recit (le package est un export {}; vide, le nom n'est pas réservé) et recit export pdf (la commande n'existe pas) — c'était la première instruction lue par un visiteur de GitHub. Remplacé par la recette vérifiée de recit.dev ;
  • « emaki », nom de code abandonné, restait dans la référence CLI publique ;
  • showCopyPage: false sur l'accueil de la doc : clé inexistante, le bouton restait affiché. Plus important que la correction : scripts/check-frontmatter.mjs refuse désormais toute clé de front matter qu'aucun code ne lit, branché dans pnpm check (vérifié : il attrape bien la faute) ;
  • @recit/extract figurait dans la page Architecture comme package planifié — il n'existe pas ;
  • UPSTREAM.md aurait effacé le CHANGELOG de Récit à la première synchronisation après une release : il était classé dans les chemins supprimés d'upstream alors que nx release l'écrit. Passé en « Ours », et la commande checkout --ours complétée des cinq fichiers qu'elle oubliait plus l'étape « Moved ».

vision.recit.dev (sites/vision, port 3015, e2e 4315) : six pages — la thèse, le problème, les neuf formats et ce qui en mérite un dixième, les principes avec leur prix, l'ordre des phases, et ce que Récit refuse de faire, risques compris. Il ne documente rien (règle anti-doublon de 07) et n'annonce aucun format comme acquis. Il n'est pas dans le sélecteur d'en-tête, comme recit.dev ; on l'atteint par le pied de page.

63 captures -darwin sont désormais périmées : ajouter un site ajoute un lien au pied de page de tous les autres, et les corrections de doc changent trois pages. Les -linux sont régénérées et vertes ; les -darwin ne peuvent pas l'être ici. Sur un Mac : pnpm build && pnpm test:e2e:update. Elles sont laissées en place plutôt que supprimées, pour que l'écart échoue bruyamment au lieu d'être réécrit sans relecture.

Reste à faire, dans l'ordre

  1. Lots faits : 1, 2, 3, 5, 10, et le début du 6 (TE-4). Restent : 4 (vocabulaire motion), la suite du 6 (typecheck des sites et de e2e/), 7 (frontières de packages), 8, 9, 11 à 20.
  2. Lot 12 : polices embarquées et goldens en conteneur, qui débloque goldens:check.
  3. Relecture par l'utilisateur des traits inferred de Broc (sites/plans/pages/annexes/broc/).
  4. Mise en œuvre des couleurs par format (01 §1).
  5. P5 et la suite (06-roadmap.md).

Ordre conseillé par l'analyse, mesuré : le reste du lot 6 (les scripts typecheck ne doivent être commités qu'avec le tsconfig qui les rend verts, sinon la CI part avec 6 × 23 erreurs), puis 7.1 et 7.2 (tags nx et test d'arêtes, en tolérant les deux violations connues) avant les lots 11 et 13 qui déplaceront du code entre packages.

Lot 6 de l'audit (typecheck partout) — 26/10

TE-3 disait « les sites, site-kit, les exemples et e2e/ ne sont jamais typecheckés, alors que des erreurs existent ». Mesuré avant : 11 projets avaient une cible typecheck ; après : 27.

Les tsconfig des sites étaient des copies du gabarit du framework de sites, sans extends ni strict : six fichiers identiques qui n'héritaient de rien et incluaient des chemins absents (sidebar.ts dans www, presentation, motion et vision ; src dans presentation et vision). Ils étendent maintenant tsconfig.site.json — celui écrit pour TE-4, puisqu'un site compile les sources du fork — avec un include exact. Même chose pour site-kit et pour recit.

Les 23 erreurs de site-kit et les 28 de recit venaient toutes des sources du fork compilées plus strictement que lui : zéro après le changement d'extends, sans toucher une ligne de code. Les 4 de www et les 28 de components étaient un @types/react manquant.

recit est un package écrit à la main, sans build (ST-13) : ses .js et .d.ts sont désormais vérifiés tels qu'ils sont publiés (allowJs, checkJs), et ses exports passent en { types, default }.

e2e/ devient un projet du workspace (recit-e2e), ce qui lui donne sa cible typecheck et fait voir à nx son arête vers @recit/cli — elle existait déjà dans le code, non déclarée. Son tsconfig couvre aussi playwright.config.ts, qui vit à la racine mais appartient à cette suite.

Deux vrais défauts, trouvés par le typecheck lui-même.

  1. e2e/support.ts réimportait serveStatic depuis packages/cli/dist/commands/export-pdf.js, un chemin interne que les types publiés ne déclarent pas (tsdown ne sort dans le .d.ts que ce qui est atteignable depuis l'entrée). La suite marchait parce que le JS, lui, l'exporte. serveStatic est maintenant exporté par l'entrée publique de @recit/cli, et e2e passe par elle comme pour buildProject, exportPdf et le reste.
  2. e2e/site-i18n.spec.ts destructurait un string[][] : href était string | undefined, donc l'appel toHaveAttribute(name, href) choisissait silencieusement la surcharge toHaveAttribute(name, options) — l'attribut n'était pas comparé à la valeur attendue. Le tableau est passé en as const.

Non fait, et pourquoi. Les 11 packages planifiés (create-recit, data, time, les six formats non commencés, studio) n'ont qu'un export {}; : une cible typecheck y ajouterait onze dépendances et onze tâches de CI qui ne peuvent rien attraper. À poser en même temps que leur premier vrai fichier.

Vérifié : pnpm check vert, nx run-many -t build typecheck 54 tâches, 54 vertes (contre 37 avant).

Lot 7, étapes 1 et 2 (frontières de packages) — 26/10

ST-6 : aucun scope:* dans le dépôt, donc la règle de direction de sites/plans/pages/03 §8 n'était vérifiée par rien — c'est ainsi que ST-5 est passé. Et la liste des tags était écrite deux fois, en se contredisant : six dans sites/plans/pages/06, quatre dans sites/plans/pages/03 §8.

Une seule liste, neuf tags, dans sites/plans/pages/03 §8 : foundation, format, integration, tool, distribution, site, example, test, vendor. Posés dans les 41 package.json qui nous appartiennent. Les deux projets venus d'upstream (packages/site, examples/with-config) ne sont pas tagués : les taguer ajouterait un patch à reporter à chaque synchronisation, pour rien. Le script les classe vendor par leur nom, et c'est écrit à côté de la liste.

scripts/check-package-boundaries.js lit les tags et les dépendances workspace:*, et refuse toute arête hors du tableau. Deux choix qui comptent :

  • la liste des violations tolérées porte, pour chacune, le constat qui la possède ;
  • une entrée tolérée qui ne correspond à aucune arête échoue aussi. Sans cela, la liste survit à sa raison d'être et la règle s'affaiblit en silence.

Vérifié pour de vrai : une dépendance @recit/core → @recit/deck ajoutée à la main fait bien échouer pnpm check, avec le nom des deux scopes. Dix tests dans scripts/__tests__/check-package-boundaries.spec.ts, dont trois qui mesurent le dépôt lui-même.

Une seule violation aujourd'hui : @recit/site-stories → @recit/cli (ST-5), qui attend le déplacement de la lecture des fichiers dans @recit/core/node, après P4.

ST-20 réglé pour la partie déclarative. Trois sites importaient ../../examples/deck-mdx/components et deck-showcase/components, et site-i18n importait les composants et le récit de deck-showcase, sans qu'aucun package.json ne le déclare : nx ne voyait pas l'arête, et un build affecté pouvait sauter un site dont un exemple venait de changer. Les quatre dépendances sont déclarées. Reste de ST-20 : donner à site-i18n ses propres récits, ou le retirer de la liste degit (DO-2).

Une instabilité e2e réelle, pas une régression des lots 6 et 7 — 26/10

Les passes e2e de la nuit du 26/10 ont montré, par intermittence, React #418 (« le texte rendu par le serveur ne correspond pas à celui du client ») sur les deux pages qui embarquent un deck MDX : /demos/mdx de presentation.recit.dev et /documentation/writing/mdx de docs.recit.dev. Le test échoue par expect(errors).toEqual([]) : la page s'affiche juste, la capture passe, c'est l'erreur de console qui est refusée.

Ce qui a été mesuré, et écarté :

HypothèseMesure
Régression des lots 6 ou 7aucun code de rendu n'y change ; la passe du lot 6 était à 166/166
examples/*/dist présent pendant le build du sitereconstruit avec et sans : 6/6 vert dans les deux cas
Build non déterministedeux builds comparés fichier à fichier : seuls diffèrent les horodatages dehydratedAt
Ordre de build changé par les dépendances déclaréesnx run presentation:build puis 3/3 vert
Charge de la machineconfirmé, c'est la cause (voir ci-dessous)

La mesure qui tranche. La même passe conteneur, relancée sur le commit du lot 6 — celui qui était à 166/166 — est rouge elle aussi : 165 passés, un échec, mais sur un autre test (site-embed, un timeout de page.goto), et en 4,1 min au lieu de 2,0. Le code n'y est donc pour rien : c'est le temps. Une autre suite Playwright tournait en parallèle sur la machine. Le test qui tombe change à chaque passe : site-i18n, puis les deux pages MDX à la fois, puis site-embed. Isolé, chacun passe : 16 fois de suite pour /demos/mdx, et jusqu'à 6× de bridage CPU sans reproduire.

L'erreur est donc réelle mais masquée par le temps : l'hydratation du deck arrive après l'assertion quand la machine est libre, avant quand elle est chargée. Ce n'est pas un défaut du test : c'est la page qui hydrate avec un texte différent de celui du serveur. À instruire comme un constat à part entière — la CI est un conteneur chargé, elle verra l'erreur.

Piste suivante : comparer le DOM prérendu et le premier rendu client sur /demos/mdx avec un React non minifié (le message minifié ne donne pas le texte fautif). Le deck MDX contient une expression (export const year = 2026, puis {year}) et un composant à état (<Counter />) ; les deux sont les candidats à vérifier d'abord.

Lot 4 de l'audit (vocabulaire motion) — 26/10

CG-1 était le seul constat critique : « deck » désignait deux choses incompatibles — le diaporama, et le modèle de rendu vidéo. Le modèle de rendu s'appelle maintenant film, partout et d'un seul tenant :

AvantAprès
DeckSchema, Deck, SceneFilmSchema, Film, FilmScene
parseDeck, parseDeckSourceparseFilm, parseFilmSource
readDeckFile, writeDeckFilereadFilmFile, writeFilmFile
storyToDeck, deckToMarkdownstoryToFilm, filmToMarkdown
DeckFormatSourceSyntax (il nommait la syntaxe, pas un deck)
*.deck.json, --to deck*.film.json, --to film
build_deck, validate_deck (MCP)build_film, validate_film
recit://schema/motionrecit://schema/motion-film

Un piège trouvé en chemin. deckDuration (le temps de lecture déduit du nombre de mots de la voix off) et le filmDuration de motion-blocks (la durée réelle, en images) seraient devenus le même nom. Ils mesurent deux choses : le premier s'appelle désormais readingDuration. Le renommage aveugle aurait fait un doublon silencieux — c'est le typecheck qui l'a dit.

Ce qui n'a pas été renommé, et pourquoi. recit init --format deck, describe_format deck, LoadedDeck, DeckSource, l'option decks: et l'attribut data-deck parlent du diaporama : ils restent. (data-deck deviendra data-story au lot 15, CG-3.)

CG-2 et TY-9, la phrase qui manquait. La ressource MCP disait « le schéma du deck auquel toute story se compile », ce qui désignait le modèle de rendu comme s'il était le jumeau. Elle dit maintenant que le jumeau typé est la source kind: "story", et que le film.json est un artefact de build. La même phrase est dans AGENTS.md, le README de motion et formats/motion.mdx.

DO-22 : « emaki » ne reste que dans les lignes d'attribution.

Vérifié : pnpm check, build et typecheck de 41 projets, suite unitaire complète.

Lot 8 de l'audit (un seul système de types), première moitié — 26/10

La règle d'AGENTS.md dit « dériver les types TS des schémas zod ». Cinq endroits faisaient l'inverse, ou les deux à la fois.

TY-4 et TY-1 : les listes écrites deux fois. SlideLayout était une union écrite à la main, puis SLIDE_LAYOUTS recopiait les dix mêmes chaînes, et le zod dérivait du tableau. Idem pour SlideTransition et StoryFormat. Les ratios étaient pires : AspectRatioSchema dans config/validate.ts, SLIDE_ASPECT_RATIOS dans parser/frontmatter.ts, et les dimensions 1280×720 écrites une fois de plus dans parser/aspect-ratio.ts. Tout cela vit maintenant une seule fois, dans types/index.ts : un z.enum, puis z.infer pour le type et .options pour le tableau. SLIDE_DIMENSIONS donne les pixels, et aspect-ratio.ts les lit au lieu de les réécrire.

TY-3 : le front matter était casté. footer: et rules: passaient par un as : en Markdown, rules: { maxWordCount: "abc" } passait sans un mot, alors que le même contenu en JSON était refusé. Les deux clés sont maintenant lues par FooterConfigSchema et RulesConfigSchema, celles du jumeau ; une valeur refusée est ignorée et remonte en diagnostic frontmatter-invalid. Trois tests le prouvent, dont celui de l'audit mot pour mot.

TY-2 : les attributs de séparateur vérifiés à la main. split-slides.ts contrôlait layout, transition, cols et step à la main, avec des casts, et n'émettait que des warnings ; le chemin JSON, lui, utilisait zod et levait des erreurs. Il y a maintenant un SlideMetadataSchema (looseObject) dont SlideMetadata dérive, utilisé par les deux. Les messages d'avertissement sont inchangés, au mot près, et step — que l'ancien type ne déclarait pas — est enfin validé.

TY-8 : deux ThemeMode. Celui de @recit/deck valait "light" | "dark", celui de core ajoutait "system" : deux types, même nom, exportés tous les deux. Celui du deck s'appelle ResolvedThemeMode — le mode réellement appliqué au DOM — et le deck importe ThemeMode et SlideTransition de core. Le cast de Presentation.tsx disparaît avec eux.

TY-7 : isLocaleTag existait deux fois, avec deux définitions de « valide » (length === 1 d'un côté, [0] !== undefined de l'autre). Une seule reste, dans i18n/locales.ts, et I18nOptions dérive désormais de I18nConfigSchema au lieu de le recopier.

Reste du lot 8 : TY-5 (six formes de diagnostic), TY-6 (types publics manquants), TY-12 à TY-17, TE-13.

Lot 9 de l'audit (forme des packages publiés) — 26/10

ST-3, le piège le plus coûteux. Les onze packages publiés exposaient une condition d'export "development" pointant vers src/*.ts, et ils publient src. Or webpack et Next activent development par défaut, et ne transpilent pas le TypeScript de node_modules : un utilisateur de Next aurait reçu nos sources et un build cassé, sans rien avoir demandé. La condition s'appelle désormais @recit/source — un nom qui n'appartient qu'à nous, donc jamais activé par accident. vite.config.mjs la demande explicitement, et sites/plans/pages/03 §8 comme le skill recit-import le disent.

ST-4 : aucun package n'avait de publishConfig, donc un package scopé aurait été publié en restricted — invisible. Tous ont maintenant access: public, engines, author, un LICENSE.md et un README.md portant la mention verbatim qu'exige la règle 6 de NOTICE.md. Les packages issus d'emaki portent aussi le copyright de Yathavan Prabhakar. nx release publish passe --access public en plus.

ST-11 : files: ["src"] embarquait src/**/__tests__ et ses fixtures dans les tarballs. Exclu, avec les *.spec.* et *.test.*.

ST-12 : sideEffects: ["**/*.css"] était recopié dans core, components, deck et cli, qui n'ont aucun CSS — le bundler gardait du code mort par précaution. Passés à false.

Le garde-fou : scripts/__tests__/published-packages.spec.ts vérifie les onze packages sur six points (public, engines, author, MIT ; dépôt et bugs pointant sur Récit ; README avec la mention et LICENSE ; pas de condition development ; pas de tests dans files ; sideEffects CSS seulement avec du CSS). 66 assertions, qui échouent dès qu'un package naît sans sa forme.

ST-7 n'est pas réglé : @recit/site-stories dépend toujours d'un patch du fork non publié. C'est une décision à prendre avant P5 (faire accepter le patch en amont, publier @recit/site-stories-core, ou générer un recit.config.ts) ; la limite est maintenant écrite dans son README.

Lots 17 et 14 de l'audit (synchronisation, code mort) — 26/10

Lot 17 — la synchronisation amont. DO-3 et DO-4 avaient été réglés le 25/10 (CHANGELOG en « Ours », commande checkout --ours complétée). Restaient trois choses :

  • SK-10, la plus dangereuse : le remote upstream acceptait les push — vers le dépôt d'origine. Une faute de frappe suffisait à y pousser Récit. UPSTREAM.md fait maintenant poser git remote set-url --push upstream DISABLED dès l'ajout du remote.
  • SK-2 : la règle « jamais de commit ni de push sans demande » n'existait que dans la tête de Claude ; UPSTREAM.md faisait même commiter à l'étape 7. La règle est dans AGENTS.md, et l'étape 7 est marquée HUMAN ONLY.
  • SK-7 : le skill et UPSTREAM.md lançaient deux listes de vérifications différentes. Le skill renvoie maintenant à l'étape 6, la seule.
  • DO-20 : la description du patch 2 nommait build.dependsOn alors que le diff porte sur build:ci.dependsOn, et la cible copy:docs copiait ../../docs/pages/docs/, un dossier déplacé il y a longtemps. La description est corrigée et la cible supprimée.

Lot 14 — le code mort. Sept fichiers de @recit/deck n'étaient atteints que par src/index.ts — donc exportés publiquement, et jamais montés : le service worker PWA (qu'aucune commande n'écrit), useOfflineStatus, SlideEmbed (avec un 1280×720 en dur et une prop jamais lue), useAutoPlay (un loop jamais lu), AnimatedProgress (dont la couleur par défaut var(--primary) est un triplet HSL brut, donc invalide), et la paire AudioRehearsalPlayer/useAudioRehearsal qui ne s'utilisait qu'elle-même. Supprimés, avec leurs trois tests et les trois clés i18n qui n'existaient que pour eux.

CM-3, le plus gênant : unified-pipeline.ts portait un second pipeline Markdown, sans sanitisation ni plugins de slide, et pipeline.spec.ts le testait — c'est-à-dire qu'il testait du code que le produit n'exécute jamais, pendant que le vrai pipeline (compile-story.ts) n'était pas couvert par ces cas. Le second pipeline est supprimé ; le spec appelle compileStory. Il a fallu préchauffer la coloration syntaxique : le premier appel dépasse les 5 s par défaut.

Lot 15 de l'audit (vocabulaire et configuration), première moitié — 26/10

CG-3 : la section stories: avait été renommée publish: parce qu'elle publie des récits, pas des diaporamas — mais ses clés disaient encore « deck ». publish.decks devient publish.entries, publish.loadDeckConfig devient publish.loadStoryConfig, et l'API interne suit : DeckSource → StorySource, collectDeckSources → collectStorySources, compileDeckSources → compileStorySources.

Les deux anciennes clés ne sont pas simplement retirées : elles sont refusées par leur nom, avec la phrase qui dit où aller (z.never + message), comme le fait déjà stories:. Un fichier de config qui les utilise ne se tait pas, il explique. Deux tests le vérifient.

CG-10 : la CLI utilise déjà -o, --output partout ; seule une ligne d'aide parlait encore de deck.json. Le regroupement des commandes motion (recit motion extract|dur|render) reste à faire.

Reste du lot 15 : la fusion de slides: dans deck: (CG-5), theme → colorMode pour le mode d'affichage (DO-6), data-deck → data-story, et le glossaire de CG-6. Ce sont des changements d'API visibles : ils méritent d'être faits d'un bloc, avec leurs messages de renommage, pas à la sauvette.

Lot 19 de l'audit (hygiène du dépôt) — 26/10

DB-18, le plus utile. Vingt-quatre dépendances partagées étaient épinglées à la main dans plusieurs manifestes : motion 12.43.0 écrit quatre fois, tailwindcss 4.3.3 quatre fois, shiki et yaml trois fois chacun… Seul zod passait par le catalogue pnpm, que le skill recit-import exige pourtant. Les vingt-quatre sont dans le catalogue ; neuf manifestes pointent dessus par catalog:. Les peers (react: >=19.2.7) restent tels quels : un peer est un contrat d'intervalle, pas une épingle.

SK-9 : il n'existait aucune commande locale équivalente à la CI. pnpm verify fait check + build + typecheck + test, et pnpm verify:e2e y ajoute les e2e. pnpm typecheck existe aussi.

CM-9 : build:clean lançait tsc --build --clean alors qu'il n'y a pas de tsconfig.json racine — il ne pouvait que casser ; reset l'appelait. reset est devenu nx reset, build:clean a disparu. check-admonitions.mjs ignorait sites/docs/pages/docs/markdown/admonitions.md, un chemin déplacé : il ignore désormais le vrai.

SK-5 : pnpm fix ne corrigeait pas les extensions d'import (la règle ne s'applique qu'avec --only), alors que pnpm check les refuse. On pouvait donc lancer fix puis voir check échouer sur ce que fix prétend corriger. fix:imports est ajouté et appelé par fix.

SK-6 et BP-20 : les commandes nx nues (introuvables sans pnpm exec) sont corrigées dans la doc, les skills et les cinq vercel.json.

CM-10 et DO-13 : .gitignore visait un exemple disparu ; sites/docs/lychee.toml s'annonçait encore le nom de la doc d'origine et excluait les URL du dépôt d'origine au lieu de celles de Récit. Corrigé, et les domaines *.recit.dev — pas encore en ligne — sont exclus explicitement, avec la raison.

Lot 16 de l'audit (documentation d'entrée) — 26/10

CG-7, le plus visible. AGENTS.md — que CLAUDE.md pointe, donc le premier fichier que lit tout agent — s'ouvrait sur « # le framework de sites Codebase Guide ». Il citait packages/create-site (supprimé), donnait pnpm biome ci au lieu de pnpm check, un mauvais chemin de plugins, et la section Récit commençait à la ligne 161 sur 185. Inversé : Récit d'abord, avec un bloc « Start here » (pnpm verify), un tableau de commandes, et le prérequis e2e que rien ne disait (construire avant de lancer, ports dans e2e/ports.ts). Le guide le framework de sites garde sa valeur pour packages/site : il est en fin de fichier, annoncé comme tel. Trois règles qui disaient « toujours » (SiteError, site/ui, site/icons) disent désormais où elles valent.

DO-1 et DO-2 : la doc proposait quatre installations incompatibles, dont pnpm add -D @recit/cli pour un package qui n'est pas publié. Un seul encadré, le même mot pour mot, dit que les packages arrivent avec l'alpha et donne la recette qui marche aujourd'hui ; la page des gabarits dit que degit ne marche pas encore hors du dépôt.

DO-8 : sites/plans/pages/03 interdisait la sous-commande export html puis annonçait export pdf, export images, export epub… Une seule règle, écrite une fois : export <file> avec --to <format>, jamais de sous-commande par format. Reporté dans les onze plans qui l'écrivaient autrement.

Reste du lot 16 : la réorganisation du sidebar de docs.recit.dev (DO-5) et la page Architecture générée depuis les package.json (DO-7).

Lots 18 et 20 de l'audit (plans, doc des sites) — 26/10

Lot 18 — les plans.

  • DO-9 : sites/plans/pages/00-cadrage.md s'annonçait « point de reprise » alors que context.md et AGENTS.md désignent sites/plans/pages/journal.md. Il porte maintenant un encadré « Historique » : c'est le cadrage du 24/10, avant la première ligne de code, et on ne le met plus à jour.
  • DO-15 : SETUP.md listait des étapes faites. Un encadré dit ce qui reste vraiment à faire à la main (§3.3 Vercel et DNS, §3.4 jeton npm) ; le reste décrit comment c'est branché.
  • DO-18 : la règle 3 de NOTICE.md exige une ligne par chemin, et il n'y en avait aucune pour sites/plans/pages/, context.md, SETUP.md, audit.md, .claude/skills/ ni .nx/version-plans/. Ajoutée. sites/* était attribué à Récit sans réserve, alors que sites/docs hérite de pages du framework de sites : la réserve est écrite.
  • DO-21 : les chemins de ma machine (/Users/vguerand/…, /private/tmp/claude-501/…) sont partis des plans et de context.md. Seul le journal les garde, parce qu'il raconte l'histoire.

Lot 20 — la doc des sites.

  • DB-17 : deux cartes de presentation.recit.dev pointaient sur la racine de docs au lieu de la page qu'elles décrivent — un lien faux, pas un style. Corrigées. GITHUB était déclaré deux fois : site-kit l'exporte, Hero.tsx l'importe.
  • TY-19 : Format.id était un string libre dans le manifeste partagé. C'est un StoryFormat de @recit/core : une faute de frappe dans un id de format ne compile plus.
  • DO-10 : les 14 pages de composants titraient « Props » ou « Attributes » selon l'humeur. Un seul titre : « Attributes and props ».
  • DO-17 : sh dans le dépôt, bash sur les sites. bash partout.

Reste du lot 20 : les 38 URL *.recit.dev écrites en dur dans les pages (un SiteLink est à écrire), le gabarit de page de composant, et le dédoublonnage docs ⇄ components (DB-14).

Lot 13, première pièce : size-limit et publint (TE-8) — 26/10

Les deux outils étaient installés depuis le début et n'avaient jamais tourné : publint ne servait qu'au prepublishOnly d'un package privé, et size-limit n'avait ni configuration ni script. C'est pourtant publint qui aurait dit, avant npm, qu'une carte exports est cassée.

  • pnpm publint (scripts/publint-all.mjs) lit la liste release.projects de nx.json — pas une seconde liste — et vérifie les onze packages publiés. Tous passent aujourd'hui.
  • pnpm size mesure deux budgets qui comptent pour un lecteur : le runtime de @recit/deck (20,6 kB brotli, limite 26) et le entry.client d'un site avec un récit embarqué (105,2 kB, limite 120). Les limites sont serrées exprès : un budget qui ne peut pas être dépassé ne mesure rien. Premier réflexe à avoir : size-limit compresse en brotli, donc une limite écrite d'après la taille du fichier sur disque est fausse d'un facteur dix.
  • Les deux tournent en CI, après pnpm test.

Une vraie régression du lot 4, trouvée par les e2e — 26/10

La passe e2e finale a échoué sur motion-dev.spec.ts avec TypeError: Cannot read properties of undefined (reading 'scenes'). Ce n'était ni une capture ni un aléa : le serveur de dev motion sert son état sur /__deck.json, et cet état est le MotionFilmRead renommé au lot 4 — il porte film, plus deck. Le test lisait encore payload.deck.scenes. Aucun typecheck ne pouvait le voir : la réponse traverse HTTP en any.

Corrigé, et l'incohérence de nom avec : le serveur de dev motion sert désormais /__film.json (celui du deck garde /__deck.json — ce sont deux serveurs, deux formats). Le client, le test e2e et le test unitaire suivent.

Trois captures -linux régénérées, et trois seulement : les pages où j'ai ajouté un encadré (docs/index, quickstart, templates). 166 e2e verts.

Lot 12 de l'audit (polices embarquées, goldens) — 26/10

C'était le lot à faire en premier parmi les P2 : goldens:check était rouge hors macOS depuis le début, et personne ne pouvait savoir si un rendu avait changé.

BP-14 — la cause. Les thèmes nommaient « Space Grotesk », « Yeseva One », « Josefin Sans »… et rien ne les chargeait. Le rendu prenait ce que la machine avait. Les six polices (toutes SIL OFL 1.1) sont maintenant embarquées par @recit/theme/fonts.css, sous-ensembles latin et latin-ext seulement — l'interface est en anglais et en français, et tout embarquer mettait un méga-octet de polices dans chaque package. Un récit en grec ou en cyrillique s'affiche toujours, avec la police système du lecteur pour ces caractères ; c'est écrit dans le fichier.

Satoshi est retiré de la pile distribuée. Sa licence Fontshare interdit la redistribution : le nommer, c'était promettre un rendu qui ne marche que sur les machines qui l'ont déjà.

Trois pièges trouvés en chemin, tous parce que le script ne tournait jamais :

  1. goldens/goldens:check demandaient la condition --conditions development, que le lot 9 a renommée en @recit/source. Le script était cassé depuis ce commit et rien ne l'a dit.
  2. Il appelait encore readDeckFile, renderDeckStills et parsed.deck — le vocabulaire d'avant le lot 4.
  3. Le diff PNG était calculé puis jeté : on apprenait qu'une image avait changé de 2,8 %, jamais en quoi. Il est écrit dans fixtures/goldens-diff/ quand une prise échoue, avec l'image obtenue, et la CI l'archive.

Les URL des polices. styles.css est assemblé à la main, pas par un bundler : les url() des @fontsource pointaient dans node_modules/.pnpm/…, c'est-à-dire la machine de build et nulle part ailleurs. Les polices passent désormais par le même esbuild que KaTeX, qui écrit les woff2 dans dist/fonts/ et réécrit les URL. Seul le woff2 est conservé : le .woff hérité doublait le tarball pour des navigateurs que Récit ne vise pas.

Résultat : goldens:check passe, 32 rendus à 0,000 % (deux films × huit blocs × JSON et Markdown), et le job est en CI avec l'archivage du diff.

CM-7 : les sept jetons --sp-* (14 déclarations), les trois --slide-design-* et le sélecteur .slide-container de print.css n'étaient lus par personne. Supprimés ; sites/plans/pages/03 ne les présente plus comme une API.

Lot 13, accessibilité (BP-7, BP-8) — 26/10

BP-7 — le deck ignorait prefers-reduced-motion. La seule occurrence du réglage dans tout le dépôt était dans MotionPlayer. Le deck, lui, faisait glisser chaque diapositive de 32 px quoi qu'en dise le système du lecteur (WCAG 2.3.3). <MotionConfig reducedMotion="user"> enveloppe désormais tout le deck : Motion abandonne les transformations et garde l'opacité, donc un fondu court à la place d'un glissement, et les composants de diapositive suivent par le même contexte, sans prop à passer. Côté CSS, les étapes, la barre de progression et les contrôles tombent à 1 ms et l'étape masquée perd son translateY(8px).

BP-8 — role="application" était posé à l'envers. Il était mis sur le deck intégré dans une page, c'est-à-dire exactement là où il fait le plus de mal : il coupe le mode lecture du lecteur d'écran pour tout le sous-arbre, donc le texte des diapositives devenait inatteignable sur une page de doc. Il est maintenant réservé au plein écran, où le deck possède réellement le clavier ; intégré, le deck est une region. Par-dessus, le motif carrousel WAI-ARIA : aria-roledescription="carousel" sur le deck, la diapositive vivante en group nommé « 3 / 12 », et une région aria-live="polite" qui annonce « Slide 3 of 12 : » à chaque changement. L'annonce est hors de la transition : posée sur la diapositive elle-même, le lecteur aurait relu toute la diapositive au lieu d'une phrase.

Mesuré : 5 assertions dans presentation-a11y.spec.tsx (les deux rôles, le libellé du groupe, l'annonce en français et en anglais). Au passage, les commentaires français restés dans les trois CSS de @recit/theme sont traduits — la règle du dépôt est « code et docs en anglais ».

Lot 13, performance (BP-5) — 26/10

Le constat, mesuré. entry.client du site d'exemple site-embed pesait 105,63 ko brotli, et tout le runtime deck était dedans : plugin.tsx et mdx-components.tsx importaient StoryViewer statiquement, qui importait Presentation, qui importait motion au complet. Un lecteur qui ouvrait une page de doc sans la moindre histoire téléchargeait quand même Motion, les composants de diapositive, la console présentateur et l'encodeur QR.

Trois coupes.

  1. StoryStage : la scène (le Presentation et les deux rendus de diapositive) part dans un module à elle, chargé par React.lazy. StoryViewer ne garde de @recit/deck que des types.
  2. SlideMotion : un seul fournisseur <LazyMotion features={…} strict> pour tout ce qui rend une diapositive, et les composants passent de motion.* à m.*. Les 37 ko de features arrivent après le premier rendu. strict fait échouer un motion.* oublié : la règle de CLAUDE.md est tenue par le runtime, plus par la relecture.
  3. qrcode (~20 ko) : importé dynamiquement à l'ouverture de la fenêtre de télécommande, la seule chose qui s'en sert.

Un piège qui valait la mesure. m.* hors d'un LazyMotion ne plante pas : il rend l'état initial et s'arrête là. Avec fadeUp, l'état initial est opacity: 0 — le contenu serait resté invisible. Le fournisseur est donc posé aux trois endroits par lesquels passe toute diapositive (Presentation, HtmlSlide, MdxSlide), et il s'efface quand il est déjà là, pour ne pas empiler trois contextes identiques par diapositive.

Résultat : entry.client 105,63 → 74,80 ko brotli (−29 %), la scène de l'histoire à 6,93 ko, et plus une seule trace du runtime deck dans le chunk d'entrée (deck-viewport, slide-content, useSlideEnhancements : zéro occurrence). Les budgets .size-limit.json sont resserrés sur ces chiffres et un troisième budget garde la scène. entry-path.spec.ts nomme la cause plutôt que le symptôme : il échoue si StoryViewer réimporte une valeur de @recit/deck (vérifié en le cassant exprès).

Lot 13, fin : premier rendu, prémisse d'hydratation, axe (BP-10, BP-11, TE-7) — 26/10

BP-10 — une diapositive Markdown arrivait vide. HtmlSlide rendait un div vide et le remplissait dans un useLayoutEffect. La page prérendue ne portait donc aucun texte visible : rien sans JavaScript, et rien à peindre pour le navigateur avant que le bundle n'arrive (le LCP attendait). Le HTML est maintenant rendu par React. L'effet ne garde que l'extraction des :::component — et html reste dans ses dépendances, non parce qu'il le lit, mais parce qu'il signale que React a réécrit le balisage et que les nœuds de composants sont revenus.

Le piège, trouvé par les e2e en conteneur (6 échecs : étapes jamais révélées, aucun diagramme Mermaid dessiné, onglets de code morts). React 19 compare dangerouslySetInnerHTML par identité d'objet, pas par la chaîne __html. Écrit dangerouslySetInnerHTML={{ __html: html }}, c'est un objet neuf à chaque rendu, donc React réécrivait tout le sous-arbre à chaque rendu — et jetait au passage tout ce que les effets d'enrichissement y avaient posé : data-visible des étapes, SVG Mermaid rendus, onglet de code sélectionné. L'ancien code ne rencontrait pas le problème parce qu'il écrivait l'innerHTML lui-même, hors de la vue de React. L'objet est désormais mémoïsé sur [html] : React ne réécrit que quand le HTML change, c'est-à-dire exactement quand les effets se relancent aussi (content: html est leur dépendance).

Mesure : les 6 spécifications échouaient (deck-features, site-components ×2, site-docs, exports PDF et HTML autonome) ; elles passent après la mémoïsation, et la cause a été confirmée en revenant à l'ancien code puis en n'ajoutant que le useMemo.

BP-11 — un risque, pas un défaut, et c'est ce qu'il fallait vérifier. Presentation lit window.location.search et useThemeMode lit localStorage et matchMedia au premier rendu. En cherchant le chemin où cela casserait, il n'y en a aucun : le deck autonome est monté avec createRoot (jamais hydraté), et sur une page du framework de sites StoryViewer attend le client avant qu'un deck existe. Plutôt que de réécrire deux états initiaux sans défaut à corriger, la prémisse est tenue par un test : no-deck-on-the-server.spec.tsx échoue le jour où un deck est rendu côté serveur, et les commentaires des deux fichiers disent quoi changer à ce moment-là.

TE-7 — axe, et ce qu'il a trouvé du premier coup. Trois formes de page (une doc avec une histoire intégrée, une histoire seule, un deck plein écran), règles WCAG 2.1 A et AA.

  • Les pages d'histoire et de deck plein écran étaient prérendues sans <title>. Ce sont des routes ajoutées par le plugin : pas de front matter, donc pas de titre, donc titleTemplate de le framework de sites renvoyait une chaîne vide. Onglet, signet et résultat de recherche affichaient l'URL. storyHead émet désormais le titre de l'histoire, et le framework de sites le compose en « Starter | Presentation ».
  • Un défaut de contraste sur le CTRL+K du bouton de recherche (4,39:1 mesuré, 4,5:1 demandé). Il vient du chrome du framework de sites, pas du balisage Récit. Le header est exclu du scan, avec la mesure écrite dans le test : désactiver la règle color-contrast aurait masqué les nôtres.

Les trois pages passent. Ce n'est pas une assertion complaisante : elle a listé quatre violations avant correction.

Trois serveurs HTTP qui ne se fermaient jamais — 26/10

Trouvé en refusant de classer une instabilité e2e en « charge machine ». Le symptôme : un test différent à chaque passe (deck-features, motion-dev) expirait, puis passait en reprise en 3,4 secondes. Un test de 3,4 s qui bloque 3 minutes n'est pas lent, il est bloqué.

La démarche. Une passe de contrôle sur le commit déjà poussé : 166 passés, zéro instable. Donc c'était bien le travail en cours. Puis l'élimination : ce n'était pas les trois nouveaux tests axe (la suite sans eux reste instable au même endroit), ni le poids des pages prérendues (66 482 → 67 379 octets sur la galerie, templates.html identique à l'octet près), ni recit build (qui ne fait aucun rendu serveur, donc le changement de HtmlSlide ne peut pas l'atteindre).

La cause. Le contexte d'erreur de motion-dev montrait le texte exactement attendu par la dernière assertion. Le corps du test était donc fini : le blocage était dans le finally, sur dev.close(). server.close() de Node ne ferme rien : il refuse les nouvelles connexions et attend la fin des existantes. Un navigateur garde ses sockets keep-alive ouvertes tant que la page l'est — la promesse ne se résout jamais.

Trois serveurs avaient le défaut : commands/dev.ts, commands/motion-dev.ts et serveStatic (export-pdf.ts, exporté par la CLI et utilisé par l'export PDF). Chacun appelle désormais server.closeAllConnections() après server.close(). Ce n'est pas qu'un problème de test : recit dev pouvait ne jamais rendre la main à la fermeture tant qu'un onglet était ouvert.

Mesure : 169 passés, zéro instable, 1,7 min — contre 3,8 min et un test rouge à chaque passe avant. Les trois minutes perdues étaient les expirations elles-mêmes.

Au passage, un diagnostic intermédiaire a été retiré : j'avais sérialisé deck-features et motion-dev dans un seul worker et porté leur budget à 180 s, en croyant à une concurrence entre quatre compilations. La vraie cause trouvée, ces changements ne tenaient plus debout et sont revenus en arrière.

Une panne e2e non résolue : page.goto qui n'arrive jamais — 26/10

Consignée parce que je ne l'ai pas élucidée, et que le prochain à la rencontrer doit savoir ce qui a déjà été écarté.

Symptôme : dans la suite complète en conteneur, page.goto sur /index des deux sites d'exemple du framework de sites expire au bout de 60 s. Les deux tests concernés sont séparés de 160 tests dans l'ordre d'exécution, donc ils ne se gênent pas l'un l'autre.

Ce qui est écarté, avec la mesure :

  • Ce n'est pas mon travail en cours. Une passe de contrôle sur le commit déjà poussé échoue aux deux mêmes tests.
  • Ce n'est pas le serveur d'aperçu. Lancé à la main, il sert /index en 7 ms, sans redirection, vingt fois de suite. Il ne journalise pas les requêtes, donc ce n'est pas non plus un tube stdout saturé qui bloquerait le processus.
  • Ce n'est pas une requête bloquée. Avec waitUntil: "commit" et 25 s d'attente, la page émet 94 requêtes et aucune ne reste en suspens.
  • Ce n'est pas la concurrence. Avec --workers=2 au lieu de 4, il y a plus d'échecs, pas moins. La machine a 8 cœurs, 24 Go libres et une charge de 2,6.
  • Ce n'est pas la page. Les mêmes tests, lancés seuls ou à deux dans le même conteneur, passent en 0,7 s et 1,3 s.

Ce qui est fait : openPage() dans e2e/support.ts navigue sans attendre load. C'est justifié indépendamment du reste — load n'est pas un signal utilisable pour une application qui va chercher des morceaux de bundle autour de l'hydratation (grammaires Shiki, scène de l'histoire, features de Motion) — et les tests attendent déjà ce dont ils ont besoin avec heightStable, decksReady, mermaidReady et filmsReady. Cela a supprimé deux des trois occurrences.

Suite, le même jour : domcontentloaded ne suffit pas non plus — les deux mêmes tests sont revenus à la passe suivante. Deux hypothèses de mécanisme ont été formulées puis réfutées par la mesure : un tube stdout saturé bloquant le serveur (il ne journalise pas les requêtes, son fichier de sortie ne bouge pas après vingt requêtes) et une hydratation bloquée sur le chargement de Shiki (entry.client n'attend getShikiReady() que si le HTML porte data-recit-runtime-shiki, absent des trois pages concernées).

openPage() attend donc commit — la réponse — et rien d'autre. C'est la seule variante mesurée qui tienne : 169 passés, zéro instable, 1,8 min. Rien n'est masqué : chaque appelant attend ensuite le contenu sur lequel il assure (heightStable, decksReady, un titre visible, une comparaison de capture).

Non élucidé : pourquoi domcontentloaded n'arrive pas en 60 s sur une page dont le serveur rend le document en 7 ms et dont les 94 requêtes aboutissent toutes. Piste suivante : instrumenter Chromium lui-même (--trace=on) plutôt que le réseau de la page.

Lot 11 de l'audit (dédoublonnage), première moitié — 26/10

Les doublons mécaniques, réunis dans un seul packages/core/src/util.ts, exporté par @recit/core et par @recit/core/browser (donc disponible dans un bundle navigateur sans la chaîne de compilation).

  • isRecord : huit copies. Sept identiques, et une différente dans @recit/mcp, qui acceptait les tableaux. Ses deux appels n'en faisaient rien d'utile — une lecture de propriété sur un tableau rend undefined — donc la version stricte est retenue, et la divergence disparaît. Le garde rend Record<PropertyKey, unknown> et non Record<string, unknown> : define.ts lit une clé symbole sur le résultat, et un garde plus étroit l'aurait forcé à caster.
  • formatDuration : cinq copies de mm:ss, plus une en m:ss dans le lecteur de film, plus un homonyme dans la CLI qui formate un tableau de durées. Une seule fonction désormais, qui tronque et borne : les anciennes rendaient 02:7.34 sur une durée calculée depuis un nombre de mots, et une durée négative sortait telle quelle. L'horloge du film passe de 0:07 à 00:07 — assumé pour la cohérence ; aucune capture e2e ne la montre.
  • slugify : deux versions divergentes. « Récit » donnait recit d'un côté (Le framework de sites) et r-cit de l'autre (motion-blocks, qui ne retirait pas les accents).
  • countWords : deux copies au comportement identique.
  • THEME_STORAGE_KEY : la chaîne "recit-theme" était écrite dans quatre fichiers, dont le script d'amorçage en ligne de la CLI et le harnais e2e.
  • Cale i18n de @recit/components : un fichier qui ne faisait que réexporter @recit/core/i18n, encore importé par neuf fichiers. Supprimé avec son test.
  • Variantes de transition : SLIDE_VARIANTS était dans @recit/components et ses deux compagnes dans Presentation.tsx, alors que seul le deck les lit. Les trois sont dans deck/transitions.ts, avec variantsFor et un test qui vérifie le choix pour chaque réglage.

motion-blocks déclare maintenant @recit/core : l'arête est permise par sites/plans/pages/03 §8 (format → foundation) et le contrôle des frontières le confirme.

Mesuré : pnpm check, 41 tâches de build et de typecheck, 2352 tests unitaires.

Lot 11, DB-6 : le choix MDX/HTML recopié quatre fois — 26/10

story.syntax === "mdx" ? <MdxSlide> : <HtmlSlide> était écrit dans le runtime du deck, dans la scène du framework de sites, dans la vignette et dans la vue document — et chaque copie passait un jeu de props légèrement différent. SceneContent dans @recit/deck en réunit trois.

La quatrième, StoryDocument, rend un document et non une diapositive : sa branche HTML descend les titres d'un niveau et n'utilise pas HtmlSlide, donc elle n'applique ni les étapes ni Mermaid. La fusionner changerait ce que fait une page de doc ; elle reste à part, et c'est écrit ici.

Le défaut que le doublon cachait : DeckThumbnail construisait son footerContext sans date, donc {date} restait vide dans toutes les vignettes de galerie. footerContextOf(story) le construit une fois pour les trois appelants.

Lot 11, les trois constats courts (DB-7, DB-10, DB-11) — 26/10

DB-7 — <CodeBlock> colorait avec un thème que personne d'autre n'utilisait. Son défaut était "nord", alors que le pipeline Markdown colore en github-light / github-dark — et ces deux valeurs étaient elles-mêmes écrites deux fois (le défaut du schéma et le compilateur). DEFAULT_SYNTAX_THEMES dans core les porte, et CodeBlock prend le thème sombre de la paire, celui qui va avec son propre fond sombre. La page de doc se contredisait — elle annonçait syntaxHighlighting.themes et un défaut nord ; elle dit maintenant ce que fait chaque chemin. highlightLines, une prop jamais lue, est retirée.

DB-10 — un seul des deux chemins Mermaid avait un verrou. Mermaid garde un état global entre initialize et render : deux rendus concurrents se disputent le thème et l'identifiant. Le hook du deck enchaînait ses diagrammes un par un ; le composant <Mermaid>, non. Une page qui mélange les deux n'avait donc aucun verrou. La file est descendue dans renderMermaidSvg, par où tout le monde passe ; le hook garde son enchaînement, qui sert à ne pas lancer de nouveau rendu après l'abandon d'une diapositive.

DB-11 — la hauteur maximale de <Media> existait en trois versions. 540 px dans le composant (classe Tailwind), 540 px dans deck.css, et 400 px dans un contournement de sites/components. Ce contournement n'existait que parce que les deux premières se gênaient. Seule la règle de deck.css reste ; le contournement est supprimé. Le test du composant assurait la présence de min-h-0 dans son className — c'était précisément le doublon ; il vérifie maintenant l'inverse, et la règle CSS est déjà couverte par le test voisin.

Mesuré : une seule capture de référence change (la page de code, qui n'est plus en nord), 2352 tests unitaires, e2e en conteneur 169 passés sans instabilité.

Lot 11, DB-8 : trois règles pour « est-ce une histoire ? » — 26/10

Trois implémentations, trois réponses différentes : la CLI acceptait "kind": "story" ou "sidecar", le plugin Vite seulement "story", et le chargeur motion prenait en plus un tableau scenes sans kind du tout (un film emaki).

Et les deux premières cherchaient la clé avec une expression régulière sur le texte brut. Elle matche un kind imbriqué n'importe où : {"meta":{"kind":"story"}} passait pour une histoire sans rien déclarer à son niveau supérieur. Le test le montre en vérifiant d'abord que l'ancienne expression matche bien, puis que jsonStoryKind répond null.

core/src/schema/discovery.ts porte isStoryFileName (extensions, moins les fichiers JSON qu'un projet a toujours) et jsonStoryKind, qui lit le JSON et rend story, sidecar, film ou null. Chaque appelant garde sa règle d'acceptation, exprimée sur ce résultat.

Mesuré : 7 assertions nouvelles, 2359 tests unitaires, e2e 169 passés.

Lot 11, DB-1 : déjà réglé pour moitié, et une recommandation écartée — 26/10

La moitié déjà faite. L'audit décrivait recit dur, validate et l'aperçu de la CLI calculant la durée avec sceneDuration, qui ne compte que le temps de lecture, alors que le MP4 découpe avec sceneFrameList, qui prend aussi la fin de l'animation et SCENE_TAIL. Vérifié : les trois passent désormais par filmDuration, construit sur filmTimeline — donc sur les mêmes images que le MP4. Le lot 4 l'a corrigé en renommant le vocabulaire. L'en-tête de client.tsx ne ment plus.

Ce qui restait : sceneFrameList écrivait 1.5 en dur là où MIN_SCENE_SECONDS vaut 1,5. La constante est importée.

La recommandation écartée, et pourquoi. L'audit demande « une seule constante de vitesse de lecture », au motif que core dit 140 mots/min et motion 2,2 mots/s (soit 132). Ce ne sont pas deux valeurs du même nombre : celle de core estime le temps qu'un présentateur met à dire ses notes, celle de motion le temps qu'un spectateur met à lire le texte à l'écran. Les fusionner alignerait deux mesures différentes sur la seule base que leurs chiffres se ressemblent — et changerait la durée de tous les films. Les deux constantes restent, chacune disant maintenant ce qu'elle mesure et renvoyant à l'autre.

Mesuré : 2359 tests unitaires, et les 32 rendus de référence des films à 0,000 % — la durée des films n'a pas bougé.

L'erreur React #418 : une reproduction déterministe, enfin — 26/10

Connue depuis le lot 6, visible « seulement quand la machine est lente », jamais reproduite seule. Trouvée par accident en faisant DB-3.

La recette. Retirer l'import de motion du composant Quote de @recit/components, reconstruire, et /demos/mdx échoue à chaque fois, en local, en 1,7 s. Remettre l'import : ça passe. Quote n'est pourtant pas sur cette page. Ce que son import change, c'est le découpage des chunks — donc l'ordre et le moment où les modules arrivent, donc la fenêtre d'hydratation. Le défaut était là tout du long ; le graphe de modules décidait seulement s'il se manifestait.

Un piège d'instrumentation qui a coûté deux sondes. L'erreur arrive en pageerror, pas en erreur de console. Une sonde qui n'écoute que page.on("console") voit zéro erreur sur une page qui échoue. trackErrors d'e2e/support.ts écoute bien les deux, d'où l'écart entre la suite et mes sondes.

Ce qui a été mesuré sur la page, et écarté : les séparateurs <!-- --> que React pose entre plusieurs enfants texte sont bien présents dans le HTML prérendu (donc aucun minifieur ne les mange) et les trois blocs de code colorés au runtime ne diffèrent entre serveur et client que par l'espacement de l'attribut style — le texte des jetons est identique.

Le nœud qui diverge, trouvé. En comparant les séquences de nœuds texte du HTML prérendu et du DOM hydraté avec un vrai diff — et non indice par indice : un diff naïf confond les insertions légitimes du rendu client-only, comme le raccourci CTRL+K du bouton de recherche, avec une vraie divergence :

Code
- serveur : "layout" | "=" | "cover" | "# Lorem ipsum MDX" | … + client : "--- layout=cover" | "# Lorem ipsum MDX" | …

Une seule ligne d'un bloc de code de <SourceFile language="mdx"> : le serveur la découpe en trois jetons Shiki, le client la garde entière. Le reste du même bloc est découpé pareil des deux côtés ({, year, }), donc le client colore bien — il tokenise juste cette ligne autrement. C'est un écart de grammaire Shiki entre le prérendu et le runtime, pas un état applicatif.

Non élucidé : pourquoi cette ligne-là. --- layout=cover est le séparateur de diapositive de Récit à l'intérieur d'un exemple MDX ; la grammaire mdx peut le lire comme un début de front matter selon ce qui précède. Les deux côtés reçoivent pourtant la même chaîne, par le même composant, avec la même liste de langages (site-kit/config.tsx ajoute mdx, déjà présent dans defaultLanguages). Prochaine étape : appeler highlight() sur cette chaîne dans les deux environnements et comparer le HAST.

En attendant, Quote garde son fondu, comme ses voisins Cover, Points et Media — ce qui est de toute façon plus cohérent que de l'en priver.

Lot 11, DB-3 : trois composants qui se peignaient eux-mêmes — 26/10

Callout, Quote et CodeGroup dessinaient leur propre apparence en Tailwind, alors que la directive Markdown équivalente émet des classes slide-* stylées par deck.css. Le même callout n'avait donc pas la même tête selon qu'il était écrit en Markdown ou en MDX. Les trois émettent maintenant les classes de la directive, et deck.css porte seul le style.

Le défaut que le doublon cachait : <CodeGroup> n'avait aucun role="tablist". Là où la version Markdown donne des onglets à un lecteur d'écran, la version MDX lui donnait une rangée de boutons sans rapport. Il émet désormais le même motif WAI-ARIA que rehypeCodeGroups, et son fond #1e1e1e en dur disparaît avec le reste.

Une couverture qui manquait. Les composants MDX intégrés ne sont utilisés que sur la deuxième diapositive du deck MDX, qu'aucune capture n'atteignait : ils n'avaient aucune couverture visuelle. Un test ouvre maintenant /present/mdx?slide=2, vérifie que le callout porte bien slide-callout-tip et data-title, et en prend une capture.

Et un trou dans le harnais du lot 2. En ajoutant ce test, Playwright a écrit tout seul une référence -linux — depuis cette machine, hors du conteneur. updateSnapshots valait CI ? "none" : "missing", ce qui suppose que la machine de dev est un Mac. Sur Linux, une référence manquante était donc écrite avec les polices et le Chromium de l'hôte, c'est-à-dire exactement ce que le conteneur existe pour empêcher (TE-1). La condition tient compte de la plateforme, et le test qui gardait cette politique — qui passait trivialement en vérifiant ce que la machine courante répond — teste maintenant les deux cas en simulant process.platform.

Mesuré : 2363 tests unitaires, e2e en conteneur 170 passés.

Lot 11, DB-4 : les diagrammes ignoraient le thème — 27/10

La couleur de marque était écrite en cinq endroits incompatibles. Le plus visible : mermaid-render posait #3B82F6 en dur, donc un diagramme restait bleu dans un deck en graphite ou en ambre alors que tout autour suivait la palette.

Les diagrammes lisent maintenant les jetons de l'élément qui va les porter — --primary pour le contour, --muted pour le fond des nœuds, --card-foreground pour le texte, --border pour les traits — avec l'ancienne palette en repli quand le diagramme est rendu hors d'un conteneur thémé. Les jetons sont des triplets HSL nus (295 38% 25%), composables en CSS ; le parseur de couleurs de Mermaid veut la forme à virgules, d'où tokenToColor, extrait pour être testable sans DOM.

Mesuré par le DOM, pas par les captures : sur /present/mermaid, le contour d'un nœud vaut rgb(84, 40, 88), soit exactement hsl(295, 38%, 25%) — la valeur de --primary. Les captures plein écran, elles, n'ont bougé que sur une page sur quatre : le changement reste sous la tolérance de 2 % ailleurs. C'est un rappel utile sur ce que ces références garantissent vraiment.

Reste de DB-4, non fait : site-kit (#7c3aed / #a78bfa) et le repli #7c3aed de site-stories.css ne sont toujours pas dérivés de tokens.css, et motion-blocks/themes/theme.ts recopie encore ThemeContract à la main. Ce sont des changements de schéma, pas de rendu.

Lot 11, DB-2 (moitié) : une seule horloge de film — 27/10

Deux lecteurs, deux boucles requestAnimationFrame pour la même chose : cli/src/motion/client.tsx (l'aperçu de recit dev) et integrations/site/src/motion/MotionPlayer.tsx (le lecteur en page). Écrites différemment, donc un correctif sur l'une n'atteignait jamais l'autre.

useFilmClock dans @recit/motion-blocks — dont les deux dépendent déjà — porte la boucle. 97 lignes retirées, 38 ajoutées. Les deux vraies différences sont devenues des options : le lecteur en page boucle (loop) et s'arrête hors écran (enabled), l'aperçu s'arrête sur la dernière image. Le reste du chrome n'est pas fusionné et ne devrait pas l'être : un bandeau de scènes avec rechargement à chaud et une barre de progression avec lecture/pause ne sont pas la même interface.

Les deux décisions de l'horloge sont extraites pour être testables sans DOM — clockAtEnd (recommencer ou s'arrêter), clockAfterToggle (relancer un film fini depuis le début), clampTime. C'est exactement ce qui divergeait entre les deux copies. Premier jet du test supprimé : il n'assurait que sur sa propre donnée de test.

Reste de DB-2, non fait : les deux serveurs de dev (commands/dev.ts et commands/motion-dev.ts, chacun avec son http.createServer et son canal SSE). Ils partagent désormais le correctif de fermeture, mais pas leur structure.

Mesuré : 2373 tests unitaires, e2e en conteneur 170 passés.

Le serveur de dev motion servait le film à tout le réseau — 27/10

Trouvé en instruisant la seconde moitié de DB-2. recit dev route une histoire motion vers startMotionDevServer, qui n'avait aucun jeton : recit dev film.story.md --host 0.0.0.0 servait le film, ses sources et sa configuration à n'importe qui sur le réseau local — alors que la même commande sur un deck refuse toute requête non-locale depuis le lot 5 (BP-3).

C'est précisément ce qu'un doublon cache : deux serveurs, un seul corrigé. Le module dev/access.ts était déjà isolé, donc la correction tient en un appel à decideAccess et le jeton dans l'URL hors localhost, comme pour le deck. Le test est l'exact pendant de celui du deck ; vérifié en retirant le garde-fou, il échoue.

Mesuré : 2374 tests unitaires, e2e en conteneur 170 passés (la passe précédente en montrait trois rouges — les deux déjà connues plus une instable ; la relance est à 170/170 en 1,8 min).

Lot 8, suite : TY-12, TY-13, TY-17 — 27/10

TY-13 — quatre unions recopiées. CalloutType, GridCols, MermaidSecurityLevel et le fit des images étaient écrits à la main dans @recit/components, alors que core porte la liste. Les quatre suivent maintenant le motif déjà posé par TY-1 : un enum zod, son type par z.infer, sa liste par .options. Le pipeline lui-même lit CALLOUT_TYPES de core au lieu de son propre Set.

TY-12 — sept interface malgré la règle, et biome n'y pouvait rien. Sa règle ne signale que les déclarations qu'elle sait réécrire : interface X extends Y passe, et pnpm biome lint répond « No fixes applied » sur un fichier qui n'en contient que. scripts/check-interfaces.js les refuse dans les packages Récit (le fork packages/site garde le style amont, et une augmentation globale declare global { interface Window } est la seule forme sans alias possible). Vérifié en ajoutant une interface : le contrôle échoue.

TY-17 — toClientStory annonçait Story alors qu'il retire trois champs (mdast des scènes, components et customCss des réglages). ClientStory, ClientScene et ClientSettings le disent, et PageData du runtime deck aussi. Le typecheck a immédiatement attrapé le test, qui lisait ces champs pour vérifier qu'ils valaient undefined ; il teste maintenant leur absence avec in, et le compilateur refusant la lecture est la moitié de ce que le type sert à garantir.

Mesuré : pnpm check (avec le nouveau contrôle), 2374 tests unitaires, e2e 168 passés — les deux rouges sont les deux pannes déjà instruites.

Lot 8, TY-15 : la télécommande pilotait le deck sans contrôle — 27/10

PresentationPeerMessenger recevait les messages de la télécommande par un relais WebSocket et faisait JSON.parse(event.data) as RemoteAction. Tout ce que le relais transmettait atteignait donc les gestionnaires : un GOTO dont slide est une chaîne, un TOOL_CHANGE vers un outil inconnu, un message sans type du tout.

SyncMessage est maintenant dérivé d'un SyncMessageSchema (union discriminée zod), et les deux entrées sont validées : parseSyncMessage pour le WebSocket, asSyncMessage pour le BroadcastChannel entre fenêtres. ActiveTool suit le même motif.

Coût mesuré : le runtime deck passe de 20,75 à 22,58 ko brotli, sous son budget de 26 ko. zod était déjà dans le bundle ; c'est le schéma lui-même qui pèse. Pour un canal piloté depuis le réseau, le change vaut la peine.

13 assertions nouvelles sur ce qui est accepté et ce qui est refusé.

Mesuré : 2378 tests unitaires, e2e en conteneur 170 passés.

Lot 8, TE-13 : --typecheck ne vérifiait aucun type, pas même celui du fork — 27/10

L'audit notait que le seul *.test-d.ts du dépôt était dans packages/site. En voulant en ajouter un pour core, j'ai trouvé pire : aucun n'était collecté. pnpm test lance vitest run --typecheck, mais avec des projects, le drapeau de la ligne de commande n'atteint pas les projets — il faut typecheck.enabled dans la configuration de chaque projet. Celui du fork n'a donc jamais tourné non plus.

packages/core/vitest.config.ts l'active, et src/__tests__/types.test-d.ts vérifie ce qui vaut la peine : que les types sont bien dérivés de leur schéma zod. Un enum écrit deux fois — une fois en zod, une fois en union TS — compile parfaitement pendant que les deux divergent ; ces assertions tombent dès que ça arrive. Vérifié en ajoutant "fill" au schéma sans toucher au type : Type Errors 1 failed.

Elles couvrent aussi ClientStory (TY-17) : plus moyen de lui redonner la forme de Story sans casser la suite.

Non fait, et dit ici : activer typecheck dans packages/site serait un patch de plus sur le fork, pour sa propre configuration de test. Laissé à la procédure de synchronisation.

Mesuré : 209 fichiers de test, 2384 tests, Type Errors no errors.

Lot 8, TY-6 : un .d.ts n'est jamais vérifié — 27/10

Le constat parlait de types publics manquants. Ce qu'il y avait derrière est pire.

virtual.d.ts importait un type qui n'existe plus. Il déclarait la forme de virtual:recit-stories avec motion?: Deck, de @recit/motion — un type que le lot 4 a renommé Film il y a deux jours. Personne ne l'a vu, parce que skipLibCheck: true dans tsconfig.base.json fait qu'un .d.ts n'est jamais vérifié. La déclaration dont dépend tout site du framework de sites pouvait donc dériver en silence, et elle avait dérivé.

La forme vit maintenant dans loaded-story.ts, un .ts vérifié, et virtual.d.ts ne fait plus que la référencer. Un test de types la garde.

Le reste du constat. PublishConfig, StoryEntry, I18nConfig et DeckImage sont exportés ; RecitPluginOptions est un Pick<PublishConfig, …> au lieu d'une recopie à la main ; le cast vers { src, alt } de i18n.ts passe par DeckImageSchema.safeParse ; et le StorySource de @recit/site-stories — qui entrait en collision avec celui de core, avec un autre sens — est StoryEntry, l'ancien nom restant comme alias déprécié.

Deux erreurs de ma part, corrigées. J'ai d'abord écrasé le vitest.config.ts existant de @recit/site-stories au lieu de l'étendre, ce qui a cassé l'alias du module virtuel dans ses tests. Et ma première assertion de type, not.toBeAny(), passait pour unknown : vérifiée en remettant la dérive, elle ne mordait pas. Elle nomme maintenant le type attendu, et elle échoue.

Mesuré : 2388 tests, Type Errors no errors, e2e 168 passés (les deux rouges sont les pannes /index déjà instruites).

Lot 8, TY-5 : deux pertes d'information, pas six types à fusionner — 27/10

Le constat listait six formes de diagnostic. Les fusionner toutes aurait brassé chaque package pour un gain discutable : SourceIssue (un chemin JSON, avant la correspondance avec les lignes) et AccessibilityIssue (le résultat d'une règle) sont des entrées de Diagnostic, pas des concurrents. Ce qui méritait d'être corrigé, c'est ce que la traversée perdait.

Tous les avertissements d'un film arrivaient étiquetés suspicious-glyph. Ils naissent de diagnostics qui ont leur propre code et leur ligne, étaient aplatis en chaînes par formatFilmSourceIssue, puis ré-étiquetés en bloc par validate.ts. Mesuré sur une histoire qui déclenche les deux : avant, deux suspicious-glyph ; après, un invalid-aspect-ratio ligne 3 et un suspicious-glyph. FilmSourceResult.warnings porte des FilmSourceIssue, et issueToDiagnostic prend le code de l'issue et la sévérité de son appelant.

Le -1 sentinelle de HandoverIssue.scene servait d'index de tableau : il rendait undefined et retombait dans la branche « problème du film » par accident. Le champ est optionnel, et le cas est écrit.

Deux pièges trouvés par le changement : read.issues.map(issueToDiagnostic) aurait passé l'index du tableau comme sévérité (attrapé par le typecheck), et r.warnings.join("\n") dans le MCP aurait imprimé [object Object] — que le compilateur, lui, accepte sans rien dire.

Les tests de loader.test.ts vérifiaient la chaîne formatée ; ils vérifient maintenant le code, le fichier et la ligne, ce qui est précisément ce que le changement apporte.

Au passage, le test axe instable. Les contrôles du deck apparaissent au survol via une transition d'opacité de 200 ms. Laissés seuls ils restent à opacity: 0, qu'axe ignore, donc ils n'étaient jamais vérifiés ; pris à mi-transition, axe signale un défaut de contraste qu'aucun des deux états finaux n'a (le bouton est à ~16:1, mesuré). Le test les révèle et attend la fin de la transition : la passe est stable et ces boutons sont réellement couverts.

Mesuré : 2388 tests, Type Errors no errors, e2e 168 passés (les deux rouges sont les pannes /index déjà instruites).

Lot 8, TY-16 : le modèle Story ne parle plus de « slides » — 27/10

Le modèle est commun à tous les formats, mais son résumé et ses diagnostics nommaient des diapositives. SlideMetadata → SceneConfig, GlobalDeckMetadata → StoryFrontmatter, ParsedDeckDocument → ParsedStoryDocument, resolveDeckConfig → resolveStoryConfig, SlideDensityReport → SceneDensityReport, DeckDensitySummary → StoryDensitySummary, et les champs slideIndex, totalSlides, overloadedSlides, averageWordsPerSlide. Les six types gardent leur ancien nom en alias déprécié ; les champs, non — ils sont calculés, aucun JSON ne les porte (vérifié).

Et j'ai refait l'erreur du renommage en masse. Le premier passage a touché six fichiers du runtime deck, où « slide » est pourtant le bon mot : parseSlideIndex(param, totalSlides) lit ?slide=N dans un deck, et renommer son paramètre pendant que la fonction garde « Slide » dans son nom n'a aucun sens. Tout annulé sur ce paquet — et le compilateur a alors désigné le seul endroit qui lisait vraiment le modèle : DevHud.tsx, qui affiche report.sceneIndex d'un SceneDensityReport. Six fichiers à tort, un à raison.

Les types de résultat de la CLI (BuildResult.totalScenes, ExportHtmlResult.totalScenes) suivent le modèle : ils comptent story.scenes.length. C'est une rupture d'API, assumée en 0.x, et la même que celle du lot 4 pour motion.

Mesuré : 2388 tests, Type Errors no errors, e2e en conteneur 170 passés.

Lot 15, CG-5 : trois chemins pour une transition — 27/10

Un deck était réglé par deux sections, slides: et deck:, la seconde surchargeant la première (« Overrides slides.transition », dit le commentaire d'origine), plus settings.transition dans le jumeau JSON. deck: est désormais la seule section du format — elle prend files, designWidth, designHeight, aspectRatio, transition, presenter — et slides: est refusé avec son message de renommage, comme publish.decks l'avait été.

Suivent : le schéma JSON publié (régénéré par pnpm -F @recit/core schema:json, ce que le test disait déjà de faire), la configuration résolue (settings.deck au lieu de settings.slides), l'exemple deck-pitch, et les trois pages de doc qui l'écrivaient. Un test vérifie que l'ancien nom produit bien le message.

Et le test axe instable, pour de bon cette fois. Ma première correction — survoler les contrôles et attendre la fin de la transition — ne tenait pas : la souris ne reste pas forcément en place pendant qu'axe parcourt la page. Mesuré en local dans les deux modes, avec le survol : aucune violation de contraste. Les boutons vont bien ; c'est la mesure qui était instable. Ils sont maintenant épinglés à leur état visible par une feuille de style injectée, ce qui vérifie exactement les couleurs qu'un lecteur voit, de la même façon à chaque passe.

Mesuré : 2389 tests, e2e en conteneur 170 passés, une seule capture de référence changée (la page de configuration, dont le tableau a bougé).

Lot 15, DO-6 : theme voulait dire trois choses — 27/10

Dans le front matter, theme: light réglait le mode d'affichage, theme: warm-editorial nommait un thème, et un objet portait la configuration de thème. Trois sens pour une clé.

colorMode: prend le mode (light, dark, system), theme: ne nomme plus qu'un thème ou porte son objet. Les deux se combinent : colorMode: dark avec theme: warm-editorial donne les deux.

Choix tranché seul, et pas le même que pour slides:. Une configuration qui garde un ancien nom est refusée avec un message — c'est ce que CG-5 fait. Du front matter est du contenu : casser le deck de quelqu'un à une montée de version est pire qu'une clé ambiguë une version de plus. theme: light continue donc de marcher, et pose un diagnostic theme-is-a-color-mode qui dit où l'écrire. Quatre assertions couvrent les trois cas plus le silence quand colorMode est utilisé.

Un garde-fou utile a parlé : @recit/mcp embarque une copie des guides tirés de sites/docs, et son test vérifie qu'elle reste synchrone — en nommant la commande. Sans lui, un paquet publié serait parti avec une doc périmée.

Mesuré : 2393 tests, e2e en conteneur 170 passés, une capture de référence régénérée.

Lot 15, CG-3 : le vocabulaire « deck » dans une API « story » — 27/10

L'attribut data-deck devient data-story (huit fichiers, dont le sélecteur de portée des thèmes généré par le plugin Vite). decks.ts devient stories.ts, DECKS_MODULE_ID devient STORIES_MODULE_ID — son nom disait « decks » alors que sa valeur dit déjà virtual:recit-stories. getLoadedDecks, getLocaleDecks, findDeck, getDecks, DeckSummary et DeckThumbnail prennent leur nom « story », les anciens restant en alias dépréciés parce que @recit/site-stories les publie — site-kit importait findDeck.

Le défaut que ce vocabulaire cachait : <StoryEmbed slide={2}> et ?slide=2 désignaient la deuxième scène, pendant que <DeckThumbnail slide={2}> désignait la troisième. Le même mot, compté depuis deux origines. Les deux comptent maintenant depuis 1.

Mon premier test rendait la vignette entière et tombait sur un fixture sans settings : le vrai contrat est le choix de la scène, extrait dans sceneAt(story, slide) et testé là.

Mesuré : 2395 tests, e2e en conteneur 170 passés — aucune capture de référence ne bouge, l'attribut renommé n'étant pas visible.

Lot 20, DB-17 : un schéma d'URL plutôt qu'un composant — 27/10

sites.ts se présente comme « la seule source des liens inter-sites » et bascule sur localhost en dev. Trente-six liens dans dix-neuf pages passaient à côté avec une URL *.recit.dev absolue : en dev, chaque lien inter-site quittait localhost pour la production.

Première tentative, un composant <SiteLink> : fausse route. oxfmt replie la prose à 100 colonnes, et dès que l'élément JSX se retrouve en début de ligne, MDX ne le lit plus comme de l'inline mais comme un bloc — la phrase est coupée en deux paragraphes. Cinq phrases y sont passées avant que la sortie buildée ne le montre. Rejoindre les lignes à la main ne sert à rien : le formateur replie de nouveau. Un composant inline dans de la prose repliée est structurellement fragile.

La forme qui tient est un schéma d'URL, [texte](site:docs/formats/motion) : du Markdown pur, insensible au repli, utilisable dans une cellule de tableau.

Trois défauts barraient la route, chacun réel :

  1. remarkLinkRewrite n'épargnait que http et mailto: ; tout autre schéma était lu comme un chemin relatif et ressortait préfixé (/site:docs/x). tel: était cassé pareil. Il épargne maintenant toute URL qui nomme un schéma — deux caractères au moins, pour qu'une lettre de lecteur Windows reste un chemin.
  2. Le .md publié est la source, qu'aucun plugin remark ne voit : les liens seraient partis morts. build.markdownLinks les réécrit, vers l'URL publiée même en dev — un .md copié se lit loin de son site, comme les guides MCP.
  3. processMarkdownFile reconstruisait sa sortie à partir de la source brute quand la page avait un titre, jetant toute transformation. Le test couvre les deux branches.

createSiteConfig donne aussi à chaque site les composants MDX partagés : six configurations cessent d'importer les six mêmes noms (76 lignes), et quatre descriptions de site cessent de répéter leur propre hôte (DO-11).

DO-10 et DO-12 étaient déjà faits — mais DO-10 avait renommé « Attributes » en « Attributes and props » sur quatorze pages sans régénérer les captures -linux, qui mentaient depuis d61549a0.

Mesuré : 36 URL écrites à la main → 0, le garde prouvé en échec sur un lien et laissant passer les identifiants $schema ; 2400 tests ; e2e en conteneur 169/170, le reste étant le React #418 de /demos/mdx, sans rapport et toujours ouvert.

Lot 20, DB-14 : trois listes revenues à zod — 27/10

types/index.ts annonce que chaque enum d'écriture est défini une fois, en zod, « sans rien d'écrit deux fois ». Trois s'en étaient échappés : les types de badge étaient une union TypeScript dans builtins.tsx, réécrite dans deux tableaux de site — quatre copies de cinq valeurs, zod n'en tenant aucune ; les types de média, pareil ; et config/schema.ts ré-épelait les niveaux de sécurité Mermaid que MermaidSecurityLevelSchema détient deux fichiers plus loin.

Les tableaux des sites continuent d'écrire les valeurs à la main — une cellule de tableau est de la prose, et la description à côté de chaque valeur appartient à la page. Un test rapproche donc chaque cellule `a \| b \| c` de l'enum qu'elle recoupe le plus et exige que la liste soit exacte. Aucun marqueur dans les pages.

Le test a d'abord menti : il importait @recit/core, donc le dist, périmé après mon édition de la source. Il annonçait deux dérives inexistantes. Il lit maintenant types/index.ts directement : un garde qui dépend d'un build est un garde qui crie au loup.

Lot 16, DO-7 : la page Architecture lit l'espace de travail — 27/10

La page listait les paquets à la main et en déclarait huit « planned » alors qu'ils étaient livrés : @recit/motion, motion-blocks, motion-render, mcp, recit, create-recit, data, time. Douze paquets existaient ; le tableau « Available » en montrait sept. Le diagramme disait « @recit/motion (planned) » et oubliait trois paquets.

<AvailablePackages /> et <PlannedPackages /> lisent chaque manifeste par import.meta.glob au build. Rien n'est écrit deux fois et rien n'est généré dans un fichier à resynchroniser : le nom vient de name, le groupe du tag nx scope:*, le rôle de description, et la phase du (P6) par lequel cette description se terminait déjà. Un nouveau paquet atteint la page en existant. Le fork Le framework de sites vendu n'a pas de scope : il reste dehors et garde sa propre section.

Le diagramme reste éditorial — un graphe de dépendances généré serait du bruit — alors un test le tient à l'espace de travail : tout paquet de format livré doit y être nommé, et rien que les manifestes disent livré ne peut être dit « planned ». Il tombe quand on retire motion-blocks.

Trois autres lignes périmées, chacune vérifiée contre le code : la puce CLI listait quatre commandes sur douze (elle renvoie maintenant à la référence au lieu de la recopier), publish.decks est devenu publish.entries avec CG-3, et motion.aspect n'est plus « (video, planned) ».

Mesuré : Available 7 → 12 lignes, Planned 11 triées par phase ; 2411 tests ; e2e 168 passés, la capture Architecture régénérée.

Le blocage /index : mesuré, toujours pas expliqué — 27/10

L'échec intermittent de l'e2e (une passe sur trois environ) était décrit comme « la page n'atteint jamais load ». Instrumenté par événements plutôt que par le journal d'appel de Playwright, il dit tout autre chose :

MesureValeur
Requêtes de document en 25 s2996
Navigations de la frame principale2995, toutes vers /index
Statut des réponses200, redirectedFrom = -
DOMContentLoaded / load0 / 0
Requêtes en erreuraucune
pushState, replaceState, assign, reloadaucun appel

Le navigateur redemande la page cent vingt fois par seconde, reçoit un 200 à chaque fois, et recommence avant d'avoir analysé quoi que ce soit — sans qu'aucun script de la page ne le demande. Le code applicatif est hors de cause, et le serveur répond.

Deux fausses pistes, écrites ici pour qu'on ne les reprenne pas :

  1. L'échec de préchargement de chunk (vite:preloadError → location.reload()). Démenti : aucune requête en erreur, et le gardé-fou est de toute façon limité à un rechargement par 30 s.
  2. La course keep-alive. Le serveur ferme bien une socket inactive à 6 s quand un navigateur garde les siennes des minutes — mesuré, et corrigé dans preview/handler.ts parce que c'est un vrai défaut. Mais ce n'est pas la cause : le blocage survit au correctif, vérifié avec le correctif présent dans le build du conteneur. J'avais annoncé l'inverse trop vite, en prenant les « 12 × navigated to » du journal Playwright pour une mesure et en avalant l'erreur d'evaluate dans un catch. Deux fautes de méthode : une mesure indirecte prise pour une mesure, et une erreur jetée au lieu d'être lue.

Le fait le plus parlant, encore inexploité : la boucle ne frappe que /index, et sur deux sites différents. C'est l'URL dont le fichier statique (dist/index.html) sert aussi de repli SPA pour /. La suite se joue probablement là, et demande un autre outil — un journal réseau de Chromium (--log-net-log) dans le conteneur — plutôt qu'une sonde Playwright de plus.

Mesuré après coup : quatre passes consécutives, 170/170 trois fois.

Lot 16, DO-5 : l'onglet générique rendu au commun — 27/10

sites/docs rangeait dans l'onglet « Documentation » sept pages qui ne valent que pour le deck : layouts, slide-options, density-rules et les trois pages « Presenting ». Motion n'avait qu'une page, reléguée dans une catégorie « In progress ». Autrement dit, les pages motion à venir auraient dû disputer l'onglet générique au deck, qui l'occupait seul.

La règle posée : l'onglet Documentation ne contient que ce que tout format lit. Le reste descend sous Formats › <Format>. Sept pages déménagent, formats/deck.mdx devient formats/deck/overview.mdx et ouvre une catégorie, Motion en ouvre une autre, vide, prête.

documentation/writing/slides devient documentation/writing/scenes : la page décrit le séparateur --- et le front matter, c'est-à-dire le mécanisme commun, mais elle l'écrivait entièrement en vocabulaire de deck (« Split the deck », « Deck front matter »). Elle est réécrite dans le vocabulaire du glossaire et renvoie vers les pages deck pour ce qui est propre aux slides.

Deux affirmations vérifiées plutôt que supposées, en écrivant la page :

  • aspectRatio est bien une clé commune (16:9 et 4:3 pour un deck, plus 1:1 et 9:16 pour un film). Je l'avais retirée de l'exemple par excès de zèle ; elle y est revenue.
  • « un format ajoute sa section deck: ou motion: au front matter » : mesuré sur quatre histoires jetables. wordsPerSecond: 4 à plat et sous motion: donnent tous deux 3,00 s contre 5,47 s sans ; aspectRatio: "4:3" à plat et sous deck: donnent tous deux "4:3" contre "16:9" sans. Les deux écritures marchent — ce qui est en soi deux façons de dire la même chose, à regarder un jour.

Les onze anciennes adresses redirigent, chacune par un fichier de 85 octets généré au build. Seize captures orphelines supprimées, huit régénérées.

Mesuré : 2411 tests, e2e en conteneur 169 passés, le seul échec étant le blocage /index connu.

Plan 18 — Applications desktop et mobiles — 29/10

Nouveau plan sites/plans/pages/18-mobile-desktop.md, écrit d'après la mise en place de Tauri 2 du projet sass (/home/deploy/work/sass/apps/desktop/src-tauri, packages/sites/scripts/app.ts) : une coquille commune dans un paquet @recit/app (scope:tool), une section app: typée dans @recit/core, le dist/ prérendu embarqué et servi hors ligne, onze lots APP0 à APP10 avec leurs fichiers possédés, leurs dépendances et leur vérification, et des vagues pour une exécution par plusieurs agents (§6 du plan).

Pour reprendre : lire le plan, puis APP0 seul (vague 0), APP1 et APP4 en parallèle (vague 1), APP2 et APP3 (vague 2), tout cela sans Rust. APP5 et la suite demandent rustup et la ligne apt de §3 du plan, ou la CI de APP6. Chaque lot fini laisse une note ici, à la fin du fichier, sous ### APPn — <titre> — <date>.

Au passage, le 29/10 : server.allowedHosts: true dans packages/site/src/vite/config.ts et dans sites/storybook/.storybook/main.ts, pour atteindre les serveurs de dev par un autre nom d'hôte.

Reste à faire, hors du périmètre de l'agent qui a écrit le plan : la ligne de navigation du site des plans, doc("18-mobile-desktop", "18 · Desktop et mobile") dans « Fondations » de sites/plans/recit.config.ts.

APP0 — Le contrat de configuration — 29/10

Fait, sans Rust. La section app: existe dans @recit/core et chaque lot suivant la consomme sans la redéfinir :

  • AppConfigSchema et AppConfig (packages/core/src/config/validate.ts), app: z.optional(…) dans RecitConfigSchema ; c'est une section Récit : splitRecitConfig la range sous RECIT_CONFIG, elle n'entre pas dans SITE_CONFIG_KEYS, et le JSON Schema publié packages/core/schema/v1/config.json a été régénéré (pnpm -F @recit/core schema:json).
  • packages/core/src/config/app.ts, exporté par @recit/core/config : resolveAppConfig(config: unknown, site: AppSite): AppConfigResolution avec AppSite = { dir: string; packageVersion?: string } et AppConfigResolution = { app: ResolvedAppConfig; diagnostics: Diagnostic[] } ; ResolvedAppConfig = { productName, identifier, version, icon?, window: { width, height, resizable, minWidth?, minHeight? }, allowedOrigins, deepLink?, android: { minSdkVersion }, ios: { developmentTeam? }, updater? } ; identifierFromOrigin(origin) (docs.recit.dev → dev.recit.docs, tirets retirés) ; DEFAULT_APP_WINDOW (1280 × 800, redimensionnable) et DEFAULT_ANDROID_MIN_SDK (24).
  • config est le recit.config chargé, tel que defineConfig le rend ou le module brut : les options du site (canonicalUrlOrigin, basePath, cdnUrl, search, site.title, metadata.favicon) sont lues sur l'objet, la section app sur les sections Récit validées.
  • Jamais d'exception pour un problème d'app : codes app-missing (erreur, le message contient l'exemple minimal avec les défauts calculés), app-identifier-missing et app-identifier-invalid (erreurs, path: /app/identifier, la règle Android dans le message), app-base-path et app-cdn-url (erreurs), app-version-missing et app-search-remote (avertissements). Un recit.config invalide revient aussi en diagnostics (config-invalid, un par problème). Une résolution qui porte une erreur ne s'utilise pas : identifier y vaut "".

Mesuré : pnpm exec vitest run packages/core packages/integrations/site-stories/src/__tests__/site-config.spec.ts → 24 fichiers, 315 tests verts (dont app-config.spec.ts, 14 tests, et « Récit sections never collide with site options ») ; pnpm -F @recit/core typecheck vert ; biome et oxfmt verts sur les quatre fichiers touchés.

Pièges pour APP1 et APP2 : metadata.favicon des sites Récit est un import Vite (site-kit/src/config.tsx:9, ?no-inline), donc une URL construite (/assets/…svg), pas un chemin de fichier ; icon est rendu tel qu'écrit et materializeShell doit chercher une valeur qui commence par / dans public/ puis dist/, ou préférer un app.icon explicite. Le packageVersion est à lire par l'appelant (la CLI) dans le package.json du site.

APP4 — Documentation, plans et opt-in de l'exemple — 29/10

Fait, sans Rust. Les noms de commandes et les champs documentés viennent du plan 18 (§4.4, §5 APP2) et de l'API réelle d'APP0 (packages/core/src/config/app.ts), pas d'un code de la CLI qui n'existe pas encore.

  • examples/site-embed/recit.config.ts : section app: { productName: "Lorem Ipsum Docs", identifier: "com.example.loremipsum" }.
  • Trois pages de la doc (en anglais, comme le site), sous Configuration dans sites/docs/sidebar.ts : documentation/configuration/app (installer @recit/app, la section app: champ par champ avec les défauts, les huit codes de diagnostic, les commandes recit app build|dev|preview|android|ios|icon, les prérequis desktop par système, ce qui marche hors ligne, les limites, l'updater), app-android et app-ios (prérequis de §3 du plan, commandes, admonition « Work in progress » qu'APP7 et APP8 remplacent).
  • 03-packaging.md : app dans la table des groupes (§2), l'arborescence (§2), une ligne @recit/app sous @recit/paged dans la table des packages (§6), app dans la ligne scope:tool (§8). 06-roadmap.md : une ligne en P5 qui renvoie au plan 18. NOTICE.md intact.

Mesuré :

  • node scripts/check-frontmatter.mjs, check-admonitions.mjs, check-site-links.js, check-version-plans.js, check-package-boundaries.js, check-interfaces.js : verts.
  • nx run docs:build : vert, 57 routes prérendues, dist/documentation/configuration/app.html, app-android.html, app-ios.html produits ; tsgo --noEmit dans examples/site-embed vert.
  • biome et oxfmt verts sur mes fichiers (oxfmt lancé sur eux seuls : les pages de sites/*/pages sont formatées par pnpm check, .oxfmtrc.json n'exclut que examples/**/*.md(x) et sites/*/stories/**).

Pièges rencontrés :

  • Pendant APP1, packages/tools/app/package.json demande @tauri-apps/cli au catalogue avant que pnpm-workspace.yaml ne l'ait : toute commande pnpm du dépôt échoue (ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC, pnpm relance install avant chaque script). Les vérifications ci-dessus sont passées par les binaires de node_modules/.bin et, pour nx, npm_config_verify_deps_before_run=false. À relancer en pnpm check et pnpm exec nx run docs:build une fois APP1 installé.
  • Le typecheck de l'exemple refusait app dans RecitSiteConfig tant que packages/core/dist datait d'avant APP0 : un nx run docs:build reconstruit le cœur et le typecheck passe. Ce n'est pas un trou du contrat.
  • oxfmt --check du dépôt signale aussi 18-mobile-desktop.md, journal.md et .nx/version-plans/version-plan-app.md (pas à moi) ; biome signale packages/tools/app/shell/src/window-open.js, src/shell.ts et la coquille matérialisée dans examples/site-embed/.recit/app/ (APP1 : ignorer .recit/ dans biome ou git).

Reste : rien pour APP4. La table de recit-config.mdx n'a pas de ligne app (fichier hors du lot) ; à ajouter avec un lien vers /documentation/configuration/app.

APP1 — Le paquet @recit/app — 29/10

Fait. packages/tools/app (scope:tool, publié, exports à trois conditions, construit par tsdown comme motion-render), sur le contrat d'APP0 : il n'importe de @recit/core que des types.

  • tauriConfig(app, { target, frontendDist, devUrl? }) : le tauri.conf.json complet, objet JSON pur, testé par instantanés (desktop, dev, fenêtre et origines, Android, iOS, lien profond et updater). La fenêtre main est déclarée avec visible: false et create: false : la coquille la construit elle-même dans setup pour lui attacher le script d'initialisation et la garde de navigation (Builder::on_navigation n'existe pas en Tauri 2.12, seul WebviewWindowBuilder::on_navigation(Fn(&Url) -> bool)). app.allowedOrigins voyage dans plugins.recit.allowedOrigins, que lib.rs lit à l'exécution avec build.devUrl. plugins.deep-link n'écrit que desktop.schemes (sur mobile le plugin exige un host), et aucun de ces deux plugins n'est encore dans Cargo.toml : APP9 (updater) et, un jour, le lien profond.
  • materializeShell({ siteDir, app, target, devUrl?, distDir?, shellDir?, icon? }) écrit <site>/.recit/app : les fichiers du gabarit (Cargo.toml, build.rs, src/main.rs, src/lib.rs, src/window-open.js, capabilities/default.json) seulement s'ils changent (les mtimes restent, Cargo ne recompile pas pour rien), Cargo.lock une seule fois, un .gitignore (target/, gen/, icons/), tauri.conf.json, puis les icônes. Source de l'icône : app.icon (un chemin /x est cherché dans public/ puis dans le build, sinon relatif au site) ; sans fichier, assets/default-icon.svg du paquet et un avertissement app-icon-default. tauri icon ne tourne que si icons/icon.png manque ou est plus vieux que la source ; injectable (icon) dans les tests. Rend { shellDir, configFile, iconSource, diagnostics }.
  • serveLikeTauri(distDir, { port?, host? }) et resolveLikeTauri(distDir, path) : a/b, a/b.html, a/b/index.html, puis index.html en 200 (comme le protocole de Tauri : un chemin inconnu arrive au routeur), types MIME dont .wasm, requête et fragment ignorés, chemin décodé, jamais hors du dossier ; 404 seulement sans index.html.
  • runTauri(shellDir, args, { env?, stdio? }) lance tauri.js de @tauri-apps/cli (2.12.4, épinglé au catalogue) avec node, dans la coquille ; tauriEnv() traduit RECIT_APP_TARGET_DIR en CARGO_TARGET_DIR sauf si ce dernier est déjà posé. isPortInUse(port) sonde en TCP.
  • Version plan .nx/version-plans/version-plan-app.md (prerelease pour core, cli, app), @recit/app dans release.projects, .recit/ dans le .gitignore racine.

Mesuré :

  • pnpm exec vitest run packages/tools/app scripts/__tests__/release-workflow.spec.ts scripts/__tests__/published-packages.spec.ts : 6 fichiers, 94 tests verts (21 pour le paquet, 6 instantanés écrits).
  • pnpm exec nx run @recit/app:build, pnpm -F @recit/app typecheck, pnpm exec publint packages/tools/app : verts. pnpm check:imports et pnpm check:other verts (46 projets, une arête tool → foundation de plus, autorisée). pnpm check s'arrête sur oxfmt --check : la note APP0 de ce journal (lignes autour de 1570-1593) n'est pas au format ; le paquet, lui, passe biome et oxfmt.
  • La chaîne Rust est installée sur cette machine depuis ce matin (rustup stable 1.99, WebKitGTK 2.50.6, xvfb) avec un cache Cargo partagé dans ~/.cache/recit-app-target : la coquille matérialisée pour examples/site-embed (vraie passe tauri icon sur l'icône par défaut) compile en cargo build --release --manifest-path examples/site-embed/.recit/app/Cargo.toml en 24 s sans avertissement, binaire de 25 Mo qui embarque le dist/, et tourne 12 s sous xvfb-run sans erreur (seul un avertissement AT-SPI). APP5 part de là.

Pièges :

  • tauri::generate_context!() exige icons/icon.png et l'existence de frontendDist à la compilation : materializeShell garantit les icônes, et recit app build doit construire le site avant toute commande Cargo (y compris cargo check).
  • frontendDist se résout depuis le dossier du tauri.conf.json : ../../dist depuis .recit/app/.
  • Deux fichiers hors du périmètre d'APP1 attendent une ligne : scripts/publint-all.mjs (la table des dossiers, sans quoi pnpm publint refuse @recit/app) et scripts/__tests__/published-packages.spec.ts (RELEASED), vérifiés à la main entre-temps.

APP6 — CI desktop sur trois systèmes — 29/10

Nouveau workflow .github/workflows/app.yaml, manuel (workflow_dispatch, entrée site, défaut examples/site-embed), matrice ubuntu-24.04, macos-15, windows-2025, fail-fast: false, shell: bash partout. Étapes : checkout, pnpm, Node (.tool-versions), Rust stable par dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 (commit master, que le tag v1 pointe aussi ; la chaîne vient de l'entrée toolchain), cache Cargo par Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 (v2.9.3, clé sur le Cargo.lock du gabarit packages/tools/app/shell, cible .cargo-target à la racine, cache-on-failure), la ligne apt de §3 plus xvfb et xauth sur Linux, pnpm install --frozen-lockfile, le build du site par nx run <projet>:build (le nom vient du package.json du dossier site, ^build tire le framework, la CLI et @recit/app), puis pnpm --filter ./<site> exec recit app build --skip-site-build avec RECIT_APP_TARGET_DIR=.cargo-target ; secrets macOS (APPLE_*) et Windows (WINDOWS_CERTIFICATE*) lus en env facultatif, le job passe sans eux ; test de fumée (binaire .cargo-target/release/recit-app[.exe], sous xvfb-run sur Linux, lancé en arrière-plan et tué après 10 s s'il vit encore, sinon son code de sortie fait échouer l'étape : pas de timeout, macOS ne l'a qu'en gtimeout) ; actions/upload-artifact v7.0.2 des .deb, .AppImage, .dmg, .msi et .exe NSIS, if-no-files-found: error. Rien dans build.yaml ni dans pnpm verify.

Pas encore exécuté : aucun push depuis cette machine. Pour le lancer : onglet Actions, workflow « App », Run workflow sur main, ou gh workflow run app.yaml -f site=examples/site-embed. Le premier run compile Tauri sans cache (une dizaine de minutes par système) ; les suivants le réutilisent.

Vérifié : pnpm exec vitest run scripts/__tests__/release-workflow.spec.ts → 7 tests verts (toutes les actions épinglées par SHA) ; pnpm exec oxfmt --check .github/workflows/app.yaml vert ; biome ignore les YAML (Checked 0 files, comme build.yaml) ; analyse YAML par le paquet yaml : 11 étapes, 6 uses, 3 systèmes.

Dépend d'APP2 : recit app build --skip-site-build et @recit/app dans les dépendances de examples/site-embed doivent exister avant le premier run. À vérifier au premier run : rust-cache accepte le gabarit comme workspace (il y lance cargo metadata), les bundles atterrissent bien sous .cargo-target/release/bundle/ (Tauri suit CARGO_TARGET_DIR), et le binaire nu démarre sur macOS hors de son .app.

APP3 — Le bundle vérifié en e2e — 29/10

Le dist/ d'examples/site-embed servi comme le protocole de Tauri le résout (serveLikeTauri de @recit/app), et vérifié par Playwright sans Rust.

  • Fichiers : e2e/site-app-bundle.spec.ts (nouveau, cinq tests, aucune capture d'écran), e2e/serve-app-bundle.mjs (nouveau : node e2e/serve-app-bundle.mjs <dist> <port>, le serveur du webServer), e2e/ports.ts (APP_BUNDLE_PORT = 4379, hors de E2E_PORTS), playwright.config.ts (une entrée webServer de plus), e2e/package.json (@recit/app).
  • Ce que chaque test prouve : la page d'accueil vient d'index.html en 200 avec ses assets ; un clic vers /stories/lorem-ipsum est une navigation du routeur (un marqueur posé sur <html> survit, un rechargement l'aurait perdu) ; l'adresse profonde rechargée est servie par son propre .html (le <title> du récit, pas le repli) ; la recherche Pagefind rend des résultats et tout (pagefind.js, wasm.<lang>.pagefind, .pf_index, .pf_fragment) vient du bundle ; /nulle-part reçoit index.html en 200 et le routeur affiche « Page not found ». Chaque test relève toutes les requêtes (page.on("request")) et échoue à la première qui sort de l'origine du serveur : aucune, polices comprises.
  • Mesuré : pnpm exec playwright test e2e/site-app-bundle.spec.ts → 5 passed (30 s) ; avec --repeat-each=2 → 10 passed. check-package-boundaries vert (scope:test dépend de tout), biome, oxfmt et tsgo du paquet recit-e2e verts.
  • Non fait : pnpm test:e2e:linux e2e/site-app-bundle.spec.ts. L'image v1.64.1-noble et le volume de cache sont là, mais le conteneur reconstruit les 47 projets, ce qui dépasse les dix minutes d'une commande de l'agent, et il aurait copié les fichiers en cours d'APP2. À lancer après la vague 2. Le dist/ de l'exemple date du 28/10 (avant APP0) et n'a pas été reconstruit ici : le bundle ne dépend pas de la section app:.
  • Constats pour APP5 et le plan : (1) les assets sont référencés en absolu depuis la racine (/assets/entry-….css), pas en relatif comme le dit §4.5 ; c'est bon à la racine de tauri://localhost et c'est même mieux pour le repli index.html sur un chemin profond, et un basePath le casserait (APP0 le refuse). (2) index.html est la page d'accueil prérendue entière (/ redirige vers /index), donc un chemin inconnu reçoit le HTML de l'accueil et le routeur rend la 404 après hydratation, au prix d'une réhydratation (le test n'exige pas zéro erreur console sur ce cas) ; 404.html existe dans le bundle mais la chaîne de Tauri ne le consulte jamais. À regarder sous le vrai protocole. (3) Le wasm de Pagefind n'a pas d'extension .wasm : pagefind.js le charge en binaire et le décompresse lui-même, le type MIME application/wasm n'entre pas en jeu. (4) Un clic avant l'hydratation est une navigation complète : le test ouvre d'abord la recherche (openSearch réessaie) comme signal d'hydratation.

APP2 — La commande recit app — 29/10

Fait : recit app build|dev|preview|android|ios|icon dans @recit/cli (packages/cli/src/commands/app.ts, enregistrée dans src/index.ts), @recit/app en peer optionnel chargé à la demande par loadOptional (le motif de @recit/motion-render), et en devDependency d'examples/site-embed pour que nx voie l'arête.

  • build [dir] [--target desktop|android|ios] [--skip-site-build] : recit.config chargé (le module tel quel, options du site comprises), version du package.json du site, resolveAppConfig, diagnostics imprimés comme ceux de recit build et arrêt sur erreur ; recit-site build lancé dans le dossier du site (son node_modules/.bin, remonté jusqu'à la racine) avant toute commande Cargo ; materializeShell ; tauri build, tauri android build ou tauri ios build.
  • dev [dir] [--port] [--target] : port du recit.config (port), sinon 3000 ; isPortInUse réutilise un serveur qui répond, sinon recit-site dev --port est lancé et attendu (60 s, arrêt si le processus meurt avant d'écouter) ; coquille avec devUrl ; tauri dev ; le serveur lancé est arrêté à la fin.
  • preview [dir] [--port] : serveLikeTauri(dist), URL imprimée, jusqu'à SIGINT.
  • android|ios <init|dev|build> [--dir] [-- …] : arguments après -- transmis tels quels ; les prérequis de §3 (ANDROID_HOME, NDK_HOME, JAVA_HOME ; macOS pour iOS) sont listés sans rien lancer quand ils manquent, et un échec de tauri renvoie à la page de doc de la plateforme.
  • icon [source] [--dir] : icons/icon.png oublié puis materializeShell, qui régénère.

Mesuré :

Code
pnpm exec vitest run packages/cli 5 fichiers, 93 tests verts (23 pour recit app) pnpm -F @recit/cli typecheck vert pnpm exec nx run @recit/cli:build vert ; recit app --help depuis examples/site-embed pnpm exec nx run recit-example-site-embed:build puis recit app preview --port 4311 : / 200 text/html · /stories/lorem-ipsum 200 · /nulle-part 200 (index.html) · /pagefind/pagefind.js 200 text/javascript pnpm check vert (oxfmt, biome, frontières, version plans, liens)

Écart au plan : packages/cli/src/config/load-config.ts reçoit un champ raw (le module chargé, options du site comprises), parce que loadConfig ne rendait que les sections Récit alors que resolveAppConfig lit canonicalUrlOrigin, basePath, cdnUrl, search et site.title sur l'objet complet. Trois lignes, additives.

Pour APP5 : pnpm --filter recit-example-site-embed exec recit app build enchaîne le build du site, la coquille dans examples/site-embed/.recit/app et tauri build ; poser RECIT_APP_TARGET_DIR=~/.cache/recit-app-target pour réutiliser le cache. recit app dev ne crée pas dist/ : si tauri dev exige frontendDist, construire le site une fois. Pour le mobile, tauri android dev reçoit devUrl en localhost : à vérifier (APP7) si le --host de la CLI Tauri est nécessaire pour un appareil physique.

APP8 — iOS — 29/10

Lot écrit sur une machine Linux, d'après la documentation de Tauri 2 : rien n'a tourné sur un Mac. Livré, vérifié ici pour ce qui se vérifie sans Xcode :

  • packages/tools/app/src/ios.ts (exporté par @recit/app) : parseIosExportMethod (noms Xcode 15 app-store-connect, release-testing, debugging ; les anciens app-store, ad-hoc, development traduits ; défaut debugging), iosDevelopmentTeam (app.ios.developmentTeam puis APPLE_DEVELOPMENT_TEAM), iosEnv (pose APPLE_DEVELOPMENT_TEAM pour tauri ios init|build), iosSigning et canSignIos (certificat, profil, clé API lus dans l'environnement), iosBuildArgs (--export-method toujours explicite, --build-number optionnel), IOS_PROJECT_DIR, IOS_BUILD_DIR. Spec ios.spec.ts, 8 tests.
  • .github/workflows/app-ios.yaml : manuel, macos-15, Rust avec aarch64-apple-ios et aarch64-apple-ios-sim, rust-cache, versions Xcode au journal, build du site, CLI Tauri de @recit/app sur le PATH (la phase de script du projet Xcode l'appelle), recit app ios init -- --ci, puis sans secrets une compilation simulateur non signée (xcodebuild build … CODE_SIGNING_ALLOWED=NO, schéma *_iOS relevé par xcodebuild -list), avec secrets (APPLE_DEVELOPMENT_TEAM, IOS_CERTIFICATE, IOS_CERTIFICATE_PASSWORD, IOS_MOBILE_PROVISION) un trousseau temporaire, recit app ios build -- --export-method <entrée> --build-number <run>, artefact .ipa, envoi TestFlight optionnel par xcrun altool (APPLE_API_ISSUER, APPLE_API_KEY, APPLE_API_KEY_P8), trousseau supprimé en fin de job. Mêmes actions et mêmes SHA que app.yaml, rien de nouveau à épingler.
  • sites/docs/pages/documentation/configuration/app-ios.mdx : prérequis, section, commandes, signature, CI et secrets, TestFlight, une section « Not verified yet » et une admonition qui dit l'origine documentaire du flux.

Mesuré :

Code
pnpm exec vitest run packages/tools/app scripts/__tests__/release-workflow.spec.ts 8 fichiers, 57 tests verts (ios.spec : 8) pnpm -F @recit/app typecheck vert oxfmt --check et biome verts sur les cinq fichiers du lot pnpm check s'arrête sur packages/tools/app/src/__tests__/deck.spec.ts (APP10, en cours) ; check:imports et check:other verts (admonitions, frontmatter, frontières, interfaces, liens) YAML (paquet yaml) 17 étapes, 6 `uses`, 3 entrées (site, export_method, upload)

Non vérifié, à regarder au premier run sur un Mac (liste reprise dans la page de doc) : la phase de script Xcode trouve bien la CLI Tauri par le PATH ; le schéma se nomme <app>_iOS ; la compilation simulateur sans signature passe la phase de script ; l'.ipa atterrit dans gen/apple/build/<arch>/ ; altool --upload-app accepte encore la clé API (sinon Transporter). recit app build --target ios (APP2) ne passe pas d'--export-method : Tauri choisit le sien ; brancher iosBuildArgs dans app.ts quand APP10 aura rendu le fichier. Lancement : gh workflow run app-ios.yaml -f site=examples/site-embed -f export_method=debugging.

APP10 — Deck seul — 29/10

recit app build <story.md|.mdx> construit un deck seul en application, avec la même coquille que les sites. Fait et vérifié ; aucune commande git.

Fichiers. Créés : packages/tools/app/src/deck.ts (isStoryFile, deckProjectDir, deckOutputDir, storyTitle, resolveDeckAppConfig, STORY_EXTENSIONS) et son spec (7 tests). Modifiés : packages/tools/app/src/index.ts (un bloc d'export), packages/cli/src/commands/app.ts (branche « fichier de récit » de loadAppSite, buildDeck injectable, refus de --remote et de recit app dev sur un deck, previewApp depuis le projet du deck), packages/cli/src/__tests__/app.spec.ts (7 tests de plus), packages/cli/src/index.ts (hors périmètre, trois ajouts : import Option, --remote caché sur app build et app dev, description de build), examples/deck-pitch/recit.config.ts (app: Lorem Ipsum).

Comment ça marche. Un argument qui est un fichier, et non un dossier, est un deck : .md ou .mdx, sinon erreur. Le projet est le dossier du recit.config trouvé en remontant, sinon celui du fichier ; dist/ et .recit/app/ y vivent comme pour un site. app: est lue dans ce config par resolveDeckAppConfig : même schéma, mêmes diagnostics, et le title: du frontmatter tient lieu de productName quand la section n'en donne pas (sans section, app-missing comme pour un site). Le deck se construit en processus par buildProject (recit build, sortie relative base: "./"), ses diagnostics s'impriment sur le fichier du récit et un blocage arrête avant la coquille ; puis materializeShell et tauri build. --skip-site-build réutilise dist/. --remote : diagnostic app-remote (« No remote control in the application… »), code 1, rien ne tourne. recit app dev slides.md : app-deck-dev, renvoie vers recit app build (la boucle de dev d'un deck reste à brancher sur startDevServer, pas sur recit-site).

Vérification. vitest run de app.spec.ts et deck.spec.ts : 37 tests verts (vitest ne signale aucune erreur de type) ; pnpm -F @recit/cli typecheck, pnpm -F @recit/app typecheck, biome sur les sept fichiers touchés et pnpm check complet : verts. Vrai run : pnpm exec nx run-many -t build -p @recit/app @recit/cli, puis dans examples/deck-pitch, RECIT_APP_TARGET_DIR=~/.cache/recit-app-target recit app build slides.md : deck reconstruit (dist/index.html, titre « Lorem — Product pitch »), coquille dans examples/deck-pitch/.recit/app (productName « Lorem Ipsum Pitch », frontendDist ../../dist), 1 min 38 avec le cache, .deb 4,35 Mio, .rpm 4,35 Mio, .AppImage 95,6 Mio dans ~/.cache/recit-app-target/release/bundle/. Avertissement app-icon-default : l'exemple n'a pas de favicon. Le binaire embarque les clés assets/main.* du deck (strings), le .deb installe usr/bin/recit-app et Lorem Ipsum Pitch.desktop, et sous xvfb-run le processus vit jusqu'au kill (code 143). xdotool ne voit aucune fenêtre sous Xvfb sans gestionnaire de fenêtres, ni pour cette coquille ni pour la coquille d'essai dont la fenêtre est visible d'emblée : le titre de la fenêtre reste à vérifier à l'écran (APP5).

APP10.2, la console présentateur : non fait, la coquille appartient à APP5. Le runtime du deck ouvre sa console par window.open sur la même origine ; shell/src/window-open.js envoie aujourd'hui tout window.open au navigateur système, donc la console partirait dehors avec une URL tauri:// injoignable. À faire sur la coquille, en une passe : (1) src/lib.rs : une commande #[tauri::command] fn open_window(app, url, label) qui construit la seconde fenêtre avec le même WebviewWindowBuilder que setup (même initialization_script, même on_navigation, montrée à PageLoadEvent::Finished), enregistrée par invoke_handler ; (2) src/window-open.js : quand l'URL résolue a l'origine de location (chemin relatif compris), internals.invoke("open_window", { url, label }) au lieu de l'opener, et retour d'un proxy { closed, close() } qui appelle plugin:window|close sur ce label, pour le close() du deck ; (3) capabilities/default.json : core:window:allow-close en plus (une commande de l'application n'a pas besoin de permission ACL) ; (4) vérifier à l'écran que le canal entre les deux fenêtres (un BroadcastChannel par deck, useSlideSync.ts ; Presentation.tsx:222 ouvre la console) fonctionne entre deux webviews WebKitGTK, puis WKWebView et WebView2.

Pièges. Le titre sert tel quel : un titre à ponctuation (« Lorem — Product pitch ») deviendrait le nom des paquets, et Debian refuse un tel nom de paquet ; l'exemple donne donc un productName explicite. recit app build relance toujours recit build sans --no-strict : un deck en surcharge est bloqué, comme recit build. packages/cli/src/commands/app.ts est rendu : APP8 peut y brancher iosBuildArgs.

APP7 — Android — 29/10

Fait ici, sur la machine de travail (pas d'appareil, pas d'émulateur) : examples/site-embed en .apk et .aab signés par un keystore de test, à partir de la coquille d'APP1 et de la commande d'APP2. Aucune commande git.

  • packages/tools/app/src/android.ts (+ __tests__/android.spec.ts, 16 tests, export dans index.ts) : androidKeystoreFromEnv (RECIT_ANDROID_KEYSTORE, _KEYSTORE_PASSWORD, _KEY_ALIAS, _KEY_PASSWORD facultatif), writeKeystoreProperties (gen/android/keystore.properties, mode 600), wireReleaseSigning (un bloc android {} ajouté en fin de app/build.gradle.kts, marqué recit:signing, import java.util.Properties garanti en tête, remplacé s'il a changé), prepareAndroidSigning (l'enchaînement ; sans keystore : app-android-unsigned, propriétés périmées supprimées ; erreurs app-android-keystore-incomplete, -missing, app-android-not-initialized), androidArtifacts.
  • .github/workflows/app-android.yaml (manuel, ubuntu-24.04, setup-java v6.1.0 et setup-android v4.0.4 épinglés par SHA, cibles Rust, rust-cache, keystore en secret base64, .apk + .aab en artefacts) : spec release-workflow.spec.ts verte, jamais lancé (pas de push d'ici).
  • sites/docs/pages/documentation/configuration/app-android.mdx : prérequis réels, commandes, signature, boucle de dev sur le LAN (-- --host), limites.

Machine : openjdk-17-jdk-headless (apt), cmdline-tools 13114758 dans ~/Android/Sdk, platform-tools, platforms;android-34, build-tools;34.0.0, ndk;27.2.12479018, les quatre cibles rustup. Gradle a ajouté seul platforms;android-36, android-37 et build-tools;36.0.0 : le gabarit de Tauri 2.12 compile contre le SDK 37 (compileSdk = 37, targetSdk = 37, minSdk = 24 depuis app.android, applicationId = com.example.loremipsum).

Ce qui a marché, dans l'ordre, avec JAVA_HOME, ANDROID_HOME, NDK_HOME, CI=true, RECIT_APP_TARGET_DIR=$HOME/.cache/recit-app-target et PATH=packages/tools/app/node_modules/.bin:$PATH :

TerminalBash
pnpm --filter recit-example-site-embed exec recit app android init -- --ci --skip-targets-install # 2 s keytool -genkeypair -keystore .recit/app/gen/android/recit-test.jks -alias recittest … # test, hors git RECIT_ANDROID_KEYSTORE=… node --input-type=module -e "import { prepareAndroidSigning } from '@recit/app'; …" pnpm --filter recit-example-site-embed exec recit app android build -- --target aarch64 --apk --config '{"version":"0.1.0"}' pnpm --filter recit-example-site-embed exec recit app android build -- --target aarch64 --aab --config '{"version":"0.1.0"}'

Mesures : Rust aarch64-linux-android en release, première compilation 1 min 42 ; .apk 2 min 07 ensuite (site 25 s, Rust en cache, Gradle) ; app-universal-release.apk 21,7 Mo (la bibliothèque librecit_app_lib.so de 20,1 Mo embarque le dist/, rien dans assets/) ; app-universal-release.aab 15,2 Mo en 1 min 26. apksigner verify --print-certs : signé par CN=Lorem Ipsum, O=Example, C=FR ; aapt2 dump badging : versionName 0.1.0, versionCode 1000, minSdkVersion 24, targetSdkVersion 37, libellé « Lorem Ipsum Docs », arm64-v8a.

Pièges, dans l'ordre où ils ont mordu :

  1. recit app android build n'a pas --skip-site-build : allowUnknownOption l'envoie à tauri android build. Le site se reconstruit à chaque fois (25 s) ; le workflow ne passe pas l'option.
  2. Tauri refuse version: 0.0.0 sur Android (« must be at least 0.0.1 ») et c'est la version du package.json de l'exemple : overlay -- --config '{"version":"0.1.0"}' en attendant un app.version dans l'exemple ou un diagnostic d'erreur pour la cible Android.
  3. Le projet Gradle généré rappelle la CLI par pnpm tauri android android-studio-script depuis la coquille, qui n'a pas de node_modules : « Command "tauri" not found ». pnpm exec retombe sur le PATH, d'où le .bin de @recit/app dans le PATH. Sans npm_execpath (la CLI lancée hors pnpm), Tauri écrit node + un argument nu tauri dans BuildTask.kt, qui ne peut pas marcher : garder le flux pnpm.
  4. Dans un script Kotlin de Gradle, java.util.Properties se lit project.java… : d'où l'import.
  5. packages/theme/dist a été reconstruit par un autre lot au milieu d'un build du site, qui a échoué une fois (UNLOADABLE_DEPENDENCY) ; relancé.
  6. Le build client du site avertit : node:path externalisé, importé par packages/core/dist/config/app.js (APP0) ; @recit/core/config entre dans le bundle navigateur.
  7. tauri android init relancé régénère BuildTask.kt et build.gradle.kts mais garde keystore.properties et le .jks ; le bloc de signature se remet en place seul.

Reste : (a) dans @recit/app, tauriEnv ajoute path.dirname(tauriBin())/../../.bin (le node_modules/.bin du paquet qui porte @tauri-apps/cli) au PATH ; (b) dans @recit/cli, mobileApp appelle prepareAndroidSigning(shell.shellDir, process.env) avant tauri android build et imprime ses diagnostics, et les commandes android|ios gagnent --skip-site-build ; (c) app.version ou un diagnostic pour 0.0.0 sur Android ; (d) le premier run du workflow ; (e) adb install sur un appareil et le mode avion, recit app android dev -- --host sur un émulateur : impossibles ici, la doc les décrit d'après la CLI.

APP5 — La coquille compilée et le premier binaire — 29/10

Tout vérifié sur cette machine (Debian sans écran, Rust 1.99, WebKitGTK 2.50.6), avec le cache Cargo partagé RECIT_APP_TARGET_DIR=~/.cache/recit-app-target. La coquille de @recit/app n'a rien eu à corriger : ce qu'elle promet (plan 18 §4.3, §4.6, §4.8) tient sur un vrai binaire.

  • APP5.1 — cargo check --manifest-path examples/site-embed/.recit/app/Cargo.toml : vert (1 min 09, métadonnées du profil dev). pnpm --filter recit-example-site-embed exec recit app build : build du site, coquille « Lorem Ipsum Docs » 0.0.0 (com.example.loremipsum), tauri build, 2 min 17 en tout dont 55 s de compilation de la seule caisse recit-app.
  • APP5.2 — Tailles : binaire nu 25 140 568 o ; .deb 14 082 630 o (13,4 Mio, paquet lorem-ipsum-docs, binaire /usr/bin/recit-app) ; .rpm 14 088 028 o ; .AppImage 109 771 256 o (104,7 Mio, WebKitGTK embarqué). Le binaire nu et l'AppImage (FUSE présent, pas besoin de --appimage-extract-and-run) ouvrent sous Xvfb la fenêtre « Lorem Ipsum Docs » 1280 × 800, Map State: IsViewable, deux processus WebKit, vivants après 20 s et 25 s, rien sur stderr hors l'avertissement AT-SPI. Le dist/ embarqué est le bon : strings du binaire donne les clés /index.html, /stories/lorem-ipsum.html, /pagefind/pagefind.js.
  • APP5.3 — Garde de navigation prouvée avec une coquille de test en debug (devUrl vers une page locale) et un faux xdg-open en tête de PATH qui journalise ses arguments : la page déclenche window.open("https://example.com/popup"), un clic sur <a target="_blank"> et location.href = "https://example.com/external". Le journal reçoit exactement ces trois URL, la page reste en place (elle répond encore still-here avec son titre 2 s plus tard), et la coquille trace on_navigation https://example.com/external refusé. La console présentateur d'un deck publié reste à vérifier en APP10.
  • APP5.4 — Fenêtre cachée jusqu'au chargement : avec une page servie après 4 s, aucune fenêtre X n'existe pendant 3 s (la requête est pourtant partie), puis la fenêtre apparaît au Finished, à la taille d'app.window (1024 × 700 dans le test), titre = productName.
  • APP5.5 — Cache partagé entre deux sites : coquille du site docs (« Récit Docs », dev.recit.docs) construite dans le même target : une seule ligne Compiling (recit-app), 36 s, 42 s avec .deb et .rpm ; Tauri n'est pas recompilé. .deb docs : 14 776 116 o.
  • recit app dev (question d'APP2) : fonctionne de bout en bout sous Xvfb : recit-site dev sur 3000 réutilisé ou lancé, tauri dev, fenêtre « Lorem Ipsum Docs » après 113 s (profil debug, dépendances déjà en cache). dist/ n'est pas nécessaire : la coquille de test s'est compilée en debug avec devUrl dans un dossier sans dist/.

Pièges et constats, pour les lots suivants :

  • D-Bus : sur cette machine le bus de session existant fait bloquer l'enregistrement GTK (g_application_register → g_dbus_proxy_new_sync, 25 s, vu à gdb) ; l'application semble muette pendant ce temps. Lancer les tests de fumée sous dbus-run-session -- <binaire> (fenêtre en 2 s) ; APP6 devrait l'ajouter au job Linux, ou laisser plus de 25 s avant de conclure. xvfb-run exige xauth ; x11-utils (xwininfo), xdotool et gdb installés pour sonder.
  • productName non ASCII : le .rpm de « Récit Docs » échoue (invalid name "récit-docs") et le .deb produit est invalide pour dpkg (champ Package avec un caractère interdit). À traiter dans tauriConfig() / resolveAppConfig : nom de paquet dérivé en ASCII, ou diagnostic.
  • Un seul nom de binaire : chaque application installe /usr/bin/recit-app ; deux apps Récit sur la même machine se marcheraient dessus. mainBinaryName (Tauri 2) dérivé de l'identifiant règle les deux points précédents.
  • RECIT_APP_TARGET_DIR partagé : le bundler AppImage vide bundle/appimage/ à chaque build, le build de deck d'APP10 a effacé l'AppImage de site-embed (reconstruite ensuite) ; .deb et .rpm coexistent. recit app build devrait copier ses paquets sous <site>/.recit/app/bundle/ et imprimer ce chemin.
  • recit app build sur les sites Récit (docs, plans, www…) échoue au chargement du recit.config : le loader esbuild de la CLI ne connaît pas import.meta.glob de @recit/site-kit (PackageTables.tsx:13) → (intermediate value).glob is not a function. Seuls les sites sans site-kit (examples/site-embed) passent ; APP5.5 a matérialisé la coquille docs par materializeShell directement. À régler dans packages/cli/src/config/load-config.ts.
  • pnpm check : fmt:check, check:imports, check:other verts ; biome lint échoue sur examples/site-embed/.recit/app/gen/android/build/reports/** (projet Android d'APP7), que biome ne lit pas dans .gitignore : ajouter !**/.recit/** aux includes de biome.json.
  • Fichiers : packages/tools/app/src/__tests__/shell-rust.spec.ts (4 tests qui verrouillent le gabarit : caisse Tauri 2 mobile, fenêtre construite dans setup et montrée au Finished, on_navigation + window-open.js, capability sans remote) ; sites/docs/recit.config.ts (app: { productName: "Récit Docs" }). pnpm exec vitest run packages/tools/app : 8 fichiers, 56 tests verts.

APP11b — Intégration @recit/app, cœur, CI, doc — 29/10

Lot d'intégration créé par le coordinateur d'après les constats d'APP5 et d'APP7, côté @recit/app, @recit/core, CI et documentation (la CLI est le lot APP11a, la coquille le lot APP9).

  • Plus de node:path dans @recit/core/config : packages/core/src/config/app.ts prenait le nom du dossier du site par path.basename, et le module atteint le bundle navigateur par @recit/core/config (avertissement « node externalized » au build des sites). Remplacé par une découpe de chaîne (directoryName), tous séparateurs et barre finale compris ; aucun autre fichier de packages/core/src/config n'importe un module Node. Test ajouté ; le build de site-embed sans cache n'émet plus l'avertissement.
  • Noms de fichiers des applications. Tauri dérive le nom du paquet Debian et RPM du productName, et les deux refusent un nom hors ASCII (Récit Docs → récit-docs, APP5). Deux aides exportées par @recit/core/config : asciiProductName (NFD, diacritiques retirés, le reste hors ASCII supprimé : Récit Docs → Recit Docs) et binaryNameFromIdentifier (dev.recit.docs → recit-docs, com.example.loremipsum → example-loremipsum). Dans tauriConfig() (shellNames) : productName = forme ASCII (ou le nom du binaire s'il ne reste rien), mainBinaryName = nom tiré de l'identifiant, et le titre de la fenêtre garde le nom exact. Deux applications ne partagent donc plus /usr/bin/recit-app. resolveAppConfig émet l'avertissement app-product-name-ascii (chemin /app/productName). Les six instantanés de tauri-config.spec.ts sont réécrits pour la clé mainBinaryName ; deux tests du cœur qui attendaient zéro diagnostic avec « Récit Docs » attendent désormais cet avertissement.
  • tauriEnv : le node_modules/.bin qui porte le shim tauri (tauriBinDir, cherché en remontant depuis le paquet) passe en tête du PATH, et npm_execpath est posé depuis le pnpm ou le npm trouvé sur le PATH quand il manque (packageManagerPath) : le projet Android généré rappelle la CLI par pnpm tauri … depuis la coquille sans node_modules. Tests avec de faux répertoires.
  • examples/site-embed/recit.config.ts : app.version: "0.1.0" (Android refuse 0.0.0).
  • .github/workflows/app.yaml : dbus dans la ligne apt, test de fumée sous xvfb-run -a dbus-run-session -- (25 s d'attente D-Bus sinon), binaire trouvé dans .cargo-target/release puisqu'il porte désormais le nom de l'identifiant. Toutes les actions restent épinglées par SHA.
  • sites/docs/pages/documentation/configuration/app.mdx : section « Names » (règle ASCII, mainBinaryName), ligne app-product-name-ascii, installateurs copiés dans .recit/app/bundle/ (APP11a) avec RECIT_APP_TARGET_DIR, test de fumée sans écran, tailles mesurées (.deb 13,4 Mio, .AppImage 105 Mio, .apk 21,7 Mo, .aab 15,2 Mo).

Vérification :

Code
pnpm exec vitest run packages/tools/app packages/core scripts/__tests__ Test Files 1 failed | 35 passed (36) Tests 1 failed | 493 passed (494) (l'échec : shell-rust.spec.ts « updates itself only when the updater is configured », lot APP9 en cours) pnpm -F @recit/app typecheck && pnpm -F @recit/core typecheck && pnpm exec nx run @recit/app:build verts (exit 0) pnpm exec nx run recit-example-site-embed:build --skip-nx-cache | grep node:path no node:path warning pnpm exec vitest run scripts/__tests__/release-workflow.spec.ts 7 tests verts ; app.yaml : 11 étapes, 6 uses, 3 systèmes pnpm check biome : 31 avertissements préexistants, aucune erreur ; oxfmt : un seul fichier hors format, packages/tools/app/shell/src/window-open.js (APP9 en cours) ; check:imports et check:other verts (admonitions, frontmatter, 15 version plans, frontières de 46 projets, liens).

Reste : APP11a copie les installateurs dans <site>/.recit/app/bundle/ et appelle prepareAndroidSigning (la doc le suppose déjà) ; le nom affiché sous Linux et macOS perd son accent (Recit Docs, le menu comme le .app), la fenêtre non ; sans gestionnaire de paquets sur le PATH, npm_execpath reste absent et le projet Android généré ne sait pas rappeler la CLI ; le workflow app.yaml n'a toujours pas tourné (aucun push d'ici).

Plan 19 — Style : Mantine et CSS modules — 29/10

Nouveau plan sites/plans/pages/19-style.md, écrit à la demande de l'utilisateur et non implémenté : tout le style passe sous Mantine 9 avec un seul mécanisme, un *.module.css par composant, et plus aucune classe utilitaire ni style dans le JSX. Le plan part de trois inventaires en lecture seule (947 className dans le framework, 54 composants ui/ dont 27 sans importeur, 21 paquets Radix ; inline partout dans les blocs motion ; Tailwind compilé par la CLI, site-stories et Storybook), fixe la loi (§0.3), la cible (Mantine, PostCSS, couches, thème depuis theme:, mode de couleur par attribut, typographie), les conventions des modules, une table de correspondance composant par composant, le protocole de captures avant/après (couverture étendue, galerie HTML, baselines Linux en conteneur par vague), le script check-style-rule.js, le skill mantine-style, dix-sept lots en sept vagues, la branche style/mantine et treize questions ouvertes. Deux relectures ont corrigé les dépendances entre lots d'une même vague (typographie et main.css regroupés dans STY5, captures « avant » seules en vague 0 parce que le conteneur e2e copie l'arbre entier).

Pour reprendre : lire le plan, répondre aux questions de §13 (props de style Mantine, Spotlight, deck.css global, défauts zinc ou violet, version), puis demander la branche ; rien ne se lance sans cette demande.

APP11a — Intégration CLI — 29/10

Lot d'intégration côté CLI, créé par le coordinateur d'après les constats d'APP5, APP7, APP8 et APP10. Fichiers : packages/cli/src/config/load-config.ts, packages/cli/src/commands/app.ts, packages/cli/src/index.ts, packages/cli/src/__tests__/{app,cli}.spec.ts. Aucune commande git.

Fait :

  • load-config.ts : plugin esbuild importMetaGlobPlugin, qui remplace import.meta.glob() (motif ou tableau de motifs littéraux, options eager, import, query) par un objet d'imports comme Vite, clés relatives au fichier qui l'appelle (à la racine de la config pour /…), * et ** seulement, ni accolades ni négations. Les sites Récit (@recit/site-kit, PackageTables.tsx) se chargent en Node : fini (intermediate value).glob is not a function.
  • recit app build : les paquets produits sont copiés dans <site>/.recit/app/bundle/<type>/ (fichiers plus récents que le build seulement, car le dossier partagé RECIT_APP_TARGET_DIR est vidé par le bundler AppImage) et imprimés avec leur taille ; Android : androidArtifacts ; iOS : .ipa et .xcarchive de gen/apple/build.
  • Android : prepareAndroidSigning avant tauri android build, diagnostics imprimés, arrêt sur erreur ; --skip-site-build est une option de recit app android|ios (il filait vers Tauri).
  • iOS : iosBuildArgs (--export-method, défaut debugging, --build-number), options de app build et de app ios build, un --export-method après -- gagne ; iosEnv (APPLE_DEVELOPMENT_TEAM) sur init, dev et build.
  • Version 0.0.0 refusée pour android et ios (app-version-required), avertissement seul sur desktop. Description de app build : paquets dans .recit/app/bundle.
  • Tests : sept de plus dans app.spec.ts (signature et son erreur, iOS, version, copie des paquets, --skip-site-build mobile, options CLI), un dans cli.spec.ts (glob eager, lazy, import, query, /…).

Mesures :

  • pnpm exec vitest run packages/cli : 5 fichiers, 105 tests verts ; pnpm -F @recit/cli typecheck et nx run @recit/cli:build verts.
  • examples/site-embed, recit app build --skip-site-build avec RECIT_APP_TARGET_DIR=~/.cache/recit-app-target : 3 min 04, trois paquets copiés dans .recit/app/bundle/ (deb 15,1 Mio, rpm 15,1 Mio, AppImage 106,2 Mio), binaire example-loremipsum 0.1.0.
  • sites/docs, même commande : la config se charge, mais app-identifier-missing : aucun site Récit ne pose canonicalUrlOrigin (createSiteConfig ne le fait pas), il faut identifier: "dev.recit.docs" dans la section app: de sites/docs/recit.config.ts, hors de mes fichiers.
  • pnpm check : rouge sur oxfmt pour packages/tools/app/shell/src/window-open.js (APP9, en cours), le reste vert ; 31 avertissements biome préexistants.

Pièges : une reconstruction concurrente des paquets (tsdown vide dist/) fait échouer un recit app build lancé au même moment (@recit/core/dist ou @recit/site-stories/dist introuvable) : relancer. Les options de import.meta.glob doivent être des littéraux.

APP9 — Mise à jour automatique, et APP10.2 — Console présentateur — 29/10

Fait et vérifié sur cette machine, sans commande git.

Coquille (packages/tools/app/shell/) :

  • Cargo.toml : tauri-plugin-updater = "2" (résolu 2.14.1) en dépendance desktop seulement ([target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]) ; pas de tauri-plugin-process, le redémarrage est AppHandle::restart() de Tauri. Cargo.lock mis à jour par le build.
  • src/lib.rs : le plugin updater n'est enregistré que si tauri.conf.json a plugins.updater (contains_key("updater") sur le contexte) ; au démarrage, check() puis une fenêtre de proposition servie par un schéma recit-update:// de la coquille (HTML dans lib.rs, pas de plugin dialog : aucune dépendance de plus) avec « Install and restart » (ou Entrée) et « Later » (ou Échap) ; commandes install_update (download_and_install, puis app.restart()) et skip_update. Commande open_window(url, label, width, height) : une page du site demandée par window.open ouvre une seconde fenêtre native avec le même script, la même garde on_navigation et le même affichage à Finished (site_window() sert aux deux) ; un label déjà ouvert navigue et prend le focus ; WindowEvent::Destroyed émet recit:window-closed.
  • src/window-open.js : même origine que location → open_window et un proxy { closed, close() } (plugin:window|close, closed suivi par l'événement) ; autre origine → opener.
  • capabilities/default.json : core:window:allow-close en plus. Les commandes updater ne passent jamais par le webview, donc ni updater:default ni permission mobile.

Prouvé :

  • Cycle complet 0.1.0 → 0.1.1 (scratchpad/app9/cycle.sh) : deux AppImage signées par une clé de test (tauri signer generate, clé privée hors dépôt), latest.json servi en local ; l'app 0.1.0 journalise update 0.1.1 available, ouvre la fenêtre « Update » (440×200), Entrée → installing update 0.1.1, update downloaded (GET de l'AppImage par tauri-plugin-updater/2.14.1, signature vérifiée), update installed, restarting, nouveau processus, up to date ; l'AppImage sur disque est octet pour octet le build 0.1.1.
  • Seconde fenêtre (scratchpad/app9/win/, coquille en tauri dev sur une page locale) : window.open("/second.html", "presenter-test", "popup,width=800,height=600") rend un proxy, une fenêtre native 800×600 apparaît, BroadcastChannel passe dans les deux sens entre les deux webviews WebKitGTK (second got hello, first got world), popup.close() ferme la fenêtre et closed passe à true ; window.open externe et target=_blank externe vont à xdg-open.
  • cargo check de examples/site-embed/.recit/app (cache partagé) vert ; vitest packages/tools/app scripts/__tests__/release-workflow.spec.ts : 9 fichiers, 69 tests ; pnpm check vert.

Workflow .github/workflows/app-release.yaml (nouveau) : manuel, trois systèmes, build signé (TAURI_SIGNING_PRIVATE_KEY, _PASSWORD exigés), artefacts renommés sans espace, latest.json composé par un script Node depuis les .sig, GitHub Release en brouillon par gh. Non exécuté : gh workflow run app-release.yaml -f site=examples/site-embed. Dépend de createUpdaterArtifacts dans la config générée (ci-dessous).

À faire ailleurs :

  • tauriConfig() : bundle.createUpdaterArtifacts: true quand app.updater est renseigné (sans lui, tauri build n'émet aucun .sig ; patché à la main dans les coquilles de test).
  • resolveAppConfig : erreur si un endpoint n'est pas https (le plugin refuse l'app au démarrage : PluginInitialization) ; les tests locaux passent par dangerousInsecureTransportProtocol.
  • Dans examples/deck-pitch, le raccourci s ouvre la console par ce chemin ; non piloté au clavier sous Xvfb (le clic et la touche synthétiques n'atteignent pas la page du deck).

Pièges : un tauri build concurrent dans le même CARGO_TARGET_DIR fait échouer linuxdeploy (le bundler vide bundle/appimage/) et renomme le binaire ; les tests ont un target privé. Sans gestionnaire de fenêtres, aucune fenêtre n'a le focus : openbox (installé) sous xvfb-run rend windowactivate et les touches XTEST fiables ; le clic synthétique n'est pas reçu par WebKitGTK. Tauri ne reporte pas document.title sur le titre natif. Sous dbus-run-session, la fenêtre arrive en 2 s.

Bilan du 29/10 — Plan 18 exécuté, plan 19 écrit — 29/10

Coordination de la journée (un agent par lot, en vagues, sur le même arbre). Tous les lots APP0 à APP11 du plan 18 sont livrés ; la table « État au 29/10/2026 » en tête du §5 du plan résume lot par lot, et chaque lot a sa note ci-dessus.

Corrections faites par le coordinateur entre les lots : exclusion de .recit/ dans biome.jsonc ; identifier: "dev.recit.docs" dans sites/docs/recit.config.ts ; ligne app dans la table de recit-config.mdx ; bundle.createUpdaterArtifacts dans tauriConfig() quand app.updater est renseigné ; diagnostic app-updater-endpoint-insecure (endpoints https obligatoires) ; un code span coupé par une ligne vide dans app-android.mdx qui cassait le build de la doc.

Vérification finale : pnpm verify vert (60 tâches de build et typecheck, 1 591 tests unitaires) ; e2e en conteneur, après régénération de quatre références Linux changées à dessein (docs-documentation-configuration-recit-config, docs-documentation-reference-architecture, presentation-demos-pitch, presentation-demos-site) : 182/182 au premier passage, 181/182 au second avec le React #418 intermittent connu de /custom-components (voir le 26/10).

Reste hors de portée de cette machine : les quatre workflows (app.yaml, app-android.yaml, app-ios.yaml, app-release.yaml) n'ont jamais tourné, il faut un push ; l'installation d'un .apk sur un appareil et la boucle de dev sur le LAN ; tout iOS (Mac) ; la console présentateur d'un vrai deck par le raccourci sous Xvfb (le mécanisme est prouvé sur une page d'essai) ; recit app dev d'un deck seul n'est pas branché (diagnostic app-deck-dev).

Machine : Rust stable, WebKitGTK, xvfb, xauth, openbox, x11-utils, xdotool, gdb, dbus, JDK 17, SDK Android 34 + NDK 27.2 et les cibles Rust Android installés ce jour ; cache Cargo partagé ~/.cache/recit-app-target (RECIT_APP_TARGET_DIR). Le commit « Claude limit » est un point de contrôle de l'utilisateur pendant une coupure de limite d'usage ; les agents coupés ont été repris avec leur contexte.

Plan 19 (19-style.md, Mantine et CSS modules) : écrit, relu trois fois, pas implémenté ; il attend la demande explicite de l'utilisateur et la branche style/mantine qu'il créera lui-même.

STY10.1 — Skill mantine-style et CLAUDE.md — 29/10

Premier lot du plan 19 (vague 1, indépendant de la branche). Créés : .claude/skills/mantine-style/SKILL.md (188 lignes : quand l'utiliser, la loi de §0.3 en anglais, permis, interdit, exceptions, la recette d'un composant avec Callout.tsx + Callout.module.css, les conventions de §3, la recette de migration d'un composant Tailwind, le mode sombre et le pont .dark jusqu'à STY9, Shiki, la vérification et la liste de contrôle) et .claude/skills/mantine-style/tailwind-to-css.md (table de 70 utilitaires Tailwind → CSS module ou Mantine). Modifié : AGENTS.md (CLAUDE.md est un lien symbolique vers lui, rétabli après qu'un perl -i l'avait remplacé par une copie), section « Styling » après « UI », marquée en cours sur la branche style/mantine, avec la loi en deux puces et le renvoi au skill ; rien d'autre n'a bougé (STY10.2 fera la forme finale : « Core tech », « UI », « Bundle Size »).

Vérification : oxfmt sur les quatre fichiers, diff CLAUDE.md AGENTS.md vide, pnpm check vert (biome, formats, imports, frontmatter, admonitions, version plans, frontières, interfaces, liens). Aucune commande git.

À savoir pour la suite : le skill cite scripts/check-style-rule.js, useCssVariable, scripts/style-rule-allowlist.json et la galerie, qui n'existent qu'à partir de STY0 et STY1 ; il le dit à chaque mention. Les valeurs de l'échelle Mantine dans la table (espacements, rayons, points de rupture) sont celles du thème par défaut de Mantine 9 : à relire quand STY2 fixe createRecitTheme.

STY1 — Captures avant — 29/10

Vague 0 du plan 19, sur l'arbre intact. Le jeu « avant » est généré, la galerie existe, le skill est à jour. Aucune commande git.

Couverture ajoutée (playwright.config.ts, e2e/*.spec.ts) :

  • Trois projets Playwright : chromium (inchangé, mêmes noms de référence), chromium-dark (tous les site-*.spec.ts avec le choix mémorisé theme de next-themes et recit-theme des decks posés à dark par storageState, plus colorScheme: "dark" ; références -dark-linux.png) et mobile (390 × 844, tactile, tests @mobile de site-docs.spec.ts seulement ; -mobile-linux.png). Le helper darkMode(page) de e2e/support.ts fait la même chose pour un test isolé ; SITE_THEME_STORAGE_KEY y est exporté pour STY2.
  • site-plans.spec.ts : les 28 pages du site en pleine page (tallPageScreenshot coupe à 16 000 px les pages plus hautes, support.ts) ; /journal exclu, il change à chaque lot sans raison visuelle.
  • site-docs.spec.ts : les trois pages app*, la 404 (/404, la page prérendue : recit-site preview répond un 404 nu sur un chemin inconnu), la recherche ouverte avec un résultat, l'en-tête et le pied de page seuls, et quatre captures mobiles (accueil, recit-config, /present/showcase, tiroir de navigation ouvert). Pas de bannière : aucun site n'en configure.
  • site-motion.spec.ts (/present/bridge), site-i18n.spec.ts (index et récit anglais), site-app-bundle.spec.ts (accueil, récit, 404, assertions DOM gardées), deck-features.spec.ts (console présentateur en sombre, ?print=1 de showcase).
  • storybook.spec.ts (nouveau) : les 16 stories de index.json en clair et en sombre par iframe.html?id=…&globals=theme:…, servies depuis sites/storybook/storybook-static sur le port 4380 par e2e/serve-app-bundle.mjs ; un test vérifie que la liste suit l'index.
  • Le rapport JSON de Playwright (test-results/report.json) alimente la galerie.

Références : 213 -linux.png avant, 443 après (+221 -dark, +4 -mobile, 56 plans, 32 Storybook, le reste docs, motion, i18n, bundle, deck) ; aucune référence existante modifiée. Passages en conteneur : génération 444/444, passage 1 444/444, passage 2 442/444 avec le React #418 intermittent connu (26/10) sur presentation /demos/course en clair et en sombre, puis la spec site-presentation seule 50/50. Après la mesure écrite dans 19-style.md §7.3 (page /19-style), ses deux références sont régénérées par pnpm test:e2e:update:linux e2e/site-plans.spec.ts et confirmées par un passage.

Mesure d'un passage ([e2e-linux] <phase> dans scripts/e2e-linux.sh, 8 cœurs, caches chauds) : 7 à 8 min au total, dont 1 s de copie, 25 à 45 s d'installation, 2 à 2 min 30 de construction, 4 min 20 à 5 min 20 de tests pour 444 tests. STY1.5 (saut d'installation) : hors périmètre, il ne rendrait que 30 s.

Galerie : node scripts/screenshot-gallery.js écrit test-results/gallery/index.html depuis test-results/linux/report.json (avant, après, diff, ratio de pixels, filtre par spec) ; node scripts/screenshot-gallery.js --serve 4390 la sert sur toutes les interfaces (machine sans écran) et le bouton « Accept » enregistre test-results/gallery/decisions.json par POST. Test scripts/__tests__/screenshot-gallery.spec.ts (5 tests, diff fabriqué, GET, POST). Le script est en .js et non .mjs : la règle useImportExtensions de biome ne connaît pas .mjs.

Navigateur : chaque entrée webServer reçoit env: { ...process.env, BROWSER: "none" } (recit-site preview héritait de server.open et, par le $BROWSER de VS Code Remote, ouvrait un onglet par site sur le poste de l'utilisateur) ; toute commande locale de ce lot a tourné avec BROWSER=none exporté.

Vérifications : pnpm -F recit-e2e typecheck, biome, oxfmt, pnpm exec vitest run scripts/__tests__ (6 fichiers, 127 tests), pnpm check verts.

Reste : .claude/skills/mantine-style/SKILL.md (STY10.1) nomme encore screenshot-gallery.mjs ; les références -darwin ne sont pas régénérées (poste local) ; test-results/ n'est pas exclu de biome.jsonc (le rapport JSON y passe sans erreur aujourd'hui).

Plan 20 — Exécution multi-agent — 29/10

Nouvelle page sites/plans/pages/20-multi-agents.md, écrite par le coordinateur d'après la journée : ce que l'outillage permet (forks, agents frais, Explore, reprise après coupure, Workflow, skills), les limites mesurées (budget par fenêtre de cinq heures, un seul conteneur e2e, verrou Cargo, dist/ vidé par tsdown, lockfile, processeur, fichiers partagés, navigateur de l'utilisateur, processus orphelins, contexte du coordinateur), les rôles et le protocole (porte entre vagues, brief type en dix éléments, reprise), le découpage le plus parallèle des vagues restantes du plan 19 (vingt-trois agents, jamais plus de cinq à la fois dont trois lourds ; STY0 redécoupé en STY0a à STY0d dès la vague 1, STY5 et STY6.3 en deux), le budget et ce que l'utilisateur voit. Le plan 21 (tests et Storybook) est en cours d'écriture par un agent.

Plan 21 — Tests et Storybook — 29/10

Nouveau plan sites/plans/pages/21-tests.md : Storybook comme spécification exécutable de l'interface — une story par composant exporté, un play par comportement, l'accessibilité et une capture en clair et en sombre par story — exécuté par l'addon Vitest de Storybook dans Chromium (Playwright déjà présent ; test-runner écarté, stories portables en complément). Inventaire de l'existant (1 591 tests, 24 .test.tsx de rendu sans helper partagé, 16 stories, 443 références e2e, goldens motion), structure de Storybook (titres par surface, décorateurs withMantine, withSite, withDeck, withFilm, harnais des modules virtuels du framework), catalogue des stories par composant, conventions (rôles et noms, pas de snapshot de DOM, data-testid listé, la loi du plan 19 pour les stories), compteur check-stories.js et cliquet de couverture, câblage CI.

Sept lots TST (TST-A filet comportemental e2e, TST0 socle, TST1 à TST4 stories par surface, TST5 clôture), dix agents, imbriqués dans les vagues 2 à 6 du découpage de 20-multi-agents.md §4 sans allonger le plan 19. Pas d'implémentation avant la demande de l'utilisateur ; questions ouvertes en §10 (projet storybook dans pnpm test, sort des .test.tsx de rendu, captures par story).

STY0d — Le compteur de la règle — 29/10

STY0.5 du plan 19, en mode avertissement. Fichiers : scripts/check-style-rule.js (nouveau, .js : useImportExtensions refuse .mjs), scripts/style-rule-baseline.json (généré), scripts/style-rule-allowlist.json, scripts/__tests__/check-style-rule.spec.ts, la ligne check:other de package.json. Aucune dépendance : le script lit l'AST avec typescript, déjà dépendance racine, ce qui donne les formes tolérées de §8.1-1 (classes.x, condition entre deux classes.*, className relayé) sans regex fragile. Aucune commande git.

Ce qu'il compte (§8.1, les sept signaux, par dossier) : (1) un className dont l'attribut contient une chaîne ou un gabarit, directement ou dans l'expression (cn("…", x) compte, pas cn(classes.a, className)) ; (2) style={…} et style: dans les props d'un createElement, hors fichiers de la liste d'exclusion, dont le compte est affiché à part ; (3) les props de style Mantine de §8.1-3, sur un composant seulement (<svg opacity> est du SVG) ; (4) les imports de clsx, tailwind-merge, class-variance-authority, tailwindcss et @tailwindcss/*, cmdk, vaul, next-themes, @radix-ui/*, @base-ui/*, y compris import type, export … from et import() ; (5) tout bloc d'un .css hors .module.css, packages/theme et sites/*/styles.css ; (6) @apply, @theme, @custom-variant, @source, @plugin, @utility et la variante dark: (pas --dark: ni .dark:is()) dans tout .css ; (7) un .tsx avec un className et sans import de module. Racines : packages/, sites/*/src, sites/storybook/.storybook, examples/*/components pour les scripts ; packages/, sites/* (et .storybook, que le glob n'entre pas seul), examples/*/components pour les feuilles ; exclus tests, stories, __tests__, fixtures, dist, node_modules, storybook-static. Modes : défaut ou --baseline (échec sur une hausse par signal et dossier, avec un rappel de --write-baseline quand un compte a baissé), --strict (échec sur tout compte, STY9), --write-baseline [dossier] (ne régénère que les dossiers donnés, le reste est gardé), un argument dossier restreint le relevé et la comparaison. Liste d'exclusion : engine.tsx, player.tsx, ui/render.tsx de motion-blocks, RemotionAnim.tsx, useCssVariable.ts (créé par STY0a) et ThemeVars.tsx marqué "pending": "STY7" ; une entrée sans pending dont le fichier manque fait échouer le contrôle.

Baseline (1 217 occurrences, 34 dossiers, 589 fichiers lus, régénération identique au bit près) : packages/site 816 (className 509, style 20, imports 62, css 75, directives 41, sans module 109) ; autres paquets hors motion 293 (className 187 : deck 83, components 47, site-stories 32, site-kit 25 ; style 31 ; css 40 dans site-stories.css ; imports 2 dans cli/bundle) ; motion-blocks et motion-render 59 style comptés, plus 70 dans les quatre fichiers de la liste d'exclusion ; sites 40 (className 27 dont www 22) ; exemples 9. Aucune prop de style Mantine. Écarts avec §1 : les 947 className du framework comptaient chaque occurrence du mot (956 aujourd'hui, dont les props déstructurées et les relais cn(className)) ; le signal 1 ne compte que les attributs porteurs d'une chaîne, 509 (§1 : 288 littéraux + 219 cn(). Les 160 des paquets et les 60 des sites et exemples se retrouvent (187 et 59 avec site-kit), §1 ne comptant que className=". Les 118 style de motion-blocks sont 58 dans blocks/ plus 69 dans les fichiers exclus (64 style: dans render.tsx, 3 dans player.tsx, 2 dans engine.tsx).

Vérifications :

Code
$ node scripts/check-style-rule.js Checking the style rule (sites/plans/pages/19-style.md §0.3) on 589 files... directory className style props imports css directives no-module total examples/deck-mdx/components 3 0 0 0 0 0 1 4 examples/deck-showcase/components 4 0 0 0 0 0 1 5 packages/cli/src/bundle 0 0 0 2 0 0 0 2 packages/cli/src/motion 0 10 0 0 0 0 0 10 packages/components/src/mdx 9 1 0 0 0 0 1 11 packages/components/src/slides 38 2 0 0 0 0 9 49 packages/formats/deck/src/deck 50 9 0 0 0 0 4 63 packages/formats/deck/src/mdx 1 1 0 0 0 0 1 3 packages/formats/deck/src/runtime 1 0 0 0 0 0 1 2 packages/formats/deck/src/slides 2 3 0 0 0 0 1 6 packages/formats/deck/src/tools 29 1 0 0 0 0 4 34 packages/formats/motion-blocks/src/blocks 0 58 0 0 0 0 0 58 packages/formats/motion-render/src 0 1 0 0 0 0 0 1 packages/integrations/site-stories/src 26 4 0 0 40 0 5 75 packages/integrations/site-stories/src/motion 6 0 0 0 0 0 1 7 packages/site/src/app 1 0 0 0 75 41 1 118 packages/site/src/lib 1 0 0 0 0 0 1 2 packages/site/src/lib/assets/language-icons 16 0 0 0 0 0 11 27 packages/site/src/lib/components 149 11 0 8 0 0 29 197 packages/site/src/lib/components/navigation 57 3 0 3 0 0 10 73 packages/site/src/lib/errors 4 0 0 0 0 0 2 6 packages/site/src/lib/hooks 0 0 0 1 0 0 0 1 packages/site/src/lib/plugins/markdown 24 0 0 0 0 0 1 25 packages/site/src/lib/plugins/search-pagefind 19 0 0 2 0 0 3 24 packages/site/src/lib/ui 234 6 0 45 0 0 50 335 packages/site/src/lib/util 4 0 0 2 0 0 1 7 packages/site/src/vite 0 0 0 1 0 0 0 1 packages/theme/src 0 0 0 0 0 2 0 2 packages/tools/site-kit/src/components 25 0 0 0 0 0 4 29 sites/components/src 1 0 0 0 1 0 1 3 sites/docs/src 1 0 0 0 0 0 1 2 sites/motion/src 1 0 0 0 0 0 1 2 sites/storybook/.storybook 2 2 0 1 1 1 1 8 sites/www/src 22 0 0 0 0 0 3 25 total 730 112 0 65 117 44 149 1217 (70 style in the files of scripts/style-rule-allowlist.json, not counted) ✅ No rise over scripts/style-rule-baseline.json (warning mode, 1217 remaining; STY9 switches to --strict). $ pnpm exec vitest run scripts/__tests__/check-style-rule.spec.ts Test Files 1 passed (1) Tests 65 passed (65) $ pnpm check biome lint : 943 fichiers, 31 avertissements, aucune erreur oxfmt --check : « sites/plans/pages/19-style.md » non formaté (fichier hors lot, modifié avant la session, laissé tel quel) ; la commande s'arrête là $ pnpm check:imports && pnpm check:other check:imports vert ; check:other vert, dont check-style-rule.js (« No rise … 1217 remaining »)

Reste : chaque lot qui fait baisser un compte lance node scripts/check-style-rule.js --write-baseline <dossier> en fin de lot pour que le gel descende avec lui ; STY7 ajuste le chemin de ThemeVars dans la liste d'exclusion ; STY9 passe check:other à --strict.

STY0c — Un module dans un deck par esbuild — 29/10

STY0.4 du plan 19 (vague 1). Un *.module.css importé par un composant utilisateur d'un deck arrive dans la feuille du deck, sans dépendance nouvelle : esbuild 0.28.2 charge .module.css nativement (loader local-css) et émet le CSS dans outputFiles à côté de main.js. Aucune commande git.

Fichiers. packages/cli/src/bundle/js.ts : JsBundle.css, le CSS des feuilles émises (main.css puis celles des chunks, ordre stable), retirées de files (elles y écrasaient assets/main.css sans être liées par le HTML) ; les modules entrent dans dependencies par le metafile, donc dans la veille du dev. packages/cli/src/bundle/css.ts : option modulesCss (chaîne ou promesse), insérée dans l'entrée Tailwind entre les feuilles du thème et l'@import de customCss (Tailwind v4 garde une règle écrite avant un @import tardif, sondé) ; polices et KaTeX sont compilés avant d'attendre la promesse. packages/cli/src/render/build-deck.ts : bundleJs et bundleCss restent en parallèle, modulesCss: jsBundle.then((b) => b.css). examples/deck-mdx/components/Counter.tsx + Counter.module.css (le Counter sorti de index.tsx, className={classes.root}, les mêmes valeurs que les utilitaires d'avant : var(--radius-xl), hsl(var(--primary)), 0.75rem 1.5rem… ; Timeline inchangé, pour STY8) et css-modules.d.ts (declare module "*.module.css", ce qu'un utilisateur écrit chez lui). Test packages/cli/src/__tests__/css-modules.spec.ts : trois tests (dev, build minifié, HTML autonome) sur une fixture écrite dans un dossier temporaire, avec composes: slide-card from global et un customCss.

Ordre dans la feuille (deck-mdx construit, offsets dans main.css) : utilitaires Tailwind 12 751 < tokens 23 842 < deck.css 29 111 < module 38 245 < customCss (testé). Un module gagne sur deck.css et sur les utilitaires, customCss gagne sur tout : l'ordre de §2.3 par la source, sans @layer (STY6.1 posera les couches).

Noms que donne esbuild (sondé) : <Fichier>_<classe> sans hachage (Counter_root), suffixe numérique si deux fichiers portent le même nom (a/Button.module.css → Button_root, b/Button.module.css → Button_root2) ; composes: a → "Counter_root Counter_big", composes: slide-card from global → "slide-card Counter_card", :global(.x) respecté. Pas d'option localIdentName : avec minify: true les noms locaux sont minifiés en une lettre (.r, {root:"r"}), les mêmes dans la table JS et dans la feuille, et minify: true l'emporte sur minifyIdentifiers: false. Sans minification des identifiants (minifyWhitespace + minifySyntax seulement), Counter_root survit en build mais main.js passe de 288 557 à 572 672 octets (gzip 90 025 → 120 439, +34 %) : refusé, le deck garde minify entier. Donc : noms lisibles en dev, une lettre en build, comme §5.3 l'admettait (le deck n'est pas prérendu, aucune capture n'en dépend). Collision théorique d'une lettre avec une classe globale : aucune classe d'une lettre dans deck.css, Tailwind ni KaTeX.

Coût : main.js +50 octets (la table), main.css +119 (la règle) par rapport à la construction précédente de deck-mdx (288 507 / 40 630) ; recit build de deck-mdx en 11 s (Shiki compris), inchangé.

Vérification :

Code
pnpm exec vitest run packages/cli → Test Files 6 passed (6), Tests 108 passed (108), Type Errors no errors pnpm -F @recit/cli typecheck → vert ; pnpm -F recit-example-deck-mdx typecheck → vert pnpm exec nx run @recit/cli:build → 12 tâches, 45 s, vert cd examples/deck-mdx && pnpm exec recit build slides.mdx → ✅ Built 4 slides grep -o '{root:"[A-Za-z0-9_-]*"}' dist/assets/main.js → {root:"r"} grep -o '\.r{[^}]*}' dist/assets/main.css → .r{border-radius:var(--radius-xl);background-color:hsl(var(--primary));color:hsl(var(--primary-foreground));border:0;padding:.75rem 1.5rem;font-size:1.5rem;font-weight:700;line-height:2rem} recit export slides.mdx --html -o … → la même règle dans le <style> et la même table dans le <script> du HTML (779 Ko) node scripts/check-style-rule.js examples/deck-mdx/components → className 3, no-module 1, total 4 (5 avant : le Counter migré), pas de hausse pnpm check → biome vert (31 avertissements, aucun dans mes fichiers), check:imports vert ; oxfmt --check échoue sur trois fichiers d'autres lots (scripts/build-css-modules.js, scripts/__tests__/build-css-modules.spec.ts, 19-style.md) et check:other sur le compteur de style de packages/components/src/mdx (STY0b) ; mes huit fichiers passent oxfmt et biome

Deux échecs transitoires du build du deck pendant la session (@recit/core/dist/index.js puis @recit/core/browser introuvables) : les dist de core et deck étaient en cours de reconstruction par un autre lot ; vert au passage suivant.

À savoir pour STY6.1 : garder JsBundle.css → CssBundleOptions.modulesCss (promesse) et placer ce CSS entre les feuilles des paquets et customCss, dans @layer recit quand les couches arrivent. Les modules utilisateurs n'ont pas de PostCSS : pas de rem(), pas de mixin, pas de postcss-preset-mantine ; CSS brut, tokens @recit/theme (ils suivent déjà .dark, donc hsl(var(--primary)) suffit ; light-dark() supposerait color-scheme posé par le deck, à vérifier en STY6.3). esbuild applique les loaders dataurl de js.ts aux url() d'un module (.png, .svg, .woff2… ; une autre extension est une erreur de build). Un module importé par un chunk différé irait dans chunks/*.css, collecté aussi (le JS ne charge jamais une feuille). Les composants de @recit/deck et @recit/components arrivent par sharedPackagesPlugin déjà compilés (STY0.3, dist/styles.css) : leurs modules ne passent pas par ce chemin. Pour STY8 : examples/*/components écrit du CSS sans PostCSS, un css-modules.d.ts par exemple (ou partagé) ; Timeline de deck-mdx et Metric de deck-showcase restent à migrer ; useCssVariable vient de @recit/components dans un deck (jamais de @recit/site). Le skill mantine-style (STY10.1) devrait dire que rem() et les mixins ne valent pas dans examples/*/components.

STY0b — Modules dans un paquet publié — 29/10

Spike de STY0.3 (plan 19 §2.2) : comment un *.module.css vit dans un paquet publié. Aucune commande git ; pnpm install une fois, après pgrep -f "pnpm install" vide.

Décision (écrite dans 19-style.md §2.2, paragraphe « Décision (STY0b, 29/10) ») : Lightning CSS, le compilateur que Vite 8 embarque déjà, en devDependency du paquet qui compile ses modules (lightningcss: 1.33.0 au catalogue, la version déjà installée ; aucune dépendance d'exécution) ; ni postcss-modules (nouveau paquet et ses dépendances), ni @tsdown/css (version à aligner sur tsdown à chaque bump, et il ne règle pas le dev). scripts/build-css-modules.js (en .js, règle useImportExtensions) compile chaque src/**/*.module.css du paquet avec le motif recit-<paquet>-<Fichier>__[local] (préfixe littéral par fichier, pas de hachage, --variables globales) et écrit : la table de classes à côté du module, Kbd.module.css.ts (const classes = { root: "recit-components-Kbd__root slide-kbd" } as const; : le composes: slide-kbd from global est joint au nom local, clés triées, déjà au format oxfmt, et typée par ses littéraux, donc pas de .d.ts à part), et la feuille du paquet dist/styles.css (exportée ./styles.css, sideEffects: ["**/*.css"]). Le composant importe la table, pas le CSS : import classes from "./Kbd.module.css.js". C'est le mécanisme des mêmes noms en dev : sous @recit/source, Vite lit Kbd.tsx puis Kbd.module.css.ts, un objet ordinaire ; tsdown, esbuild et webpack voient la même chose dans dist ; aucune configuration Vite à aligner (le generateScopedName prévu par le plan n'existe pas avec le transformateur Lightning CSS de Vite, motif chaîne seulement). Refus explicites : deux modules du même nom dans un paquet, un nom de fichier qui ne peut pas ouvrir une classe, @import, composes depuis un autre fichier.

Kbd : packages/components/src/mdx/Kbd.tsx + Kbd.module.css (sortis de builtins.tsx, réexportés par index.ts, BUILTIN_MDX_COMPONENTS.Kbd inchangé, KbdProps exporté). Élément nu <kbd>, pas le Kbd de Mantine : @recit/components ne dépend pas de @mantine/core (package.json, §3.3 et §5.5) ; le module compose slide-kbd de deck.css (contrat public) et ajoute white-space: nowrap. Câblage : "build": "node ../../scripts/build-css-modules.js && tsdown", le même préfixe pour typecheck (la table doit exister avant tsgo) ; tsdown.config.ts nettoie dist/** sauf dist/styles.css. Storybook : preview.css importe @recit/components/styles.css après deck.css. Test scripts/__tests__/build-css-modules.spec.ts (12 tests sur un paquet fabriqué : préfixe, composes global et local, tri, variables globales, refus, tables et feuille, déterminisme et mtime inchangé, write: false, doublon de nom).

Vérifications : pnpm exec nx run @recit/components:build → NX 3 tasks: 3 succeeded, dist/ contient styles.css, mdx/Kbd.js, mdx/Kbd.module.css.js ; pnpm exec vitest run packages/components scripts/__tests__/build-css-modules.spec.ts → Test Files 5 passed (5), Tests 42 passed (42) ; pnpm -F @recit/components typecheck → @recit/components: 1 CSS module → dist/styles.css puis tsgo muet ; pnpm -F recit-storybook build → Storybook build completed successfully, et storybook-static/assets/iframe-B2TOJ11P.css contient .recit-components-Kbd__root{white-space:nowrap} ; pnpm exec recit build n'existe pas à la racine, la vraie commande est pnpm exec nx run recit-example-deck-starter:build (13 tâches, 45 s, CLI reconstruite avec l'arbre de STY0c) → la classe est absente de examples/deck-starter/dist/assets/main.css (slide-kbd y est) : packages/cli/src/bundle/css.ts n'importe pas encore @recit/components/styles.css, c'est la ligne de STY6.1 (hors des fichiers de STY0b ; STY0c y traite les modules des composants utilisateur, pas la feuille du paquet). Preuve que la feuille traverse la chaîne du deck : une copie de slides.md dans le scratchpad avec recit.config.json → theme.customCss sur packages/components/dist/styles.css, construite par examples/deck-starter/node_modules/.bin/recit build … -o <scratch>/dist → .recit-components-Kbd__root{white-space:nowrap} dans son assets/main.css (sans -o, la CLI écrit dans <cwd>/dist : un dist/ parasite à la racine a été créé puis supprimé). pnpm check → biome (31 avertissements préexistants), oxfmt (1091 fichiers), imports, admonitions, front matter, 15 version plans, frontières, interfaces, liens : verts ; check-style-rule.js (STY0d) d'abord rouge sur un seul point (packages/components/src/mdx: tsx without module rose from 1 to 2 : sa détection specifier.endsWith(".module.css") ne voyait pas l'import de la table ./Kbd.module.css.js), vert après que STY0d, prévenu par le coordinateur, a pris /\.module\.css(\.js)?$/ : No rise over scripts/style-rule-baseline.json (warning mode, 1217 remaining).

Pour STY6.1 : câbler @recit/deck, @recit/motion-blocks et @recit/site-stories de la même façon (script, build, typecheck, exports["./styles.css"], sideEffects, clean de tsdown) et importer les feuilles dans packages/cli/src/bundle/css.ts, site-stories/scripts/build-css.mjs et l'entrée Remotion ; décider du sort des tables générées dans src/ (à committer comme un .d.ts, ou .gitignore et régénération par build/typecheck : aujourd'hui Kbd.module.css.ts est non suivi) ; un mode --watch du script pour le dev ; pas de rem() ni de mixin postcss-preset-mantine dans les paquets de format (pas de Mantine, Lightning CSS ne les connaît pas) ; la feuille hors @layer recit tant que deck.css n'est pas en couche theme (STY9) ; publint à passer sur l'export ./styles.css. Pour STY6.2 : le relais d'un className d'auteur sur un élément nu ([classes.root, className].join(" ") dans Kbd.tsx, cas que la loi §0.3 ne couvre pas : à trancher, Mantine fusionne className et classNames.root lui-même).

STY0a — Socle du site — 29/10

Lot STY0 réduit par le coordinateur à STY0.1, STY0.2 et STY0.6 (STY0b, STY0c et STY0d en parallèle). Aucune commande git.

En place.

  • @mantine/core et @mantine/hooks 9.7.4, postcss-preset-mantine 1.18.0, postcss-simple-vars 7.0.1 : catalogue de pnpm-workspace.yaml, dépendances de packages/site (le Vite de chaque site les exécute).
  • packages/site/src/vite/plugin.ts : plugin recit-style, css.postcss (preset Mantine et simple-vars avec les cinq $mantine-breakpoint-*) et css.modules.generateScopedName exporté : Spinner__root dans le framework et les sites, recit-<paquet>-<fichier>__<classe> sous packages/*/src (recit-components-Kbd__root, recit-deck-SlideFrame__root, recit-site-kit-Cards__root) ; même fonction en dev et en build ; deux modules du framework ne doivent pas partager un nom de fichier.
  • Couches : packages/site/src/app/layers.css (@layer mantine, theme, base, components, utilities;) et @mantine/core/styles.layer.css importés en tête de entry.client.tsx et entry.server.tsx, avant virtual:recit-config : la feuille de site-stories importée par le recit.config d'un site précède main.tsx dans le bundle et fixait l'ordre des couches, et une déclaration dans main.css ne survit pas à Tailwind. Construit : @layer mantine{ à l'octet 0, puis properties, theme, base, components, utilities.
  • SiteRoot.tsx : MantineProvider theme={recitTheme} defaultColorScheme="light" sous le ThemeProvider de next-themes ; le script de Mantine (ColorSchemeScript appelé comme fonction, son __html) posé par useHead, donc <script data-mantine-script> dans le <head> prérendu et en dev.
  • lib/theme/theme.ts (recitTheme = createTheme(…)) : polices sur var(--font-sans) et var(--font-mono), lineHeights.md: "1.5" (piège ci-dessous) ; lib/theme/index.ts.
  • Spinner.module.css, premier module (animate-spin reporté à l'identique) ; lib/hooks/useCssVariable.ts, son test, l'export @recit/site/hooks.

Piège. La baseline de Mantine déclare body { line-height: var(--mantine-line-height) } (1.55) ; le site hérite 1.5 de html (preflight Tailwind) sans rien déclarer sur body, et une valeur héritée perd contre toute valeur déclarée, même depuis la couche la plus basse. Premier passage e2e : 64 échecs, chaque page 1 px plus haute (body à 24,8 px). Sonde Playwright sur le dist : line-height: inherit sur body, ou la couche mantine retirée, rend la hauteur d'origine. Parade dans le thème, pas dans le CSS global. Les autres propriétés de la baseline sont neutres : font-family par les variables du thème, font-size 1rem = racine, color, background-color et antialiased déjà déclarés sur body par main.css (couche base, qui gagne), color-scheme: light sans effet mesuré.

Vérifié.

  • pnpm -F @recit/site typecheck ; vitest run packages/site : 56 fichiers, 596 tests ; biome et oxfmt sur mes fichiers ; pnpm check vert (compteur de STY0d : 1217, aucune hausse).
  • Build de docs : Spinner__root dans le CSS construit (chunk partagé Alert-*.css, lié en tête de index.html) et dans le JS ; absent du HTML prérendu parce que Layout ne rend le Spinner que dans une Suspense fallback : le grep du plan sur index.html n'est pas probant, celui sur dist/assets/*.css l'est.
  • Dev (recit-site dev --port 4521, BROWSER=none) : script Mantine dans le <head>, virtual:ssr-css.css?direct commence par la déclaration de couches puis @layer mantine { et contient Spinner__root : css/collect.ts et css/plugin.ts voient les modules sans changement.
  • e2e en conteneur, e2e/site-docs.spec.ts : 106/106 (chromium et chromium-dark), galerie vide.

Mesures (STY0.6, sites/docs, gzip).

QuoiAvantAprès
CSS d'entrée43,5 ko (247 ko brut)79,0 ko (496 ko brut)
JS d'entrée139,6 ko (513 ko brut)147,7 ko (540 ko brut)
JS total3 285 ko3 295 ko

www avant : CSS 43,4 ko, JS 120,5 ko. La feuille Mantine complète pèse 35,5 ko gzip de plus (41,8 ko seule), au-dessus des ~30 ko attendus ; l'import par composant (@mantine/core/styles/Button.layer.css…) est l'option de STY5 et STY9.

Reste, pour STY2 et STY6.1. colorMode et le pont .dark (STY2) ; createRecitTheme remplace le squelette en gardant lineHeights.md ou en déclarant line-height sur body dans la feuille de base ; les deux <style data-mantine-styles> du provider sont dans #root (157 et 815 octets) ; le serveur de preview écoute sur localhost (IPv6), curl sur 127.0.0.1 est refusé ; une reconstruction concurrente des paquets par un autre lot vide leur dist un instant et fait échouer un build ou un dev (@recit/theme, @recit/site-stories), relancer suffit.

Porte de la vague 1 (plan 19) — 29/10

Coordination. Raccords après les rapports : le compteur accepte l'import d'une table générée (*.module.css.js, STY0b) ; le skill mantine-style précise qu'hors de packages/site un module est du CSS brut (Lightning CSS ou esbuild, sans rem() ni mixin) ; l'en-tête du plan 21 corrompu par une édition de sa seconde passe a été réparé et ses trois modifications perdues réappliquées ; 19-style.md formaté. Porte : pnpm check vert (1 217 au compteur, aucune hausse) ; e2e en conteneur sur storybook, deck-render, site-components, site-presentation, site-docs : 274/276, les deux échecs étant /demos/mdx en clair et en sombre, où le bloc « Components » affiche la source de deck-mdx sortie dans Counter.tsx et son module (STY0c) : différence voulue, références régénérées par test:e2e:update:linux e2e/site-presentation.spec.ts. Vague 2 lancée (STY2, STY6.1, STY7a, TST-A) ; tableau d'état en tête du §10 du plan 19.

STY7a — Blocs motion, styles statiques — 29/10

STY7.1 du plan 19 (vague 2 du plan 20), plus la ligne de Film.tsx. Fichiers : packages/formats/motion-blocks/src/blocks/blocks.module.css (nouveau) et sa table générée blocks.module.css.ts, src/ThemeVars.tsx (nouveau), src/blocks/frame.tsx (nouveau : Frame, Backdrop), les 8 blocs, src/theme.tsx (useStyles retiré), src/styles.ts (supprimé), src/index.ts, test/{render,player}.test.tsx, test/ThemeVars.test.tsx (nouveau), packages/formats/motion-render/src/Film.tsx, scripts/style-rule-allowlist.json (entrée ThemeVars.tsx : pending retiré), scripts/style-rule-baseline.json (motion-blocks et motion-render à zéro). Aucune commande git ; package.json, tsdown.config.ts et entry.tsx (import de @recit/motion-blocks/styles.css) sont de STY6.1, déjà en place dans l'arbre.

Ce qui change. makeStyles(theme) devient blocks.module.css : une classe par ancienne clé (frame, eyebrow, display, statement, statValue, rowLabel, mask) et une par élément que les blocs stylaient en ligne (chapterNumber, chapterTitle, items, item, itemIndex, statLabel, statCaption, emphasis, rows, row, rowBars, rowStacked, barRow, track, barBefore/barAfter, barValueBefore/barValueAfter, card, cardChrome, dot, cardApp, cardBody, cardLine, cardSkeleton, cardStage, cardWrap, cardCaption, backdrop). Les unités cqw de l'échelle typographique et container-type: size passent tels quels. Les variantes se lisent sur la racine de scène : data-block (le type) et data-layout (16x9, aussi le 1:1, ou 9x16) posés par Frame ; le module en tire les paddings par bloc (12% 8%, 14% 8%, 10% 8%, 6%, 7% 5%), les tailles de police portrait (clamp(48px, 12vw, 128px)…) et l'écart des lignes empilées, donc les composants Content n'ont plus de prop de taille. Les pastilles de ui-mock sont data-dot="close|minimize|zoom", les lignes squelette data-line="0…5" (90 %, 72 %… 0 %, au-delà auto comme en ligne). Les valeurs du thème que makeStyles et les blocs lisaient (colors, fonts, type, track, data) deviennent 27 variables --mb-* (--mb-bg, --mb-font-body, --mb-type-row-label, --mb-data-after-bar…) écrites par ThemeVars (themeVariables(theme), un seul objet style de variables, exception nommée de §0.3, dans la liste d'exclusion) : ThemeVars rend l'élément demandé (as, div par défaut, ou AbsoluteFill) avec la classe et les data-* de l'appelant, et lit le thème du ThemeProvider. Frame = ThemeVars + classes.frame : la racine de chaque bloc pose les variables elle-même, donc un bloc rendu seul (tests, vignettes, StaticAnim) a son thème ; Backdrop = ThemeVars + classes.backdrop (fond du film) remplace <AbsoluteFill style={{ background }}> dans Film.tsx (<Backdrop as={AbsoluteFill}>, même DOM, Remotion fusionne style et relaie className). Les blocs n'ont plus que className de la table et la couche A (engine.tsx, RemotionAnim, ClockAnim), intouchée ; AnimProps.style reste pour STY7b. API publique de @recit/motion-blocks : makeStyles, Styles, useStyles retirés (aucun usage hors du paquet), ThemeVars, themeVariables, ThemeVariables, Frame, Backdrop ajoutés ; un seul plan de version pour toute la migration, écrit par STY9 (§10.2).

Deux points vus en route. (1) compareBars posait width: value/max % en ligne sur la piste d'une barre : sans effet, la piste est un item flex: 1 (base 0) qui remplit la ligne quoi qu'il arrive, la barre étant révélée par growX ; retiré (maxValue avec), goldens identiques. Si l'intention d'emaki était des barres proportionnelles, c'est une décision produit, pas ce lot. (2) composes écarté dans ce module : avec le préfixe recit-motion-blocks-blocks__, une entrée composée de la table dépasse 80 colonnes et oxfmt la replie après le deux-points (barAfter:\n "…"), que le générateur n'écrit pas → pnpm check rouge (oxfmt, puis --check du script en désaccord). Les rôles partagés sont des listes de sélecteurs (.eyebrow, .chapterNumber, .statLabel, .cardCaption { … }), un seul nom par classe, table dans les 80 colonnes. Pour STY6.1 : tableSource devrait replier une entrée longue comme oxfmt (site-stories aura le même préfixe long). Lightning CSS normalise aussi les couleurs (rgba(0,0,0,0.06) → #0000000f, alpha 15/255 au lieu de 0.06, même dans une --variable) : sous le seuil des goldens, 0,000 % mesuré, mais à savoir.

Vérifications.

Code
$ pnpm exec vitest run packages/formats/motion-blocks packages/formats/motion-render Test Files 7 passed (7) Tests 49 passed (49) $ pnpm exec nx run-many -t build typecheck -p @recit/motion-blocks @recit/motion-render NX 7 tasks: 7 succeeded $ node scripts/check-style-rule.js packages/formats/motion-blocks packages/formats/motion-render directory className style props imports css directives no-module total total 0 0 0 0 0 0 0 0 (71 style in the files of scripts/style-rule-allowlist.json, not counted) $ node scripts/check-style-rule.js --write-baseline packages/formats/motion-blocks/src packages/formats/motion-render/src ✅ scripts/style-rule-baseline.json written: 1146 in 32 directories. $ pnpm check ✅ No rise over scripts/style-rule-baseline.json (warning mode, 1145 remaining; STY9 switches to --strict). ✅ @recit/motion-blocks: 1 CSS module, tables up to date

Compteur : packages/formats/motion-blocks/src/blocks 58 style → 0, motion-render/src 1 → 0, liste d'exclusion 70 → 71 (ThemeVars.tsx) ; 1 217 → 1 146 dans la baseline. Le premier className compté venait de tone === "before" ? … (le littéral de la condition compte, §8.1-1) : remplacé par un booléen after. Goldens (pnpm -F @recit/motion-render goldens:check, 16 scènes × 2 sources, warm 16:9 et saas 9:16) :

Code
✓ warm-title — 0.000% changed … (les 32 vues : warm et saas, title, statement, chapter, list, stat, compare, mock, scene, depuis le film.json et depuis le .story.md) ✓ saas-scene (md) — 0.000% changed ✓ all goldens match

Un premier passage s'est arrêté sur @recit/core sans dist (reconstruction concurrente d'un autre lot, comme noté en STY0a) : relancé. e2e non lancés ici (conteneur occupé par un autre lot) ; à la porte de vague, motion-dev et site-motion ne verront la feuille que si elle est chargée par leurs pages : packages/cli/src/commands/motion-dev.ts (motionPage, un <link> ou la feuille en ligne ; bundleMotionClient ne garde que le JS d'esbuild) et packages/integrations/site-stories/scripts/build-css.mjs (@import de @recit/motion-blocks/styles.css, préfixée .recit-wrapper), fichiers de STY6.3b et STY6.1. Sans cela les blocs y sont nus. Rien d'autre à accepter dans la galerie : aucune capture de ce lot ne doit changer.

Pour STY7b. render.tsx (64 style:) : constant → blocks.module.css (ou un module à côté, même générateur), par temps → couche ; player.tsx : le MotionFrame (fond theme.colors.bg, dimensions natives, transform: scale) passe par Backdrop et un module, l'échelle par variable ou calcul CSS ; AnimProps.style et StaticAnim/FramerAnim/RemotionAnim restent la couche ; ThemeVars est le seul endroit où le thème devient du style ; la caption de UiSceneView (fontFamily: theme.fonts.mono…) peut lire var(--mb-font-mono) dès maintenant, les variables sont posées par Frame au-dessus d'elle. Puis goldens:check sans régénération et les e2e motion.

STY6.1 — Outillage des paquets publiés — 29/10

Vague 2 du plan 19 (§10, STY6.1 ; 20-multi-agents.md §4), en parallèle de STY2, STY7a et TST-A. Aucune commande git ; un pnpm install après pgrep vide (lockfile : lightningcss dans @recit/deck, @recit/motion-blocks, @recit/site-stories ; @recit/deck, postcss-preset-mantine, postcss-simple-vars dans sites/storybook).

Correction au plan pour cette vague. Les paquets portent encore leurs classes Tailwind jusqu'à STY6.2 et STY6.3 (vagues 3 et 4) : la CLI garde @import "tailwindcss" et ses @source, @recit/theme/tailwind.css et son peer restent, Storybook garde @tailwindcss/vite ; ce lot ajoute les feuilles des modules à chaque consommateur, modules et utilitaires coexistent, et les captures deck-render restent identiques. STY5c retire Tailwind. tailwind.css le dit en tête ; packages/theme/package.json n'est pas touché.

Script généralisé (scripts/build-css-modules.js, 23 tests dans scripts/__tests__/build-css-modules.spec.ts) : PACKAGES (les quatre paquets), --all, --check (table absente, périmée ou orpheline, feuille dist/ construite et périmée ; une feuille pas construite n'est pas une faute, pnpm check précède pnpm build en CI), --watch (fs.watch récursif sur src/, recompile à la volée), et recit.cssModulesSheet dans le package.json d'un paquet dont ./styles.css est une autre feuille. Même motif recit-<paquet>-<Fichier>__<local>, même table typée à côté du module.

Piège de format des tables (STY7a) : une entrée composes longue dépasse 80 colonnes et oxfmt la replie après les deux-points. tableEntry reproduit la règle de Prettier, sondée sur oxfmt 0.74 : une entrée clé: "valeur", de plus de 80 colonnes (virgule comprise) passe sur deux lignes (clé: puis la chaîne indentée de quatre), sauf quand la clé fait moins de cinq colonnes (root, mask, body : jamais repliées, quelle que soit la longueur) ; une clé citée compte ses guillemets. Test unitaire aux bornes 80/81 ; blocks.module.css.ts (36 classes) et Kbd.module.css.ts passent oxfmt --check.

Politique des tables générées (écrite dans 19-style.md §2.2, « Tables générées (STY6.1) ») : committées avec leurs modules, régénérées par build et typecheck (node ../../../scripts/build-css-modules.js && tsdown / && tsgo --noEmit), vérifiées par node ./scripts/build-css-modules.js --check --all ajouté à check:other ; l'utilisateur les committe avec le lot qui les change (aujourd'hui packages/components/src/mdx/Kbd.module.css.ts et packages/formats/motion-blocks/src/blocks/blocks.module.css.ts de STY7a, non suivis).

Paquets : @recit/deck et @recit/motion-blocks reçoivent exports["./styles.css"], sideEffects: ["**/*.css"], le script devant tsdown et tsgo, clean: ["dist/**", "!dist/styles.css"]. @recit/site-stories : son ./styles.css est la feuille d'intégration, donc ses propres modules vont dans dist/modules.css ("recit": { "cssModulesSheet" }), que scripts/build-css.mjs importe après @recit/components/styles.css, @recit/deck/styles.css et @recit/motion-blocks/styles.css (le MotionViewer est dans .recit-wrapper, donc sa feuille aussi), tous passés par la passe de préfixage .recit-wrapper ; tsdown garde modules.css et styles.css ; la construction accepte zéro module (feuille réduite à son en-tête) comme plusieurs (STY6.3b). publint : 12 paquets verts.

CLI (packages/cli/src/bundle/css.ts) : @recit/components/styles.css et @recit/deck/styles.css importés dans l'entrée Tailwind après les feuilles du thème, avant les @source, le CSS des modules utilisateur (modulesCss, STY0c) et customCss ; ordre prouvé par un test ajouté à css-modules.spec.ts (.slide-card < .recit-components-Kbd__root < .Counter_root < .lorem-custom ; Tailwind retire les commentaires d'en-tête des feuilles, la règle de Kbd sert de repère). js.ts, build-deck.ts et packages/cli/package.json n'ont pas eu besoin de changer (modulesCss en promesse, dépendances déjà déclarées). Un deck ne bundle jamais @recit/motion-blocks (SHARED_PACKAGES de resolve-plugin.ts, aucun import dans deck ni components) : sa feuille n'entre pas dans le deck. Le lecteur de recit dev d'une story motion (packages/cli/src/commands/motion-dev.ts, accordé par le coordinateur après le rapport de STY7a, STY6.3b le reprend) n'avait aucune feuille (un <style> en ligne) : bundleMotionClient rend maintenant { js, css } (outdir + entryNames: "client", le CSS = @recit/motion-blocks/styles.css lu par cliRequire suivi des .css qu'esbuild émettrait pour les sources du lecteur, aucun aujourd'hui), la page pose <link rel="stylesheet" href="/__motion/styles.css"> et le serveur sert cette route en text/css ; test ajouté à motion.spec.ts (page, route, type, en-tête de la feuille) ; sondé sur hello.story.md : 57 règles recit-motion-blocks-blocks__* servies.

Storybook : preview.css importe @recit/deck/styles.css après celle des composants ; main.ts ajoute dans viteFinal le PostCSS de recit-style (postcss-preset-mantine, postcss-simple-vars avec les cinq $mantine-breakpoint-*, valeurs dupliquées de plugin.ts puisque Storybook ne charge pas @recit/site) et css.modules.generateScopedName: "[name]__[local]" ; le décorateur withSlide perd ses deux style et ses deux chaînes : preview.module.css (.frame, .frameFit, .slide et .slideFit qui composent slide from global, .body qui compose slide-body), classes construites preview-module__frame, preview-module__slideFit{position:relative} après deck.css dans iframe-*.css. Compteur : sites/storybook/.storybook 8 → 3 (imports 1, css 1, directives 1 : Tailwind, jusqu'à STY5c), baseline abaissée pour ce dossier.

Remotion : packages/formats/motion-render/src/entry.tsx importe @recit/motion-blocks/styles.css à côté de fonts.css (webpack de Remotion, même chemin que les polices).

Vérifications :

Code
$ pnpm exec nx run-many -t build typecheck -p @recit/components @recit/deck @recit/motion-blocks @recit/site-stories @recit/cli recit-storybook NX Running targets build, typecheck for 6 projects and 8 tasks they depend on failed Failed tasks: - @recit/site-stories:typecheck ../../site/src/vite/plugin-theme.ts(90,7): error TS6133: 'processFont' is declared but its value is never read. (fichier de STY2 en cours, importé par @recit/site/vite ; les 18 autres tâches vertes, dont les six builds) $ pnpm exec vitest run scripts/__tests__ packages/cli packages/components Test Files 18 passed (18), Tests 354 passed (354), Type Errors no errors (un passage intermédiaire : 2 échecs transitoires pendant que STY7a écrivait blocks.module.css et que son compte className montait dans packages/formats/motion-blocks/src/blocks ; vert au passage suivant) $ node scripts/build-css-modules.js --check --all ✅ @recit/components: 1 CSS module, tables up to date ✅ @recit/deck: 0 CSS modules, tables up to date ✅ @recit/motion-blocks: 1 CSS module, tables up to date ✅ @recit/site-stories: 0 CSS modules, tables up to date $ pnpm check biome, oxfmt, imports, admonitions, front matter, version plans, frontières, interfaces, liens : verts check-style-rule.js : ✅ No rise over scripts/style-rule-baseline.json (warning mode, 1204 remaining) build-css-modules.js --check --all : les quatre ✅ ci-dessus $ pnpm publint ✅ 12 published packages pass publint. $ pnpm exec nx run recit-example-deck-starter:build (14 tâches, 50 s) $ grep -o '\.recit-components-Kbd__root{[^}]*}' examples/deck-starter/dist/assets/main.css .recit-components-Kbd__root{white-space:nowrap} (offset 38 070, après .slide-kbd à 30 936, 40 432 octets) $ grep -o '\.preview-module__slideFit{[^}]*}' sites/storybook/storybook-static/assets/iframe-*.css .preview-module__slideFit{position:relative} $ pnpm exec vitest run packages/cli/src/__tests__/motion.spec.ts → Tests 26 passed (26) ; pnpm -F @recit/cli typecheck → vert $ BROWSER=none pnpm test:e2e:linux e2e/deck-render.spec.ts e2e/storybook.spec.ts (conteneur, 18:07 → 18:10) [e2e-linux] built 18:09:36 52 passed (1.1m) git status e2e/__screenshots__ → vide : captures deck-render et storybook identiques $ pnpm -F @recit/motion-blocks build && pnpm -F @recit/motion-render goldens:check @recit/motion-blocks: 1 CSS module → dist/styles.css (36 classes) ; ✓ all goldens match (32 plans, 0.000% changed : la feuille des blocs importée par entry.tsx rend les mêmes pixels que les styles en ligne d'avant STY7a) $ BROWSER=none pnpm test:e2e:linux e2e/motion-dev.spec.ts e2e/site-motion.spec.ts (conteneur, 18:11 → 18:14, arbre avec les blocs de STY7a sans style en ligne) [e2e-linux] built 18:14:03 31 passed (45.4s) git status e2e/__screenshots__ → vide : captures motion-dev et site-motion identiques

Deux passages conteneur avortés avant celui-là : l'un parce qu'un conteneur d'un autre agent tournait (garde docker ps --filter ancestor=…playwright…, mon pgrep -f e2e-linux se reconnaissait lui-même), l'autre sur un Build failed de rolldown dans l'arbre copié à 18:02, absent du passage suivant et du build complet sur l'hôte (état transitoire d'un autre lot ; sur l'hôte, le seul échec fut components:build sur @recit/core/dist/i18n vidé par une reconstruction concurrente, la règle « un seul lot reconstruit un paquet partagé » de 20-multi-agents.md §2).

Pour STY6.2 (@recit/components) : écrire X.module.css à côté de X.tsx, importer ./X.module.css.js, lancer pnpm -F @recit/components build (ou node ../../scripts/build-css-modules.js --watch en dev) ; CSS brut (pas de rem(), pas de mixin), tokens @recit/theme ; composes: slide-x from global pour les classes de deck.css ; les utilitaires Tailwind retirés d'un composant ne sont plus compilés nulle part dès qu'aucun fichier scanné ne les cite : vérifier storybook.spec.ts et deck-render. Pour STY6.3a-b : même câblage déjà en place dans @recit/deck et @recit/site-stories ; site-stories.module.css ira dans dist/modules.css puis dans styles.css préfixé ; motion-dev.ts sert déjà la feuille des blocs, le module du client motion (client.tsx) arrivera par bundleMotionClient().css. Pour STY7b : la feuille des blocs est chargée par Remotion (entry.tsx), par site-stories (préfixée .recit-wrapper) et par recit dev (/__motion/styles.css) ; la table blocks.module.css.ts est à committer ; une nouvelle classe longue se replie toute seule.

TST-A — Le filet comportemental e2e — 29/10

Vague 2 du plan 21 (§8), en parallèle de STY2, STY6.1 et STY7a. Aucune commande git de ma part : le coordinateur a committé les specs dans le point d'étape 0ac806b9, les corrections ci-dessous sont dans l'arbre de travail.

Livré.

  • e2e/site-behaviour.spec.ts (nouveau) : sites/docs en preview, projets chromium et chromium-dark ; le tiroir en mobile (@mobile). Ctrl+K ouvre le dialogue « Search » (pressé jusqu'à ce que l'hydratation réponde), saisie « tutorial », flèche bas, Entrée mène à l'URL de l'option sélectionnée ; le ThemeSwitch bascule le schéma (lu sur html : classe dark, data-mantine-color-scheme, data-theme ou color-scheme) et le choix stocké, un rechargement garde les deux ; le sommaire marque la section atteinte, puis plus rien en haut de page ; le bouton « Copy code » remplit le presse-papiers (test.use({ permissions })) ; la pagination mène à « Tutorial » et revient à « Quickstart » (dernier lien du nom dans main : la barre latérale et le fil d'Ariane ont les leurs) ; le tiroir « Navigation » s'ouvre, « Formats » mène à /formats/overview, le tiroir est fermé. CodeTabs : test.fixme, aucune page d'aucun site ne porte <CodeTabs> (vérifié dans sites/, examples/, Storybook) ; la fixture est une décision du site docs.
  • e2e/deck-behaviour.spec.ts (nouveau) : sites/presentation, /present/starter (trois diapositives). Flèches, Espace, Home et End avec ?slide=N qui suit ; ?slide=2 ouvre à la deuxième ; s ouvre la console présentateur (context.waitForEvent("page"), URL mode=presenter), « Slide N / 3 » suit l'auditoire et « Next → » le pilote ; ?print=1 rend autant de .print-slide que de diapositives.
  • e2e/support.ts, ajouts en fin de fichier : colorScheme, storedSiteTheme (clé SITE_COLOR_SCHEME_STORAGE_KEY de STY2, repli sur la clé héritée theme), themeSwitch, toc, activeTocEntry, slideAt (le group « N / total » du motif carrousel), slideTotal. Les specs ne parlent que rôles, noms, URL, localStorage et presse-papiers, aucune capture ; la seule classe (text-primary, voir ci-dessous) vit dans le helper, jamais dans une spec.
  • playwright.config.ts : testMatch du projet mobile élargi à site-(docs|behaviour) — une ligne hors de ma liste de fichiers, sans quoi un test @mobile ne tourne dans aucun projet.

Trouvé, pour le plan 19 (rien corrigé ici).

  1. Sommaire : chaque entrée porte aria-current="page" (le NavLink du routeur, le chemin correspond), l'attribut ne dit donc pas la section ; seule la classe text-primary la marque. Et à 1280 × 720 l'observateur (ViewportAnchorContext, rootMargin −80 % en bas, seuil 0,75) regarde les 144 px du haut : un titre amené à son scroll-margin (124 px, sous un en-tête de 115 px) n'y entre jamais, la section cliquée dans le sommaire n'est pas marquée ; elle l'est à partir de 94 px, donc sous l'en-tête. Mesuré par sonde Playwright (scrollIntoView puis scrollBy) ; à 900 px de haut le marquage suit, d'où le test.use({ viewport }) du test. Attendu : aria-current="location" sur l'entrée active seule, et une zone qui tient compte de l'en-tête.
  2. Thème : STY2 a remplacé la clé theme par recit-color-scheme pendant le lot ; la constante SITE_COLOR_SCHEME_STORAGE_KEY est arrivée dans support.ts au même moment que la mienne (doublon résolu en gardant la sienne). Le darkStorageState de la config garde theme, que SiteRoot.tsx copie une fois : le projet chromium-dark ouvre toujours en sombre.
  3. Protocole : pgrep -f e2e-linux se trouve lui-même quand la boucle d'attente contient la chaîne (mon garde a tourné 80 min pour rien) ; pgrep -f 'e2e-linux\.j[s]' ou docker ps filtré sur l'image Playwright ne se trompent pas. Un passage local pendant qu'un autre lot reconstruit un paquet échoue au démarrage des webServer (@recit/theme/dist/index.js absent), comme STY0a l'avait noté.

Vérifications. Deux passages en conteneur, BROWSER=none pnpm test:e2e:linux e2e/site-behaviour.spec.ts e2e/deck-behaviour.spec.ts :

Code
[e2e-linux] start 19:18:01 · copied 19:18:03 · installed 19:18:30 · built 19:20:30 2 skipped 15 passed (39.4s) [e2e-linux] tested 19:21:10 · done 19:21:11 [e2e-linux] start 19:21:30 · copied 19:21:31 · installed 19:22:08 · built 19:24:07 2 skipped 15 passed (39.1s) [e2e-linux] tested 19:24:48 · done 19:24:48

Les deux ignorés sont le fixme CodeTabs en chromium et chromium-dark. pnpm -F recit-e2e typecheck : tsgo --noEmit, sortie 0. pnpm check : sortie 0 (compteur de style 1145, aucune hausse). En local, pnpm exec playwright test sur les deux specs : 15 réussis, 2 ignorés, 40 s.

STY2 — Thème et mode de couleur — 29/10

Lot STY2 du plan 19, vague 2, sur feature/mantine. Aucune commande git (point de contrôle 0ac806b9 posé par le coordinateur à la demande de l'utilisateur).

En place.

  • packages/site/src/lib/theme/createRecitTheme.ts : createRecitTheme(theme) construit le thème Mantine depuis la section publique theme: (polices sur --font-sans/--font-mono, headings, lineHeights.md: "1.5" gardé de STY0a, defaultRadius: "md", palette recit de dix nuances depuis theme.light.primary quand la couleur se lit : #hex, rgb(), hsl() ou le triplet « H S% L% » ; oklch() et les noms laissent la palette de Mantine). recitCssVariablesResolver pointe chaque variable de Mantine vers une variable du site (--mantine-color-body → --background, text → --foreground, dimmed/placeholder → --muted-foreground, anchor et primary-color-filled → --primary, default-border → --border, error → --destructive, les rayons xs…xl sur l'échelle Tailwind de --radius), donc aucune valeur propre à un mode : les variables du site changent déjà avec lui. colorScale.ts : trente lignes, testées ; @mantine/colors-generator non pris. Instantané du thème de sites/docs.
  • Mode de couleur : Mantine est la source de vérité. localStorageColorSchemeManager sur la clé recit-color-scheme, defaultColorScheme depuis site.colorMode (light, dark, system ; system = auto quand rien n'est dit) ; next-themes retiré de SiteRoot (le paquet reste installé jusqu'à STY9, plus rien ne l'importe). Le script posé dans <head> par useHead fait trois choses avant le premier rendu : copie une fois le choix mémorisé par next-themes (clé theme) sous la clé de Mantine, pose data-mantine-color-scheme (script de Mantine), puis reflète l'attribut sur la classe .dark de <html>. À l'exécution, ColorSchemeBridge tient la classe égale au schéma calculé : les vingt lecteurs de .dark (§1.4) ne voient aucune différence jusqu'à STY9. useTheme (packages/site/src/lib/hooks/useTheme.ts, exporté par @recit/site/hooks et @recit/site/components) garde la forme de celui de next-themes : theme (light/dark/system), resolvedTheme, setTheme.
  • plugin-theme.ts : registre shadcn retiré (shadcn-registry.ts supprimé avec ses tests, GOOGLE_FONTS conservé) ; les variables sombres sont émises sous DARK_SELECTOR = .dark, [data-mantine-color-scheme="dark"]. SiteConfig.ts : theme.registryUrl refusé avec le message « theme.registryUrl is gone with shadcn/ui: set theme.light and theme.dark instead » (superRefine, clé marquée @deprecated jusqu'à 1.0), site.colorMode ajouté. defaultTheme.css inchangé : les défauts restent zinc (question 5, iso-rendu).
  • ThemeSwitch.tsx + ThemeSwitch.module.css : UnstyledButton de Mantine, data-mode et data-active, le module reporte les utilitaires d'avant valeur par valeur (p-0.5, gap-0.5, border, rounded-full, ring-3) ; plugins/search-inkeep lit data-mantine-color-scheme.
  • Hors de la liste du lot, additifs : app/main.tsx passe theme={config.theme} et colorMode={config.site?.colorMode} à SiteRoot (seul chemin vers la config côté client) ; Header.test.tsx et MobileTopNavigation.test.tsx enveloppent leur arbre dans <MantineProvider> (le ThemeSwitch lit le schéma de Mantine) ; e2e/support.ts : darkMode() pose aussi recit-color-scheme=dark (SITE_COLOR_SCHEME_STORAGE_KEY), question 14.

Vérifié. pnpm exec vitest run packages/site : 58 fichiers, 606 tests (un instantané écrit) ; pnpm -F @recit/site typecheck et pnpm -F @recit/site-stories typecheck verts (la variable inutilisée processFont signalée par STY6.1 est retirée) ; nx run docs:build vert (chunk ThemeSwitch-*.css, script data-mantine-script dans le <head> prérendu) ; node scripts/check-style-rule.js packages/site : 816 → 808, baseline abaissée pour lib/components ; pnpm check vert ; e2e conteneur e2e/site-docs.spec.ts : 106/106 (chromium et chromium-dark), galerie vide, ThemeSwitch compris.

Pour STY3 et STY4. Lire le mode : useTheme().resolvedTheme depuis @recit/site/hooks, ou useComputedColorScheme de Mantine ; en CSS, light-dark() ou [data-mantine-color-scheme="dark"] (la classe .dark n'est qu'un miroir jusqu'à STY9). Les variables de Mantine qui existent déjà et suivent le site : --mantine-color-{body,text,dimmed, placeholder,anchor,default,default-hover,default-color,default-border,error}, --mantine-primary-color-{filled,filled-hover,contrast,light,light-hover,light-color}, --mantine-radius-{xs,sm,md,lg,xl,default} ; theme.primaryColor vaut recit quand le site a une couleur lisible. useComputedColorScheme rend light au serveur et jusqu'au premier effet : un composant qui affiche l'état attend l'hydratation (useIsClient), comme ThemeSwitch.

Pièges. Un processus d'attente dont la ligne de commande contient e2e-linux se voit lui-même dans pgrep -f e2e-linux et attend pour toujours : tester avec pgrep -f 'e2e-linux\.j[s]' ou docker ps --filter ancestor=… (trouvé par TST-A). Deux pgrep/pkill sur son propre motif tuent le shell courant (code 144).

STY6.2 — @recit/components — 29/10

Vague 3 du plan 19 (§10, STY6.2 ; 20-multi-agents.md §4), en parallèle de STY2, TST-A et STY7b. Fichiers : packages/components/src/** (les 11 composants, leurs 17 modules et tables, les 9 stories, cx.ts, hooks/useCssVariable.ts, les tests), scripts/style-rule-baseline.json (deux dossiers à zéro), une ligne dans .claude/skills/mantine-style/SKILL.md. Aucune commande git, pas de pnpm install, pas de pnpm fix.

Le mécanisme, composant par composant. Un X.module.css à côté de chaque X.tsx, importé par sa table ./X.module.css.js (STY0b, STY6.1), CSS brut compilé par Lightning CSS, sans rem() ni mixin. Les composants qui ne faisaient que poser des classes de deck.css (Quote, Callout, CodeGroup, Card, Col/Left/Right, Image, Badge, Step, Kbd) composent le contrat (composes: slide-callout slide-callout-info from global), une entrée de table par variante (classes.info, classes.cols3, classes.contain, classes.primary) choisie par une Record typée hors du JSX ; une valeur inconnue venue d'une directive retombe sur la variante par défaut, comme avant. Les composants qui portaient des utilitaires Tailwind (Cover, Points, Media, CodeBlock, Mermaid, l'avertissement de Grid) reçoivent la traduction règle par règle, aux valeurs relevées dans la feuille compilée de Storybook (storybook-static/assets/iframe-*.css, Tailwind 4.3.3) plutôt que dans la table du skill quand les deux diffèrent : bg-primary/10 est color-mix(in oklab, hsl(var(--primary)) 10%, transparent) (pas hsl(… / 0.1)), rounded-xl est var(--radius-xl) que tokens.css redéfinit en calc(var(--radius) * 0.8) (donc 1rem, pas 0,75rem, et il suit theme.radius), text-sm porte line-height: calc(1.25 / 0.875), shadow-lg ses deux ombres, md: est @media (width >= 48rem). builtins.tsx est découpé : un fichier par composant exporté (Card.tsx, Columns.tsx, Col.tsx pour les trois colonnes, SlideImage.tsx, Badge.tsx, Step.tsx), le registre BUILTIN_MDX_COMPONENTS et l'alias SlideGrid restent dans builtins.tsx, index.ts exporte les nouveaux fichiers : même API publique. Le relais d'un className d'auteur (question laissée par STY0b) passe par cx(...classes) exporté par @recit/components (className={cx(classes.root, className)}, ne voit jamais de littéral ; Kbd l'utilise aussi, les className = "" par défaut disparaissent) : une ligne dans le skill, test cx.spec.ts. CodeBlock : les deux style={{ fontSize }} deviennent data-size (lg/md/sm/xs selon le nombre de lignes, base sans autoFit), lu par le module ; la classe-marqueur has-line-numbers reste dans le HTML par composes (classes.numbered). Columns : le ratio ("1:2", toute chaîne, pas un ensemble fini : data-columns ne peut pas le porter, et le pipeline Markdown émet le même gabarit en style dans son HTML) devient la variable --columns écrite par useCssVariable (§0.3 permis 4c), le même hook que @recit/site/hooks, copié dans packages/components/src/hooks parce qu'un paquet publié n'importe pas le framework ; le module lit grid-template-columns: var(--columns), invalide donc none sans ratio, comme l'élément sans style. Aucune story ni aucun exemple n'emploie <Columns ratio>. Variante sombre (l'avertissement de Grid seul) : .warning:where(:global(.dark) *, [data-mantine-color-scheme="dark"] *), les deux jusqu'à STY9. Stories : plus de className ni de style ; les cartes des stories Grid et la pile de Callout/AllTypes viennent d'un module de fixtures slides/Stories.module.css (.card, .stack, mêmes valeurs qu'avant), compilé comme les autres, donc deux règles de plus dans la feuille publiée et deux fichiers dans src/ du tarball ; à déplacer si le plan 21 donne un autre toit aux fixtures.

Pourquoi les pixels ne bougent pas. Les utilitaires vivaient dans @layer utilities et deck.css hors couche : deck.css gagnait toujours (.slide h1 fixe la taille du titre de Cover, .slide p, .slide li celle des points, text-5xl et space-y-4 n'avaient aucun effet dans une diapositive). Les modules sont hors couche aussi, après deck.css, à la même spécificité (0,1,0) que les utilitaires : tout ce que deck.css règle par un sélecteur d'élément (0,1,1) continue de gagner, et les classes composées n'apportent aucune déclaration (règles vides), donc rien de neuf n'entre en concurrence avec .slide-callout, .slide-grid, .slide-card… Le seul cas où une spécificité aurait monté, space-y-4, est écrit :where(.list > :not(:last-child)) à zéro, comme Tailwind l'émet. Les md: sont conservés tels quels (sans effet dans .slide, fidèles ailleurs).

Lightning CSS, vu en route. (1) composes: root d'une classe locale ne reprend pas les from global de cette classe : la table joindrait cols2 root grid-cols-2 sans slide-grid ; chaque variante nomme donc ses globales elle-même (composes: slide-grid grid-cols-2 from global, accepté avec plusieurs noms). (2) @keyframes pulse est préfixé (recit-components-Mermaid__pulse) et l'animation réécrite, mais pas à l'intérieur d'un var() : écrire le nom en clair. (3) calc(1.25 / 0.875) est évalué en 1.42857 (six chiffres, invisible), rgb(0 0 0 / 0.1) devient #0000001a (ce que l'optimiseur de Tailwind faisait déjà dans les feuilles d'avant), un color-mix entre deux couleurs littérales est précalculé en oklab(). (4) Les règles vides des classes qui ne font que composer restent dans dist/styles.css et sont retirées par les minifieurs des consommateurs (recit-components-Card__root absent de main.css d'un deck, la classe est dans le HTML). (5) Biome : deux suppressions commentées, noDuplicateProperties sur un double composes (local puis global, la syntaxe des modules) et noUnknownPseudoClass sur :global ; css.parser.cssModules: true dans biome.jsonc (fichier partagé, pas de ce lot) les rendrait inutiles. (6) Le littéral d'une condition dans l'expression className compte pour le compteur (fit === "contain" ? …, comme STY7a) : la Record hors JSX règle le point.

Vérifications.

Code
$ pnpm -F @recit/components build && pnpm -F @recit/components typecheck @recit/components: 17 CSS modules → dist/styles.css (… Stories.module.css → card, stack) ✔ Build complete in 2104ms (63 files, 48.31 kB) ; tsgo muet $ pnpm exec vitest run packages/components Test Files 5 passed (5) Tests 34 passed (34) $ pnpm exec vitest run packages/cli/src/__tests__/css-modules.spec.ts packages/formats/deck packages/integrations/site-stories Test Files 22 passed (22) / Tests 120 passed (120) $ node scripts/build-css-modules.js --check --all ✅ @recit/components: 17 CSS modules, tables up to date (deck 0, motion-blocks 1, site-stories 0 : ✅) $ node scripts/check-style-rule.js packages/components directory className style props imports css directives no-module total total 0 0 0 0 0 0 0 0 $ node scripts/check-style-rule.js --write-baseline packages/components/src ✅ scripts/style-rule-baseline.json written: 1085 in 30 directories. $ pnpm check biome (31 avertissements préexistants), oxfmt (1144 fichiers), imports, admonitions, front matter, 15 version plans, frontières, interfaces, liens : verts ✅ No rise over scripts/style-rule-baseline.json (warning mode, 1085 remaining) ✅ @recit/components: 17 CSS modules, tables up to date $ pnpm publint → ✅ 12 published packages pass publint. $ cd examples/deck-mdx && recit build slides.mdx (CLI déjà construite, sans nx) recit-components-Cover__root 1, recit-components-Stories__card 1, .slide-grid.grid-cols-2 1, .text-5xl 0, .max-w-4xl 0 : les utilitaires retirés ne sont plus compilés, la feuille du paquet y est $ BROWSER=none pnpm test:e2e:linux e2e/deck-render.spec.ts e2e/storybook.spec.ts e2e/site-components.spec.ts [e2e-linux] start 19:33:27 · installed 19:33:54 · built 19:36:00 · tested 19:37:32 120 passed (1.5m) $ node scripts/screenshot-gallery.js 0 changed screenshot(s) git status e2e/__screenshots__ → vide

Compteur : packages/components/src/mdx 11 → 0, packages/components/src/slides 49 → 0 ; baseline 1145 → 1085. Captures deck-render (6 decks, clair, sombre, impression), storybook (16 stories × 2 modes) et site-components (14 pages) identiques : rien à accepter dans la galerie.

Pour STY6.3 et la suite. Même recette dans @recit/deck et site-stories : relever les valeurs dans une feuille compilée plutôt que les deviner, garder la spécificité des utilitaires (un :where là où Tailwind en met un), nommer les globales dans chaque variante composée, pas de composes local + global sans la suppression biome. useCssVariable existe maintenant dans @recit/components (exporté) ; cx aussi, pour tout élément nu qui relaie un className. La liste d'exclusion du compteur n'a pas eu besoin de l'entrée hooks/useCssVariable.ts (le compteur ne compte que le style du JSX), à ajouter si l'on veut la symétrie avec celle du site. Le module de fixtures Stories.module.css attend le plan 21.

STY7b — Blocs motion, couche d'animation — 29/10

STY7.2 et STY7.3 du plan 19 (vague 3), repris sur le disque d'un agent coupé par la limite d'usage : son travail (render.tsx réécrit, render.module.css et sa table, MotionFrame, tests) était complet et sain, gardé tel quel ; ce lot l'a vérifié, formaté (oxfmt sur ses seuls fichiers) et prouvé. Fichiers : packages/formats/motion-blocks/src/ui/render.tsx, src/ui/render.module.css (nouveau) et render.module.css.ts, src/player.tsx, src/blocks/uiScene.tsx, src/blocks/blocks.module.css (.motionFrame, .motionStage) et sa table, test/{render,player}.test.tsx, scripts/style-rule-allowlist.json (raisons de player.tsx et render.tsx réécrites), scripts/style-rule-baseline.json. RemotionAnim.tsx intouché, aucune commande git, aucun package.json.

Ce qui change. render.tsx (65 style en ligne, 1 490 → 1 002 lignes) : tout ce qui est constant est une classe de render.module.css (≈ 70 classes : scene, stage, mock, box, text, badge, dot, chip, row, shell, topbar, rail…), les ensembles finis des data-* (data-tone, data-size, data-weight, data-variant, data-active, data-dir, data-align, data-justify, data-border, data-shadow, data-fit, data-mock, data-camera, data-fill/data-stroke d'un chemin de vecteur, data-dot). Les mélanges alpha (withAlpha) sont des color-mix(in srgb, var(--mb-…) N%, transparent) du même token : une feuille pour tous les thèmes, le thème arrive par les --mb-* de Frame. Une seule fabrique, el(), écrit style : (1) ce que le temps de scène échantillonne (fondus d'état, sampleSpec d'un enter/exit, la caméra, le voile --mb-ui-dim), et (2) ce qu'un nœud auteur fixe librement (une largeur, une taille, une couleur hex, un gap) en variables --mb-ui-* que la classe lit, comme ThemeVars ; chaque classe remet ses variables à initial, donc un nœud imbriqué n'hérite pas de celles du parent. Point pour le coordinateur : (2) n'est ni par temps ni fini ; c'est l'analogue de ThemeVars (valeur de données, nécessaire au premier rendu, Remotion), nommé dans la raison de l'entrée render.tsx de la liste d'exclusion. La caption de UiSceneView lit var(--mb-font-mono), le prop theme de UiSceneView est retiré. MotionFrame : cadre classes.motionFrame dimensionné par le module depuis data-aspect (1920×1080, 1080×1080, 1080×1920) et une variable --mf-scale (largeur mesurée / largeur native, premier rendu), scène ThemeVars + classes.motionStage (composes: backdrop, transform: scale(var(--mf-scale))) : le fond du film passe par le module.

Preuve d'identité. Au-delà des goldens (un seul ui-scene à mi-scène), un banc A/B jetable (dans le scratchpad, rien dans le dépôt) a rendu l'ancien render.tsx (git show HEAD) et le nouveau dans le même Frame, 19 scènes × 2 thèmes : tous les types de feuilles et leurs variantes, les trois chromes, les conteneurs avec bg/border/radius/shadow/justify/align, et une scène à trois états avec caméra et voile échantillonnée à 8 instants (fondus, enter en ligne, exit par preset) : 38 paires, 0 pixel différent (pixelmatch, seuil 0). Écart connu, hors rendu : une couleur auteur nommée (red) recevait withAlpha sans effet, elle reçoit maintenant l'alpha de color-mix ; aucun fixture ne l'emploie.

Vérifications.

Code
$ pnpm exec vitest run packages/formats/motion-blocks packages/formats/motion-render Test Files 7 passed (7) Tests 52 passed (52) $ pnpm -F @recit/motion-render goldens:check ✓ warm-title — 0.000% changed … (32 vues, warm et saas, film.json et .story.md) ✓ all goldens match (sans régénération) $ pnpm exec nx run-many -t build typecheck -p @recit/motion-blocks @recit/motion-render NX 7 tasks: 7 succeeded $ node scripts/build-css-modules.js --check --all ✅ @recit/motion-blocks: 2 CSS modules, tables up to date (les trois autres ✅) $ node scripts/check-style-rule.js packages/formats/motion-blocks packages/formats/motion-render total 0 0 0 0 0 0 0 0 (7 style in the files of scripts/style-rule-allowlist.json, not counted) $ node scripts/check-style-rule.js --write-baseline packages/formats/motion-blocks/src ✅ scripts/style-rule-baseline.json written: 1086 in 30 directories. $ pnpm check ✅ … (warning mode, 1085 remaining) — vert $ BROWSER=none pnpm test:e2e:linux e2e/motion-dev.spec.ts e2e/site-motion.spec.ts 31 passed (1.0m) $ node scripts/screenshot-gallery.js 0 changed screenshot(s)

Compteur : style dans les fichiers de la liste d'exclusion 71 → 7 (dans render.tsx, la seule fabrique el ; dans player.tsx, ClockAnim et --mf-scale) ; hors liste 0. Aucune capture acceptée, aucun golden régénéré.

STY3c — Primitives ui, code — 29/10

Lot STY3c du plan 19, vague 3. Aucune commande git.

En place.

  • CodeBlock.tsx + CodeBlock.module.css : cadre, barre de titre, bouton copier (balise button, pas de composant Mantine : les tests n'ont pas besoin de MantineProvider), contenu ; utilitaires reportés valeur pour valeur (rounded-xl → calc(var(--radius) + 4px), bg-black/4, text-sm en calc(1.25 / 0.875), scrollbar de main.css en classe du module, not-prose par composes: not-prose from global jusqu'à STY5). showLineNumbers → data-line-numbers. Exporte HIGHLIGHT_CODE_BLOCK_CLASS (= classes.code), codeBlockClasses et withClassName (relais du className d'un importeur, sans littéral dans le JSX).
  • Contrat Shiki : les règles .shiki du @layer base de main.css passent dans CodeBlock.module.css. Les règles propres aux blocs (grille, lignes pleine largeur, --highlighted-bg, numéros) sont portées par .code ; celles de toute sortie Shiki (paire --shiki-light/--shiki-dark, fond, mots et lignes surlignés) restent globales par :global, le code en ligne {:ts} et les decks montrés dans un site les lisant aussi. Le mode passe par [data-mantine-color-scheme="dark"] et .dark (jusqu'à STY9) ; !important conservés (biome-ignore motivés) pour que les couleurs en ligne d'un deck perdent comme avant.
  • HIGHLIGHT_CODE_BLOCK_CLASS retiré de shiki-constants.ts. Écart au plan : ce n'est pas plugin-shiki.ts qui le consomme (il ne lit que defaultLanguages, inchangé), mais shiki.ts et MdxComponents.tsx. shiki.ts est chargé par Node (plugins Vite) et ne peut pas importer un module CSS : highlight() reçoit la classe en sixième paramètre (codeClassName), passée par HighlightedCode ; MdxComponents l'importe de ui/CodeBlock.js (deux lignes).
  • CodeTabs sur Tabs de Mantine (unstyled, classNames sur list, tab, tabLabel), cadre et contenu par codeBlockClasses ; onglet actif par data-active. Pas de composes d'un autre fichier : postcss-modules recopie alors les règles importées, Shiki global compris, dans la feuille du chunk (vu au premier build : CodeBlock__* en double dans CodeTabs-*.css).
  • EmbeddedCodeBlock : Button de ui/ remplacé par une balise button et le variant outline/icon-xs reporté dans son module (indépendant de la réécriture de STY3a) ; data-full-height, data-show-copy, data-hidden. SyntaxHighlight sans classe, toujours paresseux (MdxComponents). Stepper.module.css : les règles .stepper de @layer utilities, qui restent dans main.css pour STY5 (le lot ne touche que les règles Shiki).
  • CodeBlock.test.tsx lit classes.root, classes.header… (un test de plus : classes et data-line-numbers) ; Stepper.test.tsx inchangé, vert.

Vérifié. vitest run packages/site/src/lib/ui : 2 fichiers, 11 tests ; vitest run packages/site : 13 échecs hors lot (BrowserWindow, LandingPage, Main, Navigation : « MantineProvider was not found », le Button migré par STY3a). pnpm -F @recit/site typecheck : erreurs hors lot seulement (Dialog, Drawer, Pagination, ToggleGroup, STY3a/STY3b). docs:build (avec --exclude-task-dependencies, voir ci-dessous) : le JS de CodeBlock et sa feuille dans un chunk partagé lié dans le <head> de chaque page, sans Shiki (10 ko) ; HTML prérendu CodeBlock__root, CodeBlock__code. Compteur : mes six .tsx à 0 ; packages/site/src/lib/ui 335 → 31, baseline réécrite. pnpm check : biome, imports, format et check:other propres sur mes fichiers ; une erreur hors lot (noConsole dans sites/storybook/.storybook/vitest.setup.ts).

Non fait : e2e et galerie. Le conteneur (test:e2e:linux e2e/site-docs.spec.ts e2e/site-presentation.spec.ts) s'arrête à @recit/site:build : check-external-deps refuse storybook importé par packages/site/src/lib/components/Header.stories.tsx (fichier d'un autre lot, hors de ma liste). À relancer quand il est déclaré ou exclu. Différences à attendre : aucune voulue ; à surveiller, la hauteur de ligne (1.42857 minifié contre calc(1.25 / .875)), le fond des blocs de code d'un deck montré dans un site (!important en couche contre hors couche).

STY3b — Primitives ui, surcouches — 29/10

Lot STY3b du plan 19 (vague 3) : Dialog, Drawer, Popover, Tooltip, DropdownMenu, Command, Collapsible, NavigationMenu de packages/site/src/lib/ui, leurs modules, deux tests (Command.test.tsx, Overlays.test.tsx), @mantine/spotlight/styles.layer.css importé à côté de la feuille de Mantine dans entry.client.tsx et entry.server.tsx. Le catalogue et packages/site/package.json portaient déjà @mantine/spotlight 9.7.4, installé : pas de pnpm install (lockfile intact). Aucune commande git.

En place. Chaque fichier garde ses exports et l'API composée de Radix pour ses importeurs (asChild, open/onOpenChange, side/align/sideOffset sur le contenu, onCloseAutoFocus, onInteractOutside), sans cn ni cva ; une classe passée par className est jointe à celle du module.

  • Dialog → Modal.Root composé (portail, overlay, piège et retour du focus, verrou du défilement de Mantine) ; le bouton de fermeture en pastille est reporté tel quel. onInteractOutside annulable (clic sur l'overlay), onEscapeKeyDown aussi. Animation fade-in-0 zoom-in-95 → transition Mantine zoomFade (exportée), 200 ms, overlay 150 ms.
  • Drawer (vaul) → Drawer.Root de Mantine, direction → position, contenu placé par inset selon data-direction (un tiroir left reste à gauche en RTL), glissement de vaul (500 ms, cubic-bezier(0.32, 0.72, 0, 1)), overlay bg-black/80.
  • Popover → Popover de Mantine, trapFocus et retour du focus ; le placement de PopoverContent remonte à la racine par le contexte. Tooltip → Tooltip de Mantine : la racine lit ses enfants TooltipTrigger/TooltipContent et passe le contenu en label ; ouvre au survol et au focus clavier, flèche de Mantine (TooltipArrow ne rend plus rien).
  • DropdownMenu → Menu (rôles, flèches, focus) ; asChild d'un item par renderRoot (le lien devient l'item, sans type), cases et radios sur Menu.CheckboxItem/RadioItem, sous-menus sur Menu.Sub. Collapsible → Collapse (expanded, sans animation comme avant : les keyframes .CollapsibleContent de main.css ne s'y appliquaient pas), data-state gardé sur le déclencheur et le contenu. NavigationMenu → nav/ul/li et module ; un item à déclencheur ouvre un Menu (click-hover) sous son déclencheur, le viewport partagé de Radix disparaît (NavigationMenuViewport reste, vide) ; navigationMenuTriggerStyle() rend classes.trigger, et .link.trigger reproduit l'ordre de la feuille Tailwind (colonne, rounded-sm, px-4 py-2).
  • Command → @mantine/spotlight, sans thème (unstyled, le module dessine) : CommandDialog = Spotlight.Root sur un store à lui, sans raccourci global (shortcut={null}, le site ouvre par open), clearQueryOnClose={false} ; CommandInput = Spotlight.Search avec les rôles de cmdk (combobox, aria-controls, aria-activedescendant) ; CommandItem = Spotlight.Action (option, aria-selected, data-value), asChild par renderRoot ; CommandList = la listbox dont l'id est donné au store. value/onValueChange de cmdk tenus au pas de l'index de Spotlight ; shouldFilter ignoré (pas de filtre) ; CommandEmpty masqué en CSS (.root:has([data-action])) ; content.className n'est plus relayé, le module porte la largeur qu'il donnait (100 % jusqu'à 750 px, 36rem dès 40em : sm:max-w-xl gagnait sur max-w-[750px]).
  • Modules en @layer components, comme STY3a : une classe Tailwind d'un importeur (couche utilities) gagne encore, comme par cn(). sm: de Tailwind = 40em, palier absent de Mantine : @media (min-width: 40em) écrit en clair ; md: = $mantine-breakpoint-sm.

Pièges. Le store de Spotlight change l'index choisi par updateState, qui ne notifie personne : useSpotlight ne voit pas les flèches ; la recherche relit l'index dans une micro-tâche après chaque touche et chaque saisie. Spotlight.Action ne transmet pas id (passé par renderRoot). Un onOpenChange contrôlé est souvent un simple onClose : CommandDialog ne le rappelle que sur un changement. Les éléments flottants de Mantine restent cachés sous happy-dom sans <MantineProvider env="test">.

Bloquant, hors lot : l'ordre des couches. Vite lie la feuille d'un chunk partagé avant celle de l'entrée ; Alert-*.css (module @layer components de STY3a) est la première feuille de chaque page et crée components avant la déclaration de layers.css : components passe sous mantine et sous la base de Tailwind, dont le * { padding: 0 } gagne sur tous nos modules (liens d'en-tête sans marge, palette de recherche sans cadre, constaté au dist). Une déclaration @layer mantine, theme, base, components, utilities; en tête de la page avant les feuilles rend les captures (essai local sur une copie du dist). À poser au raccord : préfixer chaque feuille émise de cette déclaration (generateBundle du plugin recit-style), ou la poser en tête du <head>.

Raccords demandés. plugins/search-pagefind/IndexingDialog.tsx importe DialogTrigger de @radix-ui/react-dialog : hors d'un dialogue Radix il lève une erreur, en dev seulement (le bouton « Build Search Index ») ; l'importer de @recit/site/ui/Dialog.js. Sans importeur désormais, à retirer au prochain pnpm install : cmdk, vaul, @radix-ui/react-{dropdown-menu,popover,tooltip,navigation-menu} (react-dialog attend IndexingDialog, react-collapsible NavigationCategory, react-visually-hidden trois importeurs).

Vérifié. vitest run packages/site : 61 fichiers, 620 tests (12 nouveaux) ; pnpm -F @recit/site typecheck vert ; docs:build vert (--exclude-task-dependencies) ; check-style-rule.js packages/site/src/lib/ui : 0 (baseline réécrite, dossier retiré) ; pnpm check vert. Sonde Playwright locale sur le dist (déclaration de couches ajoutée) : Ctrl+K, « tutorial », flèche bas, Entrée mènent à la page de l'option choisie ; palette et tiroir mobile aux mêmes colonnes de pixels que docs-search-open-linux.png (clair et sombre) et docs-mobile-drawer-mobile-linux.png.

Non fait : e2e en conteneur et galerie. @recit/site:build échoue toujours sur storybook importé par Header.stories.tsx (TST0) ; et sans le raccord des couches, chaque capture de site changerait. À relancer après les deux : e2e/site-docs.spec.ts e2e/site-behaviour.spec.ts. Différence attendue : aucune voulue.

STY3a — Primitives ui, actions et texte — 29/10

Vague 3 du plan 19 (§10, STY3a), en parallèle de STY3b, STY3c, STY7b et TST0. Aucune commande git.

En place. Les treize fichiers de packages/site/src/lib/ui (Button, ButtonGroup, Badge, Kbd, Toggle, Input, InputGroup, Label, Textarea, Separator, Card, Alert, Callout) sur le composant Mantine de §4.1 (Button/UnstyledButton, Button.Group et Button.GroupSection, Badge, Kbd, ActionIcon pour Toggle, TextInput, Input.Wrapper, Input.Label, Textarea, Divider, Card/Card.Section/Text, Alert), chacun avec son X.module.css branché par classNames ; plus de cn, cva, Radix ni Base UI dans ces fichiers. Les 27 fichiers sans importeur de §4.1 sont supprimés (l'export ./ui/* est un joker : rien à retirer de package.json). Nouveau : ui/cx.ts, même contrat que le cx de @recit/components, pour les deux endroits que le className de Mantine n'atteint pas (l'<input> d'un TextInput, l'enfant d'un asChild).

Le mécanisme.

  • Tous les composants Mantine sont unstyled : le module dessine seul, valeur pour valeur depuis la feuille compilée d'avant (sites/docs/dist, Tailwind 4), et aucune règle de Mantine (hauteur, text-box-trim, ::before, mantine-active, focus) n'entre en concurrence. Les enveloppes que Mantine ajoute (inner/label du bouton, label du badge, root/wrapper d'un TextInput, wrapper/body/message de l'alerte, icon de l'ActionIcon) sont display: contents : les enfants restent des éléments flex ou grid de la racine, comme avant ; les sélecteurs [&>svg] deviennent .message > svg, .label > svg.
  • Couches : les modules sont dans @layer components, sous utilities. Une classe Tailwind passée par un importeur (className="h-7 text-xs" de PagefindSearch, size-6 de NavigationCategory, flex flex-col gap-2 de Mermaid) gagne donc comme tailwind-merge la faisait gagner. STY3b et STY3c ont fait le même choix. Trois familles de règles restent hors couche, parce que l'utilitaire qu'elles remplacent battait aussi ce qui les entoure : display et padding-inline des tailles de Button (un enfant asChild portait flex ou p-5, que Tailwind émettait avant inline-flex et px-2.5 : Pagination, lien « modifier » de MdxPage), les règles sur les paragraphes et liens de AlertDescription (elles battent .prose, rangé dans utilities), et le contenu de Callout (a, ul, ol).
  • cva → data-* : data-variant et data-size (posés par la Box de Mantine depuis variant et size), data-fit de Alert, data-orientation de ButtonGroup/Separator, data-type de Callout qui choisit l'une des onze --callout-* (le style inline et le --callout-color calculé disparaissent).
  • asChild (Button, Badge, ButtonGroupText) : renderRoot de Mantine ; pour Button, UnstyledButton, sans les deux <span> du Button de Mantine, pour que space-x-1 de Pagination voie toujours ses enfants directs. Les classes du Slot et de l'enfant se joignent.
  • Couleurs : var(--mantine-*) là où STY2 les fait pointer exactement sur la variable du site (body, text, dimmed, placeholder, primary-color-filled/contrast, default-border, error, rayons) ; --muted, --secondary, --input, --ring, --card, --warning, --accent sinon ; light-dark() pour chaque dark: (preset Mantine : règle [data-mantine-color-scheme=dark]) ; la palette bleue de l'alerte info écrite en lab(), les variables --color-blue-* de Tailwind n'étant émises que tant qu'un utilitaire les emploie.
  • Un module par fichier, une classe racine par composant exporté (Kbd : .root, .group ; Card : .root, .section lu par data-slot, .title, .description).

API. Props inchangées pour les importeurs. Retirés, sans utilisateur : buttonVariants, badgeVariants, toggleVariants, buttonGroupVariants (des chaînes de classes), asChild de Separator, l'attribut HTML title d'Alert (pris par Mantine). Toggle garde pressed, defaultPressed, onPressedChange(pressed, event). Un Separator décoratif porte role="none" et, Mantine filtrant les undefined, un aria-orientation en plus.

Hors liste, additif. Quatre tests de lib/components rendent sous MantineProvider (wrapper: MantineProvider) : BrowserWindow, LandingPage, Main, navigation/Navigation.

Vérifié.

Code
$ pnpm exec vitest run packages/site Test Files 61 passed (61) Tests 620 passed (620) $ pnpm -F @recit/site typecheck → tsgo muet $ pnpm exec nx run docs:build --excludeTaskDependencies → vert (sans l'option, @recit/site:build échoue en postbuild : `storybook` importé par Header.stories.tsx, TST0) $ node scripts/check-style-rule.js packages/site/src/lib/ui total 0 0 0 0 0 0 0 0 (mes treize fichiers à zéro avant que STY3b ne finisse les siens) $ node scripts/check-style-rule.js --write-baseline packages/site/src/lib/ui ✅ scripts/style-rule-baseline.json written: 751 in 29 directories. (ui : 335 → 0) $ pnpm check biome : 3 erreurs `suppressions/unused` dans Slot.test.tsx et createRedirectRoutes.test.tsx (hors lot) ; mes fichiers : avertissements seulement (`@mixin` inconnu de biome, specificité descendante) ; fmt:check : Command, Dialog, DropdownMenu, Tooltip (STY3b, en cours) ; check:imports et check:other verts.

Rendu SSR vérifié à la main (renderToString de chaque composant sous MantineProvider) : classes de module puis classe de l'auteur, data-variant/data-size, enveloppes Mantine attendues ; les style de variables posés par Mantine (--button-height…) restent dans le HTML, sans effet (unstyled).

Non fait : e2e et galerie. pnpm test:e2e:linux e2e/site-docs.spec.ts e2e/site-www.spec.ts e2e/site-components.spec.ts lancé une fois : la construction du conteneur échoue sur @recit/site:build (même postbuild, Header.stories.tsx de TST0) ; attendu vingt minutes, pas corrigé. Il bloquerait de toute façon sur le défaut relevé par STY3b : dans le dist, Alert-*.css (module @layer components) est la première feuille liée et ouvre components avant layers.css, donc sous mantine et base (vérifié dans sites/docs/dist/index.html). À relancer après les deux raccords ; différences attendues : aucune voulue.

TST0 — Le socle Storybook test — 29/10

Vague 3 du plan 21 (§8), en parallèle de STY3a-c, STY6.2 et STY7b. Aucune commande git, pas de pnpm fix.

Dépendances. @storybook/addon-vitest 10.6.3 (aligné sur le Storybook installé : pas besoin de 10.6.5) et @vitest/browser-playwright 4.1.11 au catalogue ; sites/storybook reçoit en plus des paquets déjà dans le lockfile (@recit/site, @mantine/core, motion, @unhead/react 3.4.2, vitest 4.1.11 pour que le pair soit le Vitest racine) ; packages/site reçoit en dev @storybook/react-vite et storybook (le typecheck de tsconfig.app.json voit les stories) et l'export ./theme. Un seul Vitest dans le lockfile, résolu avec @vitest/browser-playwright.

En place.

  • Projet Vitest storybook (sites/storybook/vitest.config.ts, ajouté aux projects racine) : storybookTest, Chromium headless par Playwright, prefers-reduced-motion: reduce, retry: 1, axe en erreur. Le plugin applique lui-même les annotations de preview.tsx (pas de setProjectAnnotations). .storybook/vitest.setup.ts : presse-papiers en mémoire, échec sur tout console.error. .storybook/vitest.chromium.ts (global setup) : sans Chromium, arrêt avec « Install it once with: pnpm exec playwright install chromium » ou pnpm test --project '!storybook'. Fenêtre : 1200 × 900 par défaut, globals.viewport sinon.
  • Décorateurs (.storybook/decorators/), chacun décide par le premier segment du titre (surface.ts) : withSlide (déplacé de preview.tsx, withSlide.module.css = l'ancien preview.module.css), withSite, withMantine (createRecitTheme + recitCssVariablesResolver, forceColorScheme depuis globals.theme), withReducedMotion (MotionConfig), withTheme (classe .dark et data-mantine-color-scheme), viewports.ts (mobile 390 × 844, tablet, desktop 1280 × 720), stylesheets.ts (loader), a11yFindings.ts (voir plus bas).
  • Harnais du site : @recit/site/testing exporte en plus SiteProvider, SlotProvider, ViewportAnchorProvider, SiteContext (SiteRoot garde un SiteContext par onglet, deux stories le partageraient). withSite monte un routeur mémoire sur parameters.site.path, un QueryClient neuf, le contexte de sites/storybook/src/harness/config.tsx (site Lorem Ipsum, trois liens, une recherche de fixture : Modal Mantine « Search ») fusionné avec parameters.site.options, Suspense. Plugin recitVirtualModules dans main.ts : virtual:recit-config et virtual:recit-theme.css ; aucun module de src/lib n'importe de module virtuel aujourd'hui, un module manquant échoue avec son nom. Les globs des huit surfaces de §3.1 sont dans main.ts (quatre vides : un avertissement « No story files found » chacun).
  • Feuilles : celle du site (Mantine, defaultTheme.css, main.css Tailwind du framework, sites/storybook/styles.css pour le fond) est chargée par un loader, seulement pour les surfaces Mantine : elle redéfinit --background et les utilitaires de preview.css dans un autre format. layers.css du site est importé en tête de preview.tsx. En storybook dev, recharger la page après une story Site/ avant de revoir une diapositive.
  • Story pilote packages/site/src/lib/components/Header.stories.tsx (Site/Components/Header) : play qui vérifie aria-current="page" sur « Sit amet », ouvre la recherche au clic, trouve le dialog « Search » par rôle, le champ focalisé, ferme par Échap. Vérifié qu'un nom faux échoue.
  • e2e/storybook.spec.ts lit storybook-static/index.json : deux captures par story, attente de currentRender.phase completed/finished (le play fini), puis wait-mermaid / wait-motion lus dans les tags ; plus d'attente fixe de 700 ms pour toutes. PENDING_TAGS donne ces tags aux stories de @recit/components (cover, media, mermaid, points, quote) jusqu'à TST1.
  • scripts/check-stories.js (+ scripts/stories-baseline.json, spec 14 tests) dans check:other, en avertissement : 142 (story 133, play 1, testid 8) ; --list ou un dossier listent les fichiers. Coverage racine : **/*.stories.{ts,tsx} exclus.
  • packages/site/scripts/check-external-deps.ts ignore **/*.stories.* comme les tests : le postbuild de @recit/site refusait l'import storybook de la story pilote, ce qui a cassé une construction en conteneur de STY3a (corrigé à 20:45).

Trouvé. axe (color-contrast) : slides-codeblock--type-script, nom de fichier et langue sur l'en-tête sombre (2,62:1), passé en todo par decorators/a11yFindings.ts (liste qui ne fait que baisser, à TST1) ; le ⌘K du bouton de recherche (4,39:1, text-muted-foreground sur bg-muted), a11y.test: "todo" dans la story pilote, à STY4a.

Vérifié.

Code
$ pnpm test --project storybook → 10 fichiers, 17 tests (16 + le pilote), 13 s $ pnpm test → 176 fichiers, 1758 tests, 69 s (1 min 12 mur) avant, `--project '!storybook'` : 55 s (58 s mur) ; le projet ajoute ~15 s $ pnpm -F recit-storybook build && typecheck ; pnpm -F @recit/site typecheck ; build → verts $ pnpm exec vitest run scripts/__tests__ → 9 fichiers, 230 tests $ node scripts/check-stories.js → ✅ No rise (warning mode, 142 remaining) $ pnpm check → vert (avertissements biome préexistants) $ BROWSER=none pnpm test:e2e:linux e2e/storybook.spec.ts 33 passed, 2 failed (les deux captures absentes du pilote) : les 32 existantes inchangées $ BROWSER=none pnpm test:e2e:update:linux e2e/storybook.spec.ts → 35 passed, deux écrites $ BROWSER=none pnpm test:e2e:linux e2e/storybook.spec.ts → 35 passed (47,6 s)

Références nouvelles : e2e/__screenshots__/storybook.spec.ts/storybook-site-components-header--default-{light,dark}-linux.png.

Couverture (lignes / branches, pnpm test:coverage une fois, stories exclues) : core 89,8 / 73,9 ; motion 86,6 / 73,3 ; scripts 87,1 / 81,7 ; app 73,1 / 57,0 ; site-stories 68,6 / 51,0 ; site 66,4 / 54,7 ; site-kit 66,7 / 37,7 ; components 63,1 / 35,3 ; motion-blocks 59,0 / 33,1 ; mcp 54,8 / 41,8 ; cli 48,7 / 38,3 ; deck 27,2 / 20,2 ; motion-render 20,6 / 6,9 ; total 66,8 / 54,3.

Recette pour TST1 à TST5. Une story X.stories.tsx à côté de X.tsx, titre de la surface (Slides/…, Site/UI/…, Site/Components/…) : les décorateurs suivent seuls ; Site/ reçoit parameters.site.{path,options} ; play avec storybook/test, rôles et noms, findBy* après une interaction, within(canvasElement.ownerDocument.body) pour un portail ; tag wait-mermaid ou wait-motion si la story se stabilise tard (TST1 vide PENDING_TAGS) ; une violation axe se corrige ou passe en a11y: { test: "todo" } avec sa raison. Puis pnpm test --project storybook, node scripts/check-stories.js <dossier> et --write-baseline <dossier>, et à la porte pnpm test:e2e:update:linux e2e/storybook.spec.ts. Une story d'un site ou d'un exemple ajoute ce projet aux devDependencies de sites/storybook (ST-20).

Porte de la vague 3 — 29/10

  • Ordre des couches : Vite liait la feuille d'un chunk (Alert-*.css, en @layer components) avant celle de l'entrée, ce qui rangeait components sous mantine et sous la base de Tailwind. recit-style (packages/site/src/vite/plugin.ts) ouvre désormais chaque feuille émise sur @layer mantine, theme, base, components, utilities;.
  • IndexingDialog prend DialogTrigger de @recit/site/ui/Dialog.js (il importait encore Radix).
  • e2e complet dans le conteneur : 454 passés, 5 échecs expliqués. plans-home change par le contenu (plans 18 à 21 dans le tableau et la carte) et /demos/mdx par le changement voulu de la vague 1 : références régénérées. La pagination de site-behaviour perdait un clic pendant l'hydratation, plus longue depuis les chunks de Dialog et Spotlight : le test réessaie le clic comme ceux du thème et de la recherche. Un clic perdu pendant l'hydratation reste à comprendre côté navigation (STY4b).
  • Galerie vide ; pnpm check vert. ui/util.tsx reste : index.ts et Search.tsx l'importent encore (STY4a).

TST2a — Stories Site/UI, actions et code — 29/10

Vague 4 du plan 21 (§8, TST2), en parallèle de TST1, TST2b, STY4a-b, STY6.3a-b et STY8. Aucune commande git, pas de pnpm fix.

En place. Dix-neuf fichiers packages/site/src/lib/ui/<X>.stories.tsx, titre Site/UI/<X> (withMantine, withTheme, withSite suivent seuls) : Button, ButtonGroup, Badge, Kbd, Toggle, Input, InputGroup, Label, Textarea, Separator, Card, Alert, Callout, Stepper, CodeBlock, EmbeddedCodeBlock, CodeTabs, CodeTabPanel, SyntaxHighlight. 84 stories, 43 play, les états et interactions de §4.1 : clic, Entrée, Espace et focus visible du bouton, désactivé sans rappel, asChild en lien ; aria-pressed du Toggle au clic et à Espace ; saisie et onChange des champs, label qui focalise son champ, addon qui focalise l'entrée ; role="separator" ou rien ; role="alert" et role="note" ; liste ordonnée du Stepper ; copie dans le presse-papiers (moqué par vitest.setup.ts) et bouton désactivé deux secondes ; onglets au clic et aux flèches, aria-selected, onglet partagé par syncKey et mémorisé dans localStorage. Variantes disposées par Group/Stack de Mantine (gap, align) : ni classe ni style, compteur de la règle à zéro. Rappels en fn() d'args.

  • Hors liste, nouveau : packages/site/src/lib/testing/syntaxHighlighting.ts (syntaxHighlightingFixture), le surligneur qu'un site reçoit sans section syntaxHighlighting (thèmes et defaultLanguages par imports explicites), passé par parameters.site.options aux cinq stories de code.
  • CodeBlock.test.tsx réduit à getCodeBlockLabel et au code de MdxComponents (qu'aucune story ne rend) ; les trois tests de rendu (titre, nom de langue, classes) sont dans les play et les captures. Stepper.test.tsx reste tel quel : il teste l'enregistrement des ancres auprès de l'IntersectionObserver, ce qu'une story n'affirme pas.

Défauts trouvés (composants hors lot, stories en todo avec la raison) :

  • Input, Textarea, InputGroupInput perdent aria-invalid : l'Input de Mantine le pose depuis sa prop error et écrase celui de l'auteur ; Input.module.css n'a pas d'état invalide et la règle :has([aria-invalid="true"]) d'InputGroup.module.css ne peut pas s'appliquer. Les trois stories Invalid sont en tags: ["!test"], commentées (STY3a).
  • CodeTabs copie le code de tous les onglets : Tabs de Mantine garde les panneaux cachés montés (keepMounted) dans l'élément copié. Story CopiesTheVisibleTab en !test (STY3c).
  • axe aria-allowed-attr : un Separator décoratif (role="none") porte aria-orientation (Separator, ButtonGroup avec séparateur).
  • axe color-contrast : Alert destructive (4,36:1) et texte de l'info (4,43:1) ; libellé d'un onglet inactif de CodeTabs (4,42:1) ; l'orange de github-light, thème clair par défaut (3,48:1, #e36209), dans SyntaxHighlight.
  • Harnais : une story Site/ plus haute que la fenêtre (900 px) échoue à scrollable-region-focusable sur <body> (le overflow-y-auto du body de main.css) ; Callout/Types tient sur une ligne par type pour l'éviter.

Vérifié.

Code
$ pnpm test --project storybook → 44 fichiers passés, 1 sauté ; 161 tests (un premier passage concurrent : 13 échecs hors lot, connexion du navigateur fermée, verts au second) $ pnpm exec vitest run packages/site --project '!storybook' → 5 échecs, tous dans navigation/Navigation.test.tsx (STY4b en cours) $ pnpm -F @recit/site typecheck → muet $ pnpm -F recit-storybook typecheck → 3 erreurs navigation/ (navigationListItem, STY4b) $ pnpm -F recit-storybook build → vert (un premier essai : packages/theme/dist reconstruit par un autre agent) ; 118 stories Site/UI dans index.json $ node scripts/check-stories.js packages/site/src/lib/ui → 0 ; baseline réécrite (site/ui retiré) $ node scripts/check-style-rule.js packages/site/src/lib/ui → 0 $ pnpm check → biome : 2 erreurs hors lot (biome.jsonc useBiomeIgnoreFolder, createRedirectRoutes.test.tsx) ; fmt:check : 9 fichiers navigation/ (STY4b) ; check:other : `sites` 7 → 9 composants sans story (hors lot) ; mes fichiers propres (biome, oxfmt)

Premier passage après l'ajout des grammaires Shiki : Vite découvre @shikijs/langs/* en cours de route et recharge (13 échecs), vert au passage suivant ; un cache neuf les trouve au scan initial. Captures des nouvelles stories : à la porte de la vague (pnpm test:e2e:update:linux e2e/storybook.spec.ts).

TST1 — Stories de @recit/components — 29/10

Vague 4 du plan 21 (§8), en parallèle de STY4a-b, STY6.3a-b, STY8, TST2a-b. Aucune commande git, pas de pnpm install, pas de pnpm fix.

Stories. 18 fichiers, 51 stories sous Slides/ (13 avant). Les 9 existants gardent leurs exports (les références e2e restent valables) et gagnent leurs états : Callout (WithoutTitle, LongContent), CodeBlock (LongCode à 26 lignes, WithoutLineNumbers), CodeGroup (SwitchingTabs, SingleTab), Cover/LongTitle, Grid (ThreeColumns, Overloaded), Media/MissingImage, Mermaid/InvalidChart, Points (WithoutTitle, ManyItems), Quote (AuthorWithoutRole, LongQuote). Nouveaux : Card, Columns, Col (Left/Right), SlideImage (cover, contain, légende, image manquante), Badge, Kbd, Step, SlideMotion et Builtins/Composition (le registre BUILTIN_MDX_COMPONENTS composé comme un auteur l'écrirait : builtins.tsx exporte SlideGrid, le compteur y voit un composant). Une image manquante est une URL data: indécodable : aucune requête n'échoue dans la console (la spec e2e la refuserait).

play. CodeGroup : premier onglet sélectionné, clic sur « yarn », aria-selected et panneau seul visible ; CodeBlock : en-tête (fichier, langue) puis attente de Shiki (un mot-clé devenu un nœud de texte à lui seul), et le code d'une langue inconnue reste du texte ; Mermaid : dessin (étiquettes des nœuds, SVG nommé par le titre) puis message d'erreur sans dessin ; Step : indices portés dans l'ordre de la source, tous visibles hors d'un deck ; plus Card (titre de niveau 3), Media/SlideImage (image nommée par alt, légende), Grid/Overloaded (avertissement), SlideMotion/Nested (le contexte imbriqué s'efface et l'entrée finit à opacité 1). Les play interagissent par le canvas et le userEvent du contexte de la story, sans expect ni fn : @recit/components n'a pas storybook en dépendance de dev, storybook/test se résout dans Vite mais pas au typecheck du paquet (TS2307), et la consigne excluait d'ajouter une dépendance. Les requêtes par rôle et par état (selected: true) valent assertion (elles lèvent) ; deux gardes lèvent une Error à la main. Aucun rappel dans ces composants : pas de fn() à poser. Pour la suite : "storybook": "catalog:" en dev de @recit/components (comme packages/site à TST0) rendrait expect disponible.

Ce que la loi change aux stories. withGrid (.storybook/decorators/withGrid.tsx + grid.module.css, stack ou row par parameters.grid, premier décorateur de preview.tsx) dispose les variantes que la story rend côte à côte (Callout/AllTypes, Badge/AllTypes, Kbd/Shortcut, Step/Indexed). Les cartes des stories Grid sont des <Card> (la composition d'un :::grid) : slides/Stories.module.css (+ table) est supprimé, ses deux règles quittent dist/styles.css du paquet publié (16 modules).

Tags. wait-motion sur Cover, Media, Points, Quote, SlideMotion (meta), wait-mermaid sur Mermaid/Flowchart seule (l'erreur n'a pas de SVG) ; PENDING_TAGS retiré de e2e/storybook.spec.ts (tagsOf lit index.json), entrée CodeBlock retirée de a11yFindings.ts (liste vide).

Contraste. L'en-tête de CodeBlock prend la couleur atténuée du thème sombre dans les deux modes (hsl(215 20.2% 65.1%), 4,87:1 sur #333434) : le chrome est sombre partout. Différence de capture voulue : slides-codeblock--type-script-light (en-tête plus clair) ; en sombre, la valeur est celle d'avant. Autres différences voulues : slides-grid--two-columns et --four-columns (les cartes de deck.css au lieu des cartes de fixture), et les 76 captures des stories nouvelles.

Trouvé, hors lot. axe color-contrast en a11y.test: "todo" dans la story, raison écrite : Badge succès, attention, danger (deck.css, 2,72 à 3,81:1), l'avertissement de Grid (4,45:1), le message d'erreur de Mermaid (3,14:1). renderMermaidSvg ajoute role="img" au SVG titré, mais Mermaid y a déjà mis role="graphics-document document" : le second attribut est ignoré par le parseur, le SVG reste un graphics-document (nommé, donc sans effet pour axe). CodeBlock n'a pas de bouton copier : le play « copier » de §4.3 n'a rien à cliquer.

Tests réduits. slides.spec.ts ne garde que le contrat HTML partagé avec le pipeline Markdown et les règles de deck.css (l'avertissement de surcharge et la ligne d'auteur de Quote sont aux stories) ; code-block.spec.tsx rend les stories par composeStories (renderToStaticMarkup, fallback échappé), l'en-tête passe au play.

Vérifié.

Code
$ pnpm exec vitest run --project storybook packages/components → 18 fichiers, 51 tests $ pnpm test --project storybook → 44 fichiers (+1 ignoré), 161 passés, 5 todo/ignorés $ pnpm exec vitest run packages/components → 23 fichiers, 83 tests $ pnpm -F @recit/components build && typecheck ; pnpm -F recit-storybook build && typecheck → verts $ node scripts/check-stories.js packages/components → 0 ; --write-baseline packages/components $ node scripts/check-style-rule.js packages/components → 0 $ node scripts/build-css-modules.js --check --all → ✅ $ pnpm check → rouge par d'autres lots : oxfmt sur navigation/NavigationItem.tsx et NavigationBadge.module.css (STY4b), check-stories `sites` 7 → 9 ; biome et imports verts

Pendant le lot, deux passages du projet storybook ont échoué par les autres : un export manquant de navigation/utils.ts en cours d'écriture, puis @recit/core/browser introuvable pendant une reconstruction de dist. Repassés verts sans rien changer. À la porte : pnpm test:e2e:update:linux e2e/storybook.spec.ts, revue de galerie.

Vague 4, raccords de l'orchestrateur — 29/10

  • Deux régressions de la vague 3 trouvées par les stories de TST2a, corrigées dans lib/ui : CodeTabs gardait les panneaux cachés montés (keepMounted de Mantine), si bien que le bouton copier copiait tous les onglets ; keepMounted={false} rend le comportement de Radix. Input et Textarea perdaient le aria-invalid de l'auteur, que Mantine recalcule depuis error : error en est maintenant dérivé. Les stories CopiesTheVisibleTab et les trois Invalid sont réactivées (19/19).
  • NavigationMenuLink active repose aria-current="page" comme Radix (trouvé par TST2b).
  • TST1 : storybook manque en devDependency de @recit/components (les play n'y utilisent pas expect) ; à ajouter à la porte de la vague 4, hors de tout agent, puis passer les play sur expect.

TST2b — Stories Site/UI, surcouches — 29/10

Vague 4 du plan 21 (§8, TST2), en parallèle de TST1, TST2a, STY4a-b, STY6.3a-b et STY8. Aucune commande git, pas de pnpm install, pas de pnpm fix.

En place. Huit fichiers packages/site/src/lib/ui/<X>.stories.tsx, titre Site/UI/<X> : Dialog, Drawer, Popover, Tooltip, DropdownMenu, Command, Collapsible, NavigationMenu. 34 stories, toutes avec un play ; rappels en fn() d'args (onOpenChange, et un onSelect d'args typé à côté du composant pour le menu et la palette).

  • Dialog, Drawer : ouverture au clic et à Entrée, nom par le titre, aria-modal, focus piégé (Tab répété reste dans le dialogue), Échap ferme et rend le focus au déclencheur, clic dehors (l'élément sous le coin, elementFromPoint : l'overlay, sans sélecteur), bouton de fermeture, formulaire (champ data-autofocus focalisé, saisie, Enregistrer ferme) ; Dialog non fermable par clic extérieur (onInteractOutside annulé, seul Échap ferme) ; Drawer à droite et à gauche, bord mesuré.
  • Popover : clic et Entrée ouvrent, focus dedans et piégé, Échap et clic dehors ferment, focus rendu ; positions top-start, right, left. Tooltip : survol et focus clavier ouvrent, aria-describedby (description accessible du déclencheur), Échap ferme ; positions.
  • DropdownMenu : clic ouvre un menu, clic sur un item choisit et ferme, flèches (l'item désactivé est sauté), Entrée choisit, Échap ferme et rend le focus ; sous-menu par flèche droite ; cases (menuitemcheckbox) et radios (menuitemradio) avec aria-checked.
  • Command (Spotlight) : la palette filtrée par l'importeur, comme la recherche du site ; dialogue nommé, combobox focalisé et aria-controls vers la listbox, deux groupes, flèches et aria-activedescendant, Entrée choisit et ferme (onOpenChange(false) une seule fois), saisie qui filtre, aucun résultat (état vide visible), pointeur (survol sélectionne, clic choisit), Échap, ouverture par l'importeur et focus rendu. Ctrl+K n'est pas à CommandDialog (shortcut={null}) : il appartient à Search (TST3).
  • Collapsible : aria-expanded, aria-controls vers le contenu, clic, Entrée et Espace, désactivé. NavigationMenu : navigation nommée, survol ouvre la liste sous le déclencheur, Entrée l'ouvre, Échap la ferme sur le déclencheur, un lien la ferme.

Tests. Command.test.tsx ne garde que l'asChild de CommandItem (l'option rendue en lien, la classe de l'item, aria-activedescendant) ; rôles, flèches, Entrée, fermeture signalée une fois et état vide sont aux stories. Overlays.test.tsx perd Dialog, Tooltip et l'aria-controls de Collapsible (aux stories) et garde l'API Radix des importeurs : onCloseAutoFocus et id du Drawer, classes jointes et data-align du Popover, asChild d'un item de menu en lien, data-state du Collapsible, classes du lien de NavigationMenu.

withMantine sans env="test". Inutile dans Chromium : les flottants de Mantine s'y montrent après leur transition ; env="test" ne sert qu'à happy-dom (les deux tests le gardent).

Défauts trouvés (composants hors lot, story en a11y.test: "todo" avec la raison) :

  • axe aria-required-children sur un menu ouvert : Menu.Dropdown de Mantine pose sa sentinelle de focus (<div role="presentation" tabindex="-1" data-autofocus>) dans le menu (DropdownMenu/Open, CheckboxesAndRadios) ; dans NavigationMenu, le menu contient de plus des liens au lieu de menuitem (Expanded).
  • axe aria-required-children : la listbox de Command sans résultat ne contient que l'état vide (role="presentation"), comme celle de cmdk (NoResults).
  • axe color-contrast : CommandShortcut sur l'item sélectionné, 4,39:1 (Filtered), le même couple que le ⌘K de Search noté à TST0.
  • NavigationMenuLink active ne pose plus aria-current="page" (Radix le faisait) : seulement data-active. Non affirmé par la story, à rétablir dans NavigationMenu.tsx.
  • Sans largeur passée par l'importeur, un Drawer left/right couvre toute la page (comme celui de vaul) : pas de clic dehors possible ; la story Right ferme par Échap.

Pièges. Dans le projet Vitest, parameters.layout ne s'applique pas : le canevas est dans le coin de la page, et Mantine retourne un flottant placé au-dessus ou à gauche ; seules les positions avec de la place sont mesurées (les captures, centrées, montrent les autres). Un clic sur un lien href="#…" d'une story fermait la page du navigateur de test (« Browser connection was closed ») : les liens de NavigationMenu cliqués font preventDefault.

Vérifié.

Code
$ pnpm test --project storybook → 45 fichiers passés, 1 sauté ; 170 tests (un échec axe corrigé en todo, puis les 6 de Command verts) ; plusieurs passages perdus à des reconstructions de dist par d'autres lots (@recit/core, @recit/components) et à navigation/ en cours d'écriture $ pnpm exec vitest run packages/site --project '!storybook' → 60 fichiers, 613 tests $ pnpm -F @recit/site typecheck ; pnpm -F recit-storybook typecheck → muets $ pnpm -F recit-storybook build → vert ; 34 stories de ces huit composants dans index.json $ node scripts/check-stories.js packages/site/src/lib/ui → 0 (site/ui déjà retiré de la baseline) $ node scripts/check-style-rule.js packages/site/src/lib/ui → 0 $ pnpm check → vert

Une assertion faussée (onSelect attendu avec un autre item) fait échouer Command/Default. Captures des 34 stories : à la porte de la vague (pnpm test:e2e:update:linux e2e/storybook.spec.ts).

STY6.3a — @recit/deck — 29/10

Vague 4 du plan 19 (§10, STY6.3), en parallèle de STY4a-b, STY6.3b, STY8, TST1, TST2a-b. Aucune commande git, pas de pnpm install, pas de pnpm fix.

Fichiers. packages/formats/deck/src/** : 11 modules et leurs tables (Presentation, PresenterView, SlideFrame, SlideStage, MdxSlide, HtmlSlide, DeckApp, AnnotationCanvas, DevHud, MobileRemoteView, RemoteQrCodeModal), les composants, deux tests (first-render.spec.tsx lit classes.content, first-paint.spec.tsx nouveau) ; packages/core/src/config/theme-css.ts et son test theme-css-deck.spec.ts ; scripts/style-rule-baseline.json (les cinq dossiers du deck retirés). Hors liste, une ligne : packages/core/src/compile/compile-story.ts passe { design, scenes } à buildThemeCss.

Premier rendu (§0.3 permis 4), aucun useCssVariable dans le deck. buildThemeCss prend un quatrième argument { design, scenes } et émet, dans la portée du thème, --deck-design-width/height, puis une règle par scène [data-slide-id="…"] { --slide-progress; --slide-bg } (préfixée par la portée quand le récit est embarqué, id échappé en chaîne CSS ; un bg qui contient ;, {, } ou < est refusé avec le diagnostic invalid-scene-background). SlideStage ne mesure plus rien : le cadre est un conteneur size, l'échelle est max(0, min(tan(atan2(100cqw - 2p, W)), tan(atan2(100cqh - 2p, H)), 1.5)), W/H lus dans les variables du récit, avec data-aspect (16:9/4:3, dérivé de design) comme repli quand aucune feuille de récit n'est là ; data-padding (0, 4, 8, 24) remplace le padding numérique (type StagePadding), la prop maxScale disparaît (personne ne la passait ; computeStageScale reste, même formule). Plus de visibility: hidden au premier rendu : l'échelle est juste dès la première peinture. SlideFrame : une classe par mise en page (Record<SlideLayout, …> qui compose slide slide-layout-x), data-bg quand la scène a un fond ([data-bg] { background: var(--slide-bg) }), barre de progression en calc(var(--slide-progress) * 100%). Le mode impression lit les mêmes variables de design. Sondé sur l'hôte (Chromium, deck-showcase construit) : 1280×800 → échelle 0,9625 (1232/1280), 800×600 → 0,5875, console présentateur juste.

Le reste. Les classes de deck.css et de print.css restent dans le HTML par composes … from global (deck-viewport, deck-stage, print-slide, deck-hud, dev-hud, annotation-canvas, remote-modal, speaker-notes-container…), les sélecteurs e2e ne changent pas. Aucune imbrication CSS : Lightning CSS sans targets la laisse native, la passe de préfixage de site-stories et les consommateurs la verraient telle quelle. display: contents → classe (MdxSlide, HtmlSlide). Laser d'AnnotationCanvas : dessiné sur un second canevas, repeint à chaque mouvement, plus de left/top ; les canevas sont dimensionnés quand le calque s'affiche (avant, l'effet ne tournait qu'au montage, quand le canevas n'existait pas encore : 300×150 étiré), sans réécrire une taille inchangée (ce qui effacerait le stylo). PresenterView passe de la palette slate aux tokens (--background, --card, --muted, --border, --primary) ; MobileRemoteView et RemoteQrCodeModal gardent leurs valeurs relevées dans la feuille Tailwind compilée (slate en oklch, shadow-2xl, backdrop-blur-sm 8px). cx de @recit/components relaie les className.

Vérifications.

Code
$ node scripts/check-style-rule.js packages/formats/deck → total 0 (108 avant) $ node scripts/check-style-rule.js --write-baseline packages/formats/deck → 643 in 24 directories $ pnpm exec vitest run packages/formats/deck packages/core → 35 fichiers, 359 tests verts (un passage : « dual Shiki themes » en délai sous charge, vert seul) $ pnpm exec nx run-many -t build typecheck -p @recit/deck @recit/core → 6 tâches vertes $ node scripts/build-css-modules.js --check --all → @recit/deck: 11 CSS modules, tables up to date $ pnpm exec nx run recit-example-deck-showcase:build → vert ; main.css porte recit-deck-SlideStage__frame{…container-type:size}, index.html les règles [data-slide-id] $ pnpm check → biome sur deck et core vert ; échecs d'autres lots : `SyntaxError` d'une story (noShadowRestrictedNames), oxfmt sur 17 fichiers de site/site-kit, check-stories (sites 7 → 9) $ BROWSER=none pnpm test:e2e:linux e2e/deck-features.spec.ts e2e/deck-render.spec.ts e2e/deck-behaviour.spec.ts e2e/site-components.spec.ts e2e/site-embed.spec.ts e2e/motion-dev.spec.ts 80 passed, 28 failed (5.8m) ; deux passages avortés avant (tar sur un arbre en écriture, puis `www:build` sur Hero.module.css de STY8) $ node scripts/screenshot-gallery.js → 0 changed screenshot(s)

Les 28 échecs sont les 14 pages de site-components × 2 modes, par la hauteur (2767 → 2779 px) : le texte de la page a changé (« in any page of a site », pied de page sans l'ancien nom du framework), les tableaux et l'en-tête aussi (lots du site) ; la démo du deck y est identique. La galerie ne les liste pas (tailles différentes). deck-features, deck-render, deck-behaviour, site-embed et motion-dev verts : les captures de la console présentateur (clair et sombre) passent sous le seuil de 2 % (le fond sombre du thème par défaut est proche du slate), donc la différence attendue par le plan n'a rien à faire régénérer. Aucune référence régénérée.

STY8 — Sites et exemples — 29/10

Lot STY8 du plan 19, vague 4, sur feature/mantine. Aucune commande git.

Fait. Un module par composant, les valeurs des utilitaires Tailwind d'avant reprises valeur par valeur (lues dans la feuille compilée de sites/docs).

  • sites/www/src : Hero, FormatGrid, OneSource (SideBySide, Caption) + leurs modules. not-prose par composes: not-prose from global ; règles en @layer components, sous les utilities de Tailwind, pour que le @utility prose de main.css (.prose a, .prose code, qui ignorent not-prose) continue de gagner comme avant (STY5 le retire). Points de rupture : le md de Tailwind est le sm de Mantine (mixin) ; sm (40em) et lg (64em) n'ont pas d'équivalent Mantine, écrits en @media (min-width: …) comme les modules de packages/site. Les deux logos (dark:hidden/dark:block) et dark:invert passent par @mixin dark. Un composes vers une classe définie dans un @layer échoue au build (postcss-modules-scope: referenced class name not found) : sélecteurs groupés à la place.
  • sites/docs/src/StatusBadge, sites/components/src/FormatSupport : data-status, couleurs emerald/amber de la palette Tailwind en oklch() et light-dark(). StatusBadge reste un <span> et non le Badge de Mantine que le plan nommait (hauteur, padding et majuscules changeraient les tableaux).
  • sites/motion/src/VideoDownloads : module.
  • examples/deck-mdx/components : Timeline.module.css (CSS brut pour esbuild, jetons @recit/theme) ; examples/deck-showcase/components : Metric.module.css et css-modules.d.ts. .value garde composes: metric-value from global, le crochet que deck-showcase/styles.css (customCss) et la page docs/theme/custom-css montrent ; la règle .metric-value vit désormais dans le module. Timeline et Metric restent dans index.tsx : en fichiers séparés, check-stories.js les compte (« sites » 7 → 9) et une story demanderait deux références Storybook nouvelles ; à faire avec TST.
  • sites/components/src/stories.css supprimé (sa règle est dans Metric.module.css), import retiré de sites/components/recit.config.ts (hors liste, une ligne). media.mdx : className="media-demo" retiré à la main (aucune règle ne l'a jamais visé).
  • Inchangés : sites/{motion,presentation}/styles.css (.typography attend STY5), packages/tools/site-kit/src/config.tsx (rien à changer, vérifié).

Vérifié. node scripts/check-style-rule.js sites examples/deck-mdx/components examples/deck-showcase/components : 44 → 3 (les 3 de sites/storybook/.storybook, hors lot) ; baseline réécrite pour sites/{www,docs,motion,components}/src et les deux components. nx run-many -t build typecheck -p www docs motion components presentation recit-example-deck-mdx recit-example-deck-showcase vert (après des échecs transitoires : dist de core, components, site-stories reconstruits par d'autres lots pendant le build). recit build des deux decks : tables {root:"e",item:"i",step:"o"} et "metric-value e". pnpm check vert (avertissements biome noUnknownAtRules sur @mixin, comme les modules de packages/site).

e2e conteneur (site-www, site-docs, site-motion, site-components, deck-render) : 175 passés, 60 échecs. deck-render entier vert (Timeline, Counter, Metric identiques dans les decks), components-present-* verts (Metric dans le site identique, stories.css en moins). Les 60 échecs sont toutes les captures de pages de site, clair et sombre, plus hautes de 12 à 112 px, textes décalés d'1 px jusque dans l'en-tête et le pied de page (hors lot : mise en page et typographie des lots concurrents de la vague 4), plus des changements de contenu postérieurs aux références (« in any page of a site », « Documentation pages in a site », date de dernière modification). Mes zones recadrées et comparées (héros, grille des formats, tableau FormatSupport) ne diffèrent que par ce décalage global. La galerie affiche 0 : elle ne retient pas les échecs de taille d'image. Aucune référence régénérée.

Point de reprise — plan 19, vague 4 interrompue — 29/10 au soir

Travail arrêté à la demande, aucun agent ni conteneur ne tourne. Branche feature/mantine, dernier commit 0ac806b9 (vagues 0 à 2) ; tout le reste est dans l'arbre de travail, non commité (≈ 414 fichiers). Compteur node scripts/check-style-rule.js : 147 restants (baseline à abaisser par --write-baseline à la porte).

Fait et vérifié : vagues 0 à 3 (porte de la vague 3 : e2e 459/459, références plans-home et /demos/mdx régénérées).

Vague 4, fait (rapports dans les notes ci-dessus) : TST1, TST2a, TST2b, STY6.3a, STY8, plus les raccords de l'orchestrateur (CodeTabs keepMounted={false}, aria-invalid d'Input/Textarea, aria-current de NavigationMenuLink).

Vague 4, à terminer : trois lots coupés en plein travail, leurs modifications partielles sont sur le disque, sans note de journal :

  • STY4a pages et mise en page (packages/site/src/lib/components hors navigation/, app/main.tsx, Layout.module.css) ; supprimer lib/ui/util.tsx quand index.ts et Search.tsx ne l'importent plus ; contraste du « ⌘K » de Search (et du raccourci de Command/Filtered).
  • STY4b navigation et plugins (lib/components/navigation/*, plugins/markdown/MdxPage.tsx, plugins/search-pagefind/*, search-inkeep, Slot.tsx) ; enquête sur le clic de pagination perdu pendant l'hydratation (le test e2e réessaie en attendant).
  • STY6.3b site-kit composants, site-stories, packages/cli/src/motion/client.tsx, motion-dev.ts.

Chaque reprise commence par inventorier le partiel (git status --short <dossiers>, git diff), le garder s'il est sain, puis finir selon le lot du plan 19 §10 et les règles du plan 20.

Porte de la vague 4, à faire ensuite, dans cet ordre :

  1. Galerie à réparer — réparée le 29/10 au soir : Playwright nomme ses pièces jointes <nom>-actual.png et le script n'acceptait que <nom>-actual, si bien qu'aucune entrée n'a jamais été retenue (le test du script fabriquait des noms sans extension). L'expression accepte l'extension, l'« avant » vient de la copie -expected.png du dossier de l'essai (repli sur la référence du dépôt), et le test utilise les vrais noms. Conséquence : les « galeries vides » des vagues 0 à 3 ne prouvaient rien, mais les tests de capture verts, eux, valent (identiques au seuil près) ; seule la vue avant/après des échecs manquait.
  2. Hauteur des pages de site : STY6.3a et STY8 voient toutes les pages de site plus hautes de 12 à 112 px avec un texte décalé d'1 px jusque dans l'en-tête et le pied de page, pendant que STY4a-b tournaient. À attribuer (STY4a-b probablement) et ramener à l'identique ; distinguer des changements de contenu (textes modifiés depuis les références).
  3. Ajouter "storybook": "catalog:" en devDependency de @recit/components (un pnpm install, hors de tout agent), puis passer les play de TST1 sur expect de storybook/test.
  4. pnpm check, e2e complet BROWSER=none pnpm test:e2e:linux, revue des captures, puis pnpm test:e2e:update:linux e2e/storybook.spec.ts (≈ 110 nouvelles stories de TST1, TST2a-b) et les différences voulues : en-tête CodeBlock des slides en clair (contraste), Grid à 2 et 4 colonnes (vraies Card), « ⌘K ».
  5. Mettre à jour le tableau d'état de 19-style.md §10 et écrire la note de porte.

Défauts trouvés, en todo dans les stories (à confier à un lot) : contrastes Badge succès/attention/danger (deck.css), avertissement de Grid, erreur de Mermaid, Alert destructive et info, onglet inactif de CodeTabs, orange Shiki github-light ; axe aria-required-children sur les menus Mantine ouverts et Command vide ; aria-orientation d'un Separator décoratif ; role="img" du SVG Mermaid ignoré ; CodeBlock des slides sans bouton copier ; Drawer latéral sans largeur couvre la page ; stories manquantes pour Timeline, Metric, Counter (exemples). Changement d'API publié à citer dans le version plan (STY9.4) : SlideStage perd maxScale.

Ensuite : vague 5 (STY5a typographie, STY5b démontage de main.css ; TST3a-b, TST4a-b), 5' (STY5c retrait de Tailwind), 6 (STY9 bascule finale, STY10.2, TST5), selon 20-multi-agents.md §4 et 21-tests.md §8.1. Règles pour les agents : 20-multi-agents.md (conteneur e2e un à la fois, attente par docker ps --filter ancestor=mcr.microsoft.com/playwright:v1.64.1-noble, jamais pgrep -f/pkill -f sur un motif de sa propre ligne, jamais docker stop, aucune régénération de référence hors porte).

Biome, la loi du style et les sites en Markdown — 29/10

  • biome.jsonc : erreurs pour les imports de l'ancienne pile (clsx, class-variance-authority, tailwind-merge, cmdk, vaul, next-themes, @radix-ui/*, @base-ui/*, util/cn), sauf les cinq derniers importeurs (Typography, Heading, shiki.ts, cn.ts, index.ts, liste qui ne fait que décroître, vidée par STY5c) ; @mixin permis là où PostCSS Mantine tourne (packages/site, sites, site-kit) et refusé en erreur dans les paquets publiés et les exemples de deck ; !important permis dans print.css et deck.css ; !**/.recit sans /**.
  • Plugins GritQL dans scripts/biome-plugins/ : style-law.grit (un className chaîne, gabarit ou concaténation, et toute prop style, en avertissement jusqu'à STY9, hors couche d'animation) et sites-markdown-only.grit (un composant exporté dans un site est une erreur, Storybook à part).
  • Règle voulue par l'utilisateur : un site, c'est du Markdown et de la config ; tout composant vit dans un paquet (@recit/site-kit). Six composants antérieurs sont tolérés dans biome.jsonc (www : Hero, FormatGrid, OneSource ; docs/StatusBadge ; motion/VideoDownloads ; components/FormatSupport) : lot à prévoir après STY6.3b pour les déplacer dans packages/tools/site-kit/src/components/ (avec leurs modules) et vider la liste.
  • Trou du plan 19 relevé à l'audit : packages/site/src/lib/assets/language-icons (27 signaux du compteur) n'appartient à aucun lot ; à joindre à STY5.
  • 41 avertissements Biome (57 avant), aucune erreur.

STY6.3b — site-kit, site-stories, client motion — 29/10

Vague 4 du plan 19 (§10, STY6.3), reprise d'un lot coupé : le partiel des deux agents précédents était sain et presque complet, gardé tel quel après relecture. Aucune commande git, pas de pnpm install, pas de pnpm fix, aucune référence régénérée.

Fichiers. packages/tools/site-kit/src/components/ : Cards, ExampleHeader, SourceFile, Tutorial et leurs quatre modules (paquet privé, compilé par le Vite du site comme @recit/site : PostCSS, rem(), mixins Mantine, règles en @layer components). packages/integrations/site-stories : site-stories.css remplacé par src/site-stories.module.css et sa table (scripts/build-css-modules.js), MotionViewer, StoryDocument, StoryViewer, mdx-components, i18n, motion/MotionPlayer, scripts/build-css.mjs (le module n'est plus préfixé par .recit-wrapper : ses sélecteurs portent leur portée), deux tests adaptés. packages/cli/src/motion/client.tsx + client.module.css (+ css-modules.d.ts), bundlé par esbuild (local-css) et servi après la feuille des blocs ; motion-dev.ts perd le <style> de la page ; motion.spec.ts vérifie l'ordre et l'absence de <style>.

Choix. Un seul module pour site-stories (chaque composant dessine le même wrapper). Les classes publiques restent dans le HTML par composes … from global (recit-wrapper, recit-embed, recit-embed-actions, recit-gallery-card, recit-motion, recit-motion-player, recit-document), les sélecteurs e2e et sites/motion/styles.css ne changent pas. Plus de style en ligne : l'aspect d'un film et d'un deck passe par data-aspect (énumérations 16:9/1:1/9:16 et 16:9/4:3), la scène et la barre du client motion par useCssVariable de @recit/components. Corrigé à la reprise : l'import de client.tsx placé avant l'en-tête de licence, et prose-sm du corps de TutorialStep — composé depuis le module, Tailwind ne le voyait plus (il ne scanne pas le CSS) et ne le générait pas ; un commentaire de Tutorial.tsx le nomme jusqu'à STY5 (le texte des étapes de motion.recit.dev reprenait la taille prose, page +12 px).

Vérifications.

Code
$ node scripts/check-style-rule.js packages/tools/site-kit packages/integrations/site-stories packages/cli/src → 2 (packages/cli/src/bundle : imports Tailwind du bundle des decks, hors lot, STY5c/STY9) $ node scripts/check-style-rule.js --write-baseline (mêmes dossiers) → baseline inchangée $ pnpm exec nx run-many -t build typecheck -p @recit/site-kit @recit/site-stories @recit/cli → vert (deux passages rouges avant : dist de @recit/core vidé par le build concurrent d'un autre agent) $ pnpm exec vitest run packages/cli packages/integrations/site-stories packages/tools/site-kit → 21 fichiers, 202 tests verts $ node scripts/build-css-modules.js --check --all → @recit/site-stories: 1 CSS module, à jour $ pnpm check → vert (avertissements `@mixin` de Biome, comme les modules de packages/site) $ BROWSER=none pnpm test:e2e:linux site-components site-embed motion-dev site-motion → 101 passed, 4 failed ; après le correctif `prose-sm` : site-components + site-motion 94 passed, 2 failed

Les deux échecs restants : /custom-components (clair et sombre) voit une erreur React #418 (texte différent à l'hydratation) dans le conteneur ; capture identique. Le même test passe sur l'hôte (playwright test contre recit-site preview du site construit), et une sonde du build de production n'y voit aucune erreur : hors de ce lot, à attribuer à la porte (la page affiche « Last modified on », date issue de git, que le conteneur n'a pas ; l'erreur était masquée avant par les échecs de capture qui précèdent l'assertion). site-motion vert : la page d'accueil ne diffère plus que par la police des films, désormais chargée (la référence avait le repli serif), et le temps 00:01 au lieu de 0:01 (formatDuration de @recit/core, hors lot), sous le seuil. site-embed et motion-dev verts.

Déploiement Vercel : doc, commandes, VERCEL.md — 29/10

  • Page de doc documentation/configuration/deploy-vercel.mdx (barre latérale « Configuration ») : ce qui part chez Vercel (dist/ d'un deck, .vercel/output d'un site du framework, détecté par VERCEL), vercel.json, déploiement par Git (Root Directory, preset « Other », Ignored Build Step), par la CLI (vercel pull / build / deploy --prebuilt, link --repo en monorepo), domaines, et les sites de Récit.
  • scripts/vercel.js (+ scripts/__tests__/vercel.spec.ts, 7 tests) et les scripts racine vercel:sites, vercel:link, vercel:deploy, vercel:deploy:prod : un projet recit-<site> par site qui a un vercel.json, construit ici puis envoyé --prebuilt.
  • VERCEL.md à la racine : la procédure complète (projets, liaison, déploiement, DNS).
  • Rien n'a été déployé ni testé contre Vercel (pas de compte relié ici). La nouvelle entrée de la barre latérale change les captures des pages de docs où « Configuration » est dépliée : à accepter à la porte de la vague 4, comme changement de contenu.

STY4a — Pages et mise en page — 29/10

Reprise du lot coupé (plan 19, vague 4), sur feature/mantine. Aucune commande git, pas de pnpm install, pas de pnpm fix.

Inventaire du partiel. Le travail sur disque était complet et sain : les 26 composants du périmètre (LandingPage, Layout, Main, Header, HeaderNavigation, TopNavigation, MobileTopNavigation, MobileTopNavigationDrawer, Footer, Banner, PageProgress, BrowserWindow, Framed, Anchor, Pagination, Search, DeveloperHint, CategoryHeading, AiAssistantMenuItems, NotFoundPage, ErrorPage, LanguageIcon, ScrollableTable, InlineCode, Markdown, Mermaid), errors/ErrorAlert et errors/RouterError (prop placement au lieu d'un className, app/main.tsx passe placement="route"), chacun avec son module, et leurs tests sur classes.*. lib/ui/util.tsx est supprimé (plus aucun import ; index.ts réexporte cn depuis lib/util/cn.js). Rien n'a été effacé ni réécrit : relu, vérifié, complété par la baseline et cette note. Hors liste, déjà sur disque : vite/plugin-theme.ts (+ test) émet --recit-logo-width/height, --recit-footer-logo-width/height et --recit-banner-color depuis site (valeurs nécessaires à la première peinture, §0.3 permis 4 ; valeur numérique → px, valeur avec ;{}<> refusée).

« ⌘K » : dessiné dans la couleur du texte (Search.module.css), 4,39:1 → conforme ; a11y todo retiré de Header.stories.tsx. Différence voulue (le raccourci passe de gris à noir/blanc). Pas de jeton partagé : le todo de Command/Filtered (CommandShortcut, --mantine-color-dimmed sur --muted) reste, à confier au propriétaire de Command.module.css.

Hauteur des pages (STY6.3a, STY8). Elle ne vient pas de ce lot. Analyse ligne à ligne des -actual contre les références : l'en-tête, la grille de la page, la colonne de contenu et le pied de page sont identiques au pixel, hors deux points ci-dessous. Les +96 / +112 px de tutorial et formats/deck/exporting venaient de TutorialStep de site-kit (texte en text-base au lieu de text-sm, STY6.3b en cours) : verts au second passage. Les −1 px restants (/, writing/overview, accueil de www) naissent dans les cartes de site-kit (FeatureCard, « Learn more » et interlignes des descriptions) et dans les boutons du héros de www. Le reste est du contenu : « embedded in a Récit site », barre latérale « Writing » (Scenes), FormatGrid (« Documentation pages in a site »), pied de page sans l'ancien nom du framework. Le texte des liens de la navigation d'en-tête est 1 px plus haut (1 523 px, sous le seuil) : pas dû à ce lot. Remettre Header.tsx et HeaderNavigation.tsx de HEAD (Tailwind) dans une construction de docs donne le même décalage, qui vient donc de ui/NavigationMenu (STY3b) ; les boîtes sont identiques (lien à top: 14.5, 36 px), seul l'arrondi du texte diffère.

Vérifications.

Code
$ node scripts/check-style-rule.js packages/site/src/lib/components → 5 (Heading 4, Typography 1 : STY5) ; packages/site/src/lib/errors → 0 $ node scripts/check-style-rule.js --write-baseline packages/site/src/lib/components → className 146 → 2, style 11 → 0, imports 5 → 1, noModule 28 → 2 $ pnpm -F @recit/site typecheck → seule erreur : plugins/markdown/MdxPage.test.tsx (STY4b) $ pnpm exec vitest run packages/site --project '!storybook' → 60 fichiers, 613 tests verts $ pnpm test --project storybook → 46 fichiers, 170 tests verts $ pnpm exec nx run-many -t build -p docs www presentation → 18 tâches vertes $ pnpm check → vert $ BROWSER=none pnpm test:e2e:linux e2e/site-docs.spec.ts e2e/site-www.spec.ts e2e/site-behaviour.spec.ts e2e/accessibility.spec.ts → 128 passés, 6 échecs (1.7m)

Les 6 échecs : /, writing/overview et l'accueil de www, clair et sombre, −1 px de hauteur (cartes de site-kit, contenu, voir plus haut). site-behaviour et accessibility verts. Aucune référence régénérée.

STY4b — Navigation et plugins — 29/10

Reprise du lot coupé (plan 19, vague 4), sur feature/mantine. Aucune commande git, pas de pnpm install, pas de pnpm fix, aucune référence régénérée.

Inventaire du partiel. Sain et presque complet, gardé après relecture : les 11 composants de lib/components/navigation avec leurs modules (NavigationListItem.module.css et navigationListItem.ts remplacent le cva de utils.ts, que Node charge par plugin-docs.ts et qui ne peut donc pas importer de module ; NavigationCategory passe de @radix-ui/react-collapsible à Collapse de Mantine, animé seulement après un clic comme avant ; NavigationBadge par data-color/data-invert, ColorMap disparaît, sans importeur), plugins/markdown/MdxPage et plugins/search-pagefind/{IndexingDialog,PagefindSearch,ResultList} avec leurs modules, deux tests adaptés (MantineProvider autour de Navigation.test.tsx). VisuallyHidden vient de Mantine (MobileNavigationDrawer, PagefindSearch) ; NavigationWrapper, NavigationFrames et StackDrillRow perdent un className sans appelant. search-inkeep lisait déjà data-mantine-color-scheme (commit 0ac806b9), Slot.tsx n'a rien à changer.

Terminé à la reprise. MdxPage.test.tsx : le typecheck refusait classList.contains sur une classe de module (string | undefined). Toc : l'entrée lue porte aria-current="location", les autres "false" — NavLink les marquait toutes page, et la classe text-primary que le test e2e « the table of contents marks the section scrolled to » lisait en repli n'existe plus (activeTocEntry de e2e/support.ts peut perdre a.text-primary).

Le clic perdu de la pagination (porte de la vague 3). Cause trouvée, corrigée dans NavigationWrapper. Au montage, donc à l'hydratation, il amenait l'entrée active de la barre latérale par scrollIntoViewIfNeeded → scrollIntoView({ block: "center" }), qui fait défiler tous les ancêtres, la fenêtre comprise. Page descendue jusqu'à la pagination, la barre latérale collante est poussée vers le haut par la fin de sa ligne de grille : sur quickstart (1280×720) l'entrée active est à top: -17 px, hors de la fenêtre, et l'ancien code remontait la page de 423 px (scrollY 1916 → 1493). Playwright venait d'amener le lien « Tutorial » à l'écran et cliquait à ses anciennes coordonnées, sur autre chose : URL inchangée, aucune erreur. Le correctif (centerInNav) ne fait défiler que le nav, et seulement si l'entrée sort de sa zone visible. Sonde sur le dist (scripts du site retardés de 1,5 s, lien amené à l'écran avant l'hydratation) : scrollY reste à 1916 après l'hydratation et le clic mène à tutorial. Le réessai du test reste.

Vérifications.

Code
$ node scripts/check-style-rule.js --strict packages/site/src/lib/components/navigation packages/site/src/lib/plugins → 0 $ node scripts/check-style-rule.js --write-baseline (mêmes dossiers) → baseline inchangée (déjà sans eux) $ pnpm -F @recit/site typecheck ; pnpm -F recit-storybook typecheck → verts $ pnpm exec vitest run packages/site --project '!storybook' → 60 fichiers, 613 tests verts $ pnpm test --project storybook → 46 fichiers, 170 tests verts $ pnpm exec nx run-many -t build -p docs www → 17 tâches vertes (un échec transitoire avant : dist vidés par le build concurrent d'un autre lot) $ pnpm check → biome et imports verts, check:other vert ; fmt:check rouge sur 4 fichiers .md/.mdx d'autres lots (SKILL.md ×2, AGENTS.md, deploy-vercel.mdx) $ BROWSER=none pnpm test:e2e:linux e2e/site-docs.spec.ts e2e/site-behaviour.spec.ts e2e/accessibility.spec.ts → 116 passés, 4 échecs

Les 4 échecs : / et writing/overview, clair et sombre, −1 px de hauteur, déjà attribués par STY4a (cartes de site-kit, contenu, texte de l'en-tête). Dans mes zones : la table des matières de / est identique au pixel, la barre latérale de writing/overview ne diffère que par son contenu (« Scenes » au lieu de « Slides »). Différence voulue : aucune visible ; aria-current de la table des matières.

Porte de la vague 4 — 30/10

  • e2e complet (12,4 min, 767 tests) : 447 passés, 318 échecs triés. 306 stories nouvelles sans référence (générées ensuite par pnpm test:e2e:update:linux e2e/storybook.spec.ts) ; 8 captures de pages à −1 px (docs / et writing/overview, presentation/templates, vision /, www accueil) dont le décalage naît dans la description des FeatureCard de site-kit et dans le héros de www (lot GATE4, et TST4b pour le héros) ; site-i18n cherchait la classe sr-only disparue : il cherche maintenant le titre par son rôle. e2e/support.ts : activeTocEntry ne lit plus que aria-current="location". /custom-components (erreur #418 vue par STY6.3b) n'a pas échoué dans ce passage.
  • storybook en devDependency de @recit/components (un pnpm install hors agents).
  • Boucle courte : pnpm test:related, pnpm test:stories, pnpm test:e2e:host ; les agents ne lancent plus le conteneur (AGENTS.md « Inner loop », 20-multi-agents.md §3.2 bis).
  • Vague 5 lancée avec GATE4 (corrections 1 px, contraste de Command, expect des stories de TST1) et INFRA (e2e Docker ciblé et chaud, ressources plafonnées, lefthook).

Références Storybook de la vague 4 — 30/10

  • pnpm test:e2e:update:linux e2e/storybook.spec.ts : 308 références nouvelles écrites (313 tests passés en 4,1 min). Échantillon relu : boutons, Card des slides, Dialog ouvert.
  • Défaut vu : la Dialog ouverte montre un petit point gris dans son coin haut gauche (en sombre et en clair), sans doute une sentinelle de focus ou un bouton de fermeture sans icône rendu visible : à corriger dans lib/ui/Dialog (puis références à régénérer).
  • 28 échecs de play dans le Storybook construit (stories de code de lib/ui vides, presse-papiers refusé, slides-grid--overloaded) : lot SBFIX.

INFRA — e2e plus rapide, hooks git — 30/10

  • Ciblé : scripts/e2e-projects.js donne, pour chaque spec de e2e/, les projets nx qu'elle ouvre (plus @recit/cli, importé par e2e/support.ts). pnpm test:e2e:linux <specs> passe la liste au conteneur (E2E_PROJECTS) : il ne construit qu'eux et leurs dépendances, et playwright.config.ts ne démarre que leurs webServer ; sur l'hôte, la config lit les specs de sa ligne de commande (pnpm test:e2e:host e2e/site-docs.spec.ts ne sert que docs). Sans spec, ou avec un filtre inconnu de la table : tout, comme la CI (inchangée : pnpm test:e2e sans argument). Un test exige une ligne par spec et chaque siteUrl(…) dans sa ligne.
  • Chaud : la copie vit dans le volume recit-e2e-work ; chaque passage efface tout sauf les node_modules (et leurs .vite/.cache), recopie l'arbre, relance pnpm install --frozen-lockfile (« Already up to date », 2 s). Verrou flock : un second passage s'arrête net. La copie tolère un fichier modifié pendant le tar (code 1), et la recopie de test-results n'est plus fatale (un test:e2e:host concurrent recrée le dossier : vu deux fois ici).
  • Plafonné : --cpus=${E2E_CPUS:-6}, --memory=${E2E_MEMORY:-12g} sans swap en plus, E2E_WORKERS = 2/3 des CPU (4).
  • Mesures (e2e/site-docs.spec.ts, machine chargée par les autres agents) : avant, copie 2 s, installation 35 s, build de tout en échec après 3 min 40 (voir plus bas) ; après, à froid : nettoyage 0 s, copie 3 s, installation 47 s, build docs + 14 dépendances 2 min 04, tests 2 min 33 ; à chaud : nettoyage 1 s, copie 2 s, installation 2 s, build 1 min 58, tests 2 min 44 (97 passés, 8 captures en écart dues aux lots en cours, 1 instable).
  • Hooks (lefthook.yml, scripts/pre-push.sh) : pre-commit = Biome + oxfmt sur les fichiers indexés, check-style-rule.js (2 s) et check-stories.js (0,1 s) ; pre-push = pnpm check puis typecheck nx affected --exclude-task-dependencies (34 à 55 s quand tout est touché) et pnpm test:related sur les fichiers poussés (59 s pour les 72 du dernier commit). Base : l'amont, sinon origin/main, sinon message et succès. PRE_PUSH_BASE=HEAD~1 bash scripts/pre-push.sh pour l'essayer ; LEFTHOOK=0, --no-verify, LEFTHOOK_EXCLUDE=related.
  • À traiter (hors lot) : sites/storybook/src/harness/pagefind est ignoré par .gitignore (pagefind/) et exclu de la copie : le build de recit-storybook échoue dans le conteneur (« Failed to load static files ») et échouera en CI tant que le dossier n'est pas versionné ou généré.

STY5a — Typographie — 30/10

Vague 5 du plan 19 (§2.6, STY5.0), sur feature/mantine. Aucune commande git, pas de pnpm install, pas de pnpm fix, aucune référence régénérée, conteneur e2e non lancé.

Fait.

  • Typography.tsx : Typography de Mantine, unstyled, avec Typography.module.css ; ProseClasses disparaît (aucun importeur). Le module reporte règle à règle, dans l'ordre de la feuille compilée, ce que @tailwindcss/typography (prose, dark:prose-invert) et les @utility typography/@utility prose de main.css donnaient : .prose devient .root, --tw-prose-* devient --prose-*, l'exclusion [class~="not-prose"] reste (les modules qui composent not-prose en dépendent). Palette du site (currentColor), identique en clair et en sombre, sauf les trois couleurs que typography laissait au plugin (ombres de kbd, pre brut), en light-dark(). Règles en @layer utilities, là où était le plugin : les modules en components perdent contre elles comme avant, ceux hors couche gagnent toujours. La classe globale typography reste sur la racine (composes), crochet des styles.css de motion et presentation.
  • Heading.tsx : Title de Mantine, unstyled, niveau lu par data-order dans Heading.module.css (tailles du cva en @layer utilities, au-dessus des h1–h4 de la prose comme avant ; interlignes en rapport tenu par --heading-lh, sinon le minifieur arrondit calc(2 / 1.5) et h2 perd 1/64 px) ; lien d'ancre et icône hors couche, comme Anchor. theme.headings n'est pas utilisé : avec la prose au-dessus du Title stylé, il n'aurait rien porté, et le thème est hors lot ; Title écrit encore ses variables --title-* en style (code de Mantine, sans effet).
  • MdxComponents.tsx + MdxComponents.module.css : p explicite, img/vidéo → .media (rounded-lg), pre → .pre (not-prose composé, my-4), code en ligne sans la classe inline (valeur par défaut de code), cx au lieu de cn ; node de react-markdown retiré de p et pre.
  • main.css : @plugin "@tailwindcss/typography" et les deux @utility retirés (le reste est à STY5b, qui a démonté le fichier en parallèle ; la dépendance part avec STY5c).
  • biome.jsonc : Typography.tsx et Heading.tsx sortis de la liste d'exception.
  • Hors liste, signalé : site-kit Tutorial.module.css/Tutorial.tsx composaient prose prose-sm from global ; sous la liste not-prose, seul ce que le plugin posait sur le corps lui-même arrivait (gris-700/gris-300 du plugin, 0.875rem/1.71429) : ces trois valeurs sont maintenant dans .body, le bloc --tw-prose-invert-* et le commentaire qui gardait prose-sm dans la feuille disparaissent. MdxPage.test.tsx cherche la colonne par classes.article au lieu de .prose.

Mesures d'identité. docs construit avant (copie de dist de la vague 4) et après, servis en statique sans JS, transitions coupées ; Playwright relève 35 propriétés calculées (couleur, tailles, interlignes, marges, bordures, puces, ::before/::after/::marker, boîte) de chaque élément sous la racine de prose, sur les 57 pages en clair et en sombre (78 pages avec racine, 14 420 éléments), plus un fragment injecté qui contient tout ce que la doc n'a pas (lead, blockquote, hr, h4 à h6, kbd, listes imbriquées et de tâches, table avec tfoot, pre brut, figure, dl, not-prose). Avant contre avant : 0 écart. Avant contre après : 0 écart de la prose et des titres, hors :

  • citation (fragment seulement, aucune dans la doc) : la couleur sort lab(48.496 0 0 / 0.75) au lieu de oklab(0.556 0 0 / 0.75) (le minifieur calcule le color-mix littéral), et des écarts de 1e-5 en sombre : même couleur ;
  • hors lot : Cards__icon de site-kit à 32px au lieu de 31.9999px (GATE4), la pagination « Deploy on Vercel » (contenu), et un bloc data-recit-runtime-shiki de recit-config dont les jetons portent une autre couleur claire dans le HTML prérendu (--shiki-light:#D73A49 sur { defineConfig }) : surlignage d'exécution, à attribuer.

Vérifications.

Code
$ node scripts/check-style-rule.js packages/site/src/lib/components packages/site/src/lib/util → components 0 ; util 2 (cn.ts, STY5c) $ node scripts/check-style-rule.js --write-baseline (mêmes dossiers) → components retiré, util className 4 → 0, noModule 1 → 0 $ pnpm test:related (mes fichiers) → 6 fichiers, 42 tests verts $ pnpm test:stories ErrorPage NotFoundPage Markdown InlineCode → 4 fichiers, 8 tests verts $ pnpm -F @recit/site typecheck → erreurs seulement dans des stories d'autres lots $ pnpm -F @recit/site-kit typecheck → vert $ pnpm exec nx run docs:build → vert (avec --excludeTaskDependencies : les dist des dépendances étaient vidés par les builds concurrents) $ BROWSER=none pnpm test:e2e:host e2e/site-docs.spec.ts e2e/site-behaviour.spec.ts → 56 passés, 1 ignoré $ pnpm check → rouge sur des fichiers d'autres lots (e2e/.sbfix-*.mjs, stories, deck.css, Alert/Card/InputGroup.module.css), aucun des miens

Différence voulue : aucune. Le HTML change (classes de module, data-order et style de Title, plus de prose, dark:prose-invert, inline, group, relative, text-*), les captures ne devraient pas bouger.

GATE4 — Corrections de la porte de la vague 4 — 30/10

Corrections après l'e2e de la porte de la vague 4, sur feature/mantine. Aucune commande git, pas de pnpm install, pas de pnpm fix, aucune référence régénérée.

Cause des −1 px. Le minifieur CSS de la construction des sites plie line-height: calc(1.25 / 0.875) en 1.42857 (et calc(2 / 1.5), calc(1 / 0.75) en 1.33333) : 14 × 1,42857 = 19,99998 px, soit une ligne de 19,984 px en unités de mise en page (1/64 px) au lieu de 20. Tailwind gardait la valeur dans une propriété personnalisée (--text-sm--line-height: calc(1.25 / 0.875)), que le minifieur laisse telle quelle. Dans FeatureCard, titre, icône et « Learn more » perdent chacun 1/64 px : la carte passe de 202,5 à 202,453 px (docs /), de 141 à 140,969 px (www), et l'arrondi des lignes qui suivent descend d'un pixel. La description (1.625, valeur exacte) n'y est pour rien. Même cause pour le texte des liens de l'en-tête (ui/NavigationMenu, .trigger et .link) : le texte est peint 1 px plus haut (haut du texte à 22,5 au lieu de 23,5).

Corrections. site-kit/.../Cards.module.css (.icon, .title, .more) et ui/NavigationMenu.module.css (.trigger, .link) : la valeur passe par une propriété personnalisée (--cards-leading, --navigation-menu-leading), gardée telle quelle dans la feuille construite. Piège ajouté au skill mantine-style. Reste ailleurs : 89 line-height: calc(…) dans les modules (lib/ui/DropdownMenu, InputGroup, site-stories, deck, components…) sont pliés de même ; à reprendre là où une capture bouge, par le propriétaire du fichier.

Mesures (Chromium de l'hôte, 1280 × 720, clair et sombre ; « avant » = valeurs Tailwind de HEAD injectées sur la feuille construite, « vague 4 » = valeurs pliées) :

Code
carte titre Learn more texte du lien d'en-tête (top) docs / avant 202.5 20 16 23.5 vague 4 202.4531 19.9844 15.9844 22.5 GATE4 202.5 20 16 23.5 docs writing/overview : identique à docs / www / avant 141 20 16 23.5 vague 4 140.9688 19.9844 15.9844 22.5 (clair) GATE4 141 20 16 23.5

CommandShortcut : la couleur du texte de l'élément (color: inherit), comme le ⌘K de Search (STY4a) ; a11y: { test: "todo" } retiré de Command/Filtered, qui passe en mode erreur. Différence voulue : le raccourci de la palette n'est plus gris (aucune capture de site : seule la story l'utilise).

TST1 suite. Les play des stories de packages/components (SlideMotion, SlideImage, Builtins, Card, Step, Media, CodeGroup, CodeBlock, Mermaid, Grid) passent sur expect/waitFor de storybook/test ; plus aucun throw new Error. Aucun composant n'a de rappel : pas de fn. CodeBlock/UnknownLanguage vérifie en plus qu'aucun <b> n'est rendu. Contre-épreuve : Step/Indexed attendant ["1", "9", "2"] échoue (- Expected + Received), rétabli ensuite.

Vérifications.

Code
$ pnpm test:stories packages/components/src packages/site/src/lib/ui/Command.stories.tsx → 19 fichiers, 57 tests verts $ pnpm test:related (NavigationMenu, Command, Cards, leurs modules) → 8 fichiers, 44 tests verts $ pnpm exec nx run-many -t build -p docs www → www vert ; docs rouge par recit-example-deck-{showcase,mdx}:build (« Could not resolve "@recit/theme" » depuis core/dist/config/theme-css.js, fichier d'un autre lot) ; docs reconstruit ensuite par un autre lot, avec mes modules $ BROWSER=none pnpm test:e2e:host e2e/site-docs.spec.ts → 50 passés, 1 échec : writing/mdx, React #418 (hydratation, texte), pas de CSS en cause ; les autres échecs vus venaient des dist vidés par les builds concurrents et repassent seuls $ pnpm check → rouge hors de mes fichiers (e2e/.sbfix-*.mjs, oxfmt de Heading, Typography, stories de navigation…, compteur de stories « kit » 4 → 10)

TST3a — Stories du framework, pages — 30/10

Vague 5 du plan 21 (§8, TST3), en parallèle de GATE4, INFRA, STY5a-b, TST3b, TST4a-b. Aucune commande git, pas de pnpm install, pas de pnpm fix, pas de conteneur.

En place. Vingt-quatre fichiers packages/site/src/lib/components/<X>.stories.tsx, titre Site/Components/<X> : Header (complété), TopNavigation, HeaderNavigation, MobileTopNavigation, MobileTopNavigationDrawer, ThemeSwitch, Search, Footer, Banner, PageProgress, Spinner, BrowserWindow, Framed, Anchor, Pagination, CategoryHeading, NotFoundPage, ErrorPage, DeveloperHint, LanguageIcon, ScrollableTable, InlineCode, Markdown, Mermaid, Slot. 75 stories dans index.json, un play pour chaque composant interactif : banner, lien actif (aria-current="page"), logo à ses dimensions, bannière, recherche au clic, à Ctrl+K et ⌘K, un seul dialogue par placement ; target/rel des liens externes (barre du haut, tiroir, pied de page) ; menus de header.navigation ouverts et fermés par Échap ; tiroir mobile (fenêtre mobile) : aria-expanded/aria-controls, groupe déplié, Échap rend le focus, un lien ferme, onOpenChange en fn() ; ThemeSwitch bascule vraiment (un MantineProvider propre à la story, schéma en mémoire, sans élément racine ni stockage : forceColorScheme de withMantine en faisait un no-op) ; contentinfo ; bannière fermée ; zoom de BrowserWindow (pas, retour, limites) ; arrêt de tabulation d'un tableau large ; Markdown riche et composants du MDXProvider ; diagramme Mermaid et erreur (wait-mermaid) ; ordre des Slot.Source. Variantes par Group/Stack, compteur de la règle à 0.

  • Tests : BrowserWindow, Footer, Markdown, NotFoundPage, ScrollableTable, TopNavigation, MobileTopNavigation .test.tsx supprimés (tout est dans les play) ; Header.test.tsx ne garde que reloadDocument du logo (une prop de Link sans trace dans le DOM) ; Search.test.tsx (préchargement) et Slot.test.tsx (logique) restent, ce dernier sans ses deux data-testid.
  • Hors liste : sites/storybook/.storybook/main.ts, resolve.dedupe: ["@unhead/react"] — pnpm installe deux variantes de pair, et le Head de Header ne voyait pas l'UnheadProvider de withSite (« useHead() was called without provide context ») dès qu'un logo est configuré.
  • scripts/stories-baseline.json : site/components 54 + 7 → 18 (restent Typography, Heading, Layout, Main, LandingPage, context/*… hors lot).

Défauts trouvés (composants hors lot, stories en a11y.test: "todo" avec la raison) :

  • Banner : le bouton de fermeture n'a pas de nom (button-name) ; la fermeture n'est pas mémorisée (useState, elle revient à la page suivante) ; texte blanc sur info 3,6:1, tip 3,08:1, caution 2,76:1, danger 3,59:1 (color-contrast).
  • ui/Separator décoratif garde aria-orientation (déjà vu à TST2a) : séparateur de la barre du haut et du tiroir mobile dès qu'il y a des liens d'en-tête.
  • HeaderNavigation ouvert : liens dans un menu (TST2b). Mermaid en erreur : Alert destructive 4,36:1 (TST2a). Markdown : citation de .prose #969696 sur blanc, 2,95:1 (STY5a).
  • Sans rôle ni nom là où §4.2 les attend : Pagination n'est pas un nav, Spinner et PageProgress n'ont pas de progressbar (la barre est aria-hidden, voulu), TopNavigation et HeaderNavigation sont des navigation sans nom ; Anchor ne copie pas l'URL (simple lien #).
  • Harnais : la Modal de SearchFixture a un bouton de fermeture sans nom (axe échoue si la story finit dialogue ouvert) ; PageProgress à 50 % n'est pas atteignable sans chargeur de route.
  • DeveloperHint ne rend rien hors NODE_ENV=development : rien à voir en test ni en capture.

Vérifié.

Code
$ pnpm test:stories packages/site/src/lib/components → 37 fichiers, 111 tests verts $ pnpm test:stories (complet) → 300/321 ; échecs hors lot (deck, ui/CodeTabs…) et rechargements de dépendances Vite (cache partagé avec les runs concurrents), les miens verts au passage ciblé $ pnpm exec vitest run Header.test Slot.test Search.test → 27 tests $ pnpm -F @recit/site typecheck → seules erreurs : navigation/, plugins/ (TST3b en cours) $ pnpm -F recit-storybook typecheck → erreurs deck/motion-blocks (TST4a), aucune ici $ pnpm -F recit-storybook build → vert, 75 stories Site/Components $ node scripts/check-stories.js packages/site/src/lib/components → 18, aucun de mes composants $ node scripts/check-style-rule.js packages/site/src/lib/components → 0 $ pnpm check → biome rouge hors lot (e2e/.sbfix-*.mjs, Alert/Card.module.css…) ; biome et oxfmt verts sur mes fichiers, check:other vert

Captures des nouvelles stories : à la porte (pnpm test:e2e:update:linux e2e/storybook.spec.ts).

Suivis pour la porte de la vague 5 — 30/10

  • Interlignes pliés par le minifieur (GATE4) : line-height: calc(1.25 / 0.875) devient 1.42857 au build, une ligne de 14 px perd 1/64 px et tout ce qui suit descend d'un pixel ; Tailwind passait par une propriété personnalisée que le minifieur ne plie pas. Corrigé pour Cards et NavigationMenu (--cards-leading, --navigation-menu-leading) ; il reste 89 line-height: calc(…) dans les modules. Chercher une correction unique au build (plutôt que 89 retouches), vérifier aussi Lightning CSS pour les paquets publiés.
  • Lot d'accessibilité à ouvrir après la vague 5, avec les todo des stories : contrastes (Badge du deck, avertissement de Grid, erreur de Mermaid, Alert destructive et info, onglet inactif de CodeTabs, orange Shiki clair, texte de Banner, citation de la prose 2,95:1), noms manquants (bouton de fermeture de Banner, Modal du harnais, navigation de TopNavigation et HeaderNavigation), rôles (Pagination en nav, progressbar de Spinner/PageProgress), axe aria-required-children des menus Mantine et de Command vide, aria-orientation du Separator décoratif, point gris de la Dialog ouverte.
  • writing/mdx : React #418 (hydratation de texte) vu par GATE4 sur l'hôte, comme /custom-components à STY6.3b ; piste « Last modified » ou date de rendu.
  • sites/storybook/src/harness/pagefind ignoré par .gitignore (pagefind/) : manquera en CI. Réglé par TST3b : le dossier s'appelle pagefind-index/.
  • docs:build en échec pendant GATE4 (« Could not resolve @recit/theme » depuis core/dist) : un dist en reconstruction par un build concurrent ; l'exemple se construit seul ensuite.

TST3b — Stories du framework, navigation et plugins — 30/10

Vague 5 du plan 21 (§8, TST3), en parallèle de GATE4, INFRA, STY5a-b, TST3a, TST4a-b. Aucune commande git, pas de pnpm install, pas de pnpm fix, aucun composant modifié.

En place. 19 fichiers de stories, 57 stories (Site/Navigation/…, Site/Plugins/…), toutes avec un play sauf les variantes purement visuelles de NavigationBadge et l'Inkeep !test :

  • navigation/ : Navigation (le Sidebar de §4.2 : arborescence à trois niveaux ouverte sur la page courante, aria-current="page", Entrée sur un chevron déplie, un clic navigue et déplace la page courante ; tout plié hors de l'arbre ; filtre ; navigation empilée : la ligne ouvre le panneau, « Back » revient), NavigationItem (page, active, icône et badge, lien interne à une ancre suivi au clavier, lien externe _blank/noopener, section, séparateur décoratif, masqué), NavigationCategory (plié/déplié au clic, Entrée, Espace, focus gardé ; actif ; imbriqué ; statique ; avec lien : le chevron ne navigue pas, la ligne navigue et demande la fermeture), NavigationBadge (couleurs, inversé), NavigationFilterInput (filtre, catégorie ouverte par une correspondance, effacement, aucun résultat), NavigationFilterProvider (vidé au changement de section sans démonter le champ), NavigationFrames (racine, lien retour), NavigationWrapper (l'entrée active hors de vue amène le nav seul, la fenêtre ne bouge pas : centerInNav), SidebarToggle (clic replie et rend la barre inert, Entrée déplie ; replié ; au survol, en haut), StackRows, MobileNavigationDrawer (mobile 390 : ouvert sur une page profonde, un lien ferme ; le bouton ouvre, Échap ferme et rend le focus ; à droite), Toc (aucune entrée courante au départ, la section signalée par IntersectionObserver passe en aria-current="location", une sous-section déplace la marque, une entrée mène à son ancre ; à un niveau). Le Toc ne rend que deux niveaux : le « sommaire à 3 niveaux » de §4.2 est le titre de page plus deux.
  • plugins/markdown : MdxPage (sommaire et pager, l'entrée suit la section ; sans sommaire ; métadonnées : catégorie, date, lien d'édition ; « Copy page » copie le Markdown (fetch espionné, presse-papiers relu), le menu ouvre le Markdown et « Use in Claude » appelle window.open espionné ; pleine largeur : sommaire en popover ; contenu large : le bloc défile, pas la page), ClaudeLogo, ChatGPTLogo.
  • plugins/search-pagefind : PagefindSearch (monté comme un site le fait, SearchProvider + Search + pagefindSearchPlugin : Ctrl+K ouvre, champ focalisé, Échap ferme ; « lorem » : 3 pages et leurs sections, flèches, Entrée navigue vers /dolor#ut-labore et ferme ; aucun résultat : message et « Clear search » qui vide et refocalise), ResultList (sections triées par score, clic navigue, maxSubResults, basePath retiré, vide), IndexingDialog (en cours 38 %, terminé, échec puis « Retry » qui se reconnecte, EventSource rejoué).
  • plugins/search-inkeep/InkeepSearch.stories.tsx : le bouton seul, tags: ["!test"] avec sa raison (widget tiers chargé d'un CDN avec la clé d'un site ; le comportement reste prouvé par index.test.tsx contre un faux widget).

Harnais (fichiers partagés, ajouts seulement) :

  • .storybook/intersection.ts, importé par preview.tsx : l'IntersectionObserver du navigateur enveloppé ; un play signale un élément visible par element.dispatchEvent(new CustomEvent("storybook:intersect")). Dans preview.tsx et non vitest.setup.ts : les captures e2e jouent les mêmes play sur le build.
  • Index Pagefind de fixture : src/harness/pagefind-index/pagefind.js (trois pages Lorem Ipsum, init/options/search), servi à /pagefind/pagefind.js par staticDirs (build) et résolu par le plugin Vite src/harness/pagefindIndex.ts (Vitest enveloppe les import() et perd le @vite-ignore : le littéral de PagefindSearch était résolu, puis refusé). Dossier nommé pagefind-index : pagefind/ est dans .gitignore (suivi de la porte réglé).
  • packages/site/src/lib/testing/LocationSpy.tsx : rend null, appelle onNavigate(pathname + hash) à chaque location du routeur mémoire ; c'est le « fn sur le routeur » de §4.2, réutilisable par TST3a.

Tests migrés. MdxPage.test.tsx supprimé (classes .root/.article : la story WideContent mesure le comportement). Le describe("NavigationCategory") de Navigation.test.tsx passe à la story WithLink ; les trois tests d'état entre sections restent (router.navigate du dehors). search-pagefind/index.test.tsx garde sa logique de chargement, sans data-testid (une region nommée). useNavigationFrame.test.ts et search-inkeep/index.test.tsx restent.

Défauts trouvés (composants hors lot, stories en a11y.test: "todo" avec la raison) :

  • NavigationCategory : axe list (le ul des items contient les a sans li) et aria-allowed-attr (aria-expanded sur la ligne, un div ou le lien, sans rôle qui l'admet). Touche aussi Navigation, NavigationFilterInput, MobileNavigationDrawer.
  • NavigationFilterInput : bouton d'effacement sans nom (axe button-name).
  • NavigationBadge : contrastes, blanc sur vert clair 1,77:1 (palette Tailwind reprise telle quelle).
  • MdxPage, TocPopoverButton en bouton : nommé « Toggle table of contents », affiche « On this page » (axe label-content-name-mismatch).
  • IndexingDialog démarre la réindexation au montage, pas à l'ouverture : en développement, chaque ouverture de la recherche monte le pied et ouvre /__z/pagefind-reindex. Les stories de PagefindSearch neutralisent EventSource.
  • Deux navigations viewTransition de suite : React Router laisse la promesse rejetée de la première transition sautée (« AbortError: Transition was skipped ») ; la story Stacked retire document.startViewTransition.
  • DeveloperHint (brouillon) n'apparaît que si NODE_ENV vaut development : ni le projet Vitest ni le build de Storybook ; non affirmé.
  • Ni main ni article dans MdxPage (c'est Layout) : l'a11y de §4.2 pour cette ligne est à la page entière, en e2e.

Vérifié.

Code
$ pnpm test:stories <navigation, plugins> → 19 fichiers (1 sauté), 56 tests $ pnpm test:stories → 128 fichiers passés, 2 sautés ; 485 tests (des « Failed to import test file » transitoires : dist reconstruits par les autres lots) $ pnpm test:related Navigation.test.tsx search-pagefind/index.test.tsx → 2 fichiers, 5 tests $ pnpm -F @recit/site typecheck → muet $ pnpm -F recit-storybook typecheck → rien dans mes fichiers (erreurs de formats/deck, motion-blocks et site-kit, lots en cours) $ pnpm -F recit-storybook build → vert, 57 stories des deux surfaces, pagefind.js copié $ node scripts/check-stories.js packages/site/src/lib/components/navigation packages/site/src/lib/plugins → 0 baseline réécrite (relue juste avant) : site/plugins disparaît, site/components 18 (TST3a) $ node scripts/check-style-rule.js (mes dossiers) → 0 hors sites/storybook/.storybook (3, existants) $ pnpm check → biome rouge sur des fichiers d'autres lots (e2e/.sbfix-*.mjs, Alert/Card/InputGroup .module.css, MissingIcon, CodeBlock.test…) ; fmt:check rouge sur e2e/.sbfix-* ; check:imports et check:other verts

Captures des 57 stories : à la porte (pnpm test:e2e:update:linux e2e/storybook.spec.ts).

STY5b — main.css, mise en page, icônes de langage — 30/10

Fait. STY5.1 pour sa part hors prose (STY5a a retiré la prose et @plugin pendant ce temps).

  • Les variables de mise en page (--top-header-height … --scrollbar-width) quittent le @layer base de main.css pour lib/components/Layout.module.css, sous :global(:root) : le <html>, l'en-tête, la barre latérale, le sommaire et les pages les lisent, comme Banner.module.css qui met déjà --banner-height à 0 par :global(:root):has(…). Les --spacing(n) deviennent leurs valeurs (pas de 0,25rem : 18rem, 1.5rem, 3rem, 0.625rem). Le module est hors couche, comme tous ceux du framework : rien d'autre ne déclare ces variables.
  • Nouveau app/base.css, importé par main.tsx juste après main.css : le reset (* border-color/outline-color), html, body, #root, details summary, les ligatures du code, ::selection, .dark { color-scheme }, les curseurs, select, .scrollbar, .wavy-line, valeur pour valeur depuis la feuille compilée, sans @apply. Couche base, pas theme : le preflight de Tailwind (dans base jusqu'à STY5c) remet border: 0 solid sur tout élément, et le border-color doit passer après ; STY5c le descend dans theme avec le retrait du preflight (noté dans l'en-tête du fichier). html.dark { --dark } et select:is(.dark *) lisent maintenant :root[data-mantine-color-scheme="dark"] ; .dark { color-scheme: dark } reste (pont de SiteRoot, STY9). ::selection lit --primary et non plus --color-primary (alias du @theme inline).
  • Retirés, morts : .bg-primary ::selection (plus aucun élément n'a la classe), .CollapsibleContent et ses keyframes (jamais appliqués, cf. Collapsible.tsx), le @layer utilities .stepper (repris par Stepper.module.css, la classe n'est plus rendue).
  • lib/assets/language-icons : les onze icônes à couleur de thème importent fills.module.css (un module pour la famille : .foreground, .foregroundSoft pour fill-foreground/90, .react, .json, .tomlOutline, .tomlLetters en light-dark() / @mixin dark) ; les autres gardent leurs attributs fill.
  • scripts/check-style-rule.js (hors liste, une ligne) : isGlobalCss exempte packages/site/src/app/base.css, le CSS global du framework que nomme STY5.1 ; un cas dans check-style-rule.spec.ts.

Ce qui reste dans main.css (54 lignes) : les @import (polices, tailwindcss, tw-animate-css), les deux marqueurs @vite-plugin-inject, @custom-variant dark et le @theme inline des couleurs Tailwind : tout est à STY5c. La déclaration @layer vit dans app/layers.css (une déclaration dans main.css ne survit pas à Tailwind).

Identité. sites/docs construit avant (copie dans le scratchpad) et après, servi en statique, Playwright à 1440 et 390 px, clair et sombre, sur /, /documentation/writing/mdx, …/scenes, /documentation/reference/cli, /formats/motion, /404 : ~3 500 à 8 800 valeurs par page (boîtes et styles calculés de html, body, #root, en-tête, nav, aside, main, pied, titres, pre, liens, SVG…), zéro différence hors l'écriture des quatre variables (calc(.25rem * 72) → 18rem, même longueur). Captures pleine page identiques, sauf deux à 22-24 pixels d'un niveau d'écart sur des coins arrondis (bruit de rendu, aussi entre deux passages du même build). Remplissages des icônes (MDX, React, Markdown, YAML, JSON, Terminal, TypeScript) identiques en clair et en sombre.

Vérifié.

Code
$ node scripts/check-style-rule.js packages/site/src/app packages/site/src/lib/assets packages/site/src/lib/util app 3 css + 2 directives (main.css : @theme inline, @custom-variant ; defaultTheme.css : :root, .dark) lib/assets 0 ; lib/util 2 imports (cn.ts, STY5c) → --write-baseline sur ces dossiers $ pnpm exec vitest run scripts/__tests__/check-style-rule.spec.ts → 66 tests $ pnpm test:related main.tsx LanguageIcon.tsx language-icons/*.tsx check-style-rule.js → 7 fichiers, 108 tests $ pnpm -F @recit/site typecheck → rien dans mes fichiers (stories de Markdown, MobileNavigationDrawer, Navigation : lots TST) $ build docs, www, presentation → verts (relancés : dist des paquets reconstruits par les autres lots) $ pnpm test:e2e:host e2e/site-docs.spec.ts e2e/site-behaviour.spec.ts → 55 passés, 1 sauté ; la recherche « searches the pages… » a expiré une fois sous charge, verte seule $ pnpm check → biome rouge sur des fichiers d'autres lots (e2e/.sbfix-*.mjs, Alert/Card/InputGroup .module.css, MissingIcon, CodeBlock.test…), rien dans les miens

À reprendre : defaultTheme.css (règles globales comptées, injectées en couche theme par plugin-theme.ts) n'a pas de lot ; la liste des CSS globaux permis de la loi (SKILL.md §5, plan §0.3) devrait nommer app/base.css ; les commentaires de Header, Layout (corrigé) et autres modules qui renvoient aux variables « de main.css » sont à rafraîchir au passage.

TST4a — Stories du deck et du motion — 30/10

Vague 5 du plan 21 (§8, TST4), en parallèle de GATE4, INFRA, STY5a-b, TST3a-b, TST4b. Aucune commande git, pas de pnpm install, pas de pnpm fix, pas de conteneur.

Décorateurs. sites/storybook/.storybook/decorators/deck.tsx (+ deck.module.css) : withDeck dessine un Deck/ dans un cadre de parameters.deck.frame (small 320×180, medium 640×360, large, square, none pour le plein écran) et pose la feuille de données du récit (story.themeCss : taille de design, fond et progression par scène, que SlideFrame n'a pas sans elle) dans un <style> de head, comme DeckApp. film.tsx (+ film.module.css) : withFilm met un bloc à la taille native du film et l'échelle au cadre par CSS seul (data-aspect 16:9, 9:16, 1:1 ; tan(atan2()) comme le stage du deck), parameters.film.frame: "none" pour MotionFrame ; global motionTheme (barre d'outils, globals d'une story), options lues dans MOTION_THEMES de @recit/theme/motion. preview.tsx : withDeck, withFilm, globalTypes.

Fixtures côté paquet (dossier fixtures, ignoré par le compteur, hors files publiés) : le workshop ne résout ni @recit/core ni @recit/motion-blocks. packages/formats/deck/src/fixtures/deck.tsx : un récit Lorem Ipsum de cinq scènes (couverture avec notes, deux :::step, grille, scène à bg, citation) compilé dans le navigateur par compileStory (loaders: [loadDeck], 16:9 ou 4:3, pied de page sur demande), loadMarkdown, sceneRenderer, SceneInStage, clockAt(s) (le prochain setInterval tique s fois puis se tait : minuteur figé pour la capture, ou à 10 min), withSearch (?slide=3, ?print=1). packages/formats/motion-blocks/src/fixtures/ : withMotionTheme (le ThemeProvider du paquet depuis globals.motionTheme), loremFilm, props ui-scene parsées par le schéma.

Stories. 27 fichiers, 144 stories (60 Deck/, 84 Motion/), compteur à 0 sur les deux surfaces (27 avant). Deck : Presentation (flèches, Espace et étapes, End/Home, s ouvre la console avec window.open moqué puis refusé, b écran noir, h HUD, 4:3, intégré, impression avec notes et onPrintReady), PresenterView (minuteur figé, à 10 min, réel puis pause et remise à zéro ; boutons et onNavigate), SlideStage (trois cadres, carré, marge, 4:3 ×3), SlideFrame (mises en page, fond, progression, pied), HtmlSlide (étapes, directive :::component, composant absent), SceneContent (Markdown, MDX, MDX absent), MdxSlide, DeckApp (?slide=3, ?print=1&notes), AnnotationCanvas (tracé par pointeur vérifié aux pixels), MobileRemoteView, RemoteQrCodeModal (QR généré), DevHud. Motion : les 8 blocs × (16:9, 9:16, chaque thème, minimal, maximal, maximal 9:16), Frame/Backdrop, ThemeVars, ThemeProvider (dont thèmes de marque buildTheme), Engine (StaticAnim, FramerAnim wait-motion), Block, UiSceneView (chargé, squelette), MotionFrame (début, en cours, réglé, deuxième scène, fin, thème du film, 9:16).

Hors liste. main.ts : optimizeDeps.include des dépendances de compileStory et de qrcode, par @recit/deck > … (découvertes en cours de run, elles rechargeaient la page et faisaient échouer les suites) ; preview.css : @import par chemin de packages/formats/motion-blocks/dist/styles.css ; sites/storybook/tsconfig.json : inclut les stories des deux paquets, paths vers ses propres @storybook/react-vite et storybook/test ; tsconfig.json des deux paquets : exclude des stories ; leurs package.json : files sans *.stories.* ni fixtures (aucune dépendance, pas de version plan).

À la porte (install). @recit/motion-blocks en devDependencies de sites/storybook (l'arête nx manque : un build affecté peut sauter le dist dont preview.css lit la feuille ; puis remplacer le chemin par le paquet) ; storybook et @storybook/react-vite en dev de @recit/deck et @recit/motion-blocks (puis retirer paths et exclude). Sans eux, player.test.tsx ne peut pas composeStories : il reste en renderToStaticMarkup.

Défauts trouvés (stories en a11y.test: "todo" avec la raison, composants intouchés) :

  • Pas de navigation par chiffres dans useKeyboardNav (§4.3 l'attendait) ; c (effacer le stylo) n'est pas câblé par Presentation (onClearAnnotations absent).
  • AnnotationCanvas sur un stage mis à l'échelle : le canevas est à la taille de design (1278 px) mais les coordonnées viennent du rectangle affiché (635 px) — à l'échelle 0,5 le trait part à la moitié de la position du pointeur (sondé). Écoute la souris, pas le pointeur (tactile).
  • SlideFrame : <footer> = contentinfo dans le group de la diapositive (landmark-contentinfo-is-top-level) ; texte du pied 3,53:1 en clair, 1,93:1 sur un bg sombre. Le pied est actif par défaut dans tout récit : la fixture le coupe, WithFooter le montre.
  • PresenterView : compteur 4,34:1, « Speaker notes » en h3 sans titre au-dessus ; MobileRemoteView : position 1,68:1 (#542858 sur #090d16), notes vides 3,89:1 ; RemoteQrCodeModal : « Session » 4,46:1, et ce n'est pas un dialog (ni rôle, ni aria-modal, ni Échap, ni focus) ; DevHud : badges vert 3,76:1 et ambre 3,18:1 ; HtmlSlide : message de composant absent 3,81:1 ; corps d'un callout tip dans une diapositive 4,16:1.
  • Textes en dur non traduits : « Slide » (PresenterView, DevHud, MobileRemoteView), « PREV »/« NEXT ». MobileRemoteView n'a pas d'état connecté/déconnecté (story Disconnected = état initial).
  • Thèmes motion (@recit/theme/motion) : couleurs atténuées et d'accent sous 4,5:1 pour le petit texte (warm-editorial #8a6470 sur #f4e7d6 4,16:1, saas-product #7f8593 sur #f3f4f7 3,36:1, vert et rouge sous 3:1) : THEME_CONTRAST_TODO sur les métas des blocs, de Block, ThemeProvider, UiSceneView, MotionFrame.
  • Pas de lecture/pause dans player.tsx : MotionFrame reçoit time, les commandes sont dans MotionPlayer de @recit/site-stories et le client de recit motion dev (hors lot) ; les stories couvrent pause au début, en cours, fin.

Vérifié.

Code
$ pnpm test:stories packages/formats/deck packages/formats/motion-blocks → 27 fichiers, 144 tests $ pnpm test:stories (complet) → 451/454 ; échecs hors lot (PagefindSearch) et suites rechargées par Vite (cache partagé), les miennes vertes au passage ciblé $ pnpm exec vitest run --project '!storybook' packages/formats/deck packages/formats/motion-blocks → 18 fichiers, 96 tests $ pnpm -F recit-storybook typecheck ; nx typecheck @recit/deck @recit/motion-blocks → verts $ pnpm -F recit-storybook build → vert, 486 stories (60 Deck, 84 Motion) $ pnpm -F @recit/motion-render goldens:check → ✓ all goldens match (32 vues, aucune régénérée) $ node scripts/check-stories.js packages/formats/deck packages/formats/motion-blocks → 0 --write-baseline : deck et motion retirés de scripts/stories-baseline.json $ node scripts/check-style-rule.js packages/formats/deck packages/formats/motion-blocks → 0 $ pnpm check → biome et oxfmt rouges hors lot (e2e/.sbfix-*.mjs, Alert/Card.module.css…) ; verts sur mes fichiers ; check:imports et check:other verts

Captures des 144 stories (288 références) : à la porte (pnpm test:e2e:update:linux e2e/storybook.spec.ts).

  • (STY5b) packages/site/src/app/defaultTheme.css (règles :root et .dark) n'a pas de lot : à STY5c avec le reste de main.css. base.css est en @layer base tant que le preflight de Tailwind existe ; STY5c le passe en theme. Des commentaires de modules (Header…) citent encore « les variables de main.css » : elles sont dans Layout.module.css.
  • (TST4a) Bug : AnnotationCanvas décale le trait quand le stage est mis à l'échelle (à 0,5 le canevas fait 1278 px mais les coordonnées viennent du rectangle affiché, 635 px) et n'écoute que la souris, pas le pointeur (rien au tactile) — à corriger avec le lot d'accessibilité ou avant. Clavier : pas de navigation par chiffres dans useKeyboardNav, c (effacer) non câblé. RemoteQrCodeModal n'est pas un dialog. Contrastes du deck (présentateur, télécommande, DevHud, pied de SlideFrame 1,93:1 en sombre, callout tip 4,16:1) et des thèmes motion (@recit/theme/motion, accent et atténué sous 4,5:1). Textes « Slide », « PREV », « NEXT » non traduits.
  • (TST4a) À la porte, un pnpm install : @recit/motion-blocks en devDependency de sites/storybook (et preview.css par le paquet plutôt que par chemin) ; storybook et @storybook/react-vite en devDependencies de @recit/deck et @recit/motion-blocks, puis retirer les paths et exclude de contournement et migrer player.test.tsx vers composeStories.

TST4b — Composants des sites vers site-kit, stories du kit — 30/10

Vague 5 du plan 21 (§8, TST4), en parallèle de GATE4, INFRA, STY5a-b, TST3a-b, TST4a. Aucune commande git, pas de pnpm install, aucune référence régénérée.

Un site, c'est du Markdown et de la config. Les six composants tolérés quittent les sites pour packages/tools/site-kit/src/components/ avec leurs modules : Hero, FormatGrid, OneSource (SideBySide, Caption) de www, StatusBadge de docs, VideoDownloads de motion, FormatSupport de components. site-kit les exporte ; les recit.config.ts les importent de @recit/site-kit (déjà en dépendance des quatre sites) ; les pages .mdx ne les importaient pas (composants passés par mdx.components) et ne bougent pas. Les dossiers src/ des quatre sites sont supprimés (avec www/src/index.ts et assets.d.ts), leur tsconfig.json ne les inclut plus. L'override sites-markdown-only de biome.jsonc n'a plus d'exception ; AGENTS.md le dit.

  • VideoDownloads : le motif devient /videos/*.mp4, relatif à la racine du site (Vite, root du site ; le chargeur de config de la CLI, dossier de la config) ; vérifié par une construction de motion avec un MP4 factice (lien présent) puis sans (message). Nouvelle prop files facultative, pour la story.
  • Les modules passent par le PostCSS Mantine du Vite du framework, comme Cards.module.css (rem(), @mixin dark compilés) ; les classes deviennent recit-site-kit-*.

Héros de www, −1 px. Mesuré sur l'hôte (1280 × 720) : boutons à 37,984 px, section à 421,98. Cause : le minifieur plie line-height: calc(1.25 / 0.875) en 1.42857, soit 19,99998 px, que la mise en page arrondit à 19,984 px ; Tailwind passait par une propriété personnalisée, jamais pliée. Même correction que GATE4 (--cards-leading) : --hero-leading: calc(1.25 / 0.875) puis line-height: var(--hero-leading). Après : boutons à 38 px, section à 422, comme Tailwind (20 + 16

  • 2).

Stories (Kit/, 41 tests). Cards (une carte, badge, sans lien, lien externe, grilles de 1 à 6), ExampleHeader (avec et sans badges, avancé), SourceFile (court, long, config), Tutorial (une étape, sans durée, 5 étapes), Hero, FormatGrid (défaut, mobile), OneSource, StatusBadge (états, phase), VideoDownloads (vide, fichiers), FormatSupport ; Slides/Examples/ pour les composants de deck des exemples, dans le cadre de diapositive avec les jetons du deck : Counter (incrémente, départ), Timeline (ordre des étapes ; le composant n'a pas d'étape active, rien d'ajouté), Metric (défaut, tendance, valeur seule). Harnais : withSite et le chargeur du surligneur valent aussi pour Kit/ (les composants du kit lisent le contexte du site) ; virtual:recit-stories a sa fixture vide (src/harness/virtual/recit-stories.ts). SourceFile importe la fixture Shiki de @recit/site par chemin (non exportée : son import démarre Shiki).

Typage : site-kit et les exemples n'installent pas Storybook ; comme TST4a, leurs tsconfig excluent les stories et sites/storybook/tsconfig.json les inclut (ses paths). Pas de devDependency ajoutée à sites/storybook : ni @recit/site-kit ni les exemples n'y sont résolubles sans pnpm install (arête nx manquante, règle ST-20 : à poser avec un install, comme storybook en dev de site-kit et des deux exemples).

todo d'accessibilité (contrastes, rendu inchangé) : « Available » de FormatSupport (emerald-600, 3,65:1), tendance de Metric (emerald-500, 2,47:1).

Vérifications.

Code
$ pnpm exec biome lint → 0 erreur dans le lot (rouges hors lot : e2e/.sbfix-*.mjs, MdxPage.stories) $ node scripts/check-stories.js --write-baseline packages/tools/site-kit/src/components sites examples → kit 4 → 0, sites 7 → 0 (surfaces retirées de la baseline) $ node scripts/check-style-rule.js → aucune hausse ; node scripts/check-package-boundaries.js → vert $ pnpm exec nx run-many -t build typecheck -p @recit/site-kit www docs motion components recit-example-deck-mdx recit-example-deck-showcase → vert (au 3e passage : dist concurrents) $ pnpm exec vitest run packages/tools/site-kit --project '!storybook' → 17 tests verts $ pnpm test:stories → 485 tests, 1 échec hors lot (Deck/Presentation Embedded, TST4a) $ pnpm test:e2e:host e2e/site-www.spec.ts e2e/site-components.spec.ts e2e/site-motion.spec.ts → 54 passés, 1 échec connu (/custom-components, React #418, vu à STY6.3b)

À la porte : captures des pages www (héros à l'identique), docs, components, motion et des nouvelles stories Kit/ et Slides/Examples/ (e2e/storybook.spec.ts).

SBFIX — Stories en échec dans le Storybook construit — 30/10

Les 28 échecs de la porte de la vague 4 dans e2e/storybook.spec.ts (Storybook construit), qui passaient dans le projet Vitest storybook. Aucune commande git, aucune référence régénérée.

Cause commune. storybook build produit un React de production, où le renderToCanvas de @storybook/react remplace act() par un simple appel (getAct, process.env.NODE_ENV === "production") : le rendu est déclaré fini dès le premier commit, même suspendu, et le play court sur un #storybook-root vide. Le projet Vitest rend en développement, sous act(), qui attend la fin des suspensions : la même story y passe.

Corrections.

  • Code (CodeBlock, CodeTabPanel, CodeTabs, EmbeddedCodeBlock, SyntaxHighlight) : useHighlighter fait use(highlighterPromise), et la fixture charge dix-sept grammaires en morceaux séparés. Nouveau loader loadSiteHighlighter (.storybook/decorators/withSite.tsx, inscrit dans preview.tsx) : il attend le surligneur de parameters.site.options avant le rendu et marque la promesse status: "fulfilled", que use() lit sans suspendre.
  • Presse-papiers : permissions clipboard-read et clipboard-write dans la spec (précédent : site-behaviour.spec.ts), plutôt qu'un mock dans preview.tsx, qui casserait la copie pour qui parcourt le Storybook construit ; le mock de vitest.setup.ts reste celui du projet Vitest.
  • Slides/Grid/Overloaded : l'avertissement de surcharge de Grid.tsx n'existe qu'hors production. Le play vérifie l'avertissement en développement, son absence (et les six cartes) en production.
  • L'avertissement Language "csv" is not loaded ne vient pas du Storybook : ce sont les blocs ```csv de sites/plans/pages/annexes/broc/scenario-1.md et -5.md (le site plans charge les langues par défaut). Non bloquant, non corrigé.

Hors lot, signalé. Les stories nouvelles de TST3/TST4 tombent sur la même cause (rendu suspendu ou effet pas encore passé au moment du play, getBy* au lieu de findBy*) : Site/Components/ (Header, TopNavigation, MobileTopNavigation, MobileTopNavigationDrawer, Slot), Site/Navigation/ (MobileNavigationDrawer, NavigationWrapper, Toc), Deck/MdxSlide, Deck/MobileRemoteView, Deck/RemoteQrCodeModal ; Site/Plugins/Markdown/MdxPage lève SB_DOCS-TOOLS_0001 (argTypes de react-docgen). Remède par story : findBy*/waitFor ; ou, pour tout le Storybook, un build de développement (au prix de la taille), à décider à la porte.

Vérifications.

Code
$ pnpm -F recit-storybook build → vert $ pnpm test:e2e:host e2e/storybook.spec.ts --grep "codeblock|codetab|embeddedcodeblock|syntaxhighlight|grid--overloaded" --workers=2 → 58 passés (avec la charge de l'hôte à 15-18 et les workers par défaut, les premières stories de chaque worker peuvent dépasser les 5 s du poll de phase au démarrage à froid) $ pnpm test:e2e:host e2e/storybook.spec.ts --workers=3 → 917 passés, 56 échecs, tous hors lot (liste ci-dessus) $ pnpm test:stories → 128 fichiers, 485 tests verts

Fin de la vague 5, vague de corrections — 30/10

  • Les six lots de la vague 5 sont faits, plus GATE4, INFRA et SBFIX. pnpm install hors agents : @recit/motion-blocks, @recit/site-kit, recit-example-deck-mdx, recit-example-deck-showcase en devDependencies de sites/storybook ; storybook et @storybook/react-vite en devDependencies de @recit/deck, @recit/motion-blocks, @recit/site-kit et des deux exemples. pnpm check vert : compteur de style 17 (1 217 au départ), stories sans play ou sans story 18.
  • Avant la porte de la vague 5 (un seul passage e2e, une seule revue), cinq agents : STORYFIX2 (28 stories en échec dans le Storybook construit, contournements de tsconfig retirés, player.test.tsx), LH (interlignes pliés, correction unique au build), A11Y-SITE et A11Y-DECK (les todo d'accessibilité, le stylo d'AnnotationCanvas, le point de la Dialog), HYDRA (React #418, réindexation Pagefind au montage, viewTransition abandonnée, surlignage prérendu).

LH — Interlignes pliés par le minifieur — 30/10

Ce que plie chaque chaîne (expérience minimale sur Lightning CSS 1.33.0 et esbuild 0.28.2, ceux du dépôt) :

  • Build Vite des sites : build.cssMinify n'est pas réglé, il vaut true, ce qui veut dire Lightning CSS (minifyCSS de Vite 8 n'appelle esbuild que pour "esbuild", et le bundle SSR prend "lightningcss"). En dev, pas de minifieur, pas de pliage : dev et build différaient.
  • scripts/build-css-modules.js (Lightning CSS, minify: false) plie aussi : l'analyse range chaque nombre en f32 et l'écrit sur six chiffres. calc(1.25 / 0.875) → 1.42857, calc(1 / 0.75) → 1.33333, et même un littéral long, 1.4285714286 → 1.42857. Les feuilles publiées de @recit/components, @recit/deck et @recit/site-stories portaient déjà les valeurs pliées ; l'optimize de Tailwind dans site-stories/scripts/build-css.mjs les recopiait.
  • Rien ne plie un var() : ni --x: calc(…), ni le repli var(--y, calc(…)), ni la --text-sm--line-height de Tailwind. esbuild, lui, ne plie pas ce calc().

Correction retenue, une seule règle à deux endroits. Le module écrit le rapport tel quel, line-height: calc(1.25 / 0.875) ; le build l'enrobe dans le repli d'une propriété que personne ne définit, var(--recit-unfolded, calc(1.25 / 0.875)), que le navigateur calcule exactement (une seule déclaration, pas d'héritage de propriété locale, !important gardé) :

  • postcssUnfoldLineHeight (packages/site/src/vite/css/line-height.ts, exporté par @recit/site/vite), dernier plugin PostCSS de recit-style et du Storybook ;
  • unfoldLineHeights dans scripts/build-css-modules.js, appliqué au source avant Lightning CSS.

Seul line-height est protégé (les calc() à unités, font-size: calc(14px * 1.5) → 21px, se plient sans perte). Les contournements à la main reviennent à l'écriture simple pour rester cohérents : --cards-leading (Cards), --navigation-menu-leading, --hero-leading, --heading-lh (Heading) et les 24 littéraux « arrondis vers le haut » (1.4286, 1.3334, 1.5556, 1.1112) de 14 modules de packages/site. Les littéraux de Typography.module.css (1.33333, 1.42857, 1.71429…) copient ceux du plugin typography : gardés. Piège « Folded line heights » du skill mantine-style réécrit avec la règle finale. Plan de version version-plan-line-height.md (prerelease).

Preuves. Feuilles construites : line-height:1.42857 41 → 1 et 1.33333 26 → 1 dans sites/docs/dist (le reste : Typography), 1.4286 13 → 0 ; 56 var(--recit-unfolded,calc(1.25 / .875)), 24 …calc(1 / .75)) ; idem www et presentation ; dist/styles.css des paquets : plus aucune valeur pliée. Mesure Playwright sur l'hôte (boîtes d'une ligne, avant → après) : StatusBadge__root 12 px de docs /formats 15,984 → 16 px (×9), OneSource__caption 15,984 → 16 (×2) et CodeBlock__title 14 px 19,984 → 20 px sur www / ; hauteur de page docs / 2 279 → 2 280 px, www / 3 708 → 3 709 px, main de docs / 1 794,453 → 1 794,5 px.

Vérifications.

Code
$ pnpm exec nx run-many -t build -p docs www presentation components → vert $ node scripts/build-css-modules.js --check --all → à jour $ pnpm test:related line-height.ts build-css-modules.js plugin.ts → 34 tests verts $ pnpm check:imports && pnpm check:other → vert ; pnpm check : 3 fichiers mal formatés hors lot (AnnotationCanvas.stories.tsx, Banner.tsx, NavigationFilterInput.tsx) $ pnpm test:e2e:host e2e/site-docs.spec.ts → 51 passés

Les captures -linux bougent d'un pixel là où une ligne pliée précédait : à régénérer à la porte.

À faire à la toute fin du plan 19 (demande de l'utilisateur, 30/10)

  • Écrire audit-design.md à la racine du dépôt, avec des captures d'écran de tout : chaque page de chaque site (clair, sombre, mobile), chaque composant par ses stories, et un regard de designer sur l'ensemble (cohérence des espacements, de la typographie, des couleurs et contrastes, des états, du mode sombre, du mobile), avec les défauts et les pistes. Les images dans un dossier à côté (par ex. audit-design/), prises dans le conteneur Playwright comme les références, après la dernière porte.

HYDRA — Hydratation et défauts d'exécution — 30/10

Suivis de GATE4, STY6.3b, STY5a et TST3b. Aucune commande git, pas de pnpm install, pas de pnpm fix, aucune référence régénérée, pas de conteneur.

React #418 (/custom-components, writing/mdx) et surlignage prérendu de recit-config : une seule cause, la limite de temps de Shiki. Reproduit sur l'hôte avec le build de production (/custom-components, trois erreurs sur trois chargements) ; la différence n'était ni la date ni le fuseau mais le bloc ```tsx du Counter : le chunk client compilé portait d'autres jetons que le HTML prérendu ( c / + au lieu de c / +, )} > Clicks: {count} </ au lieu d'un seul jeton), donc d'autres nœuds texte. Les couleurs claires du chunk client étaient fausses (return en #6F42C1, button en #E36209), les sombres justes. Shiki limite la tokenisation d'une ligne à 500 ms (tokenizeTimeLimit) : une ligne qui dépasse est coupée et toutes les suivantes héritent d'un mauvais état. Le premier bloc qu'un processus surligne paie la compilation des regex de la grammaire, et le thème clair passe le premier ; sous la charge d'un build (client et SSR en parallèle dans le même processus, puis le prérendu, plus les builds des autres agents), ce premier passage dépassait parfois. Le client build, le SSR build et le prérendu obtenaient alors chacun leurs couleurs : #418 quand le MDX compilé différait, et le --shiki-light:#D73A49 sur { defineConfig } du bloc data-recit-runtime-shiki prérendu (surlignage d'exécution du SSR). Pas un état partagé entre pages au sens d'une fuite : l'état est le cache de regex du surligneur module-level, qui rend la première page plus lente que les autres. Non déterministe : un build propre de 01:31 n'avait plus l'erreur. Reproduit en Node avec tokenizeTimeLimit: 1 sur un surligneur neuf (bloc coupé après onClick=).

Correction. lib/shiki.ts : tokenizeTimeLimit: 0 dans defaultHighlightOptions, utilisé par la chaîne MDX, Markdown et highlight() (SyntaxHighlight, CodeTabs) : même jetons partout, quel que soit le temps. lib/shiki.test.ts (nouveau) : sous une horloge où chaque lecture avance d'une seconde, codeToHast et highlight() donnent le même résultat que sans ; contre-épreuve à 500 ms, les deux tests échouent. Reste ailleurs : @recit/core (compile-story.ts, @shikijs/rehype) garde la limite par défaut pour les blocs des decks ; même risque de couleurs fausses au build (sans #418 : le deck est compilé une fois), à reprendre par son propriétaire avec un plan de version.

« Last modified ». Déjà en UTC au prérendu comme au client, l'infobulle n'est rendue qu'après le montage. Vérifié sans git (GIT_DIR=/nonexistent, comme un conteneur sans .git) : git log échoue, les deux compilations prennent le mtime ; HTML prérendu et chunk client portent le même lastModifiedTime (2026-10-28T04:02:06.056Z pour custom-components). Le seul chemin où les deux compilations pouvaient différer, le repli new Date() d'un fichier introuvable, est mémorisé par chemin (vite/mdx/remark-last-modified.ts).

IndexingDialog. La réindexation démarre à l'ouverture du dialogue (onOpenChange du Dialog), plus au montage ; « Retry » reconnecte ; la fermeture et le démontage ferment le flux (référence de l'EventSource). Stories : IndexingDialog/Indexing vérifie qu'aucune connexion n'a lieu avant l'ouverture, puis une ; PagefindSearch remplace son EventSource muet par un EventSource qui enregistre (onConnect), et Closed vérifie qu'ouvrir la recherche ne connecte rien. Contre-épreuve (démarrage au montage rétabli) : Indexing, Failed et Closed échouent.

viewTransition. Cause exacte (sonde Chromium) : après skipTransition(), c'est la promesse ready de la transition sautée qui rejette (« AbortError: Transition was skipped ») ; React Router n'écoute que finished. Reproduit sur le docs construit : deux clics de barre latérale à 5 ms → pageerror. lib/util/viewTransition.ts (nouveau) : handleSkippedViewTransitions(document) enveloppe une fois (Proxy, WeakSet) document.startViewTransition et marque traité le seul AbortError de ready ; tout autre rejet reste non traité, et qui attend ready voit toujours l'erreur. Installé par SiteProvider (effet, client seulement), racine commune du site et du harnais Storybook. Contournement retiré de Navigation/Stacked ; la transition jouant désormais, l'étape « Back » attend la fin de l'animation (waitFor sur toBeVisible). Tests : viewTransition.test.ts (5) ; contre-épreuve (rejet relancé) : « Unhandled Rejection ». Après rebuild, la sonde des deux clics ne voit plus d'erreur.

Vérifications.

Code
$ pnpm exec vitest run lib/shiki.test.ts lib/util/viewTransition.test.ts → 7 verts $ pnpm test:related (shiki, viewTransition, SiteProvider, IndexingDialog, remark-last-modified) → 25 fichiers, 208 tests verts $ pnpm test:stories search-pagefind navigation → 15 fichiers, 46 tests verts (Navigation ×3) $ pnpm -F @recit/site typecheck → muet $ GIT_DIR=/nonexistent pnpm exec nx run-many -t build -p docs components → vert (deux passages rouges avant : dist de cli et de core vidés par des builds concurrents) $ sondes Playwright (preview du build) : aucune erreur console sur custom-components, writing/mdx, recit-config ; nœuds texte prérendu = client ; aucun jeton aux couleurs croisées $ BROWSER=none pnpm test:e2e:host e2e/site-docs.spec.ts e2e/site-components.spec.ts e2e/site-behaviour.spec.ts → 90 passés, 1 sauté $ pnpm check → biome vert (avertissements), check:imports et check:other verts ; fmt:check rouge sur packages/formats/deck/src/tools/AnnotationCanvas.stories.tsx (autre lot)
  • (orchestrateur, suite de HYDRA) @recit/core compile-story.ts : tokenizeTimeLimit: 0 pour @shikijs/rehype, comme packages/site/src/lib/shiki.ts ; version plan version-plan-shiki-time-limit.md (prerelease). 197 tests liés verts.

A11Y-SITE — Accessibilité du framework de sites — 30/10

Lot d'accessibilité de la porte de la vague 5 (packages/site), en parallèle de STORYFIX2, LH, A11Y-DECK, HYDRA. Aucune commande git, pas de pnpm install, pas de conteneur. Tous les a11y: { test: "todo" } de packages/site sont retirés : axe est en erreur partout ; reste InkeepSearch en !test (widget tiers chargé d'un CDN avec la clé d'un site, pas un défaut).

Contrastes (rendu changé, voulu) :

  • ui/Alert : destructive en clair light-dark(red-700, --destructive) (4,36 → 6:1) ; le texte d'info prend le bleu de l'alerte (inherit) au lieu du gris atténué (4,43:1).
  • ui/CodeTabs : onglet inactif color-mix(--muted-foreground 90 %, --foreground) (4,42 → 5,3:1).
  • ui/Kbd : même mélange (4,39:1 sur --muted, vu dans le pied de la recherche Pagefind).
  • Shiki : colorReplacements: { "github-light": { "#e36209": "#bc4c00" } } dans defaultHighlightOptions (3,49 → 5,03:1), l'orange du thème clair actuel de GitHub. Une surcharge plutôt qu'un autre thème : seul ce jeton échoue, le reste de github-light passe, et un site qui choisit ses thèmes garde leurs couleurs (la clé est le nom du thème).
  • Banner : couleurs nommées d'un cran plus sombres (blue-600, green-700, amber-700, red-600, ≥ 4,76:1) et texte blanc forcé (--mantine-color-white) : en sombre, --primary-foreground est foncé et échouait aussi.
  • Typography : citation en gris opaque lab(48.5 %) (2,95 → 4,73:1).
  • NavigationBadge : texte quasi noir sur les fonds clairs (blanc 1,77:1 sur vert) ; les badges invert passent aux 700/800 de Tailwind en clair, 400 en sombre (≥ 4,5:1 sur la barre).

Noms : bouton « Dismiss the banner » ; « Clear the filter » et champ nommé dans NavigationFilterInput ; nav « Sections » (TopNavigation), « Sidebar » (NavigationWrapper), « Pagination » ; HeaderNavigation était déjà « Main » (NavigationMenu) ; Modal du harnais SearchFixture (closeButtonProps) ; TocPopoverButton sans aria-label, nommé par son texte visible (« On this page » ou la section).

Rôles et structure :

  • Pagination en nav ; Spinner en progressbar indéterminé nommé (label, « Loading ») ; PageProgress reste aria-hidden, justifié en commentaire (valeur simulée, bruit à chaque lien).
  • NavigationCategory : les items dans un div (plus de ul sans li) ; aria-expanded et aria-controls sur le chevron seul. MobileTopNavigationDrawer : chaque séparateur dans un li aria-hidden (axe list, masqué jusqu'ici par le todo du Separator).
  • ui/Separator décoratif : renderRoot qui retire l'aria-orientation que Divider écrit toujours (ses props filtrent les undefined).
  • ui/DropdownMenu : withInitialFocusPlaceholder={false} et notre propre sentinelle aria-hidden (celle de Mantine, role="presentation" focalisable, comptait comme enfant du menu). Le comportement au clavier ne change pas.
  • ui/NavigationMenu : un menu de liens est une divulgation, pas un menu : withRoles: false (passé par étalement, absent du type de Menu), renderRoot sans role/aria-orientation, aria-expanded/aria-controls posés par le déclencheur, même sentinelle.
  • ui/Command : la liste n'est listbox (et le combobox aria-expanded) que si des items sont montés (compteur dans le contexte) ; vide, l'état vide se lit comme du texte.
  • ui/Input : bordure destructive sous aria-invalid, anneau au focus (:where, sous la règle de InputGroup qui retire l'anneau du contrôle) ; InputGroup avait déjà son état.
  • Point gris de la Dialog : le bouton de fermeture hérité du framework d'origine, un rond de 8 px en haut à gauche qui ne devenait une croix rouge qu'au survol. Remplacé par la croix de shadcn/ui en haut à droite, dans Dialog et dans CommandDialog (même rond, dans la recherche ; la ligne de saisie y gagne 40 px de marge à droite).

Banner mémorisée : oui, localStorage (recit-banner-dismissed, via useLocalStorage de @mantine/hooks, lu après l'hydratation) garde l'id de la dernière bannière fermée. Nouvelle clé site.banner.id (SiteConfig.ts) ; sans elle, un message texte est son propre id, un message JSX n'est fermé que pour la page. Une bannière fermée apparaît un instant avant l'effet (pas de script avant peinture : à reprendre si un site s'en sert).

Stories touchées hors lignes a11y (nécessaire) : Banner (beforeEach qui vide la clé, bouton trouvé par son nom), NavigationFilterInput (bouton par son nom), MdxPage FullWidth (« On this page »).

Rendu changé, à régénérer à la porte : Storybook Site/UI/Alert, CodeTabs, CodeTabPanel, SyntaxHighlight, Kbd, Dialog (tous), Command (dialogues), Input Invalid, Site/Components/Banner (couleurs), Markdown, Mermaid ParseError, Site/Navigation/NavigationBadge, NavigationItem (badge), Site/Plugins/PagefindSearch (croix, Kbd) ; pages : tout bloc de code clair avec un jeton orange (docs, components, presentation, plans…), toute citation de prose, le dialogue de recherche (site-docs « shows the search dialog »).

Vérifié.

Code
$ pnpm test:stories packages/site sites/storybook → 128 fichiers (2 sautés), 488 tests verts $ pnpm test:related <mes 54 fichiers> → 22 fichiers, 163 tests (shiki.test.ts de HYDRA rouge un instant pendant son édition, vert seul ensuite) $ pnpm -F @recit/site typecheck → seule erreur : util/viewTransition.ts (HYDRA) $ pnpm exec nx run docs:build → vert (deux échecs transitoires « Could not resolve @recit/theme » / « @recit/deck » pendant les builds concurrents) $ BROWSER=none pnpm test:e2e:host e2e/accessibility.spec.ts e2e/site-docs.spec.ts e2e/site-behaviour.spec.ts → 62 verts, 1 sauté $ node scripts/check-style-rule.js packages/site → aucune hausse (7 sous la base) $ pnpm check → biome lint et check:other verts ; fmt:check rouge sur 3 fichiers d'autres lots (AnnotationCanvas.stories, shiki.test.ts, viewTransition.test.ts)

A11Y-DECK — Accessibilité du deck, des slides et du motion — 30/10

Lot d'accessibilité ouvert par TST4a (défauts du deck et du motion) et les todo de TST1 et TST4b, en parallèle de STORYFIX2, LH, A11Y-SITE et HYDRA. Aucune commande git, pas de pnpm install, pas de conteneur. Les play existants sont intouchés ; les nouvelles stories sont des tests du lot.

Stylo d'AnnotationCanvas. Le canevas est à la taille de design (largeur du parent, qu'une transformation ne change pas), le pointeur lu à l'écran : toCanvasPoint (tools/canvas-point.ts, interne) ramène le décalage par le rapport des deux, le trait part sous le pointeur à toutes les échelles. Pointer Events (souris, doigt, stylet) avec capture du pointeur, touch-action: none sur le canevas, un trait à la fois ; l'état « en train de dessiner » passe dans une ref. Preuves : canvas-point.spec.ts (0,5 → 600 px pour 300 px à l'écran) et la story Deck/AnnotationCanvas/PenUnderThePointer (tracé au doigt dans le cadre medium, pixel peint sous le pointeur, rien à mi-distance ; rouge avec l'ancien calcul, vérifié).

Clavier. c est promis par la doc (presenter-console.mdx, quickstart.mdx) : Presentation câble onClearAnnotations, une clé qui remonte le calque (story Deck/Presentation/PenAndClear). La navigation par chiffres n'est promise nulle part : retirée du §4.3 du plan 21 (remplacée par Maj+D et c). useKeyboardNav ignore les touches venues d'un dialog ouvert (Espace y presse le bouton, pas la diapositive suivante).

RemoteQrCodeModal : un <dialog> natif ouvert par showModal() (rôle dialog, modal, page derrière inerte donc focus piégé, focus sur « Close », rendu à l'ouverture précédente à la fermeture), nommé par son titre (h2, aria-labelledby), Échap appelle onClose sans fermer de lui-même ; voile en ::backdrop. Story ModalDialog.

Repères et titres. Pied de SlideFrame en div (un <footer> dans le group de la diapositive était un contentinfo imbriqué). PresenterView : nom de la console en h1, « Speaker notes » en h2 ; MobileRemoteView : h4 → h2.

Contrastes (mesurés par axe, clair) :

DéfautAvantCorrection
Pied de SlideFrame sur un bg de scène2,4:1 (4,45 sombre)noir ou blanc selon la luminosité OKLCH du fond (oklch(from …), sous @supports)
Compteur de PresenterView4,34:1atténué mêlé à 15 % de premier plan (5,4:1)
Position de MobileRemoteView1,68:1primaire mêlée à 50 % de blanc (6,5:1)
Notes vides de MobileRemoteView3,89:1slate-400 (7:1) ; bouton « NEXT » en --primary-foreground (2,9:1 en sombre)
« Session » de RemoteQrCodeModal4,46:1atténué mêlé (5,5:1)
Badges de DevHud (deck.css)3,76 et 3,18:1emerald-700 et amber-700 sous le blanc (5,5 et 5:1)
slide-badge-* (Badge, HtmlSlide absent)2,7 à 3,8:1emerald-700, amber-800, red-700 ; 400 en sombre (.dark)
Texte d'un callout dans une diapositive4,0 à 4,2:1.slide .slide-callout :is(p, li) atténué mêlé (5:1 et plus)
Avertissement de Grid4,45:1amber-800 (6,3:1)
Erreur de Mermaid3,14:1red-700 (5,3:1), red-300 en sombre (red-500 y faisait 4,16:1)
« Available » de FormatSupport3,65:1emerald-700 en clair
Tendance de Metric (exemple)2,47:1emerald-700 en clair, emerald-500 gardé en sombre (couleur seule)

Mermaid : le role="img" ajouté après celui de Mermaid était un attribut en double, ignoré par l'analyseur HTML. Le rôle graphique de Mermaid (graphics-document, le lecteur parcourt les étiquettes) reste, le nom (aria-label du titre) remplace celui de Mermaid ; role="img" seulement pour un <svg> sans rôle.

Thèmes motion (@recit/theme) : les jetons servent au texte (surtitres, légendes, valeurs), corrigés. warm-editorial : mutedForeground 46,7 → 41 % (#795862), primary/accent/beforeNum 49,2 → 41 % (#8c4558), afterNum 42,4 → 34,5 % (#4a6661) ; saas-product : mutedForeground 53,7 → 43 % (#646a77), beforeNum 58 → 47 % (#cd2328), afterNum 33,3 → 27,5 % (#13794e) ; les barres gardent leur couleur. ui-scene : tons good/danger #116c46/#b51f24 (texte, et 4,5:1 dans la teinte d'une puce). buildTheme : atténué à 36 % du fond (42 % donnait 4,19:1). Ces thèmes servent aussi aux decks qui les nomment : couleur un peu plus sombre, voulue. THEME_CONTRAST_TODO supprimé. Goldens : goldens:check vert sans régénération (le texte bouge de 0 à 11 745 pixels exacts par vue, sous le seuil de pixelmatch à 0,1) ; les régénérer à la porte si l'on veut qu'ils portent la couleur exacte.

i18n. UiStrings gagne slide (« Slide » / « Diapositive », console, télécommande, HUD) et remotePrevious/remoteNext (« ◀ PREV »/« NEXT ▶ », « ◀ PRÉC. »/« SUIV. ▶ ») ; l'anglais est inchangé (les play et e2e qui lisent ces textes passent).

Version plans (STY9.4), paquets publiés touchés : @recit/core (trois clés d'interface, ajout), @recit/theme (jetons de deux thèmes nommés, deck.css : badges, HUD, callout), @recit/deck (stylo, c, dialogue, pied, titres, contrastes), @recit/components (Mermaid, Grid), @recit/motion-blocks (tons ui-scene, buildTheme), @recit/site-kit (FormatSupport).

À la porte, captures qui bougent : stories Deck/ (console, télécommande, QR, HUD, pied sur fond), Slides/Badge, Slides/Grid/Overloaded, Slides/Mermaid/InvalidChart, Kit/FormatSupport, Slides/Examples/Metric, Motion/ (texte atténué et d'accent) ; e2e : console du présentateur, diapositives à badges ou callouts, deck-showcase en clair (tendance), pages du site motion et decks au thème nommé.

Vérifications.

Code
$ pnpm test:stories → 130 fichiers (2 sautés), 488 tests verts, axe en erreur partout dans le lot $ pnpm exec vitest run --project '!storybook' packages/formats/deck packages/formats/motion-blocks packages/components packages/theme packages/core → 49 fichiers, 455 tests $ pnpm -F @recit/motion-render goldens:check → ✓ all goldens match (32 vues) $ node scripts/build-css-modules.js --check --all → tables à jour $ nx run-many -t build typecheck (core, theme, components, deck, motion-blocks, site-kit, site-stories, motion-render, cli, recit-storybook, deck-showcase, deck-mdx) → verts $ recit build (deck-showcase, en et fr) → 13 diapositives $ BROWSER=none pnpm test:e2e:host e2e/deck-features.spec.ts e2e/deck-behaviour.spec.ts → 12 passés $ pnpm check → vert (avertissements biome hors lot)

STORYFIX2 — Storybook construit, contournements retirés — 30/10

Suite de SBFIX : les 28 stories en échec dans e2e/storybook.spec.ts (Storybook construit, en production, sans act() : le play court avant la fin des suspensions et des effets). Aucune commande git, aucune dépendance, aucune référence régénérée, aucun !test.

Causes et corrections, par story.

  • Site/ (Header ×4, TopNavigation ×3, MobileNavigationDrawer, MdxPage…) : useCurrentNavigation suspend (useSuspenseQuery de plugin-navigation) et SiteProvider lit context.initialize par use(). Le loader loadSiteHighlighter devient loadSiteHarness (.storybook/decorators/withSite.tsx, inscrit dans preview.tsx) : il construit le client de requêtes et le SiteContext de la story, initialise les plugins, met la navigation de la page en cache (clé ["plugin-navigation", path]) et attend le surligneur ; withSite les lit dans loaded.site. Plus rien ne suspend au premier rendu.
  • Stories mobiles (MobileTopNavigation ×4, MobileTopNavigationDrawer ×2, MobileNavigationDrawer) : globals.viewport redimensionne le cadre du manager, pas la page iframe.html qu'ouvre l'e2e ; à 1280 px le bouton et le tiroir sont masqués (64em). La spec lit le viewport de la story (storyGlobals et parameters.viewport.options) dès qu'elle est connue, prend cette taille et recharge (erreurs remises à zéro). Effet sur les captures : les stories à globals.viewport (dont Kit/Hero/Mobile, FormatGrid, OneSource, MobileRemoteView) sont désormais capturées à 390 × 844.
  • NavigationWrapper/ScrolledToActive : le nav n'avait pas de hauteur, --header-height étant absent. Les variables de mise en page sont sur :root dans Layout.module.css, et Vite donne aux modules CSS moduleSideEffects: false : sans Layout rendu, la feuille est éliminée du build (un import d'effet ne suffit pas). Nouveau packages/site/src/lib/testing/layoutSheet.ts (réexporté par @recit/site/testing), lu par loadSiteHarness. Module à part : lire un export défini dans testing/index.tsx tirait tout son graphe, dont des feuilles @layer components chargées avant @layer base, ce qui inversait l'ordre des couches (logo de Kit/Hero doublé). Effet sur les captures : les stories Site/ ont maintenant les vraies variables de la mise en page.
  • Toc/FollowsTheSection, MdxPage/WithToc : l'évènement storybook:intersect partait avant l'observe() (posé dans un effet) ; il est redispatché dans le waitFor.
  • Slot/Filled : les sources s'inscrivent dans un effet ; waitFor sur la disparition du repli.
  • Deck/MdxSlide/FirstStep : les étapes sont masquées par un effet ; waitFor sur la seconde.
  • Deck/MobileRemoteView/AfterTenMinutes : le waitFor de findBy* démarrait son setInterval avant l'effet du minuteur et prenait le bouchon de clockAt. clockAt (fixture du deck) n'intercepte plus que la période d'une seconde ; la story règle parameters.clock (un seul bouchon par story, au lieu de deux beforeEach empilés).
  • Deck/RemoteQrCodeModal/Closed : racine vide (la spec attend un élément) ; la story dessine une ligne de texte sous la modale fermée.
  • MdxPage (×6) SB_DOCS-TOOLS_0001 : react-docgen ne résout pas MDXImport["default"] (type indexé) et storybook/docs-tools fait un console.error. Des argTypes explicites n'y peuvent rien (l'extraction tourne quand même) : la prop mdxComponent est typée MDXContent (mdx/types, le même type), nommé.
  • Deck/Presentation Embedded : vert au premier passage, sans changement.

Contournements retirés (TST4a, TST4b). sites/storybook/tsconfig.json : plus de paths vers les paquets Storybook ni d'include des stories des autres paquets ; tsconfig.json de @recit/deck, @recit/motion-blocks, @recit/site-kit et des deux exemples : plus d'exclude des stories (chaque paquet typecheck les siennes) ; preview.css : @import "@recit/motion-blocks/styles.css" par le paquet. Les files publiés excluent toujours stories et fixtures.

player.test.tsx (motion-blocks) : le minutage reste (logique sans DOM) ; les tests de MotionFrame rendent les stories Start, Settled, SecondScene, Portrait par composeStories (renderToStaticMarkup, args surchargés pour l'échelle).

Vérifié (hôte).

Code
$ pnpm -F recit-storybook build → vert $ BROWSER=none pnpm test:e2e:host e2e/storybook.spec.ts --workers=3 → 979 passés, 0 échec $ pnpm test:stories → 128 fichiers, 488 tests verts $ tsgo --noEmit : recit-storybook, @recit/deck, @recit/motion-blocks, @recit/site-kit, @recit/site, recit-example-deck-mdx, recit-example-deck-showcase → verts $ pnpm test:related (player.test, testing, MdxPage, fixture du deck) → 3 fichiers, 29 tests $ pnpm check → vert (avertissements biome préexistants)

À la porte : captures de e2e/storybook.spec.ts (stories mobiles à 390 × 844, stories Site/ avec les variables de mise en page, RemoteQrCodeModal/Closed).

  • (STORYFIX2) Les variables de mise en page (:root de Layout.module.css) n'existent dans une page que si Layout y est rendu (Vite élimine un module CSS dont on ne lit aucune classe) : à STY5c, les placer dans une feuille globale (app/base.css) rendrait layoutSheet inutile.

Porte de la vague 5 — 30/10

  • pnpm check vert. e2e complet dans le conteneur (installation 9 s grâce à INFRA, build 2 min 20 s, tests 21 min) : 763 passés, 640 échecs triés — 638 stories nouvelles sans référence, et plans /19-style (contenu du plan modifié). Aucune page de site ni story existante n'a bougé : la correction des interlignes (LH) a ramené les pages à l'identique des références d'avant la migration.
  • pnpm test:e2e:update:linux e2e/storybook.spec.ts e2e/site-plans.spec.ts : 1 049 passés en 6,9 min (passage ciblé : installation 1 s, build de trois projets 64 s) ; 978 références Storybook au total. Échantillon relu (deck, motion, héros mobile) : propre. À regarder à l'audit final : dans Motion/Blocks/CompareBars, 247 kB et 114 kB ont des barres de même longueur.
  • Les changements de couleur des thèmes warm-editorial et saas-product (A11Y-DECK) restent sous le seuil des captures : choix de marque à montrer à l'utilisateur avant/après.
  • Suite : STY5c (retrait de Tailwind, cn.ts, dépendances, mesure des bundles), puis la vague 6.

STY5c — Retrait de Tailwind — 30/10

Fait. STY5.2, STY5.3 et STY5.4 du plan 19, plus le retrait de Tailwind des paquets publiés que STY6.1 avait reporté ici (CLI, site-stories, @recit/theme, Storybook). Aucune commande git ; un pnpm install (−74 paquets, plus aucune ligne tailwind dans pnpm-lock.yaml).

Retiré.

  • Site : tailwindcss() de vite/plugin.ts ; dans plugin-theme.ts, les @source (racine, dépendances, __pluginDirs) et le @theme inline que transform écrivait dans main.css (le hook garde son enforce: "pre" : il injecte defaultTheme.css et theme.customCss avant que vite:css lise la feuille). __pluginDirs reste pour la fusion des vite.config des plugins et le relevé de leurs versions, documenté dans SiteConfig.ts. main.css ne garde que les deux @import de Geist et les deux marqueurs d'injection ; la déclaration de couches reste dans layers.css (commentaire mis à jour : base est vide, gardé dans l'ordre).
  • lib/util/cn.ts supprimé ; shiki.ts joint ses classes par une fonction locale (joinClassNames, sans fusion de classes Tailwind) ; l'export cn de @recit/site retiré (paquet privé, aucun importeur). MissingIcon.tsx, dernière chaîne text-red-500, passe par MissingIcon.module.css (le rouge 500 de la palette, en oklch).
  • biome.jsonc : tailwindDirectives, l'override d'exception des derniers importeurs (vidé, donc supprimé), les ignore plugin/theme/custom-variant/apply de noUnknownAtRules, désormais en erreur partout (le site garde mixin) ; l'exception noImportantStyles passe de main.css à reset.css. .vscode/settings.json perd sa clé tailwindCSS.
  • CLI : bundle/css.ts assemble la feuille d'un deck avec esbuild (déjà dépendance) au lieu de @tailwindcss/node et @tailwindcss/oxide : reset.css en @layer base, jetons, page.css, deck.css, print.css, feuilles des paquets, modules utilisateur (module virtuel recit:modules.css), customCss, dans le même ordre ; dépendances de la veille par le metafile ; url() d'un média inlinée comme dans js.ts, url(/…) laissée telle quelle ; minification par esbuild. L'option sources (scan Tailwind) disparaît de CssBundleOptions et de build-deck.ts.
  • site-stories/scripts/build-css.mjs : plus de Tailwind (thème, utilitaires, @source, déclaration de couches properties/theme…) ; esbuild concatène jetons préfixés, deck.css, feuilles des paquets et modules.css, la passe .recit-wrapper est inchangée, Lightning CSS (déjà en dev) minifie avec les cibles qu'utilisait l'optimize de Tailwind.
  • @recit/theme : tailwind.css et le peer optionnel tailwindcss supprimés.
  • Storybook : @tailwindcss/vite de main.ts, @import "tailwindcss", tailwind.css et @source de preview.css.
  • Dépendances : packages/site perd les 24 @radix-ui/*, @base-ui/react, class-variance-authority, clsx, tailwind-merge, cmdk, vaul, embla-carousel-react, react-hook-form, tailwindcss, @tailwindcss/vite, @tailwindcss/typography, tw-animate-css (36, chacun vérifié sans importeur) ; packages/cli et site-stories perdent tailwindcss, @tailwindcss/node, @tailwindcss/oxide ; sites/storybook perd tailwindcss et @tailwindcss/vite ; le catalogue perd ses quatre lignes Tailwind. Restent, comme prévu : next-themes (STY9, plus aucun importeur) et @tanem/react-nprogress (PageProgress.tsx l'importe encore).

Décisions.

  • Preflight. Il était dans toutes les surfaces (site en base, deck, Storybook) et chaque page est dessinée dessus : il devient @recit/theme/reset.css, copie règle à règle de tailwindcss/preflight.css 4.3.3 (MIT, ligne dans NOTICE.md), --theme(--x, …) écrit var(--x, …), plus les deux familles par défaut que la couche theme de Tailwind déclarait (--default-font-family: var(--font-sans), idem mono). Une seule feuille pour trois consommateurs, sans couche propre : le site l'importe en tête de base.css (layer(theme)), le deck en layer(base), Storybook en layer(theme). Le tri « ce dont le rendu dépend » n'est pas fait règle par règle : le deck et Storybook n'ont pas la baseline de Mantine, et la mesure ci-dessous montre l'identité avec la feuille entière.
  • base.css en couche theme, après le reset dans la même couche : * { border-color } gagne toujours sur son border: 0 solid. Rien d'autre n'occupe base.
  • Variables de mise en page : le bloc :global(:root) de Layout.module.css passe dans base.css (:root, couche theme) ; TopNavigation et Banner les remettent à 0 hors couche, polyfills.ts en ligne, donc ils gagnent toujours. layoutSheet.ts et son export de @recit/site/testing sont supprimés ; le harnais de Storybook (src/harness/styles.ts) importe @recit/site/app/base.css après main.css, comme main.tsx. Piège noté dans le skill. sites/storybook/styles.css (feuille de l'atelier, surfaces Mantine) rend à html/body leur flux d'avant (height: auto, overflow: visible) : base.css fait de body le conteneur de défilement d'un site, et axe refusait Site/Plugins/Markdown/ChatGPTLogo (SVG de 1 248 px sans élément focalisable, « scrollable-region-focusable »).
  • defaultTheme.css reste un fichier à part, deuxième feuille de base du framework (nommée dans la loi : SKILL.md §5, plan §0.3, isGlobalCss du compteur et son test) : theme.noDefaultTheme doit pouvoir la sauter, ce que base.css ne permet pas ; et @recit/theme/tokens.css porte les mêmes noms (--primary…) en triplets HSL lus hsl(var(--primary)) par les decks, quand le site les lit en couleurs complètes (oklch) : y mettre le zinc publierait deux formats sous les mêmes noms. STY9 tranchera avec le reste des variables shadcn (§2.4).
  • CLI : retrait ici, comme STY6.1 l'avait écrit (« STY5c retire Tailwind »). Rupture pour un auteur qui écrivait des utilitaires dans un composant ou un @apply dans customCss : version plan version-plan-tailwind-removed.md (@recit/theme, @recit/cli, @recit/site-stories en prerelease). Aucun exemple ni aucune story de sites/*/stories n'écrivait de classe utilitaire (vérifié). Les pages qui le disent encore (sites/docs/pages/theme/custom-css.mdx l. 4 et 25, theme/overview.mdx l. 38, sites/components/pages/custom-components.mdx l. 58) sont laissées à STY10.2 (documentation de la rupture, §6.4), pour ne pas bouger leurs captures dans cette vague.

Identité. dist de docs, www, presentation et des six decks d'exemple construits avant (scratchpad) et après, servis en statique ; Playwright compare, élément par élément, la boîte et toutes les propriétés calculées (plus ::before, ::after, ::placeholder, ::marker), clair et sombre, 1440 et 390 px :

  • docs (/, /documentation/writing/mdx, …/scenes, /documentation/reference/cli, /formats/motion, /404, /present/starter, /present/course, /formats/deck/layouts, /theme/named-themes) : 9,4 millions de valeurs, 0 différence.
  • www (/, /one-source, /stories/one-source) : 1,35 million, 0.
  • presentation (/, /demos/overview, …/showcase, …/mdx, …/site, /templates, /present/showcase, /present/mdx) : 7,3 millions, 0 (un premier passage de /demos/mdx mobile sombre avait capturé le deck intégré à mi-chargement ; relancé, 0).
  • Decks (les 39 diapositives des six exemples et deux de deck-showcase/fr, 1280 × 720, clair et sombre) : 1,9 million de valeurs ; seules différences, (a) les interlignes calc() des modules utilisateur que l'optimize de Tailwind pliait (.l du Metric : 1.55556 → calc(1.75 / 1.125), 28,0001 px → 28 px, quelques boîtes de 0,016 px), ce que LH demandait déjà ; (b) background-position calculée 0px 0px → 0% 0% sur pre.mermaid et les onglets de code (background: transparent que Lightning CSS réécrivait 0 0), sans image donc sans effet ; le reste, des identifiants aléatoires de Mermaid.

Sur l'hôte, avec comparaison des captures aux références -linux (tolérance 2 % de la config) : e2e/storybook.spec.ts 979 passés ; site-docs, site-behaviour, accessibility, deck-render, site-www, site-presentation : 110 passés, 1 sauté.

Bundles (STY5.4, gzip, comparés à STY0.6).

sites/docsSTY0.6 avantSTY0.6 aprèsavant STY5caprès STY5c
CSS d'entrée43,5 ko79,0 ko68,4 ko66,1 ko
CSS d'entrée, brut247 ko496 ko434 ko416 ko
JS d'entrée139,6 ko147,7 ko141,1 ko140,7 ko
JS total (somme des chunks)——3 475 ko3 466 ko

www : CSS d'entrée 68,5 → 66,3 ko, JS d'entrée 121,6 → 121,2 ko (STY0.6 avant : 43,4 et 120,5). presentation : 68,2 → 65,9 ko et 138,6 → 138,2 ko. Decks : main.css de deck-starter 8,6 → 7,9 ko (44,3 → 38,8 ko brut), deck-showcase 11,9 → 11,4 ko ; main.js inchangé. Le JS d'entrée ne baisse presque pas ici : Radix, cmdk, vaul n'avaient déjà plus d'importeur (vagues 3 et 4) ; cn et tailwind-merge sortent du chunk paresseux shiki-*.js (d'où les −9 ko du JS total). Le CSS reste au-dessus de STY0.6 avant (+22,6 ko) : c'est la feuille Mantine entière (~35 ko), dont l'import par composant reste l'option de STY9.

Compteur (node scripts/check-style-rule.js, baseline réécrite) : 17 occurrences → 1 (baseline 24 → 1). Reste la règle globale de sites/storybook/.storybook/preview.css (fond de #storybook-root et .docs-story en jetons de deck) : du chrome d'atelier, pas un composant ; sites/storybook/styles.css n'est chargé que pour les surfaces Mantine et lit les jetons du site, la règle ne peut pas y aller. À trancher pour --strict (STY9) : l'exempter dans isGlobalCss comme feuille de base de Storybook.

Vérifié.

Code
$ pnpm exec nx run-many -t build typecheck -p @recit/site docs www presentation components motion plans vision recit-storybook → 32 tâches vertes $ pnpm exec nx run-many -t build typecheck -p @recit/cli @recit/theme @recit/site-stories → 16 vertes ; pnpm publint → 12 paquets $ pnpm exec nx run-many -t build -p 'recit-example-deck-*' → vert $ pnpm exec vitest run packages/cli scripts/__tests__/check-style-rule.spec.ts packages/site/src/vite/plugin-theme.test.ts → 8 fichiers, 193 tests $ pnpm check → vert (18 avertissements biome préexistants, aucun dans mes fichiers) $ pnpm test → 2 235 passés, 3 échecs : ChatGPTLogo ×2 (corrigé ci-dessus, vert seul) et packages/mcp/test/markdown.test.ts « render reads a .md story » (délai de 5 s sous charge, 17/17 seul) $ pnpm test:stories → 128 fichiers, 488 tests $ pnpm exec nx run recit-storybook:build ; playwright --project=chromium e2e/storybook.spec.ts → 979 passés

À la porte : la galerie devrait être vide pour les sites et les decks ; plans /19-style bouge (contenu du plan) ; les stories Site/ reçoivent maintenant base.css (jusqu'ici absent du harnais : body, * { border-color }, ligatures du code…), passées sous la tolérance sur l'hôte, à relire dans le conteneur ; les interlignes dépliés des decks (≤ 0,016 px) ne devraient pas dépasser le seuil.

Porte de STY5c, vague 6 lancée — 30/10

  • Le premier passage e2e après STY5c a été coupé pendant le build par la fin de session ; relancé : 1 401 passés, 2 échecs (plans /19-style, contenu du plan modifié), 12 min (installation 1 s, build 2 min 22 s). Le retrait de Tailwind n'a fait bouger ni une page ni une story.
  • Vague 6 lancée : STY9 (attribut de mode unique, next-themes retiré, compteur et style-law.grit en erreur, version plan unique version-plan-style.md qui absorbe les trois plans de la branche, bundles, pnpm verify), STY10.2 (documentation finale, AGENTS.md, CONTRIBUTING.md, NOTICE.md, pages de thème dont mantine.mdx), TST5 (dernières stories, compteur de stories en erreur, couverture, page et skill « testing », découpage de storybook.spec.ts).
  • Après la vague 6 : porte finale (e2e complet deux fois, captures des nouvelles stories et des pages dont le contenu change, galerie avec l'utilisateur, avant/après des thèmes warm-editorial et saas-product), puis audit-design.md.

STY10.2 — Documentation finale — 30/10

Aucune commande git, pas de pnpm install, pas de conteneur.

  • AGENTS.md (jamais CLAUDE.md, lien vérifié) : « UI » ne dit plus shadcn/ui (composants Mantine enrobés de leurs modules dans @recit/site/ui, Mantine seule bibliothèque, mode par data-mantine-color-scheme et light-dark()) ; « Styling » décrit l'état final (plus « in progress ») : la loi, les feuilles globales permises, les modules hors packages/site (Lightning CSS, ./styles.css, esbuild de la CLI), le compteur et style-law.grit en erreur (STY9) ; nouvelle section « Tests » (story et play par composant, axe en erreur, pnpm test:stories, check-stories.js, page documentation/reference/testing et skill testing de TST5).
  • CONTRIBUTING.md : pre-commit relu contre lefthook.yml (les deux compteurs, sans « baseline » : STY9 a retiré celle du style) ; §4 gagne « Style » (renvoi au skill), « Stories » (la phrase de TST5) et « Inner loop ».
  • NOTICE.md : la ligne packages/site/ dit que l'interface a été réécrite sur Mantine le 30/10 et que le code shadcn/ui, Radix et Tailwind du fork est parti ; ligne Mantine 9 (MIT) dans les dépendances notables ; la ligne du reset (Tailwind 4.3.3, MIT, STY5c) inchangée.
  • Docs : theme/custom-css.mdx (description et l. 25 : feuille simple chargée après le thème et les modules, ni utilitaires ni @apply), theme/overview.mdx (carte « Custom CSS »), sites/components/pages/custom-components.mdx (module CSS à côté du composant). Nouvelle page theme/mantine.mdx (« Advanced » de la barre du thème) : où vont les styles d'un site, couches, variables Mantine, modules (rem(), mixins sur un site, CSS simple dans un deck), mode de couleur, « Migrating a site from Tailwind utilities » (ce qui ne marche plus, quoi écrire, avant/après en MDX et en @apply, table courte). Seuls changements d'oxfmt sur cette page : l'alignement des deux tableaux et un retour à la ligne (copie formatée en scratchpad, comparée, puis reprise).
  • Barre latérale : theme/mantine, et documentation/reference/testing pour TST5. La page de TST5 n'existe pas encore : tant qu'elle manque, docs:build échoue (« File not found for document 'documentation/reference/testing' ») ; build et e2e ci-dessous passés sans cette ligne, remise ensuite.
  • 03-packaging.md : @recit/theme (reset.css, fonts.css, tailwind.css et site-compat.css retirés, plus de peer), ./styles.css de components, site-stories, la CLI sans Tailwind, site-kit porte les composants des sites, paragraphe « Feuilles de style publiées », catalogue (@mantine/* 9.7.4, plus de tailwindcss).
  • Pas d'agent style-reviewer : tout ce qu'il vérifierait sur un diff est déjà mécanique et en erreur (les sept signaux de check-style-rule.js dont les props de style Mantine, style-law.grit et les imports de l'ancienne pile dans Biome, la liste « Before reporting » du skill) ; le reste, les différences de la galerie, est la revue humaine de la porte. Un agent dupliquerait la loi dans un troisième texte à tenir à jour.

Vérifié : oxfmt --check sur mes fichiers ; check-admonitions, check-frontmatter, check-site-links, check:imports verts ; check-style-rule.js à 0. pnpm check échoue sur des fichiers en cours d'autres lots (oxfmt : stories de packages/site, CodeBlock.module.css, html-template.ts, check-style-rule.spec.ts ; @recit/components dist/styles.css périmé). nx run-many -t build -p docs components vert (sans la ligne testing, après deux échecs dus à des builds concurrents de @recit/components) ; BROWSER=none pnpm test:e2e:host e2e/site-docs.spec.ts : 51 passés. /theme/mantine n'est pas dans la liste de pages de site-docs.spec.ts : à ajouter à la porte (nouvelle capture).

STY9 — Bascule finale — 30/10

Fait : STY9.1, STY9.2, STY9.4 et la mesure de STY9.3 ; aucune commande git, un pnpm install (retrait de next-themes), pas de conteneur.

Un seul attribut de mode (STY9.1). data-mantine-color-scheme sur <html> est la seule condition de mode ; plus rien n'écrit ni ne lit la classe dark.

  • Pont retiré de SiteRoot.tsx (ColorSchemeBridge et la recopie du script de tête) ; la copie de l'ancienne clé theme vers recit-color-scheme reste (donnée, pas dépendance). next-themes quitte packages/site/package.json et le lockfile ; Biome et le compteur refusent toujours son import.
  • @recit/core exporte COLOR_SCHEME_ATTRIBUTE et DARK_SCHEME (theme-css.ts, aussi par @recit/core/browser) ; le bloc sombre de buildThemeCss s'écrit [data-mantine-color-scheme="dark"] (et … <scope> en portée).
  • Feuilles : tokens.css, deck.css (Shiki, badges), defaultTheme.css, base.css, le DARK_SELECTOR de plugin-theme.ts (le .dark, disparaît du sélecteur double), les modules CodeBlock, EmbeddedCodeBlock (site), Grid, Mermaid (components), Metric de deck-showcase ; page.css et print.css n'en avaient pas. site-stories/scripts/build-css.mjs préfixe le bloc sombre des jetons en [data-mantine-color-scheme="dark"] .recit-wrapper.
  • Deck : useThemeMode pose l'attribut (light ou dark) au lieu de la classe ; useSlideEnhancements et Mermaid lisent l'attribut et l'observent (attributeFilter) ; le script de démarrage de recit build (html-template.ts) pose l'attribut avant la première peinture ; export-pdf.ts ne fait que semer recit-theme, inchangé. Inkeep lisait déjà l'attribut (commentaire). Storybook : withThemeByDataAttribute seul.
  • e2e : colorScheme() ne lit plus que l'attribut (la classe ne compte plus) ; deck-features lit l'attribut après t.
  • Aucun customCss, sites/*/styles.css ni page de doc du dépôt n'écrivait .dark. Rupture pour un auteur qui l'écrivait : dans le version plan.
  • Défaut ancien relevé, laissé : la passe .recit-wrapper de site-stories préfixe aussi les cinq règles sombres de deck.css (.recit-wrapper [data-mantine-color-scheme=dark] .slide .shiki, badges) : elles ne s'appliquent jamais, l'attribut étant sur <html> (c'était déjà vrai de .recit-wrapper .dark …). Le Shiki des sites passe par CodeBlock.module.css ; les badges d'un deck intégré restent aux couleurs claires en sombre. Corriger (réécrire ces sélecteurs en [attr] .recit-wrapper …) bougerait des captures : à décider à la porte.

Identité (dist d'avant, celle de la porte de STY5c, copiée en scratchpad ; après, reconstruite ; Playwright compare élément par élément la boîte et toutes les propriétés calculées, ::before et ::after, 1440 px, sombre et clair) : docs (/, …/writing/code-math-diagrams, /formats/deck/layouts, /theme/named-themes, /present/starter) 3,8 millions de valeurs, www (/, /one-source, /stories/one-source) 1,29 million, presentation (/, /demos/showcase, /demos/tech-talk, /present/showcase et /present/tech-talk) 3,56 millions plus les 13 diapositives de /present/showcase?slide=N en sombre 0,96 million, deck-starter seul (3 diapositives, deux modes) 77 000 : 0 différence. Sur <html>, seule la classe dark disparaît ; le deck seul porte maintenant data-mantine-color-scheme. Mermaid rendu dark et Shiki sur --shiki-dark dans le deck intégré, comme avant ; t bascule l'attribut dans le deck seul.

Compteur et Biome en erreur (STY9.2). check-style-rule.js n'a plus qu'un mode, l'erreur : baseline (scripts/style-rule-baseline.json), --baseline, --write-baseline, compare et mergeBaseline supprimés ; --strict accepté (c'est ce qu'écrit check:other). Le signal 6 compte aussi un sélecteur .dark (html.dark, :global(.dark), .dark:is()), pour que la classe ne revienne pas. isGlobalCss exempte sites/storybook/.storybook/preview.css avec sa raison (feuille de base de l'atelier : elle assemble la feuille du deck et peint le fond de l'aperçu aux jetons du deck ; sites/storybook/styles.css ne lit que ceux du site). Compte : 0. style-law.grit passe en error ; biome.jsonc sort de son includes les tests et les stories, que le compteur saute aussi (les six occurrences restantes étaient des className de données : language-ts, marqueurs de test). biome lint : 0 erreur, 13 avertissements préexistants (spécificité, gabarit). check-style-rule.spec.ts réécrit (62 tests).

Version plan (STY9.4). .nx/version-plans/version-plan-style.md, prerelease pour core, theme, components, deck, motion-blocks, motion-render, site-stories, cli : la ligne de §10.2, les deux ruptures (attribut de mode ; plus de Tailwind dans un deck, modules CSS de l'auteur compilés par recit build), ./styles.css, puis par paquet : premières valeurs du deck dans la CSS du thème et invalid-scene-background, trois clés d'interface, Shiki sans limite (core) ; SlideStage sans maxScale, stylo, c, dialogue, titres, contrastes (deck) ; jetons de warm-editorial et saas-product, deck.css (theme) ; Mermaid, Grid (components) ; tons et buildTheme (motion-blocks) ; interlignes non pliés. Absorbe et supprime version-plan-line-height.md, version-plan-shiki-time-limit.md et version-plan-tailwind-removed.md. @recit/site-kit est privé ("private": true, hors de release.projects) : absent. check-version-plans.js : 16 plans valides.

Bundles (STY9.3, gzip), comparés à STY0.6 et STY5c :

sites/docsSTY0.6 avantSTY0.6 aprèsaprès STY5caprès STY9
CSS d'entrée43,5 ko79,0 ko66,1 ko66,1 ko
CSS d'entrée, brut247 ko496 ko416 ko416 ko
JS d'entrée139,6 ko147,7 ko140,7 ko141,1 ko
JS total (chunks)——3 466 ko3 476 ko

Le JS de docs bouge avec son contenu (page et entrée de sidebar de TST5 et STY10.2) ; à contenu égal, le pont parti fait −0,4 ko brut : www JS d'entrée 121,2 → 121,1 ko (400 656 → 400 268 o brut), presentation 138,2 → 138,1 ko. CSS d'entrée +275 o brut (le sélecteur d'attribut est plus long que .dark), gzip identique à 0,01 ko près (docs 66,1, www 66,3, presentation 65,9). deck-starter : main.css 7,9 ko, main.js 89,8 ko, inchangés. Le CSS reste au-dessus de STY0.6 avant (+22,6 ko) : la feuille Mantine entière ; son import par composant reste une piste, pas faite.

Vérifications.

Code
$ node scripts/check-style-rule.js --strict → 0, « The style rule holds » $ pnpm check:other, pnpm check:imports, biome lint → verts ; oxfmt --check : seul sites/docs/pages/documentation/reference/testing.mdx (TST5, en cours) $ pnpm exec nx run-many -t build typecheck → 60 tâches vertes $ pnpm test → 2 254 passés, 6 échecs sous la charge des agents concurrents (délais de pipeline, compile-story, cli stories, mcp ; Presentation/Embedded et PagefindSearch dans le navigateur), tous verts relancés seuls (43 + 15 tests) $ pnpm test:stories → 146 fichiers, 521 tests $ BROWSER=none pnpm test:e2e:host e2e/site-behaviour.spec.ts e2e/site-docs.spec.ts e2e/deck-features.spec.ts → 64 passés, 1 sauté

pnpm verify d'une traite échoue donc sur l'oxfmt de la page de TST5 ; ses trois étapes suivantes sont vertes séparément. À la porte : aucune capture ne devrait bouger du fait de STY9 (identité mesurée) ; AGENTS.md (STY10.2) doit dire que l'attribut est la seule condition de mode et que .dark est refusé par le compteur.

TST5 — Clôture des tests — 30/10

Dernier lot du plan 21 (§8, TST5). Aucune commande git, pas de pnpm install, pas de dépendance, pas de conteneur.

5.1 — Stories. Typography (Default : toute la prose, titres 1 à 4, listes imbriquées, citation, tableau, code, kbd, filet ; Paragraphs), Heading (Default : l'ancre atteinte au clavier, son href, jamais cliquée ; WithoutAnchor ; Levels : six niveaux et leurs ancres), util/MdxComponents (Site/Components/MdxComponents : une page MDX élément par élément, h1 à h6 avec leurs ancres, listes, tableau défilant, citation, bloc de code titré avec sa copie, callout ; Links : le lien de page et le lien interne passent par le routeur, LocationSpy le prouve, le lien externe ouvre un onglet). Puis les 18 restants du compteur, tous dans site/components : AiAssistantMenuItems (menu ouvert, window.open espionné ; personnalisé ; false), AnchorLink (premier clic navigue, second fait défiler sans naviguer), Bootstrap (entrée client sur routeur mémoire ; entrée statique rendue en HTML par renderToString dans un loader, comme le prérendu), ClientOnly, LandingPage (hero, split, grid), Layout, Main (barre latérale, pleine largeur, mobile : « Menu » ouvre le tiroir, Échap le ferme), Meta et PluginHeads (balises de tête lues par getElementsByName), SiteRoot (contexte, composants MDX, mode de l'atelier), StatusPage (500, 404, message, code inconnu), et dans context/ : RouterEventsEmitter (l'évènement location écrit dans un output), SiteProvider (plugin lent, use(initialize)), SlotProvider (slots de la config, source ajoutée), ViewportAnchorContext (storybook:intersect). SiteReactContext.tsx → .ts : un contexte, sans JSX, pas un composant. 21 fichiers, 33 stories, toutes avec un play. Aucune classe ni style.

  • withSite : parameters.site.harness: false laisse une story hors du harnais (un routeur ne se rend pas dans un autre : Bootstrap). main.ts pré-groupe react-dom/server et @unhead/react/server (découverts en cours de passage, ils faisaient recharger Vite).
  • Compteur en erreur. scripts/check-stories.js n'a plus qu'un mode : tout signal échoue, la liste des manques s'affiche ; baseline, --baseline, --write-baseline, compare, mergeBaseline supprimés, scripts/stories-baseline.json supprimé, --strict accepté (c'est ce qu'écrit check:other). Spec : 11 tests. Compte : 0 (142 composants).

5.2 — Cliquet de couverture. Les seuils sont une option racine de Vitest : le vitest.config.ts d'un projet ne peut pas en poser. Ils sont donc dans le vitest.config.ts racine, un glob par paquet (lignes, branches) plus le total, au niveau mesuré arrondi à l'unité inférieure, autoUpdate: (n) => Math.floor(n) (un passage complet qui mesure plus les remonte, sans décimales ; vérifié : un passage au même niveau réécrit le fichier à l'identique).

Paquet (lignes / branches)TST0, 29/10TST5, 30/10Seuil
core89,8 / 73,990,59 / 74,9090 / 74
motion86,6 / 73,387,61 / 73,4687 / 73
scripts87,1 / 81,784,51 / 80,0484 / 80
app73,1 / 57,073,05 / 56,9673 / 56
site-stories68,6 / 51,069,56 / 51,4769 / 51
site66,4 / 54,778,81 / 70,6878 / 70
site-kit66,7 / 37,793,58 / 80,4993 / 80
components63,1 / 35,373,85 / 46,2373 / 46
motion-blocks59,0 / 33,165,74 / 40,1865 / 40
mcp54,8 / 41,854,81 / 41,8254 / 41
cli48,7 / 38,348,82 / 38,3248 / 38
deck27,2 / 20,252,34 / 39,3352 / 39
motion-render20,6 / 6,920,59 / 6,9020 / 6
theme—100 / 83,33100 / 83
recit—100 / 100100 / 100
total66,8 / 54,372,78 / 60,3772 / 60

Les stories ont fait le gain : site +12 / +16, site-kit +27 / +43, deck +25 / +19, components +11 / +11. scripts baisse : les deux compteurs ont perdu leur code de baseline, et le test « holds the law » de check-style-rule.spec.ts (STY9) expire à 5 s sous la couverture.

5.3 — Doc et skill. sites/docs/pages/documentation/reference/testing.mdx (frontmatter des voisines, icône flask-conical) : la pyramide en tableau, les tests unitaires, les stories par surface, écrire une story, écrire un play, les lancer, l'e2e (hôte et conteneur, la table des six specs Storybook), lire la galerie. Seuls changements d'oxfmt (que fmt:check exige) : retours à la ligne et alignement des tableaux, copie formatée en scratchpad puis reprise. .claude/skills/testing/SKILL.md : quel niveau, la recette d'une story et d'un play, les moqueries (presse-papiers, IntersectionObserver et storybook:intersect, EventSource, horloge clockAt, window.open/fetch, LocationSpy), les pièges (production sans act(), globals.viewport, href="#…", parameters.layout ignoré par Vitest, dépendance découverte en cours de passage, racine vide, état de module), les interdits de §5.2, la liste de contrôle. Le skill e2e-baselines connaît le projet storybook.

5.4 — Découpage de storybook.spec.ts et durées. La spec unique devient e2e/storybook.ts (le code, storybookSuite(surface), la table STORYBOOK_SURFACES par préfixe de titre) et six specs : storybook-site-ui (Site/UI, 237 tests), storybook-site (Site/Components, Navigation, Plugins, 331), storybook-slides (117), storybook-deck (127), storybook-motion (169), storybook-kit (Kit/, Sites/, 69). Chaque spec vérifie aussi qu'aucune story n'échappe à une surface. Projet Playwright storybook (testMatch des six specs, ignorées par chromium) dont le snapshotPathTemplate fixe le dossier __screenshots__/storybook.spec.ts/ : aucune référence ne change de nom ni de place. scripts/e2e-projects.js mappe les six specs sur recit-storybook ; test:e2e:host passe --project=chromium --project=storybook. L'assertion de mode lit data-mantine-color-scheme (STY9) au lieu de la classe. Une porte ciblée : pnpm test:e2e:update:linux e2e/storybook-site.spec.ts.

Code
$ pnpm test → 313 fichiers, 2 263 tests, 108 s (112 s mur) ; TST0 : 176 / 1 758, 69 s $ pnpm test:stories → 148 fichiers, 521 tests, 70,6 s (74 s mur) ; TST0 : 10 / 17, 13 s $ pnpm -F recit-storybook build → 25 s $ pnpm test:coverage → 167 s mur, seuils tenus $ BROWSER=none pnpm test:e2e:host <les six specs storybook> --workers=3 → 1 049 passés, 7,0 min (hôte chargé par d'autres lots) $ … e2e/storybook-kit.spec.ts e2e/storybook-site-ui.spec.ts → 306 passés, 3,4 min

Vérifié. pnpm check vert (compteur de stories et compteur de style en erreur, 0 chacun) ; pnpm -F @recit/site typecheck, pnpm -F recit-storybook typecheck, tsgo de e2e/ muets ; pnpm exec vitest run scripts/__tests__/check-stories.spec.ts scripts/__tests__/e2e-linux.spec.ts verts ; pnpm exec nx run docs:build vert, testing.html produit. Échecs vus, hors lot, tous sous la charge de la machine (plusieurs lots en parallèle, charge 10 à 15) : délais dépassés de compile-story.spec.ts (Shiki double thème, 20 s), check-style-rule.spec.ts « holds the law » (5 s, sous la couverture), mcp/test/markdown.test.ts (5 s), et Deck/Presentation/Embedded (axe color-contrast sur le h1, 1,07 puis 1,32:1, une fois sur trois passages : une transition de couleur saisie en cours, à regarder avec useThemeMode de STY9).

À la porte. 33 stories nouvelles, toutes sous Site/Components : 66 captures à écrire par pnpm test:e2e:update:linux e2e/storybook-site.spec.ts. Aucune autre référence ne bouge.

FINALFIX — Badges sombres intégrés, contraste de Presentation/Embedded — 30/10

Deux défauts avant la porte finale du plan 19. Aucune commande git, pas de pnpm install, pas de conteneur.

Défaut 1 — règles sombres de deck.css dans un deck intégré. La passe qui préfixe la feuille de @recit/site-stories par .recit-wrapper écrivait .recit-wrapper [data-mantine-color-scheme="dark"] … : l'attribut est sur <html>, au-dessus du conteneur, donc les cinq règles sombres de deck.css (badges success, warning, danger, couleurs et fond Shiki d'un bloc de code dans une diapositive) ne s'appliquaient jamais (déjà vrai avec .dark). La passe sort de build-css.mjs dans scripts/scope-to-wrapper.js (avec sa déclaration .d.ts) : un sélecteur qui commence par l'attribut de mode devient [data-mantine-color-scheme="dark"] .recit-wrapper …. Test : src/__tests__/scope-to-wrapper.spec.ts (5 tests). Mesuré dans la feuille construite : badge success rgb(4, 120, 87) en clair, rgb(52, 211, 153) en sombre (il restait vert foncé). Ligne ajoutée au version plan version-plan-style.md.

Rendus sombres qui changent (sites qui importent @recit/site-stories/styles.css) : tout deck intégré qui montre un badge success/warning/danger ou un bloc de code. Captures probables : components-badge-and-kbd, components-present-badge-and-kbd, components-code, components-present-code ; presentation-home et presentation-present-hero (badge success du héros, bloc de code) ; presentation-demos-showcase, presentation-present-showcase, docs-present-showcase ; presentation-demos-mdx, presentation-present-mdx, presentation-present-mdx-blocks, docs-present-mdx ; tech-talk, course, lorem-ipsum et site-embed là où leur diapositive capturée porte du code. Les decks construits par recit build (deck-*) ne bougent pas.

Défaut 2 — Deck/Presentation/Embedded. Pas une affaire de mode : site-stories et la story passent manageTheme={false}, useThemeMode n'écrit donc pas l'attribut de l'hôte. Reproduit 1 fois sur 3 machine au repos ; la cible d'axe est le h1 « Sed do eiusmod » de la diapositive 2, couleur du titre à quelques pour cent d'opacité sur le fond (#f0f2f5 sur #f8fafc) : axe passe après le play, qui finissait dès que le groupe « 2 / 5 » existait, au milieu du fondu d'entrée (0,28 s). Correction dans la story : settled() attend que la diapositive partie ait quitté le DOM et que la nouvelle soit à l'opacité 1, dans Embedded et à la fin de FirstSlide (même exposition après Home).

Code
$ pnpm test:stories packages/formats/deck/src/deck/Presentation.stories.tsx → 6 passages verts de suite $ pnpm test:stories packages/formats/deck → 3 × 63 tests verts $ BROWSER=none pnpm test:e2e:host e2e/storybook-deck.spec.ts --grep Presentation → 3 × 24 passés $ pnpm exec nx run-many -t build -p @recit/site-stories @recit/deck docs presentation → vert $ BROWSER=none pnpm test:e2e:host e2e/site-presentation.spec.ts e2e/site-docs.spec.ts e2e/deck-features.spec.ts → 87 passés $ pnpm check → vert ; typecheck de site-stories, deck, recit-storybook muets

À la porte : pnpm test:e2e:update:linux e2e/site-components.spec.ts e2e/site-presentation.spec.ts e2e/site-docs.spec.ts e2e/site-embed.spec.ts écrira les captures sombres listées plus haut.

Porte finale du plan 19 — 30/10

  • Premier passage complet : 1 399 passés, 318 échecs. Deux causes : le projet chromium-dark prenait aussi storybook-site*.spec.ts (motif site-.*) et rejouait 236 stories dans son propre dossier de captures — testIgnore: STORYBOOK_SPECS ajouté ; et /index du site multilingue rechargé en boucle, seulement dans la suite complète (la spec seule passe, dans le conteneur comme sur l'hôte), disparu une fois le doublon retiré (surcharge).
  • Références écrites pour les changements voulus : 66 stories Site/Components (TST5), les pages deploy-vercel, reference/testing, theme/mantine ajoutées à site-docs.spec.ts, et le texte changé de theme/custom-css, custom-components, plans /19-style. Les badges sombres des decks intégrés (FINALFIX) restent sous le seuil.
  • Deux passages complets verts consécutifs : 1 480 passés, 0 échec, 10,6 min chacun (installation 2 s, build 2 min 20 s). pnpm check vert. 1 659 références au total.
  • Avant/après des thèmes warm-editorial et saas-product montré à l'utilisateur (accent, texte atténué et chiffres un cran plus sombres, barres inchangées) ; décision en attente.
  • Le plan 19 est terminé. Restent à l'utilisateur : les captures darwin (STY9.3) et le commit. Suite : audit-design.md.

Audit de design — 30/10

Demande de l'utilisateur pour la fin du plan 19 : audit-design.md à la racine, un regard de designer sur tout le rendu, avec des captures. Aucune commande git, pas de pnpm install, aucun code ni aucune référence modifiés, pas de conteneur : l'audit lit les 1 659 références existantes.

  • audit-design.md : méthode et gravités, fondations (jetons, typographie, mode sombre, rayons, grille), surfaces (navigation, prose, recherche, decks, motion, mobile, cohérence des sept sites), composants par groupe de stories, états, fiabilité des captures, synthèse (forces, dix actions, jetons et composants à unifier), galerie, inventaire. 44 constats numérotés D-01 à D-44.
  • audit-design/ : 86 références copiées par surface (sites/<site>/, decks/, motion/, components/<groupe>/) et 24 recadrages de détail (details/), 9,3 Mo.
  • audit-design/inventory.mjs : réécrit le bloc « Inventaire » (chaque capture en lien vers e2e/__screenshots__/, groupée par page et variante, et la couverture calculée contre sites/*/pages, sites/*/stories et storybook-static/index.json), puis lance oxfmt.

Constat bloquant (D-01) : 199 références datent du 27/10, d'avant la migration (96 pages claires de sites, tout deck-render, tout motion-dev) ; le seuil de 2 % les garde valides. On y voit encore l'ancien nom du framework sur docs-home clair et une police de repli dans toutes les vues /present claires. app-bundle-home-linux est une capture pleine page recousue (en-tête et page répétés). À régénérer en entier avant la prochaine revue visuelle.

Dix actions de l'audit : références fiables ; une source de jetons (les triplets HSL de tokens.css et les oklch de defaultTheme.css portent les mêmes noms, hsla(var(--border)) est invalide sur le site) ; une couleur de marque ; une sans et une mono (Geist sur les sites, Inter ailleurs) et seulement des polices embarquées (Georgia des exemples) ; une teinte de surfaces sombres ; focus à 3:1 (halo de ui/Button à 1,55:1) et croix des dialogues à vérifier ; défauts de composants (CompareBars, .slide-kbd/.slide-badge étirés en colonne flex, actions de LandingPage sans style de bouton, FormatSupport hors prose, saut de Banner) ; typographie de slide ; mobile ; navigation entre sites et Mermaid aux couleurs de Récit.

Trous de couverture : mobile sur docs seulement (4 captures) ; plans 20, 21 et journal ; story named-theme de docs ; aucun survol de page ; darwin d'avant la migration (STY9.3).

Code
$ node audit-design/inventory.mjs → 1 659 captures inventoriées, 522 stories, 0 sans capture $ pnpm run fmt:check → vert $ pnpm exec biome lint audit-design → vert