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 lancergit commit,git pushnigit 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 | État | Dé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és | formulaires, 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/10 | retouches 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/10 | chargeur 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/10 | audit.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/10 | supprimé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/10 | workflow 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
| Run | Script |
|---|---|
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
Rapatrier P4-V1 et tout revérifier.Fait le 24/10.Intégrer les plans 10 à 15 en une seule passe et appliquer les décisions du §5.Fait le 24/10.P4 V2 à V7.Fait le 25/10 (voirsites/plans/pages/05-video.md). Ouverts : la copie npm deframework@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.MotionThemereste écrit à la main dans@recit/theme(sans zod), gardé par un test de types contreThemeContract. 3b. Après P4 : appliquer les lots deaudit.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).- P5 : alpha (
release.projectsdéjà rempli dansnx.json, publint, size-limit,create-recit). Aucune publication npm. - 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.
| Sujet | Décision |
|---|---|
| Webtoon en PDF | pages 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 report | sans numéros en P6 (Chromium), numérotée en P8 avec Paged.js |
| Lancement de Chromium | sous-export @recit/paged/runner, pas de nouveau package pour l'instant |
| Réglages des formats | une seule convention : à plat, settings = section du format ; tailles en valeurs littérales (A4, 6x9in) |
PDF de stories | non : stories produit des images et des vidéos |
| PDF des pages de doc | optionnel, après P8 |
| PDF balisé | activé par défaut, désactivable ; un alt manquant donne un avertissement |
| Studio d'emaki | non importé tant qu'il est simulé ; le squelette packages/tools/studio reste |
extract d'emaki | fusionné dans @recit/motion |
| Plans 10 à 14 (produit) | tranchées, voir 06 « Questions ouvertes » et le §8 de chaque plan |
Section du site dans recit.config | renommée stories: → publish: ; stories: est réservé aux réglages du format stories |
@recit/paged | outil Node utilisé par la CLI seule ; les formats et fondations n'exposent que des vues imprimables |
Mot kind | réservé au discriminant des documents JSON ; l'annuaire utilise entries, la carte mode |
Manifeste exports.json | types ajoutés : webp, gif, svg, jsonld, fountain, skill ; format facultatif, source block ou tool |
| Plan 15 | accepté (questions 1 à 6), questions 7 à 9 ouvertes |
Règle des extensions .js | appliquée par pnpm check (check:imports), coupée pour sites/** et site-kit (imports d'assets) |
| Release | manuelle 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 brocoli | ré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
| Étape | Ré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:importsrefusaitimport … from "../e2e-linux.mjs": le script est renomméscripts/e2e-linux.js(la racine esttype: module,.mjsn'apportait rien) et ses références suivent.scripts/check-admonitions.mjsgarde 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 :
-
Le conteneur ne démarrait pas. La copie du dépôt n'a pas de
.git(exclu dutar), or le scriptpreparede la racine estlefthook install, qui en exige un :exit status 128. Puis, une foisgit initajouté, git refusait un dépôt appartenant à un autre utilisateur (« dubious ownership »), le conteneur tournant en root.scripts/e2e-linux.shdéclare maintenantsafe.directoryet initialise un dépôt vide, avantpnpm install. -
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(chownd'aprèsstat -c '%u:%g' /src). -
Un test instable, et c'en était un vrai.
/documentation/reference/architectureprenait 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èsdocument.fonts.ready.e2e/support.tsa unmermaidReady()sur le modèle defilmsReady(), et les specs listent les pages concernées (DIAGRAM_PAGESdanssite-docs, le casmermaiddanssite-components). Les deux formes du DOM sont couvertes :pre.mermaid svg(pipeline Markdown) et le SVG du composant MDX, reconnu par sonaria-roledescription. -
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èsdocument.fonts.ready.heightStable()attend que la hauteur ne bouge plus avant de capturer. -
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 dedecksReady()é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 :
| Point | Ce que disait l'audit | Ce que la mesure montre |
|---|---|---|
BP-12 clobberPrefix | relever le préfixe et adapter slideAnchor | le 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-10 | release.projects à corriger | déjà corrigé |
| MCP | lignes 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.mddonnaitnpm create recit(le package est unexport {};vide, le nom n'est pas réservé) etrecit 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: falsesur l'accueil de la doc : clé inexistante, le bouton restait affiché. Plus important que la correction :scripts/check-frontmatter.mjsrefuse désormais toute clé de front matter qu'aucun code ne lit, branché danspnpm check(vérifié : il attrape bien la faute) ;@recit/extractfigurait dans la page Architecture comme package planifié — il n'existe pas ;UPSTREAM.mdaurait 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 quenx releasel'écrit. Passé en « Ours », et la commandecheckout --ourscomplé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
- 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. - Lot 12 : polices embarquées et goldens en conteneur, qui débloque
goldens:check. - Relecture par l'utilisateur des traits
inferredde Broc (sites/plans/pages/annexes/broc/). - Mise en œuvre des couleurs par format (
01§1). - 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.
e2e/support.tsréimportaitserveStaticdepuispackages/cli/dist/commands/export-pdf.js, un chemin interne que les types publiés ne déclarent pas (tsdown ne sort dans le.d.tsque ce qui est atteignable depuis l'entrée). La suite marchait parce que le JS, lui, l'exporte.serveStaticest maintenant exporté par l'entrée publique de@recit/cli, ete2epasse par elle comme pourbuildProject,exportPdfet le reste.e2e/site-i18n.spec.tsdestructurait unstring[][]:hrefétaitstring | undefined, donc l'appeltoHaveAttribute(name, href)choisissait silencieusement la surchargetoHaveAttribute(name, options)— l'attribut n'était pas comparé à la valeur attendue. Le tableau est passé enas 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èse | Mesure |
|---|---|
| Régression des lots 6 ou 7 | aucun code de rendu n'y change ; la passe du lot 6 était à 166/166 |
examples/*/dist présent pendant le build du site | reconstruit avec et sans : 6/6 vert dans les deux cas |
| Build non déterministe | deux builds comparés fichier à fichier : seuls diffèrent les horodatages dehydratedAt |
| Ordre de build changé par les dépendances déclarées | nx run presentation:build puis 3/3 vert |
| Charge de la machine | confirmé, 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 :
| Avant | Après |
|---|---|
DeckSchema, Deck, Scene | FilmSchema, Film, FilmScene |
parseDeck, parseDeckSource | parseFilm, parseFilmSource |
readDeckFile, writeDeckFile | readFilmFile, writeFilmFile |
storyToDeck, deckToMarkdown | storyToFilm, filmToMarkdown |
DeckFormat | SourceSyntax (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/motion | recit://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
upstreamacceptait lespush— vers le dépôt d'origine. Une faute de frappe suffisait à y pousser Récit.UPSTREAM.mdfait maintenant posergit remote set-url --push upstream DISABLEDdè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.mdfaisait même commiter à l'étape 7. La règle est dansAGENTS.md, et l'étape 7 est marquée HUMAN ONLY. - SK-7 : le skill et
UPSTREAM.mdlanç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.dependsOnalors que le diff porte surbuild:ci.dependsOn, et la ciblecopy:docscopiait../../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.mds'annonçait « point de reprise » alors quecontext.mdetAGENTS.mddésignentsites/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.mdlistait 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.mdexige une ligne par chemin, et il n'y en avait aucune poursites/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 quesites/docshé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 decontext.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.devpointaient 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-kitl'exporte,Hero.tsxl'importe. - TY-19 :
Format.idétait unstringlibre dans le manifeste partagé. C'est unStoryFormatde@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 :
shdans le dépôt,bashsur les sites.bashpartout.
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 listerelease.projectsdenx.json— pas une seconde liste — et vérifie les onze packages publiés. Tous passent aujourd'hui.pnpm sizemesure deux budgets qui comptent pour un lecteur : le runtime de@recit/deck(20,6 kB brotli, limite 26) et leentry.clientd'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 :
goldens/goldens:checkdemandaient 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.- Il appelait encore
readDeckFile,renderDeckStillsetparsed.deck— le vocabulaire d'avant le lot 4. - 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 :
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.
StoryStage: la scène (lePresentationet les deux rendus de diapositive) part dans un module à elle, chargé parReact.lazy.StoryViewerne garde de@recit/deckque des types.SlideMotion: un seul fournisseur<LazyMotion features={…} strict>pour tout ce qui rend une diapositive, et les composants passent demotion.*àm.*. Les 37 ko de features arrivent après le premier rendu.strictfait échouer unmotion.*oublié : la règle deCLAUDE.mdest tenue par le runtime, plus par la relecture.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, donctitleTemplatede 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+Kdu bouton de recherche (4,39:1 mesuré, 4,5:1 demandé). Il vient du chrome du framework de sites, pas du balisage Récit. Leheaderest exclu du scan, avec la mesure écrite dans le test : désactiver la règlecolor-contrastaurait 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
/indexen 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=2au 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 rendundefined— donc la version stricte est retenue, et la divergence disparaît. Le garde rendRecord<PropertyKey, unknown>et nonRecord<string, unknown>:define.tslit une clé symbole sur le résultat, et un garde plus étroit l'aurait forcé à caster.formatDuration: cinq copies demm:ss, plus une enm:ssdans 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 rendaient02:7.34sur une durée calculée depuis un nombre de mots, et une durée négative sortait telle quelle. L'horloge du film passe de0:07à00:07— assumé pour la cohérence ; aucune capture e2e ne la montre.slugify: deux versions divergentes. « Récit » donnaitrecitd'un côté (Le framework de sites) etr-citde 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/componentset ses deux compagnes dansPresentation.tsx, alors que seul le deck les lit. Les trois sont dansdeck/transitions.ts, avecvariantsForet 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
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 :
remarkLinkRewriten'épargnait quehttpetmailto:; 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.- Le
.mdpublié est la source, qu'aucun plugin remark ne voit : les liens seraient partis morts.build.markdownLinksles réécrit, vers l'URL publiée même en dev — un.mdcopié se lit loin de son site, comme les guides MCP. processMarkdownFilereconstruisait 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 :
| Mesure | Valeur |
|---|---|
| Requêtes de document en 25 s | 2996 |
| Navigations de la frame principale | 2995, toutes vers /index |
| Statut des réponses | 200, redirectedFrom = - |
DOMContentLoaded / load | 0 / 0 |
| Requêtes en erreur | aucune |
pushState, replaceState, assign, reload | aucun 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 :
- 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. - 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.tsparce 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'evaluatedans uncatch. 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 :
aspectRatioest 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:oumotion:au front matter » : mesuré sur quatre histoires jetables.wordsPerSecond: 4à plat et sousmotion:donnent tous deux 3,00 s contre 5,47 s sans ;aspectRatio: "4:3"à plat et sousdeck: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 :
AppConfigSchemaetAppConfig(packages/core/src/config/validate.ts),app: z.optional(…)dansRecitConfigSchema; c'est une section Récit :splitRecitConfigla range sousRECIT_CONFIG, elle n'entre pas dansSITE_CONFIG_KEYS, et le JSON Schema publiépackages/core/schema/v1/config.jsona é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): AppConfigResolutionavecAppSite = { dir: string; packageVersion?: string }etAppConfigResolution = { 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) etDEFAULT_ANDROID_MIN_SDK(24).configest lerecit.configchargé, tel quedefineConfigle rend ou le module brut : les options du site (canonicalUrlOrigin,basePath,cdnUrl,search,site.title,metadata.favicon) sont lues sur l'objet, la sectionappsur 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-missingetapp-identifier-invalid(erreurs,path: /app/identifier, la règle Android dans le message),app-base-pathetapp-cdn-url(erreurs),app-version-missingetapp-search-remote(avertissements). Unrecit.configinvalide revient aussi en diagnostics (config-invalid, un par problème). Une résolution qui porte une erreur ne s'utilise pas :identifiery 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: sectionapp: { 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 sectionapp:champ par champ avec les défauts, les huit codes de diagnostic, les commandesrecit app build|dev|preview|android|ios|icon, les prérequis desktop par système, ce qui marche hors ligne, les limites, l'updater),app-androidetapp-ios(prérequis de §3 du plan, commandes, admonition « Work in progress » qu'APP7 et APP8 remplacent). 03-packaging.md:appdans la table des groupes (§2), l'arborescence (§2), une ligne@recit/appsous@recit/pageddans la table des packages (§6),appdans la lignescope:tool(§8).06-roadmap.md: une ligne en P5 qui renvoie au plan 18.NOTICE.mdintact.
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.htmlproduits ;tsgo --noEmitdansexamples/site-embedvert.- biome et oxfmt verts sur mes fichiers (
oxfmtlancé sur eux seuls : les pages desites/*/pagessont formatées parpnpm check,.oxfmtrc.jsonn'exclut queexamples/**/*.md(x)etsites/*/stories/**).
Pièges rencontrés :
- Pendant APP1,
packages/tools/app/package.jsondemande@tauri-apps/cliau catalogue avant quepnpm-workspace.yamlne l'ait : toute commandepnpmdu dépôt échoue (ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC, pnpm relanceinstallavant chaque script). Les vérifications ci-dessus sont passées par les binaires denode_modules/.binet, pour nx,npm_config_verify_deps_before_run=false. À relancer enpnpm checketpnpm exec nx run docs:buildune fois APP1 installé. - Le typecheck de l'exemple refusait
appdansRecitSiteConfigtant quepackages/core/distdatait d'avant APP0 : unnx run docs:buildreconstruit le cœur et le typecheck passe. Ce n'est pas un trou du contrat. oxfmt --checkdu dépôt signale aussi18-mobile-desktop.md,journal.mdet.nx/version-plans/version-plan-app.md(pas à moi) ; biome signalepackages/tools/app/shell/src/window-open.js,src/shell.tset la coquille matérialisée dansexamples/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? }): letauri.conf.jsoncomplet, objet JSON pur, testé par instantanés (desktop, dev, fenêtre et origines, Android, iOS, lien profond et updater). La fenêtremainest déclarée avecvisible: falseetcreate: false: la coquille la construit elle-même danssetuppour lui attacher le script d'initialisation et la garde de navigation (Builder::on_navigationn'existe pas en Tauri 2.12, seulWebviewWindowBuilder::on_navigation(Fn(&Url) -> bool)).app.allowedOriginsvoyage dansplugins.recit.allowedOrigins, quelib.rslit à l'exécution avecbuild.devUrl.plugins.deep-linkn'écrit quedesktop.schemes(sur mobile le plugin exige unhost), et aucun de ces deux plugins n'est encore dansCargo.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.lockune seule fois, un.gitignore(target/,gen/,icons/),tauri.conf.json, puis les icônes. Source de l'icône :app.icon(un chemin/xest cherché danspublic/puis dans le build, sinon relatif au site) ; sans fichier,assets/default-icon.svgdu paquet et un avertissementapp-icon-default.tauri iconne tourne que siicons/icon.pngmanque ou est plus vieux que la source ; injectable (icon) dans les tests. Rend{ shellDir, configFile, iconSource, diagnostics }.serveLikeTauri(distDir, { port?, host? })etresolveLikeTauri(distDir, path):a/b,a/b.html,a/b/index.html, puisindex.htmlen 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 sansindex.html.runTauri(shellDir, args, { env?, stdio? })lancetauri.jsde@tauri-apps/cli(2.12.4, épinglé au catalogue) avecnode, dans la coquille ;tauriEnv()traduitRECIT_APP_TARGET_DIRenCARGO_TARGET_DIRsauf si ce dernier est déjà posé.isPortInUse(port)sonde en TCP.- Version plan
.nx/version-plans/version-plan-app.md(prereleasepour core, cli, app),@recit/appdansrelease.projects,.recit/dans le.gitignoreracine.
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:importsetpnpm check:otherverts (46 projets, une arêtetool → foundationde plus, autorisée).pnpm checks'arrête suroxfmt --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 pourexamples/site-embed(vraie passetauri iconsur l'icône par défaut) compile encargo build --release --manifest-path examples/site-embed/.recit/app/Cargo.tomlen 24 s sans avertissement, binaire de 25 Mo qui embarque ledist/, et tourne 12 s sousxvfb-runsans erreur (seul un avertissement AT-SPI). APP5 part de là.
Pièges :
tauri::generate_context!()exigeicons/icon.pnget l'existence defrontendDistà la compilation :materializeShellgarantit les icônes, etrecit app builddoit construire le site avant toute commande Cargo (y compriscargo check).frontendDistse résout depuis le dossier dutauri.conf.json:../../distdepuis.recit/app/.- Deux fichiers hors du périmètre d'APP1 attendent une ligne :
scripts/publint-all.mjs(la table des dossiers, sans quoipnpm publintrefuse@recit/app) etscripts/__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 duwebServer),e2e/ports.ts(APP_BUNDLE_PORT = 4379, hors deE2E_PORTS),playwright.config.ts(une entréewebServerde plus),e2e/package.json(@recit/app). - Ce que chaque test prouve : la page d'accueil vient d'
index.htmlen 200 avec ses assets ; un clic vers/stories/lorem-ipsumest 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-partreçoitindex.htmlen 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-boundariesvert (scope:testdépend de tout), biome, oxfmt ettsgodu paquetrecit-e2everts. - Non fait :
pnpm test:e2e:linux e2e/site-app-bundle.spec.ts. L'imagev1.64.1-nobleet 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. Ledist/de l'exemple date du 28/10 (avant APP0) et n'a pas été reconstruit ici : le bundle ne dépend pas de la sectionapp:. - 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 detauri://localhostet c'est même mieux pour le repliindex.htmlsur un chemin profond, et unbasePathle casserait (APP0 le refuse). (2)index.htmlest 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.htmlexiste 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.jsle charge en binaire et le décompresse lui-même, le type MIMEapplication/wasmn'entre pas en jeu. (4) Un clic avant l'hydratation est une navigation complète : le test ouvre d'abord la recherche (openSearchré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.configchargé (le module tel quel, options du site comprises),versiondupackage.jsondu site,resolveAppConfig, diagnostics imprimés comme ceux derecit buildet arrêt sur erreur ;recit-site buildlancé dans le dossier du site (sonnode_modules/.bin, remonté jusqu'à la racine) avant toute commande Cargo ;materializeShell;tauri build,tauri android buildoutauri ios build.dev [dir] [--port] [--target]: port durecit.config(port), sinon 3000 ;isPortInUseréutilise un serveur qui répond, sinonrecit-site dev --portest lancé et attendu (60 s, arrêt si le processus meurt avant d'écouter) ; coquille avecdevUrl;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 detaurirenvoie à la page de doc de la plateforme.icon [source] [--dir]:icons/icon.pngoublié puismaterializeShell, qui régénère.
Mesuré :
Code
É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 15app-store-connect,release-testing,debugging; les anciensapp-store,ad-hoc,developmenttraduits ; défautdebugging),iosDevelopmentTeam(app.ios.developmentTeampuisAPPLE_DEVELOPMENT_TEAM),iosEnv(poseAPPLE_DEVELOPMENT_TEAMpourtauri ios init|build),iosSigningetcanSignIos(certificat, profil, clé API lus dans l'environnement),iosBuildArgs(--export-methodtoujours explicite,--build-numberoptionnel),IOS_PROJECT_DIR,IOS_BUILD_DIR. Specios.spec.ts, 8 tests..github/workflows/app-ios.yaml: manuel,macos-15, Rust avecaarch64-apple-iosetaarch64-apple-ios-sim,rust-cache, versions Xcode au journal, build du site, CLI Tauri de@recit/appsur lePATH(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*_iOSrelevé parxcodebuild -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 parxcrun altool(APPLE_API_ISSUER,APPLE_API_KEY,APPLE_API_KEY_P8), trousseau supprimé en fin de job. Mêmes actions et mêmes SHA queapp.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
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 dansindex.ts) :androidKeystoreFromEnv(RECIT_ANDROID_KEYSTORE,_KEYSTORE_PASSWORD,_KEY_ALIAS,_KEY_PASSWORDfacultatif),writeKeystoreProperties(gen/android/keystore.properties, mode 600),wireReleaseSigning(un blocandroid {}ajouté en fin deapp/build.gradle.kts, marquérecit:signing,import java.util.Propertiesgaranti 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 ; erreursapp-android-keystore-incomplete,-missing,app-android-not-initialized),androidArtifacts..github/workflows/app-android.yaml(manuel,ubuntu-24.04,setup-javav6.1.0 etsetup-androidv4.0.4 épinglés par SHA, cibles Rust,rust-cache, keystore en secret base64,.apk+.aaben artefacts) : specrelease-workflow.spec.tsverte, 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 :
Bash
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 :
recit app android buildn'a pas--skip-site-build:allowUnknownOptionl'envoie àtauri android build. Le site se reconstruit à chaque fois (25 s) ; le workflow ne passe pas l'option.- Tauri refuse
version: 0.0.0sur Android (« must be at least 0.0.1 ») et c'est la version dupackage.jsonde l'exemple : overlay-- --config '{"version":"0.1.0"}'en attendant unapp.versiondans l'exemple ou un diagnostic d'erreur pour la cible Android. - Le projet Gradle généré rappelle la CLI par
pnpm tauri android android-studio-scriptdepuis la coquille, qui n'a pas denode_modules: « Command "tauri" not found ».pnpm execretombe sur le PATH, d'où le.binde@recit/appdans le PATH. Sansnpm_execpath(la CLI lancée hors pnpm), Tauri écritnode+ un argument nutauridansBuildTask.kt, qui ne peut pas marcher : garder le flux pnpm. - Dans un script Kotlin de Gradle,
java.util.Propertiesse litproject.java…: d'où l'import. packages/theme/dista été reconstruit par un autre lot au milieu d'un build du site, qui a échoué une fois (UNLOADABLE_DEPENDENCY) ; relancé.- Le build client du site avertit :
node:pathexternalisé, importé parpackages/core/dist/config/app.js(APP0) ;@recit/core/configentre dans le bundle navigateur. tauri android initrelancé régénèreBuildTask.ktetbuild.gradle.ktsmais gardekeystore.propertieset 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 profildev).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 caisserecit-app. - APP5.2 — Tailles : binaire nu 25 140 568 o ;
.deb14 082 630 o (13,4 Mio, paquetlorem-ipsum-docs, binaire/usr/bin/recit-app) ;.rpm14 088 028 o ;.AppImage109 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. Ledist/embarqué est le bon :stringsdu 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(devUrlvers une page locale) et un fauxxdg-openen tête dePATHqui journalise ses arguments : la page déclenchewindow.open("https://example.com/popup"), un clic sur<a target="_blank">etlocation.href = "https://example.com/external". Le journal reçoit exactement ces trois URL, la page reste en place (elle répond encorestill-hereavec son titre 2 s plus tard), et la coquille traceon_navigation https://example.com/externalrefusé. 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êmetarget: une seule ligneCompiling(recit-app), 36 s, 42 s avec.debet.rpm; Tauri n'est pas recompilé..debdocs : 14 776 116 o. recit app dev(question d'APP2) : fonctionne de bout en bout sous Xvfb :recit-site devsur 3000 réutilisé ou lancé,tauri dev, fenêtre « Lorem Ipsum Docs » après 113 s (profildebug, dépendances déjà en cache).dist/n'est pas nécessaire : la coquille de test s'est compilée endebugavecdevUrldans un dossier sansdist/.
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 sousdbus-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-runexigexauth;x11-utils(xwininfo),xdotooletgdbinstallés pour sonder. productNamenon ASCII : le.rpmde « Récit Docs » échoue (invalid name "récit-docs") et le.debproduit est invalide pourdpkg(champPackageavec un caractère interdit). À traiter danstauriConfig()/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_DIRpartagé : le bundler AppImage videbundle/appimage/à chaque build, le build de deck d'APP10 a effacé l'AppImage desite-embed(reconstruite ensuite) ;.debet.rpmcoexistent.recit app builddevrait copier ses paquets sous<site>/.recit/app/bundle/et imprimer ce chemin.recit app buildsur les sites Récit (docs,plans,www…) échoue au chargement durecit.config: le loader esbuild de la CLI ne connaît pasimport.meta.globde@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 parmaterializeShelldirectement. À régler danspackages/cli/src/config/load-config.ts.pnpm check:fmt:check,check:imports,check:otherverts ;biome lintéchoue surexamples/site-embed/.recit/app/gen/android/build/reports/**(projet Android d'APP7), que biome ne lit pas dans.gitignore: ajouter!**/.recit/**auxincludesdebiome.json.- Fichiers :
packages/tools/app/src/__tests__/shell-rust.spec.ts(4 tests qui verrouillent le gabarit : caisse Tauri 2 mobile, fenêtre construite danssetupet montrée auFinished,on_navigation+window-open.js, capability sansremote) ;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:pathdans@recit/core/config:packages/core/src/config/app.tsprenait le nom du dossier du site parpath.basename, et le module atteint le bundle navigateur par@recit/core/config(avertissement « nodeexternalized » au build des sites). Remplacé par une découpe de chaîne ( directoryName), tous séparateurs et barre finale compris ; aucun autre fichier depackages/core/src/confign'importe un module Node. Test ajouté ; le build desite-embedsans 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) etbinaryNameFromIdentifier(dev.recit.docs→recit-docs,com.example.loremipsum→example-loremipsum). DanstauriConfig()(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'avertissementapp-product-name-ascii(chemin/app/productName). Les six instantanés detauri-config.spec.tssont 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: lenode_modules/.binqui porte le shimtauri(tauriBinDir, cherché en remontant depuis le paquet) passe en tête duPATH, etnpm_execpathest posé depuis lepnpmou lenpmtrouvé sur lePATHquand il manque (packageManagerPath) : le projet Android généré rappelle la CLI parpnpm tauri …depuis la coquille sansnode_modules. Tests avec de faux répertoires.examples/site-embed/recit.config.ts:app.version: "0.1.0"(Android refuse0.0.0)..github/workflows/app.yaml:dbusdans la ligne apt, test de fumée sousxvfb-run -a dbus-run-session --(25 s d'attente D-Bus sinon), binaire trouvé dans.cargo-target/releasepuisqu'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), ligneapp-product-name-ascii, installateurs copiés dans.recit/app/bundle/(APP11a) avecRECIT_APP_TARGET_DIR, test de fumée sans écran, tailles mesurées (.deb13,4 Mio,.AppImage105 Mio,.apk21,7 Mo,.aab15,2 Mo).
Vérification :
Code
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 esbuildimportMetaGlobPlugin, qui remplaceimport.meta.glob()(motif ou tableau de motifs littéraux, optionseager,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_DIRest vidé par le bundler AppImage) et imprimés avec leur taille ; Android :androidArtifacts; iOS :.ipaet.xcarchivedegen/apple/build.- Android :
prepareAndroidSigningavanttauri android build, diagnostics imprimés, arrêt sur erreur ;--skip-site-buildest une option derecit app android|ios(il filait vers Tauri). - iOS :
iosBuildArgs(--export-method, défautdebugging,--build-number), options deapp buildet deapp ios build, un--export-methodaprès--gagne ;iosEnv(APPLE_DEVELOPMENT_TEAM) sur init, dev et build. - Version
0.0.0refusée pour android et ios (app-version-required), avertissement seul sur desktop. Description deapp 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-buildmobile, options CLI), un danscli.spec.ts(glob eager, lazy,import,query,/…).
Mesures :
pnpm exec vitest run packages/cli: 5 fichiers, 105 tests verts ;pnpm -F @recit/cli typechecketnx run @recit/cli:buildverts.examples/site-embed,recit app build --skip-site-buildavecRECIT_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), binaireexample-loremipsum0.1.0.sites/docs, même commande : la config se charge, maisapp-identifier-missing: aucun site Récit ne posecanonicalUrlOrigin(createSiteConfigne le fait pas), il fautidentifier: "dev.recit.docs"dans la sectionapp:desites/docs/recit.config.ts, hors de mes fichiers.pnpm check: rouge suroxfmtpourpackages/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 detauri-plugin-process, le redémarrage estAppHandle::restart()de Tauri.Cargo.lockmis à jour par le build.src/lib.rs: le plugin updater n'est enregistré que sitauri.conf.jsonaplugins.updater(contains_key("updater")sur le contexte) ; au démarrage,check()puis une fenêtre de proposition servie par un schémarecit-update://de la coquille (HTML danslib.rs, pas de plugin dialog : aucune dépendance de plus) avec « Install and restart » (ou Entrée) et « Later » (ou Échap) ; commandesinstall_update(download_and_install, puisapp.restart()) etskip_update. Commandeopen_window(url, label, width, height): une page du site demandée parwindow.openouvre une seconde fenêtre native avec le même script, la même gardeon_navigationet le même affichage àFinished(site_window()sert aux deux) ; un label déjà ouvert navigue et prend le focus ;WindowEvent::Destroyedémetrecit:window-closed.src/window-open.js: même origine quelocation→open_windowet un proxy{ closed, close() }(plugin:window|close,closedsuivi par l'événement) ; autre origine → opener.capabilities/default.json:core:window:allow-closeen plus. Les commandes updater ne passent jamais par le webview, donc niupdater:defaultni 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.jsonservi en local ; l'app 0.1.0 journaliseupdate 0.1.1 available, ouvre la fenêtre « Update » (440×200), Entrée →installing update 0.1.1,update downloaded(GET de l'AppImage partauri-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 entauri devsur 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,BroadcastChannelpasse dans les deux sens entre les deux webviews WebKitGTK (second got hello,first got world),popup.close()ferme la fenêtre etclosedpasse àtrue;window.openexterne ettarget=_blankexterne vont àxdg-open. cargo checkdeexamples/site-embed/.recit/app(cache partagé) vert ;vitest packages/tools/app scripts/__tests__/release-workflow.spec.ts: 9 fichiers, 69 tests ;pnpm checkvert.
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: truequandapp.updaterest renseigné (sans lui,tauri buildn'émet aucun.sig; patché à la main dans les coquilles de test).resolveAppConfig: erreur si un endpoint n'est pashttps(le plugin refuse l'app au démarrage :PluginInitialization) ; les tests locaux passent pardangerousInsecureTransportProtocol.- Dans
examples/deck-pitch, le raccourcisouvre 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 lessite-*.spec.tsavec le choix mémoriséthemede next-themes etrecit-themedes decks posés àdarkparstorageState, pluscolorScheme: "dark"; références-dark-linux.png) etmobile(390 × 844, tactile, tests@mobiledesite-docs.spec.tsseulement ;-mobile-linux.png). Le helperdarkMode(page)dee2e/support.tsfait la même chose pour un test isolé ;SITE_THEME_STORAGE_KEYy est exporté pour STY2. site-plans.spec.ts: les 28 pages du site en pleine page (tallPageScreenshotcoupe à 16 000 px les pages plus hautes,support.ts) ;/journalexclu, il change à chaque lot sans raison visuelle.site-docs.spec.ts: les trois pagesapp*, la 404 (/404, la page prérendue :recit-site previewré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=1de showcase).storybook.spec.ts(nouveau) : les 16 stories deindex.jsonen clair et en sombre pariframe.html?id=…&globals=theme:…, servies depuissites/storybook/storybook-staticsur le port 4380 pare2e/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
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
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/coreet@mantine/hooks9.7.4,postcss-preset-mantine1.18.0,postcss-simple-vars7.0.1 : catalogue depnpm-workspace.yaml, dépendances depackages/site(le Vite de chaque site les exécute).packages/site/src/vite/plugin.ts: pluginrecit-style,css.postcss(preset Mantine et simple-vars avec les cinq$mantine-breakpoint-*) etcss.modules.generateScopedNameexporté :Spinner__rootdans le framework et les sites,recit-<paquet>-<fichier>__<classe>souspackages/*/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.cssimportés en tête deentry.client.tsxetentry.server.tsx, avantvirtual:recit-config: la feuille desite-storiesimportée par lerecit.configd'un site précèdemain.tsxdans le bundle et fixait l'ordre des couches, et une déclaration dansmain.cssne survit pas à Tailwind. Construit :@layer mantine{à l'octet 0, puisproperties,theme,base,components,utilities. SiteRoot.tsx:MantineProvider theme={recitTheme} defaultColorScheme="light"sous leThemeProviderde next-themes ; le script de Mantine (ColorSchemeScriptappelé comme fonction, son__html) posé paruseHead, donc<script data-mantine-script>dans le<head>prérendu et en dev.lib/theme/theme.ts(recitTheme = createTheme(…)) : polices survar(--font-sans)etvar(--font-mono),lineHeights.md: "1.5"(piège ci-dessous) ;lib/theme/index.ts.Spinner.module.css, premier module (animate-spinreporté à 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 checkvert (compteur de STY0d : 1217, aucune hausse).- Build de
docs:Spinner__rootdans le CSS construit (chunk partagéAlert-*.css, lié en tête deindex.html) et dans le JS ; absent du HTML prérendu parce queLayoutne rend leSpinnerque dans uneSuspensefallback : legrepdu plan surindex.htmln'est pas probant, celui surdist/assets/*.cssl'est. - Dev (
recit-site dev --port 4521,BROWSER=none) : script Mantine dans le<head>,virtual:ssr-css.css?directcommence par la déclaration de couches puis@layer mantine {et contientSpinner__root:css/collect.tsetcss/plugin.tsvoient les modules sans changement. - e2e en conteneur,
e2e/site-docs.spec.ts: 106/106 (chromiumetchromium-dark), galerie vide.
Mesures (STY0.6, sites/docs, gzip).
| Quoi | Avant | Après |
|---|---|---|
| CSS d'entrée | 43,5 ko (247 ko brut) | 79,0 ko (496 ko brut) |
| JS d'entrée | 139,6 ko (513 ko brut) | 147,7 ko (540 ko brut) |
| JS total | 3 285 ko | 3 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
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
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
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/docsen preview, projetschromiumetchromium-dark; le tiroir enmobile(@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 surhtml: classedark,data-mantine-color-scheme,data-themeoucolor-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 dansmain: 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é danssites/,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=Nqui suit ;?slide=2ouvre à la deuxième ;souvre la console présentateur (context.waitForEvent("page"), URLmode=presenter), « Slide N / 3 » suit l'auditoire et « Next → » le pilote ;?print=1rend autant de.print-slideque de diapositives.e2e/support.ts, ajouts en fin de fichier :colorScheme,storedSiteTheme(cléSITE_COLOR_SCHEME_STORAGE_KEYde STY2, repli sur la clé héritéetheme),themeSwitch,toc,activeTocEntry,slideAt(legroup« N / total » du motif carrousel),slideTotal. Les specs ne parlent que rôles, noms, URL,localStorageet presse-papiers, aucune capture ; la seule classe (text-primary, voir ci-dessous) vit dans le helper, jamais dans une spec.playwright.config.ts:testMatchdu projetmobileélargi àsite-(docs|behaviour)— une ligne hors de ma liste de fichiers, sans quoi un test@mobilene tourne dans aucun projet.
Trouvé, pour le plan 19 (rien corrigé ici).
- Sommaire : chaque entrée porte
aria-current="page"(leNavLinkdu routeur, le chemin correspond), l'attribut ne dit donc pas la section ; seule la classetext-primaryla marque. Et à 1280 × 720 l'observateur (ViewportAnchorContext,rootMargin−80 % en bas, seuil 0,75) regarde les 144 px du haut : un titre amené à sonscroll-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 (scrollIntoViewpuisscrollBy) ; à 900 px de haut le marquage suit, d'où letest.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. - Thème : STY2 a remplacé la clé
themeparrecit-color-schemependant le lot ; la constanteSITE_COLOR_SCHEME_STORAGE_KEYest arrivée danssupport.tsau même moment que la mienne (doublon résolu en gardant la sienne). LedarkStorageStatede la config gardetheme, queSiteRoot.tsxcopie une fois : le projetchromium-darkouvre toujours en sombre. - Protocole :
pgrep -f e2e-linuxse 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]'oudocker psfiltré sur l'image Playwright ne se trompent pas. Un passage local pendant qu'un autre lot reconstruit un paquet échoue au démarrage deswebServer(@recit/theme/dist/index.jsabsent), 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
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 publiquetheme:(polices sur--font-sans/--font-mono,headings,lineHeights.md: "1.5"gardé de STY0a,defaultRadius: "md", paletterecitde dix nuances depuistheme.light.primaryquand la couleur se lit :#hex,rgb(),hsl()ou le triplet « H S% L% » ;oklch()et les noms laissent la palette de Mantine).recitCssVariablesResolverpointe chaque variable de Mantine vers une variable du site (--mantine-color-body→--background,text→--foreground,dimmed/placeholder→--muted-foreground,anchoretprimary-color-filled→--primary,default-border→--border,error→--destructive, les rayonsxs…xlsur 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-generatornon pris. Instantané du thème desites/docs.- Mode de couleur : Mantine est la source de vérité.
localStorageColorSchemeManagersur la clérecit-color-scheme,defaultColorSchemedepuissite.colorMode(light,dark,system;system=autoquand rien n'est dit) ;next-themesretiré deSiteRoot(le paquet reste installé jusqu'à STY9, plus rien ne l'importe). Le script posé dans<head>paruseHeadfait trois choses avant le premier rendu : copie une fois le choix mémorisé parnext-themes(clétheme) sous la clé de Mantine, posedata-mantine-color-scheme(script de Mantine), puis reflète l'attribut sur la classe.darkde<html>. À l'exécution,ColorSchemeBridgetient 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/hookset@recit/site/components) garde la forme de celui denext-themes:theme(light/dark/system),resolvedTheme,setTheme. plugin-theme.ts: registre shadcn retiré (shadcn-registry.tssupprimé avec ses tests,GOOGLE_FONTSconservé) ; les variables sombres sont émises sousDARK_SELECTOR=.dark, [data-mantine-color-scheme="dark"].SiteConfig.ts:theme.registryUrlrefusé avec le message «theme.registryUrlis gone with shadcn/ui: settheme.lightandtheme.darkinstead » (superRefine, clé marquée@deprecatedjusqu'à 1.0),site.colorModeajouté.defaultTheme.cssinchangé : les défauts restent zinc (question 5, iso-rendu).ThemeSwitch.tsx+ThemeSwitch.module.css:UnstyledButtonde Mantine,data-modeetdata-active, le module reporte les utilitaires d'avant valeur par valeur (p-0.5, gap-0.5, border, rounded-full, ring-3) ;plugins/search-inkeeplitdata-mantine-color-scheme.- Hors de la liste du lot, additifs :
app/main.tsxpassetheme={config.theme}etcolorMode={config.site?.colorMode}àSiteRoot(seul chemin vers la config côté client) ;Header.test.tsxetMobileTopNavigation.test.tsxenveloppent leur arbre dans<MantineProvider>(leThemeSwitchlit le schéma de Mantine) ;e2e/support.ts:darkMode()pose aussirecit-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
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
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 (balisebutton, pas de composant Mantine : les tests n'ont pas besoin deMantineProvider), contenu ; utilitaires reportés valeur pour valeur (rounded-xl→calc(var(--radius) + 4px),bg-black/4,text-smencalc(1.25 / 0.875),scrollbardemain.cssen classe du module,not-proseparcomposes: not-prose from globaljusqu'à STY5).showLineNumbers→data-line-numbers. ExporteHIGHLIGHT_CODE_BLOCK_CLASS(=classes.code),codeBlockClassesetwithClassName(relais duclassNamed'un importeur, sans littéral dans le JSX).- Contrat Shiki : les règles
.shikidu@layer basedemain.csspassent dansCodeBlock.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) ;!importantconservés (biome-ignore motivés) pour que les couleurs en ligne d'un deck perdent comme avant. HIGHLIGHT_CODE_BLOCK_CLASSretiré deshiki-constants.ts. Écart au plan : ce n'est pasplugin-shiki.tsqui le consomme (il ne lit quedefaultLanguages, inchangé), maisshiki.tsetMdxComponents.tsx.shiki.tsest 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 parHighlightedCode;MdxComponentsl'importe deui/CodeBlock.js(deux lignes).CodeTabssurTabsde Mantine (unstyled,classNamessurlist,tab,tabLabel), cadre et contenu parcodeBlockClasses; onglet actif pardata-active. Pas decomposesd'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 dansCodeTabs-*.css).EmbeddedCodeBlock:Buttondeui/remplacé par une balisebuttonet le variantoutline/icon-xsreporté dans son module (indépendant de la réécriture de STY3a) ;data-full-height,data-show-copy,data-hidden.SyntaxHighlightsans classe, toujours paresseux (MdxComponents).Stepper.module.css: les règles.stepperde@layer utilities, qui restent dansmain.csspour STY5 (le lot ne touche que les règles Shiki).CodeBlock.test.tsxlitclasses.root,classes.header… (un test de plus : classes etdata-line-numbers) ;Stepper.test.tsxinchangé, 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.Rootcomposé (portail, overlay, piège et retour du focus, verrou du défilement de Mantine) ; le bouton de fermeture en pastille est reporté tel quel.onInteractOutsideannulable (clic sur l'overlay),onEscapeKeyDownaussi. Animationfade-in-0 zoom-in-95→ transition MantinezoomFade(exportée), 200 ms, overlay 150 ms.Drawer(vaul) →Drawer.Rootde Mantine,direction→position, contenu placé parinsetselondata-direction(un tiroirleftreste à gauche en RTL), glissement de vaul (500 ms,cubic-bezier(0.32, 0.72, 0, 1)), overlaybg-black/80.Popover→Popoverde Mantine,trapFocuset retour du focus ; le placement dePopoverContentremonte à la racine par le contexte.Tooltip→Tooltipde Mantine : la racine lit ses enfantsTooltipTrigger/TooltipContentet passe le contenu enlabel; ouvre au survol et au focus clavier, flèche de Mantine (TooltipArrowne rend plus rien).DropdownMenu→Menu(rôles, flèches, focus) ;asChildd'un item parrenderRoot(le lien devient l'item, sanstype), cases et radios surMenu.CheckboxItem/RadioItem, sous-menus surMenu.Sub.Collapsible→Collapse(expanded, sans animation comme avant : les keyframes.CollapsibleContentdemain.cssne s'y appliquaient pas),data-stategardé sur le déclencheur et le contenu.NavigationMenu→nav/ul/liet module ; un item à déclencheur ouvre unMenu(click-hover) sous son déclencheur, le viewport partagé de Radix disparaît (NavigationMenuViewportreste, vide) ;navigationMenuTriggerStyle()rendclasses.trigger, et.link.triggerreproduit 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.Rootsur un store à lui, sans raccourci global (shortcut={null}, le site ouvre paropen),clearQueryOnClose={false};CommandInput=Spotlight.Searchavec les rôles de cmdk (combobox,aria-controls,aria-activedescendant) ;CommandItem=Spotlight.Action(option,aria-selected,data-value),asChildparrenderRoot;CommandList= lalistboxdont l'id est donné au store.value/onValueChangede cmdk tenus au pas de l'index de Spotlight ;shouldFilterignoré (pas de filtre) ;CommandEmptymasqué en CSS (.root:has([data-action])) ;content.classNamen'est plus relayé, le module porte la largeur qu'il donnait (100 % jusqu'à 750 px, 36rem dès 40em :sm:max-w-xlgagnait surmax-w-[750px]).- Modules en
@layer components, comme STY3a : une classe Tailwind d'un importeur (coucheutilities) gagne encore, comme parcn().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/labeldu bouton,labeldu badge,root/wrapperd'unTextInput,wrapper/body/messagede l'alerte,iconde l'ActionIcon) sontdisplay: 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, sousutilities. Une classe Tailwind passée par un importeur (className="h-7 text-xs"dePagefindSearch,size-6deNavigationCategory,flex flex-col gap-2deMermaid) gagne donc commetailwind-mergela 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 :displayetpadding-inlinedes tailles deButton(un enfantasChildportaitflexoup-5, que Tailwind émettait avantinline-flexetpx-2.5:Pagination, lien « modifier » deMdxPage), les règles sur les paragraphes et liens deAlertDescription(elles battent.prose, rangé dansutilities), et le contenu deCallout(a,ul,ol). cva→data-*:data-variantetdata-size(posés par laBoxde Mantine depuisvariantetsize),data-fitdeAlert,data-orientationdeButtonGroup/Separator,data-typedeCalloutqui choisit l'une des onze--callout-*(lestyleinline et le--callout-colorcalculé disparaissent).asChild(Button, Badge, ButtonGroupText) :renderRootde Mantine ; pourButton,UnstyledButton, sans les deux<span>duButtonde Mantine, pour quespace-x-1dePaginationvoie 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,--accentsinon ;light-dark()pour chaquedark:(preset Mantine : règle[data-mantine-color-scheme=dark]) ; la palette bleue de l'alerteinfoécrite enlab(), 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,.sectionlu pardata-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
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é auxprojectsracine) :storybookTest, Chromium headless par Playwright,prefers-reduced-motion: reduce,retry: 1, axe en erreur. Le plugin applique lui-même les annotations depreview.tsx(pas desetProjectAnnotations)..storybook/vitest.setup.ts: presse-papiers en mémoire, échec sur toutconsole.error..storybook/vitest.chromium.ts(global setup) : sans Chromium, arrêt avec « Install it once with: pnpm exec playwright install chromium » oupnpm test --project '!storybook'. Fenêtre : 1200 × 900 par défaut,globals.viewportsinon. - Décorateurs (
.storybook/decorators/), chacun décide par le premier segment du titre (surface.ts) :withSlide(déplacé depreview.tsx,withSlide.module.css= l'ancienpreview.module.css),withSite,withMantine(createRecitTheme+recitCssVariablesResolver,forceColorSchemedepuisglobals.theme),withReducedMotion(MotionConfig),withTheme(classe.darketdata-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/testingexporte en plusSiteProvider,SlotProvider,ViewportAnchorProvider,SiteContext(SiteRootgarde unSiteContextpar onglet, deux stories le partageraient).withSitemonte un routeur mémoire surparameters.site.path, unQueryClientneuf, le contexte desites/storybook/src/harness/config.tsx(site Lorem Ipsum, trois liens, une recherche de fixture :ModalMantine « Search ») fusionné avecparameters.site.options,Suspense. PluginrecitVirtualModulesdansmain.ts:virtual:recit-configetvirtual:recit-theme.css; aucun module desrc/libn'importe de module virtuel aujourd'hui, un module manquant échoue avec son nom. Les globs des huit surfaces de §3.1 sont dansmain.ts(quatre vides : un avertissement « No story files found » chacun). - Feuilles : celle du site (Mantine,
defaultTheme.css,main.cssTailwind du framework,sites/storybook/styles.csspour le fond) est chargée par un loader, seulement pour les surfaces Mantine : elle redéfinit--backgroundet les utilitaires depreview.cssdans un autre format.layers.cssdu site est importé en tête depreview.tsx. Enstorybook dev, recharger la page après une storySite/avant de revoir une diapositive. - Story pilote
packages/site/src/lib/components/Header.stories.tsx(Site/Components/Header) :playqui vérifiearia-current="page"sur « Sit amet », ouvre la recherche au clic, trouve ledialog« Search » par rôle, le champ focalisé, ferme par Échap. Vérifié qu'un nom faux échoue. e2e/storybook.spec.tslitstorybook-static/index.json: deux captures par story, attente decurrentRender.phasecompleted/finished(leplayfini), puiswait-mermaid/wait-motionlus dans les tags ; plus d'attente fixe de 700 ms pour toutes.PENDING_TAGSdonne 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) danscheck:other, en avertissement : 142 (story 133, play 1, testid 8) ;--listou un dossier listent les fichiers. Coverage racine :**/*.stories.{ts,tsx}exclus.packages/site/scripts/check-external-deps.tsignore**/*.stories.*comme les tests : le postbuild de@recit/siterefusait l'importstorybookde 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
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 rangeaitcomponentssousmantineet sous labasede Tailwind.recit-style(packages/site/src/vite/plugin.ts) ouvre désormais chaque feuille émise sur@layer mantine, theme, base, components, utilities;. IndexingDialogprendDialogTriggerde@recit/site/ui/Dialog.js(il importait encore Radix).- e2e complet dans le conteneur : 454 passés, 5 échecs expliqués.
plans-homechange par le contenu (plans 18 à 21 dans le tableau et la carte) et/demos/mdxpar le changement voulu de la vague 1 : références régénérées. La pagination desite-behaviourperdait 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 checkvert.ui/util.tsxreste :index.tsetSearch.tsxl'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 sectionsyntaxHighlighting(thèmes etdefaultLanguagespar imports explicites), passé parparameters.site.optionsaux cinq stories de code. CodeBlock.test.tsxréduit àgetCodeBlockLabelet aucodedeMdxComponents(qu'aucune story ne rend) ; les trois tests de rendu (titre, nom de langue, classes) sont dans lesplayet les captures.Stepper.test.tsxreste 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,InputGroupInputperdentaria-invalid: l'Inputde Mantine le pose depuis sa properroret écrase celui de l'auteur ;Input.module.cssn'a pas d'état invalide et la règle:has([aria-invalid="true"])d'InputGroup.module.cssne peut pas s'appliquer. Les trois storiesInvalidsont entags: ["!test"], commentées (STY3a).CodeTabscopie le code de tous les onglets :Tabsde Mantine garde les panneaux cachés montés (keepMounted) dans l'élément copié. StoryCopiesTheVisibleTaben!test(STY3c).- axe
aria-allowed-attr: unSeparatordécoratif (role="none") portearia-orientation(Separator,ButtonGroupavec séparateur). - axe
color-contrast:Alertdestructive (4,36:1) et texte de l'info(4,43:1) ; libellé d'un onglet inactif deCodeTabs(4,42:1) ; l'orange degithub-light, thème clair par défaut (3,48:1,#e36209), dansSyntaxHighlight. - Harnais : une story
Site/plus haute que la fenêtre (900 px) échoue àscrollable-region-focusablesur<body>(leoverflow-y-autodubodydemain.css) ;Callout/Typestient sur une ligne par type pour l'éviter.
Vérifié.
Code
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
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:CodeTabsgardait les panneaux cachés montés (keepMountedde Mantine), si bien que le bouton copier copiait tous les onglets ;keepMounted={false}rend le comportement de Radix.InputetTextareaperdaient learia-invalidde l'auteur, que Mantine recalcule depuiserror:erroren est maintenant dérivé. Les storiesCopiesTheVisibleTabet les troisInvalidsont réactivées (19/19). NavigationMenuLink activereposearia-current="page"comme Radix (trouvé par TST2b).- TST1 :
storybookmanque en devDependency de@recit/components(lesplayn'y utilisent pasexpect) ; à ajouter à la porte de la vague 4, hors de tout agent, puis passer lesplaysurexpect.
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 (champdata-autofocusfocalisé, saisie, Enregistrer ferme) ;Dialognon fermable par clic extérieur (onInteractOutsideannulé, 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 ; positionstop-start,right,left.Tooltip: survol et focus clavier ouvrent,aria-describedby(description accessible du déclencheur), Échap ferme ; positions.DropdownMenu: clic ouvre unmenu, 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) avecaria-checked.Command(Spotlight) : la palette filtrée par l'importeur, comme la recherche du site ; dialogue nommé,comboboxfocalisé etaria-controlsvers lalistbox, deux groupes, flèches etaria-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+Kn'est pas àCommandDialog(shortcut={null}) : il appartient àSearch(TST3).Collapsible:aria-expanded,aria-controlsvers le contenu, clic, Entrée et Espace, désactivé.NavigationMenu:navigationnommé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-childrensur un menu ouvert :Menu.Dropdownde Mantine pose sa sentinelle de focus (<div role="presentation" tabindex="-1" data-autofocus>) dans lemenu(DropdownMenu/Open,CheckboxesAndRadios) ; dansNavigationMenu, lemenucontient de plus des liens au lieu demenuitem(Expanded). - axe
aria-required-children: lalistboxdeCommandsans résultat ne contient que l'état vide (role="presentation"), comme celle de cmdk (NoResults). - axe
color-contrast:CommandShortcutsur l'item sélectionné, 4,39:1 (Filtered), le même couple que le ⌘K deSearchnoté à TST0. NavigationMenuLink activene pose plusaria-current="page"(Radix le faisait) : seulementdata-active. Non affirmé par la story, à rétablir dansNavigationMenu.tsx.- Sans largeur passée par l'importeur, un
Drawerleft/rightcouvre toute la page (comme celui de vaul) : pas de clic dehors possible ; la storyRightferme 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
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
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-proseparcomposes: not-prose from global; règles en@layer components, sous lesutilitiesde Tailwind, pour que le@utility prosedemain.css(.prose a,.prose code, qui ignorentnot-prose) continue de gagner comme avant (STY5 le retire). Points de rupture : lemdde Tailwind est lesmde Mantine (mixin) ;sm(40em) etlg(64em) n'ont pas d'équivalent Mantine, écrits en@media (min-width: …)comme les modules depackages/site. Les deux logos (dark:hidden/dark:block) etdark:invertpassent par@mixin dark. Uncomposesvers 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 enoklch()etlight-dark().StatusBadgereste un<span>et non leBadgede 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.cssetcss-modules.d.ts..valuegardecomposes: metric-value from global, le crochet quedeck-showcase/styles.css(customCss) et la pagedocs/theme/custom-cssmontrent ; la règle.metric-valuevit désormais dans le module.TimelineetMetricrestent dansindex.tsx: en fichiers séparés,check-stories.jsles compte (« sites » 7 → 9) et une story demanderait deux références Storybook nouvelles ; à faire avec TST.sites/components/src/stories.csssupprimé (sa règle est dansMetric.module.css), import retiré desites/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(.typographyattend 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/componentshorsnavigation/,app/main.tsx,Layout.module.css) ; supprimerlib/ui/util.tsxquandindex.tsetSearch.tsxne l'importent plus ; contraste du « ⌘K » deSearch(et du raccourci deCommand/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-kitcomposants,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 :
Galerie à réparer— réparée le 29/10 au soir : Playwright nomme ses pièces jointes<nom>-actual.pnget 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.pngdu 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.- 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).
- Ajouter
"storybook": "catalog:"en devDependency de@recit/components(unpnpm install, hors de tout agent), puis passer lesplayde TST1 surexpectdestorybook/test. pnpm check, e2e completBROWSER=none pnpm test:e2e:linux, revue des captures, puispnpm test:e2e:update:linux e2e/storybook.spec.ts(≈ 110 nouvelles stories de TST1, TST2a-b) et les différences voulues : en-têteCodeBlockdes slides en clair (contraste),Gridà 2 et 4 colonnes (vraiesCard), « ⌘K ».- 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) ;@mixinpermis là où PostCSS Mantine tourne (packages/site,sites,site-kit) et refusé en erreur dans les paquets publiés et les exemples de deck ;!importantpermis dansprint.cssetdeck.css;!**/.recitsans/**.- Plugins GritQL dans
scripts/biome-plugins/:style-law.grit(unclassNamechaîne, gabarit ou concaténation, et toute propstyle, en avertissement jusqu'à STY9, hors couche d'animation) etsites-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 dansbiome.jsonc(www:Hero,FormatGrid,OneSource;docs/StatusBadge;motion/VideoDownloads;components/FormatSupport) : lot à prévoir après STY6.3b pour les déplacer danspackages/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
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/outputd'un site du framework, détecté parVERCEL),vercel.json, déploiement par Git (Root Directory, preset « Other », Ignored Build Step), par la CLI (vercel pull/build/deploy --prebuilt,link --repoen monorepo), domaines, et les sites de Récit. scripts/vercel.js(+scripts/__tests__/vercel.spec.ts, 7 tests) et les scripts racinevercel:sites,vercel:link,vercel:deploy,vercel:deploy:prod: un projetrecit-<site>par site qui a unvercel.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
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
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/etwriting/overview,presentation/templates,vision /,wwwaccueil) dont le décalage naît dans la description desFeatureCarddesite-kitet dans le héros dewww(lot GATE4, et TST4b pour le héros) ;site-i18ncherchait la classesr-onlydisparue : il cherche maintenant le titre par son rôle.e2e/support.ts:activeTocEntryne lit plus quearia-current="location"./custom-components(erreur #418 vue par STY6.3b) n'a pas échoué dans ce passage. storybooken devDependency de@recit/components(unpnpm installhors 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,expectdes 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,Carddes slides,Dialogouvert.- Défaut vu : la
Dialogouverte 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 danslib/ui/Dialog(puis références à régénérer). - 28 échecs de
playdans le Storybook construit (stories de code delib/uivides, presse-papiers refusé,slides-grid--overloaded) : lot SBFIX.
INFRA — e2e plus rapide, hooks git — 30/10
- Ciblé :
scripts/e2e-projects.jsdonne, pour chaque spec dee2e/, les projets nx qu'elle ouvre (plus@recit/cli, importé pare2e/support.ts).pnpm test:e2e:linux <specs>passe la liste au conteneur (E2E_PROJECTS) : il ne construit qu'eux et leurs dépendances, etplaywright.config.tsne démarre que leurswebServer; sur l'hôte, la config lit les specs de sa ligne de commande (pnpm test:e2e:host e2e/site-docs.spec.tsne sert quedocs). Sans spec, ou avec un filtre inconnu de la table : tout, comme la CI (inchangée :pnpm test:e2esans argument). Un test exige une ligne par spec et chaquesiteUrl(…)dans sa ligne. - Chaud : la copie vit dans le volume
recit-e2e-work; chaque passage efface tout sauf lesnode_modules(et leurs.vite/.cache), recopie l'arbre, relancepnpm install --frozen-lockfile(« Already up to date », 2 s). Verrouflock: un second passage s'arrête net. La copie tolère un fichier modifié pendant letar(code 1), et la recopie detest-resultsn'est plus fatale (untest:e2e:hostconcurrent 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, builddocs+ 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) etcheck-stories.js(0,1 s) ; pre-push =pnpm checkpuis typechecknx affected --exclude-task-dependencies(34 à 55 s quand tout est touché) etpnpm test:relatedsur les fichiers poussés (59 s pour les 72 du dernier commit). Base : l'amont, sinonorigin/main, sinon message et succès.PRE_PUSH_BASE=HEAD~1 bash scripts/pre-push.shpour l'essayer ;LEFTHOOK=0,--no-verify,LEFTHOOK_EXCLUDE=related. - À traiter (hors lot) :
sites/storybook/src/harness/pagefindest ignoré par.gitignore(pagefind/) et exclu de la copie : le build derecit-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:Typographyde Mantine,unstyled, avecTypography.module.css;ProseClassesdisparaî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 prosedemain.cssdonnaient :.prosedevient.root,--tw-prose-*devient--prose-*, l'exclusion[class~="not-prose"]reste (les modules qui composentnot-proseen dépendent). Palette du site (currentColor), identique en clair et en sombre, sauf les trois couleurs quetypographylaissait au plugin (ombres dekbd,prebrut), enlight-dark(). Règles en@layer utilities, là où était le plugin : les modules encomponentsperdent contre elles comme avant, ceux hors couche gagnent toujours. La classe globaletypographyreste sur la racine (composes), crochet desstyles.cssdemotionetpresentation.Heading.tsx:Titlede Mantine,unstyled, niveau lu pardata-orderdansHeading.module.css(tailles ducvaen@layer utilities, au-dessus desh1–h4de la prose comme avant ; interlignes en rapport tenu par--heading-lh, sinon le minifieur arronditcalc(2 / 1.5)eth2perd 1/64 px) ; lien d'ancre et icône hors couche, commeAnchor.theme.headingsn'est pas utilisé : avec la prose au-dessus duTitlestylé, il n'aurait rien porté, et le thème est hors lot ;Titleécrit encore ses variables--title-*enstyle(code de Mantine, sans effet).MdxComponents.tsx+MdxComponents.module.css:pexplicite,img/vidéo →.media(rounded-lg),pre→.pre(not-prosecomposé,my-4),codeen ligne sans la classeinline(valeur par défaut decode),cxau lieu decn;nodede react-markdown retiré depetpre.main.css:@plugin "@tailwindcss/typography"et les deux@utilityretirés (le reste est à STY5b, qui a démonté le fichier en parallèle ; la dépendance part avec STY5c).biome.jsonc:Typography.tsxetHeading.tsxsortis de la liste d'exception.- Hors liste, signalé :
site-kitTutorial.module.css/Tutorial.tsxcomposaientprose prose-sm from global; sous la listenot-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 gardaitprose-smdans la feuille disparaissent.MdxPage.test.tsxcherche la colonne parclasses.articleau 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 deoklab(0.556 0 0 / 0.75)(le minifieur calcule lecolor-mixlittéral), et des écarts de 1e-5 en sombre : même couleur ; - hors lot :
Cards__icondesite-kità32pxau lieu de31.9999px(GATE4), la pagination « Deploy on Vercel » (contenu), et un blocdata-recit-runtime-shikiderecit-configdont les jetons portent une autre couleur claire dans le HTML prérendu (--shiki-light:#D73A49sur{ defineConfig }) : surlignage d'exécution, à attribuer.
Vérifications.
Code
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
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
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.tsxsupprimés (tout est dans lesplay) ;Header.test.tsxne garde quereloadDocumentdu logo (une prop deLinksans trace dans le DOM) ;Search.test.tsx(préchargement) etSlot.test.tsx(logique) restent, ce dernier sans ses deuxdata-testid. - Hors liste :
sites/storybook/.storybook/main.ts,resolve.dedupe: ["@unhead/react"]— pnpm installe deux variantes de pair, et leHeaddeHeaderne voyait pas l'UnheadProviderdewithSite(« useHead() was called without provide context ») dès qu'un logo est configuré. scripts/stories-baseline.json:site/components54 + 7 → 18 (restentTypography,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 surinfo3,6:1,tip3,08:1,caution2,76:1,danger3,59:1 (color-contrast).ui/Separatordécoratif gardearia-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.HeaderNavigationouvert : liens dans unmenu(TST2b).Mermaiden erreur :Alertdestructive 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 :
Paginationn'est pas unnav,SpinneretPageProgressn'ont pas deprogressbar(la barre estaria-hidden, voulu),TopNavigationetHeaderNavigationsont desnavigationsans nom ;Anchorne copie pas l'URL (simple lien#). - Harnais : la
ModaldeSearchFixturea un bouton de fermeture sans nom (axe échoue si la story finit dialogue ouvert) ;PageProgressà 50 % n'est pas atteignable sans chargeur de route. DeveloperHintne rend rien horsNODE_ENV=development: rien à voir en test ni en capture.
Vérifié.
Code
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)devient1.42857au 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é pourCardsetNavigationMenu(--cards-leading,--navigation-menu-leading) ; il reste 89line-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
tododes stories : contrastes (Badgedu deck, avertissement deGrid, erreur deMermaid,Alertdestructive et info, onglet inactif deCodeTabs, orange Shiki clair, texte deBanner, citation de la prose 2,95:1), noms manquants (bouton de fermeture deBanner,Modaldu harnais,navigationdeTopNavigationetHeaderNavigation), rôles (Paginationennav,progressbardeSpinner/PageProgress), axearia-required-childrendes menus Mantine et deCommandvide,aria-orientationduSeparatordécoratif, point gris de laDialogouverte. 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/pagefindignoré par.gitignore(pagefind/) : manquera en CI. Réglé par TST3b : le dossier s'appellepagefind-index/.docs:builden échec pendant GATE4 (« Could not resolve @recit/theme » depuiscore/dist) : undisten 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(leSidebarde §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 lenavseul, la fenêtre ne bouge pas :centerInNav),SidebarToggle(clic replie et rend la barreinert, 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 parIntersectionObserverpasse enaria-current="location", une sous-section déplace la marque, une entrée mène à son ancre ; à un niveau). LeTocne 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 (fetchespionné, presse-papiers relu), le menu ouvre le Markdown et « Use in Claude » appellewindow.openespionné ; 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+Kouvre, champ focalisé, Échap ferme ; « lorem » : 3 pages et leurs sections, flèches, Entrée navigue vers/dolor#ut-laboreet ferme ; aucun résultat : message et « Clear search » qui vide et refocalise),ResultList(sections triées par score, clic navigue,maxSubResults,basePathretiré, vide),IndexingDialog(en cours 38 %, terminé, échec puis « Retry » qui se reconnecte,EventSourcerejoué).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é parindex.test.tsxcontre un faux widget).
Harnais (fichiers partagés, ajouts seulement) :
.storybook/intersection.ts, importé parpreview.tsx: l'IntersectionObserverdu navigateur enveloppé ; unplaysignale un élément visible parelement.dispatchEvent(new CustomEvent("storybook:intersect")). Danspreview.tsxet nonvitest.setup.ts: les captures e2e jouent les mêmesplaysur le build.- Index Pagefind de fixture :
src/harness/pagefind-index/pagefind.js(trois pages Lorem Ipsum,init/options/search), servi à/pagefind/pagefind.jsparstaticDirs(build) et résolu par le plugin Vitesrc/harness/pagefindIndex.ts(Vitest enveloppe lesimport()et perd le@vite-ignore: le littéral dePagefindSearché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: rendnull, appelleonNavigate(pathname + hash)à chaque location du routeur mémoire ; c'est le «fnsur 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: axelist(leuldes items contient lesasansli) etaria-allowed-attr(aria-expandedsur la ligne, undivou le lien, sans rôle qui l'admet). Touche aussiNavigation,NavigationFilterInput,MobileNavigationDrawer.NavigationFilterInput: bouton d'effacement sans nom (axebutton-name).NavigationBadge: contrastes, blanc sur vert clair 1,77:1 (palette Tailwind reprise telle quelle).MdxPage,TocPopoverButtonen bouton : nommé « Toggle table of contents », affiche « On this page » (axelabel-content-name-mismatch).IndexingDialogdé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 dePagefindSearchneutralisentEventSource.- Deux navigations
viewTransitionde suite : React Router laisse la promesse rejetée de la première transition sautée (« AbortError: Transition was skipped ») ; la storyStackedretiredocument.startViewTransition. DeveloperHint(brouillon) n'apparaît que siNODE_ENVvautdevelopment: ni le projet Vitest ni le build de Storybook ; non affirmé.- Ni
mainniarticledansMdxPage(c'estLayout) : l'a11y de §4.2 pour cette ligne est à la page entière, en e2e.
Vérifié.
Code
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 basedemain.csspourlib/components/Layout.module.css, sous:global(:root): le<html>, l'en-tête, la barre latérale, le sommaire et les pages les lisent, commeBanner.module.cssqui 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é parmain.tsxjuste aprèsmain.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. Couchebase, pastheme: le preflight de Tailwind (dansbasejusqu'à STY5c) remetborder: 0 solidsur tout élément, et leborder-colordoit passer après ; STY5c le descend dansthemeavec le retrait du preflight (noté dans l'en-tête du fichier).html.dark { --dark }etselect:is(.dark *)lisent maintenant:root[data-mantine-color-scheme="dark"];.dark { color-scheme: dark }reste (pont deSiteRoot, STY9).::selectionlit--primaryet non plus--color-primary(alias du@theme inline). - Retirés, morts :
.bg-primary ::selection(plus aucun élément n'a la classe),.CollapsibleContentet ses keyframes (jamais appliqués, cf.Collapsible.tsx), le@layer utilities .stepper(repris parStepper.module.css, la classe n'est plus rendue). lib/assets/language-icons: les onze icônes à couleur de thème importentfills.module.css(un module pour la famille :.foreground,.foregroundSoftpourfill-foreground/90,.react,.json,.tomlOutline,.tomlLettersenlight-dark()/@mixin dark) ; les autres gardent leurs attributsfill.scripts/check-style-rule.js(hors liste, une ligne) :isGlobalCssexemptepackages/site/src/app/base.css, le CSS global du framework que nomme STY5.1 ; un cas danscheck-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
À 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¬es),
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é parPresentation(onClearAnnotationsabsent). AnnotationCanvassur 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>=contentinfodans legroupde la diapositive (landmark-contentinfo-is-top-level) ; texte du pied 3,53:1 en clair, 1,93:1 sur unbgsombre. Le pied est actif par défaut dans tout récit : la fixture le coupe,WithFooterle montre.PresenterView: compteur 4,34:1, « Speaker notes » enh3sans 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 undialog(ni rôle, niaria-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'uncallouttipdans une diapositive 4,16:1.- Textes en dur non traduits : « Slide » (
PresenterView,DevHud,MobileRemoteView), « PREV »/« NEXT ».MobileRemoteViewn'a pas d'état connecté/déconnecté (storyDisconnected= é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_TODOsur les métas des blocs, deBlock,ThemeProvider,UiSceneView,MotionFrame. - Pas de lecture/pause dans
player.tsx:MotionFramereçoittime, les commandes sont dansMotionPlayerde@recit/site-storieset le client derecit motion dev(hors lot) ; les stories couvrent pause au début, en cours, fin.
Vérifié.
Code
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:rootet.dark) n'a pas de lot : à STY5c avec le reste demain.css.base.cssest en@layer basetant que le preflight de Tailwind existe ; STY5c le passe entheme. Des commentaires de modules (Header…) citent encore « les variables demain.css» : elles sont dansLayout.module.css. - (TST4a) Bug :
AnnotationCanvasdé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 dansuseKeyboardNav,c(effacer) non câblé.RemoteQrCodeModaln'est pas undialog. Contrastes du deck (présentateur, télécommande,DevHud, pied deSlideFrame1,93:1 en sombre,callout tip4,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-blocksen devDependency desites/storybook(etpreview.csspar le paquet plutôt que par chemin) ;storybooket@storybook/react-viteen devDependencies de@recit/decket@recit/motion-blocks, puis retirer lespathsetexcludede contournement et migrerplayer.test.tsxverscomposeStories.
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,rootdu site ; le chargeur de config de la CLI, dossier de la config) ; vérifié par une construction demotionavec un MP4 factice (lien présent) puis sans (message). Nouvelle propfilesfacultative, pour la story.- Les modules passent par le PostCSS Mantine du Vite du framework, comme
Cards.module.css(rem(),@mixin darkcompilés) ; les classes deviennentrecit-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
À 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) :useHighlighterfaituse(highlighterPromise), et la fixture charge dix-sept grammaires en morceaux séparés. Nouveau loaderloadSiteHighlighter(.storybook/decorators/withSite.tsx, inscrit danspreview.tsx) : il attend le surligneur deparameters.site.optionsavant le rendu et marque la promessestatus: "fulfilled", queuse()lit sans suspendre. - Presse-papiers : permissions
clipboard-readetclipboard-writedans la spec (précédent :site-behaviour.spec.ts), plutôt qu'un mock danspreview.tsx, qui casserait la copie pour qui parcourt le Storybook construit ; le mock devitest.setup.tsreste celui du projet Vitest. Slides/Grid/Overloaded: l'avertissement de surcharge deGrid.tsxn'existe qu'hors production. Leplayvérifie l'avertissement en développement, son absence (et les six cartes) en production.- L'avertissement
Language "csv" is not loadedne vient pas du Storybook : ce sont les blocs```csvdesites/plans/pages/annexes/broc/scenario-1.mdet-5.md(le siteplanscharge 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
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 installhors agents :@recit/motion-blocks,@recit/site-kit,recit-example-deck-mdx,recit-example-deck-showcaseen devDependencies desites/storybook;storybooket@storybook/react-viteen devDependencies de@recit/deck,@recit/motion-blocks,@recit/site-kitet des deux exemples.pnpm checkvert : compteur de style 17 (1 217 au départ), stories sansplayou 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 (lestodod'accessibilité, le stylo d'AnnotationCanvas, le point de laDialog), HYDRA (React #418, réindexation Pagefind au montage,viewTransitionabandonné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.cssMinifyn'est pas réglé, il vauttrue, ce qui veut dire Lightning CSS (minifyCSSde 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/decket@recit/site-storiesportaient déjà les valeurs pliées ; l'optimizede Tailwind danssite-stories/scripts/build-css.mjsles recopiait.- Rien ne plie un
var(): ni--x: calc(…), ni le replivar(--y, calc(…)), ni la--text-sm--line-heightde Tailwind. esbuild, lui, ne plie pas cecalc().
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 derecit-styleet du Storybook ;unfoldLineHeightsdansscripts/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
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
- (orchestrateur, suite de HYDRA)
@recit/corecompile-story.ts:tokenizeTimeLimit: 0pour@shikijs/rehype, commepackages/site/src/lib/shiki.ts; version planversion-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 clairlight-dark(red-700, --destructive)(4,36 → 6:1) ; le texte d'infoprend le bleu de l'alerte (inherit) au lieu du gris atténué (4,43:1).ui/CodeTabs: onglet inactifcolor-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" } }dansdefaultHighlightOptions(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 degithub-lightpasse, 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-foregroundest foncé et échouait aussi.Typography: citation en gris opaquelab(48.5 %)(2,95 → 4,73:1).NavigationBadge: texte quasi noir sur les fonds clairs (blanc 1,77:1 sur vert) ; les badgesinvertpassent 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 :
Paginationennav;Spinnerenprogressbarindéterminé nommé (label, « Loading ») ;PageProgressrestearia-hidden, justifié en commentaire (valeur simulée, bruit à chaque lien).NavigationCategory: les items dans undiv(plus deulsansli) ;aria-expandedetaria-controlssur le chevron seul.MobileTopNavigationDrawer: chaque séparateur dans unli aria-hidden(axelist, masqué jusqu'ici par letododuSeparator).ui/Separatordécoratif :renderRootqui retire l'aria-orientationqueDividerécrit toujours (ses props filtrent lesundefined).ui/DropdownMenu:withInitialFocusPlaceholder={false}et notre propre sentinellearia-hidden(celle de Mantine,role="presentation"focalisable, comptait comme enfant dumenu). Le comportement au clavier ne change pas.ui/NavigationMenu: un menu de liens est une divulgation, pas unmenu:withRoles: false(passé par étalement, absent du type deMenu),renderRootsansrole/aria-orientation,aria-expanded/aria-controlsposés par le déclencheur, même sentinelle.ui/Command: la liste n'estlistbox(et le comboboxaria-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 sousaria-invalid, anneau au focus (:where, sous la règle deInputGroupqui retire l'anneau du contrôle) ;InputGroupavait 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, dansDialoget dansCommandDialog(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
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éfaut | Avant | Correction |
|---|---|---|
Pied de SlideFrame sur un bg de scène | 2,4:1 (4,45 sombre) | noir ou blanc selon la luminosité OKLCH du fond (oklch(from …), sous @supports) |
Compteur de PresenterView | 4,34:1 | atténué mêlé à 15 % de premier plan (5,4:1) |
Position de MobileRemoteView | 1,68:1 | primaire mêlée à 50 % de blanc (6,5:1) |
Notes vides de MobileRemoteView | 3,89:1 | slate-400 (7:1) ; bouton « NEXT » en --primary-foreground (2,9:1 en sombre) |
« Session » de RemoteQrCodeModal | 4,46:1 | atténué mêlé (5,5:1) |
Badges de DevHud (deck.css) | 3,76 et 3,18:1 | emerald-700 et amber-700 sous le blanc (5,5 et 5:1) |
slide-badge-* (Badge, HtmlSlide absent) | 2,7 à 3,8:1 | emerald-700, amber-800, red-700 ; 400 en sombre (.dark) |
Texte d'un callout dans une diapositive | 4,0 à 4,2:1 | .slide .slide-callout :is(p, li) atténué mêlé (5:1 et plus) |
Avertissement de Grid | 4,45:1 | amber-800 (6,3:1) |
Erreur de Mermaid | 3,14:1 | red-700 (5,3:1), red-300 en sombre (red-500 y faisait 4,16:1) |
« Available » de FormatSupport | 3,65:1 | emerald-700 en clair |
Tendance de Metric (exemple) | 2,47:1 | emerald-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
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…) :useCurrentNavigationsuspend (useSuspenseQuerydeplugin-navigation) etSiteProviderlitcontext.initializeparuse(). Le loaderloadSiteHighlighterdevientloadSiteHarness(.storybook/decorators/withSite.tsx, inscrit danspreview.tsx) : il construit le client de requêtes et leSiteContextde la story, initialise les plugins, met la navigation de la page en cache (clé["plugin-navigation", path]) et attend le surligneur ;withSiteles lit dansloaded.site. Plus rien ne suspend au premier rendu.- Stories mobiles (
MobileTopNavigation×4,MobileTopNavigationDrawer×2,MobileNavigationDrawer) :globals.viewportredimensionne le cadre du manager, pas la pageiframe.htmlqu'ouvre l'e2e ; à 1280 px le bouton et le tiroir sont masqués (64em). La spec lit le viewport de la story (storyGlobalsetparameters.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(dontKit/Hero/Mobile,FormatGrid,OneSource,MobileRemoteView) sont désormais capturées à 390 × 844. NavigationWrapper/ScrolledToActive: lenavn'avait pas de hauteur,--header-heightétant absent. Les variables de mise en page sont sur:rootdansLayout.module.css, et Vite donne aux modules CSSmoduleSideEffects: false: sansLayoutrendu, la feuille est éliminée du build (unimportd'effet ne suffit pas). Nouveaupackages/site/src/lib/testing/layoutSheet.ts(réexporté par@recit/site/testing), lu parloadSiteHarness. Module à part : lire un export défini danstesting/index.tsxtirait tout son graphe, dont des feuilles@layer componentschargées avant@layer base, ce qui inversait l'ordre des couches (logo deKit/Herodoublé). Effet sur les captures : les storiesSite/ont maintenant les vraies variables de la mise en page.Toc/FollowsTheSection,MdxPage/WithToc: l'évènementstorybook:intersectpartait avant l'observe()(posé dans un effet) ; il est redispatché dans lewaitFor.Slot/Filled: les sources s'inscrivent dans un effet ;waitForsur la disparition du repli.Deck/MdxSlide/FirstStep: les étapes sont masquées par un effet ;waitForsur la seconde.Deck/MobileRemoteView/AfterTenMinutes: lewaitFordefindBy*démarrait sonsetIntervalavant l'effet du minuteur et prenait le bouchon declockAt.clockAt(fixture du deck) n'intercepte plus que la période d'une seconde ; la story règleparameters.clock(un seul bouchon par story, au lieu de deuxbeforeEachempilé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 pasMDXImport["default"](type indexé) etstorybook/docs-toolsfait unconsole.error. Des argTypes explicites n'y peuvent rien (l'extraction tourne quand même) : la propmdxComponentest typéeMDXContent(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
À 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 (
:rootdeLayout.module.css) n'existent dans une page que siLayouty est rendu (Vite élimine un module CSS dont on ne lit aucune classe) : à STY5c, les placer dans une feuille globale (app/base.css) rendraitlayoutSheetinutile.
Porte de la vague 5 — 30/10
pnpm checkvert. 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, etplans /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 : dansMotion/Blocks/CompareBars, 247 kB et 114 kB ont des barres de même longueur.- Les changements de couleur des thèmes
warm-editorialetsaas-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()devite/plugin.ts; dansplugin-theme.ts, les@source(racine, dépendances,__pluginDirs) et le@theme inlinequetransformécrivait dansmain.css(le hook garde sonenforce: "pre": il injectedefaultTheme.cssettheme.customCssavant quevite:csslise la feuille).__pluginDirsreste pour la fusion desvite.configdes plugins et le relevé de leurs versions, documenté dansSiteConfig.ts.main.cssne garde que les deux@importde Geist et les deux marqueurs d'injection ; la déclaration de couches reste danslayers.css(commentaire mis à jour :baseest vide, gardé dans l'ordre). lib/util/cn.tssupprimé ;shiki.tsjoint ses classes par une fonction locale (joinClassNames, sans fusion de classes Tailwind) ; l'exportcnde@recit/siteretiré (paquet privé, aucun importeur).MissingIcon.tsx, dernière chaînetext-red-500, passe parMissingIcon.module.css(le rouge 500 de la palette, en oklch).biome.jsonc:tailwindDirectives, l'override d'exception des derniers importeurs (vidé, donc supprimé), lesignoreplugin/theme/custom-variant/applydenoUnknownAtRules, désormais en erreur partout (le site gardemixin) ; l'exceptionnoImportantStylespasse demain.cssàreset.css..vscode/settings.jsonperd sa clétailwindCSS.- CLI :
bundle/css.tsassemble la feuille d'un deck avec esbuild (déjà dépendance) au lieu de@tailwindcss/nodeet@tailwindcss/oxide:reset.cssen@layer base, jetons,page.css,deck.css,print.css, feuilles des paquets, modules utilisateur (module virtuelrecit:modules.css),customCss, dans le même ordre ; dépendances de la veille par le metafile ;url()d'un média inlinée comme dansjs.ts,url(/…)laissée telle quelle ; minification par esbuild. L'optionsources(scan Tailwind) disparaît deCssBundleOptionset debuild-deck.ts. site-stories/scripts/build-css.mjs: plus de Tailwind (thème, utilitaires,@source, déclaration de couchesproperties/theme…) ; esbuild concatène jetons préfixés,deck.css, feuilles des paquets etmodules.css, la passe.recit-wrapperest inchangée, Lightning CSS (déjà en dev) minifie avec les cibles qu'utilisait l'optimizede Tailwind.@recit/theme:tailwind.csset le peer optionneltailwindcsssupprimés.- Storybook :
@tailwindcss/vitedemain.ts,@import "tailwindcss",tailwind.csset@sourcedepreview.css. - Dépendances :
packages/siteperd 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/clietsite-storiesperdenttailwindcss,@tailwindcss/node,@tailwindcss/oxide;sites/storybookperdtailwindcsset@tailwindcss/vite; le catalogue perd ses quatre lignes Tailwind. Restent, comme prévu :next-themes(STY9, plus aucun importeur) et@tanem/react-nprogress(PageProgress.tsxl'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 detailwindcss/preflight.css4.3.3 (MIT, ligne dansNOTICE.md),--theme(--x, …)écritvar(--x, …), plus les deux familles par défaut que la couchethemede 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 debase.css(layer(theme)), le deck enlayer(base), Storybook enlayer(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.cssen couchetheme, après le reset dans la même couche :* { border-color }gagne toujours sur sonborder: 0 solid. Rien d'autre n'occupebase.- Variables de mise en page : le bloc
:global(:root)deLayout.module.csspasse dansbase.css(:root, couchetheme) ;TopNavigationetBannerles remettent à 0 hors couche,polyfills.tsen ligne, donc ils gagnent toujours.layoutSheet.tset son export de@recit/site/testingsont supprimés ; le harnais de Storybook (src/harness/styles.ts) importe@recit/site/app/base.cssaprèsmain.css, commemain.tsx. Piège noté dans le skill.sites/storybook/styles.css(feuille de l'atelier, surfaces Mantine) rend àhtml/bodyleur flux d'avant (height: auto,overflow: visible) :base.cssfait debodyle conteneur de défilement d'un site, et axe refusaitSite/Plugins/Markdown/ChatGPTLogo(SVG de 1 248 px sans élément focalisable, « scrollable-region-focusable »). defaultTheme.cssreste un fichier à part, deuxième feuille de base du framework (nommée dans la loi : SKILL.md §5, plan §0.3,isGlobalCssdu compteur et son test) :theme.noDefaultThemedoit pouvoir la sauter, ce quebase.cssne permet pas ; et@recit/theme/tokens.cssporte les mêmes noms (--primary…) en triplets HSL lushsl(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
@applydanscustomCss: version planversion-plan-tailwind-removed.md(@recit/theme,@recit/cli,@recit/site-storiesenprerelease). Aucun exemple ni aucune story desites/*/storiesn'écrivait de classe utilitaire (vérifié). Les pages qui le disent encore (sites/docs/pages/theme/custom-css.mdxl. 4 et 25,theme/overview.mdxl. 38,sites/components/pages/custom-components.mdxl. 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/mdxmobile 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 interlignescalc()des modules utilisateur que l'optimizede Tailwind pliait (.lduMetric: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-positioncalculée0px 0px→0% 0%surpre.mermaidet les onglets de code (background: transparentque Lightning CSS réécrivait0 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/docs | STY0.6 avant | STY0.6 après | avant STY5c | après STY5c |
|---|---|---|---|---|
| CSS d'entrée | 43,5 ko | 79,0 ko | 68,4 ko | 66,1 ko |
| CSS d'entrée, brut | 247 ko | 496 ko | 434 ko | 416 ko |
| JS d'entrée | 139,6 ko | 147,7 ko | 141,1 ko | 140,7 ko |
| JS total (somme des chunks) | — | — | 3 475 ko | 3 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
À 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-themesretiré, compteur etstyle-law.griten erreur, version plan uniqueversion-plan-style.mdqui absorbe les trois plans de la branche, bundles,pnpm verify), STY10.2 (documentation finale,AGENTS.md,CONTRIBUTING.md,NOTICE.md, pages de thème dontmantine.mdx), TST5 (dernières stories, compteur de stories en erreur, couverture, page et skill « testing », découpage destorybook.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-editorialetsaas-product), puisaudit-design.md.
STY10.2 — Documentation finale — 30/10
Aucune commande git, pas de pnpm install, pas de conteneur.
AGENTS.md(jamaisCLAUDE.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 pardata-mantine-color-schemeetlight-dark()) ; « Styling » décrit l'état final (plus « in progress ») : la loi, les feuilles globales permises, les modules horspackages/site(Lightning CSS,./styles.css, esbuild de la CLI), le compteur etstyle-law.griten erreur (STY9) ; nouvelle section « Tests » (story etplaypar composant, axe en erreur,pnpm test:stories,check-stories.js, pagedocumentation/reference/testinget skilltestingde TST5).CONTRIBUTING.md: pre-commit relu contrelefthook.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 lignepackages/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 pagetheme/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, etdocumentation/reference/testingpour 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.cssetsite-compat.cssretirés, plus de peer),./styles.cssdecomponents,site-stories, la CLI sans Tailwind,site-kitporte les composants des sites, paragraphe « Feuilles de style publiées », catalogue (@mantine/*9.7.4, plus detailwindcss).- 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 decheck-style-rule.jsdont les props de style Mantine,style-law.gritet 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(ColorSchemeBridgeet la recopie du script de tête) ; la copie de l'ancienne cléthemeversrecit-color-schemereste (donnée, pas dépendance).next-themesquittepackages/site/package.jsonet le lockfile ; Biome et le compteur refusent toujours son import. @recit/coreexporteCOLOR_SCHEME_ATTRIBUTEetDARK_SCHEME(theme-css.ts, aussi par@recit/core/browser) ; le bloc sombre debuildThemeCsss'écrit[data-mantine-color-scheme="dark"](et… <scope>en portée).- Feuilles :
tokens.css,deck.css(Shiki, badges),defaultTheme.css,base.css, leDARK_SELECTORdeplugin-theme.ts(le.dark,disparaît du sélecteur double), les modulesCodeBlock,EmbeddedCodeBlock(site),Grid,Mermaid(components),Metricdedeck-showcase;page.cssetprint.cssn'en avaient pas.site-stories/scripts/build-css.mjspréfixe le bloc sombre des jetons en[data-mantine-color-scheme="dark"] .recit-wrapper. - Deck :
useThemeModepose l'attribut (lightoudark) au lieu de la classe ;useSlideEnhancementsetMermaidlisent l'attribut et l'observent (attributeFilter) ; le script de démarrage derecit build(html-template.ts) pose l'attribut avant la première peinture ;export-pdf.tsne fait que semerrecit-theme, inchangé. Inkeep lisait déjà l'attribut (commentaire). Storybook :withThemeByDataAttributeseul. - e2e :
colorScheme()ne lit plus que l'attribut (la classe ne compte plus) ;deck-featureslit l'attribut aprèst. - Aucun
customCss,sites/*/styles.cssni 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-wrapperdesite-storiespréfixe aussi les cinq règles sombres dedeck.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 parCodeBlock.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/docs | STY0.6 avant | STY0.6 après | après STY5c | après STY9 |
|---|---|---|---|---|
| CSS d'entrée | 43,5 ko | 79,0 ko | 66,1 ko | 66,1 ko |
| CSS d'entrée, brut | 247 ko | 496 ko | 416 ko | 416 ko |
| JS d'entrée | 139,6 ko | 147,7 ko | 140,7 ko | 141,1 ko |
| JS total (chunks) | — | — | 3 466 ko | 3 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
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: falselaisse une story hors du harnais (un routeur ne se rend pas dans un autre :Bootstrap).main.tspré-groupereact-dom/serveret@unhead/react/server(découverts en cours de passage, ils faisaient recharger Vite).- Compteur en erreur.
scripts/check-stories.jsn'a plus qu'un mode : tout signal échoue, la liste des manques s'affiche ; baseline,--baseline,--write-baseline,compare,mergeBaselinesupprimés,scripts/stories-baseline.jsonsupprimé,--strictaccepté (c'est ce qu'écritcheck: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/10 | TST5, 30/10 | Seuil |
|---|---|---|---|
| core | 89,8 / 73,9 | 90,59 / 74,90 | 90 / 74 |
| motion | 86,6 / 73,3 | 87,61 / 73,46 | 87 / 73 |
| scripts | 87,1 / 81,7 | 84,51 / 80,04 | 84 / 80 |
| app | 73,1 / 57,0 | 73,05 / 56,96 | 73 / 56 |
| site-stories | 68,6 / 51,0 | 69,56 / 51,47 | 69 / 51 |
| site | 66,4 / 54,7 | 78,81 / 70,68 | 78 / 70 |
| site-kit | 66,7 / 37,7 | 93,58 / 80,49 | 93 / 80 |
| components | 63,1 / 35,3 | 73,85 / 46,23 | 73 / 46 |
| motion-blocks | 59,0 / 33,1 | 65,74 / 40,18 | 65 / 40 |
| mcp | 54,8 / 41,8 | 54,81 / 41,82 | 54 / 41 |
| cli | 48,7 / 38,3 | 48,82 / 38,32 | 48 / 38 |
| deck | 27,2 / 20,2 | 52,34 / 39,33 | 52 / 39 |
| motion-render | 20,6 / 6,9 | 20,59 / 6,90 | 20 / 6 |
| theme | — | 100 / 83,33 | 100 / 83 |
| recit | — | 100 / 100 | 100 / 100 |
| total | 66,8 / 54,3 | 72,78 / 60,37 | 72 / 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
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
À 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-darkprenait aussistorybook-site*.spec.ts(motifsite-.*) et rejouait 236 stories dans son propre dossier de captures —testIgnore: STORYBOOK_SPECSajouté ; et/indexdu 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 pagesdeploy-vercel,reference/testing,theme/mantineajoutées àsite-docs.spec.ts, et le texte changé detheme/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 checkvert. 1 659 références au total. - Avant/après des thèmes
warm-editorialetsaas-productmontré à 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ésD-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 verse2e/__screenshots__/, groupée par page et variante, et la couverture calculée contresites/*/pages,sites/*/storiesetstorybook-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