27 — Calendar : le format calendar et calendar.recit.dev
Statut : proposé · Phase : P10 · Mis à jour : 02/11/2026.
Ce plan détaille la phase P10 de 06 · Feuille de route : le format calendar, la
seconde moitié de la fondation @recit/time (heures, fuseaux, récurrences, ICS en lecture), le type
Place (règle B3, porté par 12 · Glossaire et annuaire), le pont roadmap
→ calendar et le site calendar.recit.dev. Il part de 08 · Formats §5 et le précise
sans le contredire ; les écarts que le code impose sont posés en questions ouvertes (§12). Il suit
le plan 26 (roadmap, P9), dont il reprend la table de visionneuses, l'horloge de build, l'écrivain
ICS, l'outil MCP validate_story et les variantes ics du manifeste exports.json.
Il dépend de 02 · Architecture, de 03 · Packaging §6 et §8, de
07 · Domaines, de 09 · Langues et JSON, de
12 · Glossaire et annuaire (Place, JSON-LD partagé),
13 · Cartes (qui importe Place sans le redéfinir), de
15 · Export PDF et de 21 · Tests. Le lot avancé de P10 du plan
10 · Formulaires (FO0, FO1) se déroule dans la même phase et n'est pas repris
ici. Le pont calendar → stories est le plan 28 (P11).
Chaque choix porte une recommandation. Rien n'est implémenté avant l'accord de l'utilisateur.
1. Pourquoi
- Un agenda est une liste d'événements qui se lit de quatre façons (mois, semaine, agenda, année) et s'exporte vers les agendas de tout le monde (ICS). Meetups, cours, sorties, planning éditorial : aujourd'hui, on les tient dans un tableur, un Google Agenda et une page web, à la main et en trois exemplaires. Récit en fait une source unique, en Markdown pour la prose, en JSON pour le multilingue et l'outillage.
- C'est le cas d'usage type du JSON multilingue (08 §5) : un
meetups.jsonoù chaque titre est unLocalized, sans Markdown du tout, est la meilleure démonstration du jumeau typé. - Les fuseaux et les changements d'heure sont le piège de tout calendrier. Les traiter une fois,
dans une fondation testée (
@recit/time), protège tous les formats qui auront un jour des heures. - Les formats se relient : les jalons de la roadmap remontent dans le calendrier (critère de sortie de P10), un événement passé renvoie vers son deck ou son film, et les prochains événements deviennent une story (P11).
2. Ce qui existe déjà et qu'on réutilise
| Brique | État réel au 02/11/2026, ou à la fin de P9 (plan 26) | Ce que P10 en fait |
|---|---|---|
CalendarSourceSchema | genericSource("calendar") dans packages/core/src/schema/source.ts : looseObject sans champ typé | strictObject typé (§3.3) |
| Pivot Markdown | compileStorySource passe un JSON par sourceToMarkdown avant de le compiler | le codec du calendrier dans SCENE_CODECS (plan 26 RM0) ; les objets (Place inline) passent par la forme JSON du séparateur |
| Séparateur | parser/split-slides.ts : key=value avec guillemets, ou --- {…} en JSON ; une valeur ne tient que sur une ligne | les attributs d'un événement restent sur la ligne du séparateur, ou passent en forme JSON |
| Compagnon | fusion par id de scène (mergeSidecar), listes à clé ajoutées par le plan 26 | un événement est une scène : rien de plus à faire |
@recit/time | après P9 : dates civiles, trimestres, today()/now() sur SOURCE_DATE_EPOCH, écrivain ICS de journées entières | heures et fuseaux, récurrences, VTIMEZONE, lecture ICS (§3.5, §4.4) |
Temporal | absent de Node 22.22, la version du dépôt (vérifié : typeof Temporal vaut undefined) | polyfill côté build seulement ; le client n'utilise que Intl (§3.5) |
| Semaine locale | new Intl.Locale("fr-FR").weekInfo répond dans Node 22 (firstDay: 1) ; pas dans tous les navigateurs | calculé au build, jamais dans le client |
Place, JSON-LD | spécifiés par le plan 12 (D6, D11) : un seul Place dans @recit/core/schema, un module JSON-LD partagé @recit/core/jsonld | livrés ici, premier consommateur (B3) |
@recit/roadmap | après P9 : storyToRoadmap, jalons datés | lu par le pont (§3.7), dépendance format → format permise (03 §8) |
| Sites | table FORMAT_VIEWERS, modèle calculé au build, fichiers .ics écrits à côté des pages, ExportPanel (plan 26 §6.1) | une ligne de plus, plus des routes par événement (§6.1) |
chaîne de 15 · Export PDF, liste fermée : calendar imprime « mois ou agenda » | CalendarPdfOptionsSchema | |
| Squelette | packages/formats/calendar : export {}, privé, scope:format | devient publié |
3. Le modèle
3.1 Une scène = un événement
Accepté dans 08 §0 : une scène est un événement. Un événement récurrent reste une scène, qui produit plusieurs occurrences au rendu. La mention « (ou un jour) » de 08 §0 n'a pas de syntaxe dans 08 §5 et n'est pas reprise : un jour est une case de la vue mois, pas une scène.
Conséquence pour le JSON : les événements sont les scenes du jumeau, comme pour tous les formats.
08 §5 et 09 §2.3 parlent d'un tableau events[] ; un tel tableau doublerait scenes, casserait la
fusion du compagnon par id de scène et le test d'aller-retour. Recommandation : pas d'events[],
les événements sont les scènes (question 1).
3.2 La syntaxe Markdown
Markdown
| Attribut | Rôle |
|---|---|
start | heure murale locale AAAA-MM-JJTHH:MM, dans timezone de l'événement, sinon celle du calendrier |
end ou duration | l'un ou l'autre, jamais les deux. duration : 1h, 90m, 1h30m ou ISO 8601 (PT1H30M) |
date | un événement sur la journée entière ; avec end (date), sur plusieurs jours, end compris (§3.5) |
allDay | accepté (08, 09) ; implicite avec date. allDay=true avec start est une erreur |
timezone | fuseau IANA (America/New_York) ; interdit sur un événement date |
rrule, exdate | récurrence RFC 5545 sans le préfixe RRULE: ; exceptions, liste séparée par des virgules, dans la même forme que start (ou date) |
location | chaîne libre (affichage seul), @id d'une entrée d'annuaire, ou Place inline en JSON (plan 12 D6) |
url, tags | lien (visio, billetterie), étiquettes filtrables |
status | confirmed (défaut), tentative, cancelled : un événement annulé reste affiché barré et sort dans l'ICS avec STATUS:CANCELLED |
deck, film | story liée : « voir le deck », « voir le film ». 08 §5 écrit video= ; le vocabulaire d'AGENTS.md dit film |
color | une clarté de la teinte du site (soft, base, strong), jamais une couleur libre (08 §5 : teinte unique) |
| titre et corps de la scène | le premier titre est le titre de l'événement, le reste sa description en Markdown complet |
Les attributs tiennent sur la ligne du séparateur : l'exemple de 08 §5 coupe celle d'office-hours
en deux lignes, ce que l'analyseur ne lit pas. Une ligne trop longue s'écrit en forme JSON
(--- {"id": "office-hours", …}), déjà acceptée par parseInlineConfigWithDiagnostics. Les sources
de stories ne passent jamais par oxfmt, qui ne les recoupera donc pas.
3.3 Le schéma (dans @recit/core/schema)
Primitives dans packages/core/src/schema/time.ts (créé par le plan 26), Place dans
packages/core/src/schema/place.ts (plan 12, livré ici), le reste dans source.ts.
TypeScript
locationaccepte unLocalized(une chaîne, ou une chaîne par langue : « Lorem Hall » et « Salle Lorem ») ; une chaîne qui commence par@est une référence d'annuaire (DI5.4, résolue plus tard par@recit/directory) ; un objet est unPlace. Une seule résolution, dans le cœur (plan 12 D6). Les noms exacts de zod/mini sont à vérifier à l'implémentation.- Les défauts (
view: month,window: { past: P3M, future: P12M },maxOccurrences: 500,firstDayde la langue) sont posés par la sectioncalendar:derecit.config(CalendarConfigSchema,.prefault({})), jamais par la source. - Contrôles du schéma (
z.refineau niveau de la scène) : exactement un destartoudate;endetdurationexclusifs ;endde même nature que le début (date avecdate, heure avecstart) ;timezoneinterdit sur undate;exdatede même nature que le début. timezonedu calendrier est obligatoire dès qu'un événement a une heure sans fuseau propre. Il n'y a pas d'heure « flottante » (sans fuseau) : elle change de sens d'un lecteur à l'autre.
3.4 Le jumeau et le compagnon
Le jumeau d'un calendrier Markdown est un JSON dont chaque scène porte ses champs typés ; un
calendrier peut aussi être seulement du JSON (meetups.json, kind: "story", sans .md), ce
qui est la forme naturelle d'un agenda multilingue :
JSON
- Codec dans
SCENE_CODECS(plan 26 RM0) :markdownToSourcelit le premier titre entitle, les attributs typés du séparateur en champs, le reste enattrs;sourceToMarkdownémet## titre, la description, et les attributs enkey=value— ou en forme JSON du séparateur quand un champ est un objet (Placeinline) ou contient un caractère qui ne se cite pas. - Le pivot résout les
Localizeddans une langue (compileStorySource), si bien que le calendrier compile une fois par langue, comme le deck. - Le compagnon complète un événement par son
id(statut,urlajoutée par un outil,geod'un lieu) ;recit convert meetups.ics --to jsonest la seule voie pour importer un agenda existant (§5), jamais une fusion silencieuse.
3.5 Dates, heures et fuseaux
Le cœur du plan. Principes, testés un par un (§9) :
- On stocke l'heure murale et son fuseau, jamais un instant UTC.
start=2026-11-05T18:30dansEurope/Parisest « 18 h 30 à Paris », quoi qu'il arrive aux règles de fuseau. L'instant est calculé au build parTemporal.ZonedDateTime. C'est la règle de 08 §9 (« les heures sont toujours stockées avec leur fuseau, et l'affichage est converti »). - Une récurrence garde l'heure murale. Un cours hebdomadaire à 18 h 30 reste à 18 h 30 à Paris
après le passage à l'heure d'hiver (25/10/2026) ; c'est l'instant UTC qui se décale. Exemple de
course: occurrences du 06/10 au 10/11/2026, le 13/10 à 16 h 30 UTC, le 27/10 à 17 h 30 UTC. - Heure inexistante ou ambiguë.
2026-03-29T02:30n'existe pas à Paris ;2026-10-25T02:30existe deux fois. Résolutiondisambiguation: "compatible"de Temporal (comme RFC 5545 : décalée d'une heure vers l'avant pour l'inexistante, la première pour l'ambiguë), avec un avertissement (calendar-nonexistent-time,calendar-ambiguous-time) qui porte la ligne. durationest un temps écoulé,endune heure murale.duration=2hd'un événement à 1 h 30 la nuit du changement d'heure dure deux heures réelles ;end=…T03:30finit à 3 h 30 au mur. Les deux sont légitimes et documentés.- Journée entière = dates civiles, sans fuseau.
date=2027-03-31est le 31 mars partout. Dans l'ICS,DTSTART;VALUE=DATEet unDTENDexclusif (lendemain) : l'auteur écritendcompris (« du 10 au 12 mai »), le convertisseur ajoute le jour (question 5). - Affichage dans le fuseau du calendrier, par défaut. La page prérendue affiche les heures dans
timezonedu calendrier, écrite à côté (« 18:30 CET »), pour que le HTML soit le même pour tout le monde et que l'hydratation ne diverge pas.TimezoneSwitcherpropose ensuite le fuseau du lecteur (Intl.DateTimeFormat().resolvedOptions().timeZone) et le retient enlocalStorage(préférence de confort, protégée partry). Le changement se fait côté client parIntl, sur les instants déjà calculés : un lecteur de New York voit le cours du 27/10 à 13 h 30, et celui du 03/11 à 12 h 30, sansTemporaldans le navigateur (question 6). - Fenêtre de calcul. Une récurrence sans fin (
rrulesansCOUNTniUNTIL) est développée entretoday() - window.pastettoday() + window.future, au plusmaxOccurrences(avertissementcalendar-occurrences-capped). L'ICS, lui, garde la règleRRULEentière : un client d'agenda la développe lui-même. - Une horloge.
today()etnow()de@recit/time(plan 26 §3.5), surSOURCE_DATE_EPOCH. Les vues ouvrent sur le mois de l'horloge de build ;UpcomingEventsfiltre côté client avec l'heure réelle, après l'hydratation, pour qu'un site construit lundi ne montre pas mardi un événement de lundi comme « à venir ».
Outils, dans @recit/time :
| Sous-export | Contenu | Où il tourne |
|---|---|---|
. | dates civiles, trimestres, semaines ISO, durées, today(), formatage Intl (P9) | partout, client compris |
./zoned | heure murale → instant, désambiguïsation, développement des récurrences, fenêtres ; charge le polyfill Temporal si globalThis.Temporal manque | Node (build, CLI) seulement |
./ics | écriture (P9 : journées entières ; P10 : heures, TZID, VTIMEZONE, RRULE, EXDATE, STATUS) et lecture | Node ; pur |
Dépendances : un polyfill Temporal et rrule, déjà prévus par 03 §6 (« rrule, polyfill Temporal
»). Le développement des récurrences de rrule est réputé fragile aux changements d'heure quand on
lui passe un fuseau ; d'où le spike CA0 (§10), qui tranche entre rrule et un développement maison
sur Temporal, limité au sous-ensemble utile (FREQ, INTERVAL, COUNT, UNTIL, BYDAY,
BYMONTHDAY, BYMONTH, BYSETPOS, WKST), avec les exemples de RFC 5545 §3.8.5.3 en tests
(question 2).
3.6 Les règles de validation
| Code | Gravité | Cas |
|---|---|---|
calendar-date-order | erreur | end avant start (ou avant date) |
calendar-start | erreur | ni start ni date, ou les deux ; end et duration ensemble |
calendar-unknown-timezone | erreur | fuseau inconnu d'Intl |
calendar-missing-timezone | erreur | événement à l'heure sans fuseau, dans un calendrier sans timezone |
calendar-invalid-rrule | erreur | grammaire fausse, UNTIL et COUNT ensemble, BYDAY invalide |
calendar-exdate-mismatch | avertissement | exdate qui ne tombe sur aucune occurrence |
calendar-nonexistent-time | avertissement | heure murale qui n'existe pas (passage à l'heure d'été) |
calendar-ambiguous-time | avertissement | heure murale qui existe deux fois (passage à l'heure d'hiver) |
calendar-overlap | erreur | deux occurrences se chevauchent, seulement si noOverlap: true |
calendar-occurrences-capped | avertissement | développement arrêté à maxOccurrences |
calendar-unknown-place | erreur | location=@id sans entrée d'annuaire (quand @recit/directory est présent) |
Toutes portent fichier et ligne, ou chemin JSON (meetups.json /scenes/3/rrule).
3.7 Le pont roadmap → calendar
include: [../roadmap/product.md] dans le frontmatter (08 §5). Le calendrier compile chaque roadmap
listée avec storyToRoadmap (@recit/roadmap) et en tire un événement date par jalon daté
(id préfixé : product.v1), avec les tags milestone et le slug de la roadmap. Les éléments
datés n'entrent que si la roadmap le demande (ics.items: true, plan 26 §4.4), en événements sur
plusieurs jours. Correspondance fermée (règle des ponts de 08 §0) : un jalon sans date ne passe pas,
sans erreur ; un id en double est une erreur.
Le chemin d'include est relatif au fichier et ne sort pas du projet ; sur un site, la roadmap doit
être publiée par le même site ou par un exemple déclaré en devDependencies (constat ST-20). Les
jalons inclus suivent la langue de la page (titres Localized de la roadmap).
3.8 Les langues
Localisables : title, body, le name d'un Place, une location libre. Communs : id,
heures, fuseaux, récurrences, url, tags, status (09 §2.3). recit i18n check compare en plus
les attributs temporels entre meetups.md et meetups.fr.md : une heure différente est une erreur
(attribut structurel). Les noms de jours et de mois, le premier jour de la semaine et le format des
heures (24 h ou 12 h) viennent d'Intl, dans la langue de la page.
4. Le rendu
4.1 Le modèle compilé
TypeScript
Le modèle est calculé au build, une fois par langue ; le client reçoit des instants avec leur
décalage, qu'Intl.DateTimeFormat affiche dans n'importe quel fuseau. Ni Temporal ni rrule
n'entrent dans un bundle client (règle « Bundle Size » d'AGENTS.md, size-limit).
4.2 Les quatre vues
| Vue | Ce qu'elle montre | Structure accessible |
|---|---|---|
month | grille du mois, une case par jour, les événements en liens ; « +3 » quand la case déborde ; navigation mois précédent et suivant | <table> avec <caption> (mois, année), en-têtes de jours <th> |
week | sept colonnes, les heures en lignes, les événements placés par leur instant dans le fuseau affiché ; journées entières en bandeau | liste par jour sous la grille visuelle, lue par un lecteur d'écran |
agenda | liste des occurrences à venir, groupées par jour | <ol> de jours, chaque événement un <article> avec <time datetime> |
year | douze mini-mois, les jours occupés marqués ; un clic ouvre le mois | douze <table> titrées ; marque doublée d'un nombre |
La vue et la période se lisent dans l'URL (?view=week&date=2026-11-02), les filtres aussi
(?tag=meetup). Navigation au clavier : flèches entre les jours dans la grille, Entrée sur un
événement, Escape ferme le détail. Une semaine ou un mois hors de la fenêtre de build affiche «
hors de la période publiée », jamais une grille vide trompeuse.
Composants (08 §5) : Calendar (racine), MonthView, WeekView, AgendaList, YearView,
EventCard, EventDetail (<dialog> natif, et page propre §6.1, avec « ajouter à mon agenda »),
MiniCalendar, UpcomingEvents, Countdown, TimezoneSwitcher (<select> natif),
CalendarFilters (cases à cocher), SubscribeButton. Dans
packages/formats/calendar/src/components/, un module CSS et une story chacun.
Style : la loi du plan 19, comme le plan 26 §4.3 — React et modules CSS compilés par Lightning CSS,
une feuille @recit/calendar/styles.css, pas de Mantine dans un package publié, light-dark() pour
le mode sombre. La teinte est celle du site calendar (rouge, 07 · Domaines §4) ;
color ne choisit qu'une clarté. Un événement annulé se lit par le mot « Annulé » et le barré, pas
par la couleur.
4.3 La page d'un événement et le JSON-LD
Chaque événement a sa page (/calendar/<slug>/<id>), prérendue : titre, occurrences à venir, lieu,
description, liens vers son deck ou son film, SubscribeButton pour cet événement seul
(/calendar/<slug>/<id>.ics). Elle porte un JSON-LD schema.org/Event produit par le module
partagé @recit/core/jsonld (plan 12 D11) : startDate et endDate avec décalage, location en
Place avec PostalAddress quand l'adresse est structurée (jamais depuis address libre),
VirtualLocation quand seule url est donnée, eventStatus (EventCancelled),
eventAttendanceMode. Un événement récurrent émet ses occurrences de la fenêtre, en eventSchedule
si les moteurs le lisent, sinon une entrée par occurrence (à vérifier dans CA2).
4.4 Les exports
| Sortie | Commande | Contenu |
|---|---|---|
| Web | recit build meetups.json | les quatre vues, une page par événement |
| ICS | recit export meetups.json --to ics | VCALENDAR complet : un VEVENT par événement, RRULE et EXDATE gardées, VTIMEZONE par fuseau utilisé |
| Flux abonnable | au build d'un site | /calendar/<slug>.ics (un par langue : /fr/calendar/<slug>.ics), servi en text/calendar; charset=utf-8 |
recit export meetups.json --to pdf | le mois (une page par mois de la plage) ou l'agenda, A4 portrait |
- ICS fiable :
UIDstable (<id>@<slug>.<hôte>, jamais régénéré) pour qu'un client abonné mette à jour au lieu de dupliquer ;SEQUENCEetLAST-MODIFIEDdérivés d'un hachage du contenu de l'événement et de l'horloge de build ;DTSTAMPde l'horloge de build ;TZIDIANA etVTIMEZONEcalculé avecTemporal(transitions du fuseau sur la période des événements), parce qu'Outlook l'exige ;X-WR-CALNAME,X-WR-TIMEZONEetREFRESH-INTERVAL;VALUE=DURATION:PT12Hpour les abonnés. Lignes pliées à 75 octets, CRLF. - Abonnement :
SubscribeButtonproposewebcal://…, Google Agenda (ajout par URL), Outlook (addfromweb) et le téléchargement du fichier ; aucun script tiers, aucun suivi. - PDF :
CalendarPdfOptionsSchema = z.extend(PdfOptionsSchema, { view: month | agenda, from, months })en CA3, sectioncalendar.pdfetpdf:du frontmatter (15 §3.2), vue?print=1etwhenPrintable(). exports.json: varianteicsdu plan 26, avecsubscribable: truepour les flux.
5. CLI et MCP
| Commande | Effet |
|---|---|
recit init --format calendar [file] | calendar.md Lorem, ou calendar.json multilingue |
recit dev, build | comme le plan 26 |
recit validate meetups.json | schéma, règles du §3.6, développement des récurrences dans la fenêtre |
recit convert meetups.md --to json|md | jumeau |
recit convert agenda.ics --to json|md | import d'un .ics existant en story calendar (CA1 de 08) ; les propriétés inconnues sont listées, jamais perdues en silence |
recit export … --to ics|pdf|html | §4.4 ; --locale, --all-locales |
recit schema calendar | JSON Schema |
L'import passe par recit convert (une commande par besoin, B9), comme Fountain au plan 14.
MCP : describe_format gagne calendar ; validate_story (plan 26) valide aussi un calendrier
; un outil expand_events rend les occurrences d'une story dans une fenêtre donnée et un fuseau
donné, pour qu'un agent vérifie une rrule avant de l'écrire ; guide recit://guide/calendar, qui
insiste sur l'heure murale et le fuseau.
6. Le site
6.1 L'intégration (@recit/site-stories)
- Une ligne
calendardansFORMAT_VIEWERS(plan 26 §6.1), chargée enReact.lazy. - Routes par scène : c'est le premier format qui publie une page par scène. Le plugin gagne une
option par format,
sceneRoutes, qui ajoute/<routePrefix>/<slug>/<sceneId>au prérendu, au sitemap, à Pagefind et aux routes de langue (hreflang). Le deck n'en a pas besoin et ne change pas. - Les fichiers
.ics(calendrier entier, et un par événement) sont écrits au build à côté des pages, et listés dansexports.json. <StoryEmbed slug="meetups" view="agenda" />et<UpcomingEvents slug="meetups" count={3} />dans une page MDX (recit.dev, docs) : ce dernier est le composant que www pourra montrer.
6.2 calendar.recit.dev
sites/calendar, sur createSiteConfig({ site: "calendar" }).
| Page | Contenu |
|---|---|
/ | FormatHero : le calendrier des versions de Récit (§6.3), vue mois, SubscribeButton, source dépliable |
/gallery | calendar-meetups (JSON multilingue) et calendar-course (Markdown, récurrences) dans les quatre vues, en en et fr |
/json | démo « le même agenda en JSON » : le jumeau à côté du rendu, sélecteur de langue |
/templates | meetups, cours, planning éditorial |
/calendar/<slug>[/<id>] | stories en plein écran et pages d'événement (publish.routePrefix: "/calendar", d'où /calendar/<slug>.ics de 08 §5) |
Port local 3008, port e2e 4308, projet Vercel recit-calendar, entrée calendar dans SITES
(07 · Domaines). vercel.json pose le type text/calendar sur *.ics. Doc :
sites/docs › Formats › Calendar, avec une page « Fuseaux et changements d'heure ».
6.3 Dogfooding
Le hero de calendar.recit.dev (07 §2 : « calendrier des sorties et des événements Récit ») est fait
de données réelles et ne doit rien inventer : Récit n'a ni meetup ni cours. Proposition : une
story sites/calendar/stories/releases.md qui contient les versions publiées (0.1.0-alpha…,
journée entière, film= ou deck= vers leur annonce quand elle existe) et qui inclut les
jalons datés de la roadmap de Récit (plan 26 §7) par le pont du §3.7. C'est le critère de sortie «
les jalons de la roadmap remontent dans le calendrier », démontré sur Récit lui-même. Comme un site
ne dépend pas d'un autre site, la roadmap incluse doit être publiée par le même site : soit
calendar.recit.dev republie la story de roadmap (copie), soit elle vit à un endroit que les deux
sites peuvent déclarer (question 8).
7. Les exemples
Contenu en Lorem Ipsum, lieux fictifs (Lorem Hall, ville Ipsum, example.com), règle B7.
| Dossier | Contenu |
|---|---|
examples/calendar-meetups | meetups.json seul (kind: "story"), titres et lieux Localized en en et fr, un Place inline, un événement annulé, une visio (url seule) ; recit.config.ts avec i18n |
examples/calendar-course | course.md : un cours hebdomadaire qui traverse le 25/10/2026, une exdate, des permanences sur America/New_York qui traversent le 01/11/2026, une journée entière sur trois jours, un include de ../roadmap-product/product.md |
calendar-course déclare recit-example-roadmap-product en dépendance pour le pont (ST-20).
Gabarits create-recit : calendar-meetups, calendar-course.
8. Les risques
| Risque | Parade |
|---|---|
| Erreurs aux changements d'heure | heure murale et fuseau stockés ; Temporal pour les calculs ; tests sur les quatre transitions de 2026 (Paris 29/03 et 25/10, New York 08/03 et 01/11) |
rrule faux avec un fuseau | spike CA0 avec go/no-go, repli sur un développement maison sur Temporal |
| Base de fuseaux différente entre Node et navigateurs | les instants sont calculés au build (ICU de Node, full-icu) ; le client ne fait que formater ; un test compare au fuseau UTC |
| HTML prérendu différent selon le lecteur (hydratation) | prérendu dans le fuseau du calendrier, toujours avec timeZone explicite ; le fuseau du lecteur ne s'applique qu'après l'hydratation |
| Site statique périmé (« à venir » d'hier) | UpcomingEvents filtre côté client ; reconstruction planifiée du site (question 7) |
| Clients ICS capricieux (Outlook, Apple, Google) | VTIMEZONE toujours émis, UID stable, essais manuels des trois avant la sortie de P10 ; l'ICS est aussi relu en test |
| Trop d'occurrences (récurrence quotidienne sans fin) | fenêtre et maxOccurrences, avertissement |
Le polyfill Temporal alourdit le client | sous-export ./zoned réservé à Node ; size-limit sur le bundle du calendrier |
| Fuites de données personnelles (adresse d'un particulier) | Place structuré seulement pour des lieux publics ; l'avertissement « personne avec adresse » du plan 12 s'applique aux location |
| Un lieu libre pris pour une adresse dans le JSON-LD | address libre jamais émis en PostalAddress (plan 12 D6) |
9. Les tests
| Niveau | Quoi | Où |
|---|---|---|
| Unitaire, cœur | schéma et refine (exclusivités, natures de dates, fuseau obligatoire), codec Markdown ⇄ JSON (forme JSON du séparateur pour un Place), Place et location | packages/core/src/__tests__/calendar-*.spec.ts |
| Aller-retour | examples/calendar-* et sites/calendar/stories, y compris un calendrier seulement JSON (json → md → json) | source-conversion.spec.ts |
| Unitaire, time | les quatre transitions de 2026 ; heures inexistantes et ambiguës ; récurrences : exemples de RFC 5545 §3.8.5.3, EXDATE, UNTIL en heure locale, BYSETPOS ; durées contre heures de fin ; VTIMEZONE ; ICS écrit puis relu, identique | packages/time/src/__tests__/ |
| Unitaire, format | storyToCalendar : fenêtre, plafond, annulés, pont roadmap → calendar, id préfixés ; JSON-LD validé contre le schéma Event attendu | packages/formats/calendar/src/__tests__/ |
| Stories | un play par composant : naviguer au clavier dans MonthView, ouvrir et fermer EventDetail (Escape), changer de fuseau et lire l'heure affichée (« 13:30 » à New York pour le 27/10), filtrer par tag, SubscribeButton qui expose ses liens ; axe en erreur | packages/formats/calendar/src/components/*.stories.tsx, titre Calendar/<Composant> |
| e2e site | chaque page en en et fr, les quatre vues, une page d'événement, .ics servi en text/calendar et relu, hreflang ; captures sous SOURCE_DATE_EPOCH fixe et timezoneId Playwright fixé | e2e/site-calendar.spec.ts, projet calendar |
| e2e Storybook | captures Calendar/* | e2e/storybook-calendar.spec.ts |
| pages par mois, texte extrait, signets | test de la CLI |
Un test e2e change aussi le fuseau du navigateur (timezoneId: "America/New_York") et vérifie que
la page prérendue reste celle du fuseau du calendrier jusqu'au choix du lecteur.
10. Les lots
Identifiants de 08 §5 conservés (CA1 à CA5), plus CA0, sous-tâches CAn.m (B10). Prérequis : P9
terminé (plan 26 : SCENE_CODECS, FORMAT_VIEWERS, @recit/time moitié roadmap, écrivain ICS,
validate_story). Chaque lot laisse pnpm check, build, typecheck et pnpm test:related au vert,
avec son version plan (preminor au premier lot publié de la phase, prerelease ensuite), et les
lignes de NOTICE.md des dépendances ajoutées.
CA0 — Spike récurrences et fuseaux (S), go/no-go
- CA0.1 — Polyfill
Temporal:temporal-polyfill(plus léger) ou@js-temporal/polyfill(référence du TC39), mesurés (poids, conformité sur nos cas). - CA0.2 —
rrulecontre un développement maison surTemporal, sur la batterie du §9.
Sortie : une note dans ce plan, deux dépendances choisies (ou une seule), validées par l'utilisateur.
CA1 — @recit/time, moitié calendrier (M)
- CA1.1 —
LocalDateTimeSchema,TimeZoneSchema,DurationSchema,RRuleSchemadans le cœur. - CA1.2 —
./zoned: heure murale → instant, désambiguïsation, récurrences, fenêtre. - CA1.3 —
./ics: heures,TZID,VTIMEZONE,RRULE,EXDATE,STATUS; lecture.
Sortie : tous les tests « Unitaire, time » verts ; un ICS écrit puis relu est identique.
CA2 — Le modèle, les vues, Place (L)
- CA2.1 —
PlaceSchemaetLocationSchema(plan 12 DI0.1),CalendarSourceSchematypé,CalendarConfigSchema, JSON Schemacalendar.json. - CA2.2 — Codec dans
SCENE_CODECS, règles du §3.6. - CA2.3 —
storyToCalendar,CalendarSchema. - CA2.4 — Les quatre vues et les composants, stories avec
play. - CA2.5 —
@recit/core/jsonld(Event,Place), partagé avec le plan 12. - CA2.6 — Runtime et CLI (
init,dev,build,validate,convert, import.ics).
Sortie : recit build examples/calendar-meetups/meetups.json montre les quatre vues en en et fr
; pnpm test:stories packages/formats/calendar passe, axe compris.
CA3 — Flux, PDF, pont (M)
- CA3.1 —
export --to ics, flux abonnable par langue, ICS par événement. - CA3.2 —
CalendarPdfOptionsSchema, vue imprimable,export --to pdf. - CA3.3 —
include: pont roadmap → calendar. - CA3.4 — MCP :
describe_format,expand_events, guide.
Sortie : flux abonné à la main dans Google Agenda, Apple Calendrier et Outlook, heures justes de
part et d'autre du 25/10 ; les jalons de roadmap-product apparaissent dans calendar-course.
CA4 — Les exemples (S)
- CA4.1 —
examples/calendar-meetupsetexamples/calendar-course(§7), gabarits.
Sortie : aller-retour vert, recit i18n check vert.
CA5 — calendar.recit.dev et la doc (M)
- CA5.1 —
sceneRoutesdans@recit/site-stories. - CA5.2 —
sites/calendar(§6.2),SITES, ports, Vercel, type des.ics. - CA5.3 —
sites/calendar/stories/releases.mdet l'inclusion de la roadmap de Récit (§6.3). - CA5.4 —
sites/docs› Formats › Calendar, page des fuseaux ;formats.tsde@recit/site-kit. - CA5.5 —
e2e/site-calendar.spec.ts,e2e/storybook-calendar.spec.ts, captures-linux.
Sortie (celle de P10 dans 06) : examples/calendar-meetups dans les quatre vues ; flux ICS
abonnable et récurrences justes aux changements d'heure ; jalons de la roadmap dans le calendrier ;
pnpm verify et pnpm test:e2e:linux e2e/site-calendar.spec.ts verts deux fois de suite ; version
0.6.0-alpha publiée.
Ordre et agents. CA0 d'abord (il conditionne CA1). CA1 et CA2.1–CA2.2 (cœur) peuvent avancer en parallèle une fois CA0 tranché ; CA2.3 et suivants attendent CA1.2. CA3 et CA4 se partagent ; CA5 ferme. Les lots FO0 et FO1 du plan 10 se déroulent à côté, sans fichier commun.
11. Ce qui reste hors de P10
- Synchronisation bidirectionnelle avec Google Agenda ou CalDAV : jamais dans le cœur ; l'import
.icsponctuel suffit. - Inscription à un événement : le bloc
:::formdu plan 10 peut vivre dans la description d'un événement, sans rien de propre au calendrier. - Pont calendar → stories (« à venir » en cartes) : plan 28, P11.
- Disponibilités, prise de rendez-vous, fuseaux multiples affichés côte à côte : non prévus.
12. Questions ouvertes
- Forme du JSON : les événements sont les
scenesdu jumeau (recommandé, §3.1) ; ou un tableauevents[]à part, comme l'écrivent 08 §5 et 09 §2.3 ? - Récurrences : garder
rrule(prévu par 03 §6) si le spike CA0 le valide, sinon un développement maison surTemporallimité au sous-ensemble du §3.5 (recommandé en repli) ? - Polyfill
Temporal:temporal-polyfill(recommandé, plus léger, Node seulement) ou@js-temporal/polyfill? - Lecture ICS :
ical.js(MPL-2.0), qui servirait aussi d'oracle aux tests du plan 26 (question 9) ; ou un analyseur maison limité à ce que Récit écrit et importe ? - Journée entière sur plusieurs jours :
endécrit compris par l'auteur, converti enDTENDexclusif dans l'ICS (recommandé) ; ouendexclusif comme RFC 5545 ? - Fuseau d'affichage : celui du calendrier par défaut, celui du lecteur sur demande (recommandé) ; ou celui du lecteur d'office après l'hydratation ?
- Fraîcheur : accepter une reconstruction planifiée de calendar.recit.dev (une par jour, par une tâche Vercel), pour que la vue « à venir » et le flux suivent le temps ?
- Dogfooding : le calendrier des versions de Récit inclut la roadmap de Récit (§6.3). Où vit la
story de roadmap pour que deux sites la lisent sans dépendre l'un de l'autre : copiée dans
sites/calendar, ou dans un projet de contenu commun hors deexamples/(non Lorem) ? video=→film=: renommer l'attribut de 08 §5 selon le vocabulaire d'AGENTS.md(recommandé) ?color: limiter aux trois clartéssoft,base,strongde la teinte du site (recommandé) ; ou supprimer l'attribut et ne colorer que par tag ?- Quatre vues dès P10 (critère de sortie de 06) ; ou
monthetagendad'abord,weeketyearenprereleasesuivante ?
13. À reporter dans les autres plans (par l'intégrateur)
| Fichier | Ce qui change |
|---|---|
06-roadmap.md | P10 renvoie à ce plan ; CA0 ajouté |
08-formats.md §0, §5 | « (ou un jour) » retiré ; events[] selon la question 1 ; séparateur d'office-hours sur une ligne ; export --to ics ; film= ; status |
09-langues-et-json.md §2.3 | ligne calendar : champs de scène (title, start, end, duration, date, timezone, rrule, exdate, location…) au lieu d'events[] ; ligne Place (B3) |
12-glossaire-annuaire.md | Place livré en CA2.1, @recit/core/jsonld en CA2.5 |
15-export-pdf.md §3.2 | CalendarPdfOptionsSchema ; subscribable sur la variante ics |
21-tests.md §3.1 | titre Storybook Calendar/<Composant>, surface e2e/storybook-calendar.spec.ts |
sites/plans/recit.config.ts | entrée de navigation pour 27 |