diff --git a/README.md b/README.md
index abc78aa..6b46060 100644
--- a/README.md
+++ b/README.md
@@ -110,6 +110,28 @@ Une fois un projet en place :
Faire défiler jusqu'au bord droit élargit la frise vers le futur, et jusqu'au bord gauche
vers le passé. Le bouton **Aujourd'hui** ramène la vue sur la date du jour.
+### Les notes
+
+Un projet **et** une phase portent chacun des notes, en bas de leur panneau. Elles y prennent toute
+la hauteur restante : les autres champs ont une taille dictée par leur contenu, une note fait ce
+qu'on a à dire.
+
+Le texte s'écrit en **markdown** — titres, `**gras**`, `*italique*`, listes, `- [ ] cases à
+cocher`, liens, citations, blocs de code. Le bloc s'ouvre sur l'**aperçu** quand la note existe
+déjà, sur la saisie quand elle est vide ; le crayon en haut à droite bascule d'un état à l'autre,
+et une barre de six boutons paraît en saisie (`Ctrl`+`B` et `Ctrl`+`I` marchent aussi). Rien n'est
+converti à l'enregistrement : le fichier contient le markdown tel quel, lisible et modifiable à la
+main.
+
+Les **cases à cocher se cliquent directement dans l'aperçu** — c'est le seul geste de l'aperçu qui
+change les données. Cocher une ligne ne touche à rien d'autre : votre mise en forme n'est jamais
+réécrite.
+
+Les notes d'un projet sont ce que la frise ne sait pas dire — qui pilote, sur quel budget, quelle
+décision à quelle date. Elles se lisent aussi au survol du nom du projet dans la colonne de gauche,
+et celles d'une phase au survol de sa barre : l'infobulle les affiche sans les marques de mise en
+forme, puces et cases dessinées comprises.
+
### La vue par mois
Les deux boutons **Frise** et **Par mois**, en haut à gauche, changent de regard sur les mêmes
@@ -125,12 +147,13 @@ n'apparaît qu'à son mois de départ, avec sa date de fin écrite en clair.
Les phases qui portent des **notes** le signalent par un triangle en tête de ligne, le même que
celui qui plie un projet sur la frise. Un clic **n'importe où sur la ligne** déplie la note en
-dessous, un second la referme. Rien n'est affiché d'office — trois lignes de notes sous chaque
-intitulé étaleraient un mois sur deux écrans.
+dessous, un second la referme. Elle s'y affiche rendue, comme dans le panneau. Rien n'est affiché
+d'office — trois lignes de notes sous chaque intitulé étaleraient un mois sur deux écrans.
Cette liste se **lit** — elle ne s'édite pas. Déplacer une phase demande de voir ce qu'elle
chevauche, donc la frise ; c'est là que les dates et les notes se modifient. Seuls les tags et les
-notes y sont cliquables, et ni filtrer ni déplier ne touche aux données. Le bouton **Aujourd'hui**
+notes y sont cliquables, et ni filtrer ni déplier ne touche aux données — les cases à cocher d'une
+note y sont d'ailleurs inertes, elles ne se cochent que depuis le panneau. Le bouton **Aujourd'hui**
ramène ici sur le mois en cours, et la vue affichée n'est pas mémorisée : l'outil s'ouvre toujours
sur la frise.
@@ -156,7 +179,9 @@ par les champs du panneau de détail.
## Ce que fait l'outil
- **Plusieurs projets**, chacun décomposé en **phases** ayant un nom, des dates de début et de fin,
- un statut, des notes libres. Une phase peut être un **jalon** (une date unique, rendue en losange).
+ un statut, des notes. Une phase peut être un **jalon** (une date unique, rendue en losange).
+- **Des notes en markdown**, sur un projet comme sur une phase : titres, listes, cases à cocher,
+ liens. Elles occupent toute la hauteur restante du panneau, et se cochent d'un clic dans l'aperçu.
- **Une frise commune**, les projets empilés en couloirs, pour les comparer d'un coup d'œil.
- **Une barre cumulative par projet**, segmentée en teintes selon le statut de chaque phase : la
forme d'ensemble, toujours visible sur la ligne du projet et toujours rendue pareil. Elle ne se
@@ -252,8 +277,10 @@ données. Aucune dépendance de part ni d'autre, les lanceurs intégrés à Node
| `js/mois.js` | Rendu de la vue par mois : groupes, lignes, ancrage sur le mois courant |
| `js/tags.js` | Pastilles de tags, partagées par les deux vues |
| `js/drag.js` | Glisser et redimensionner les barres |
+| `js/markdown.js` | Analyse du markdown des notes, aplatissement pour les infobulles. Sans DOM, donc testable sous Node |
+| `js/notes.js` | Rendu du markdown en DOM et bloc de notes — aperçu, barre d'outils, saisie — partagé par les deux panneaux |
| `js/detail.js` | Panneau de détail d'une phase |
-| `js/projet.js` | Panneau des paramètres d'un projet : nom, couleur, tags |
+| `js/projet.js` | Panneau des paramètres d'un projet : nom, couleur, tags, notes |
| `js/menu.js` | Menu contextuel et dialogue de confirmation |
| `js/app.js` | Amorçage, état en mémoire, câblage des événements |
diff --git a/css/style.css b/css/style.css
index b4ebc67..cff43be 100644
--- a/css/style.css
+++ b/css/style.css
@@ -855,7 +855,13 @@ button[aria-pressed="true"] {
background: none;
}
+/* `flex: 1` et `min-height: 0` : le corps prend toute la hauteur sous l'en-tête,
+ et le second garde le droit de rétrécir sous la taille de son contenu — sans
+ lui, un flex item refuse de descendre sous sa hauteur intrinsèque et c'est la
+ page entière qui déborderait au lieu du corps qui défile. */
.panneau__corps {
+ flex: 1;
+ min-height: 0;
padding: 16px;
overflow-y: auto;
display: flex;
@@ -920,6 +926,290 @@ button[aria-pressed="true"] {
margin-top: 4px;
}
+/* --- bloc de notes -------------------------------------------------------- */
+
+/* Le bloc prend toute la hauteur que les champs au-dessus lui laissent. C'est
+ ce qui distingue une note d'un champ : les autres ont une taille dictée par
+ ce qu'ils contiennent — une date, un statut —, une note n'en a aucune, et
+ quatre lignes fixes étaient un plafond arbitraire qui obligeait à écrire dans
+ une meurtrière. Le plancher, lui, reste : sur une fenêtre basse, le corps du
+ panneau défile plutôt que d'écraser la note à deux lignes. */
+.notes {
+ flex: 1 1 auto;
+ min-height: 190px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.notes__entete {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ min-height: 24px;
+}
+
+.notes__intitule {
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--texte-doux);
+}
+
+/* La barre d'outils n'apparaît qu'en saisie : en aperçu elle ne commanderait
+ rien, et sa place revient au texte. */
+.notes__outils {
+ display: flex;
+ gap: 2px;
+ margin-left: auto;
+}
+
+.notes__outils button {
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border: 1px solid transparent;
+ border-radius: var(--rayon);
+ background: none;
+ color: var(--texte-doux);
+ font-size: 13px;
+ line-height: 1;
+ cursor: pointer;
+}
+
+.notes__outils button:hover {
+ border-color: var(--bordure-forte);
+ color: var(--texte);
+}
+
+.notes__outils [data-outil="gras"] {
+ font-weight: 700;
+}
+
+.notes__outils [data-outil="italique"] {
+ font-style: italic;
+ font-family: Georgia, serif;
+}
+
+/* Le crayon reste à l'extrémité droite quelle que soit la présence de la barre
+ d'outils : c'est le seul bouton toujours là, il lui faut une place fixe. */
+.notes__bascule {
+ margin-left: auto;
+ width: 26px;
+ height: 24px;
+ padding: 0;
+ border: 1px solid var(--bordure-forte);
+ border-radius: var(--rayon);
+ background: var(--surface);
+ color: var(--texte-doux);
+ font-size: 13px;
+ line-height: 1;
+ cursor: pointer;
+}
+
+.notes__outils:not([hidden]) + .notes__bascule {
+ margin-left: 4px;
+}
+
+.notes__bascule:hover {
+ color: var(--texte);
+ border-color: var(--texte-doux);
+}
+
+.notes__apercu {
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
+ padding: 8px 10px;
+ border: 1px solid var(--bordure);
+ border-radius: var(--rayon);
+ background: var(--fond);
+ font-size: 13px;
+ line-height: 1.5;
+ cursor: text;
+}
+
+.notes__invite {
+ margin: 0;
+ color: var(--texte-doux);
+ font-style: italic;
+}
+
+/* `resize: none` contre le `resize: vertical` des autres textarea du panneau :
+ celui-ci occupe la place disponible, une poignée de redimensionnement dans
+ son coin ne ferait que se battre avec la poignée du volet. */
+.notes__saisie {
+ flex: 1;
+ min-height: 0;
+ resize: none;
+ font-size: 13px;
+ line-height: 1.5;
+ tab-size: 2;
+}
+
+/* --- rendu markdown ------------------------------------------------------- */
+
+/* Ces règles servent au panneau comme à la vue par mois : une note doit se lire
+ pareil des deux côtés, sans quoi on douterait d'avoir la même sous les yeux.
+ Les marges sont serrées — c'est un encart dans un volet, pas un article. */
+/* Le style suit `data-niveau` — le nombre de dièses écrits — et non la balise :
+ celle-ci est décalée de deux rangs pour ne pas casser la hiérarchie du
+ document, et son plafond à `h6` confond les rangs 4 à 6. Les niveaux se
+ séparent surtout par le blanc au-dessus : dans un encart de treize pixels,
+ deux corps de titre distants d'un pixel ne se distinguent pas, alors qu'un
+ interligne plus large se voit tout de suite. */
+.md-titre {
+ margin: 12px 0 4px;
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 1.3;
+}
+
+.md-titre[data-niveau="1"] {
+ font-size: 16px;
+ margin-top: 16px;
+}
+
+.md-titre[data-niveau="2"] {
+ font-size: 14px;
+ margin-top: 14px;
+}
+
+/* Au-delà du troisième rang, le titre ne grossit plus : il s'efface. C'est un
+ intertitre dans un encart, pas une section — et une note qui descend à cinq
+ niveaux a de toute façon perdu la partie. */
+.md-titre[data-niveau="4"],
+.md-titre[data-niveau="5"],
+.md-titre[data-niveau="6"] {
+ color: var(--texte-doux);
+}
+
+.md-titre:first-child {
+ margin-top: 0;
+}
+
+.md-paragraphe {
+ margin: 0 0 8px;
+}
+
+.md-paragraphe:last-child {
+ margin-bottom: 0;
+}
+
+.md-liste {
+ margin: 0 0 8px;
+ padding-left: 20px;
+}
+
+.md-liste:last-child {
+ margin-bottom: 0;
+}
+
+.md-liste li {
+ margin-bottom: 2px;
+}
+
+/* La case *est* la puce : les deux ensemble décaleraient le texte sans rien
+ ajouter. */
+.md-liste--taches {
+ list-style: none;
+ padding-left: 2px;
+}
+
+/* Une sous-liste de tâches perdrait sinon toute indentation, son retrait ne
+ venant que de la puce qu'on vient de lui retirer. Or l'imbrication est
+ justement ce qu'on est venu lire : « variante sombre » dépend de « relancer
+ sur les maquettes », et rien d'autre que le décalage ne le dit. */
+.md-liste .md-liste--taches {
+ padding-left: 20px;
+}
+
+/* `.panneau__corps label` empile ses enfants en colonne et les grise — c'est ce
+ qu'il faut d'un champ de formulaire, dont l'intitulé surmonte la saisie. Une
+ case à cocher dans une note n'est pas cela : la case précède son texte sur la
+ même ligne, et ce texte est du contenu, pas une étiquette. D'où la
+ spécificité rehaussée, seule façon de reprendre la main sur une règle qui
+ vise tous les labels du panneau. */
+.notes__apercu .md-tache,
+.mois-entree__notes .md-tache {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ gap: 6px;
+ font-size: inherit;
+ font-weight: 400;
+ color: inherit;
+ cursor: pointer;
+}
+
+/* `width: auto` contre la règle qui étire à 100 % les champs du panneau : une
+ case à cocher garde sa taille native. */
+.md-tache input {
+ margin: 3px 0 0;
+ width: auto;
+ flex-shrink: 0;
+}
+
+/* Une tâche faite s'efface sans disparaître : elle compte encore dans ce qui a
+ été décidé, elle ne compte plus dans ce qui reste. */
+.md-tache--faite {
+ color: var(--texte-doux);
+ text-decoration: line-through;
+}
+
+/* Les cases de la vue par mois sont désactivées (lecture seule, décision 24) :
+ sans cela, le curseur y promettrait un clic sans effet. */
+.md-tache input:disabled {
+ cursor: default;
+}
+
+.md-tache:has(input:disabled) {
+ cursor: default;
+}
+
+.md-citation {
+ margin: 0 0 8px;
+ padding-left: 10px;
+ border-left: 3px solid var(--bordure-forte);
+ color: var(--texte-doux);
+}
+
+.md-citation > :last-child {
+ margin-bottom: 0;
+}
+
+.md-code {
+ margin: 0 0 8px;
+ padding: 6px 8px;
+ overflow-x: auto;
+ border-radius: var(--rayon);
+ background: color-mix(in srgb, var(--texte) 7%, transparent);
+ font-size: 12px;
+}
+
+.md-code-ligne {
+ padding: 1px 4px;
+ border-radius: 3px;
+ background: color-mix(in srgb, var(--texte) 7%, transparent);
+ font-size: 0.92em;
+}
+
+.md-titre + .md-liste,
+.md-titre + .md-paragraphe {
+ margin-top: 0;
+}
+
+.notes__apercu hr,
+.mois-entree__notes hr {
+ margin: 10px 0;
+ border: 0;
+ border-top: 1px solid var(--bordure-forte);
+}
+
+.notes__apercu a,
+.mois-entree__notes a {
+ color: var(--accent);
+}
+
/* --- menu contextuel ------------------------------------------------------ */
.menu {
@@ -1269,10 +1559,12 @@ button.mois-entree__plier {
marque — les colonnes sont nommées par leur rang, ce qui laisse la grille
faire le calcul, là où un retrait en pixels aurait recopié des largeurs et se
serait désaccordé à la première retouche. */
+/* Le `pre-wrap` d'autrefois n'a plus lieu d'être : les retours à la ligne sont
+ désormais portés par le rendu markdown, qui les traduit en `
` et en
+ éléments de liste. */
.mois-entree__notes {
grid-column: 3 / -1;
margin: 2px 0 6px;
- white-space: pre-wrap;
color: var(--texte-doux);
}
diff --git a/data/exemple.json b/data/exemple.json
index 727b114..36ffc07 100644
--- a/data/exemple.json
+++ b/data/exemple.json
@@ -1,5 +1,5 @@
{
- "version": 3,
+ "version": 4,
"projects": [
{
"id": "site-web",
@@ -8,6 +8,7 @@
"tags": ["client", "web"],
"collapsed": false,
"hidden": false,
+ "notes": "## Contexte\n\nPiloté par **Marie D.**, budget voté en avril.\n\n> Le prestataire est engagé jusqu'au 31 janvier 2027.\n\n### À suivre\n\n- [x] cadrage validé en comité\n- [ ] relancer sur les maquettes\n - [ ] variante sombre\n- [ ] prévoir la reprise des anciennes URL\n",
"baselineDate": "2026-06-12",
"phases": [
{
@@ -17,7 +18,7 @@
"end": "2026-07-10",
"status": "done",
"milestone": false,
- "notes": "Ateliers avec les trois pôles. Périmètre arrêté le 8 juillet.",
+ "notes": "Ateliers avec les trois pôles.\n\nPérimètre arrêté le **8 juillet**.",
"baseline": { "start": "2026-06-15", "end": "2026-07-03" }
},
{
@@ -69,6 +70,7 @@
"tags": ["infra", "interne"],
"collapsed": false,
"hidden": false,
+ "notes": "Chantier interne, sans budget propre.\n\nContact hébergeur : [support](mailto:support@example.org)\n",
"phases": [
{
"id": "audit",
@@ -95,7 +97,7 @@
"end": "2026-09-11",
"status": "blocked",
"milestone": false,
- "notes": "En attente de la fenêtre de maintenance côté hébergeur."
+ "notes": "**Bloquée** : en attente de la fenêtre de maintenance côté hébergeur.\n\n- [x] demande déposée le 3 août\n- [ ] créneau confirmé\n"
},
{
"id": "bascule-generale",
@@ -124,6 +126,7 @@
"tags": ["interne", "RH"],
"collapsed": true,
"hidden": false,
+ "notes": "",
"phases": [
{
"id": "recensement",
diff --git a/docs/decisions.md b/docs/decisions.md
index 6c509d4..346d567 100644
--- a/docs/decisions.md
+++ b/docs/decisions.md
@@ -601,3 +601,111 @@ revenir des années avant ce qu'on regardait serait une punition pour avoir cons
Deux conséquences dans le code, toutes deux de bon aloi : les noms de mois et `formaterDateLongue`
sont remontés de `timeline.js` vers `model.js`, et les pastilles de tags dans `tags.js`. Deux vues
sœurs les emploient, aucune n'a à importer l'autre pour écrire « septembre ».
+
+## 25. Les notes sont en markdown, et le projet en a
+
+Une phase avait des notes : un `textarea` de quatre lignes au bas du panneau, du texte brut. Un
+projet n'en avait aucune. Les deux manques se tiennent.
+
+**Le projet en avait besoin.** La frise dit quand les choses arrivent ; elle ne dit pas qui pilote,
+sur quel budget, ni pourquoi le prestataire est engagé jusqu'en janvier. Ce contexte-là finissait
+dans les notes de la première phase venue, où il n'a rien à faire — il survit à la phase, il ne
+survit pas à sa suppression. Le champ est donc monté d'un cran, et le panneau d'un projet reçoit le
+même bloc que celui d'une phase : renommer un projet et renommer une phase sont la même opération à
+un niveau près (décision 21), y écrire une note aussi.
+
+**Quatre lignes fixes étaient un plafond arbitraire.** Les autres champs du panneau ont une taille
+dictée par leur contenu — une date en fait dix caractères, un statut en fait quatre. Une note n'a
+aucune taille propre : elle fait ce qu'on a à dire. Elle prend donc toute la hauteur que les champs
+au-dessus lui laissent, avec un plancher sous lequel le corps du panneau défile plutôt que de
+l'écraser.
+
+### Pourquoi un analyseur maison
+
+Le markdown demandait une dépendance, ou du code. Une dépendance aurait été le premier
+`node_modules` de l'outil, contre la décision 10, pour une syntaxe dont on n'emploie ici qu'une
+poignée de formes. On a donc écrit `js/markdown.js` : un sous-ensemble, environ trois cents lignes,
+sans DOM ni réseau — ce qui le rend testable sous Node comme `model.js`, le rendu proprement dit
+vivant dans `notes.js`.
+
+Le choix du sous-ensemble suit ce qu'on écrit dans une note de suivi : titres, emphase, code,
+listes, cases à cocher, liens, citations, séparateur. Pas de tableaux ni d'images — ils ne tiennent
+pas dans un volet latéral. Pas de HTML brut, jamais : le DOM est bâti nœud par nœud, sans un seul
+`innerHTML`. Un planning s'échange, on en ouvre un qu'un autre a écrit, et une note est du texte
+libre ; la seule façon sûre d'en afficher est de ne jamais laisser le navigateur l'interpréter comme
+du balisage. Les URL passent une liste blanche de schémas — `http`, `https`, `mailto` — et un lien
+refusé retombe sur son libellé plutôt que de disparaître.
+
+Deux écarts au standard, assumés. **Un retour à la ligne en est un** : CommonMark recolle les lignes
+d'un paragraphe et exige deux espaces en fin de ligne pour un vrai retour, piège invisible dans un
+champ où l'on jette une petite liste à la volée. Et **ce qui n'est pas reconnu reste du texte** :
+`**gras` sans fermeture s'affiche tel quel, parce qu'une note à moitié écrite est l'état normal
+d'une note et que l'aperçu doit suivre la frappe sans à-coups.
+
+### On lit d'abord, on écrit ensuite
+
+Le bloc s'ouvre sur l'**aperçu** quand la note existe, sur la **saisie** quand elle est vide. C'est
+l'usage : on ouvre une phase bloquée pour relire pourquoi, une phase vierge pour y consigner
+quelque chose. Un crayon bascule d'un état à l'autre, et la barre d'outils ne paraît qu'en saisie —
+en aperçu elle ne commanderait rien, et sa place revient au texte.
+
+Six boutons, pas plus : gras, italique, titre, liste, case à cocher, lien. Le markdown entier reste
+accessible au clavier puisque c'est du texte ; la barre n'est là que pour épargner la syntaxe des
+formes courantes. Une barre exhaustive prendrait deux rangées dans le volet et repousserait la note
+d'autant.
+
+Les **cases à cocher se cliquent dans l'aperçu**, et c'est le seul geste de l'aperçu qui modifie les
+données. Une note de suivi contient des choses à faire ; les cocher en rouvrant la saisie pour
+transformer un `[ ]` en `[x]` serait une corvée absurde. La bascule réécrit **la seule ligne visée
+dans le texte d'origine** plutôt que de régénérer la note depuis l'arbre : régénérer normaliserait
+au passage les marqueurs, l'indentation et l'emphase de l'utilisateur, qui verrait sa mise en forme
+réécrite pour avoir coché une case.
+
+Dans la **vue par mois**, en revanche, les cases sont inertes. La liste est en lecture seule
+(décision 24) et une case cliquable y serait la seule chose qu'on puisse modifier — une exception
+isolée dans une vue dont toute la promesse est qu'on n'y casse rien. On les voit cochées ou non, ce
+qui est justement ce qu'on vient y chercher quand on fait le point.
+
+### `Ctrl+Z` doit continuer de marcher
+
+Une barre d'outils écrit dans le champ à la place de l'utilisateur, et la façon évidente de le faire
+— affecter `champ.value` — **vide la pile d'annulation** du navigateur. `Ctrl+Z` cesse alors de
+remonter au-delà du bouton pressé, et la frappe qui précédait devient irrécupérable. C'est un prix
+qu'on ne peut pas faire payer à un champ de saisie : annuler est le réflexe le plus élémentaire
+qu'on ait devant du texte, et l'ancien champ de notes, lui, l'honorait sans qu'on ait rien à écrire.
+
+Toutes les écritures passent donc par `document.execCommand('insertText')`, qui laisse le navigateur
+enregistrer l'opération comme si elle avait été tapée. L'API est marquée obsolète et reste sans
+remplaçant pour cet usage : les `InputEvent` synthétiques qui devaient lui succéder ne modifient
+rien, un navigateur ignorant les événements qu'il n'a pas produits. Tenir notre propre historique
+reviendrait à réimplémenter `Ctrl+Z`, `Ctrl+Y` et la fusion des frappes voisines — sans commune
+mesure avec le service rendu par un champ de notes. Si la commande échoue, on retombe sur
+l'affectation directe : on perd l'annulation, jamais la saisie.
+
+Deux gardes vont avec. `execCommand` écrit **là où est le focus**, sans considération pour l'élément
+qu'on croit viser : on vérifie donc que le champ l'a bien pris, faute de quoi la commande irait
+insérer du markdown dans le champ « Nom ». Et une insertion vide n'en est pas une — c'est une
+suppression, que `insertText` ne traite pas partout, d'où le passage par `delete`.
+
+Une seule écriture y échappe : cocher une case depuis l'aperçu, où le champ est masqué et ne peut
+pas prendre le focus. Le geste vide donc la pile — sans grande conséquence, puisqu'on n'était pas en
+train d'y taper.
+
+### Les infobulles s'aplatissent
+
+Les **infobulles** de la frise ne peuvent pas montrer de rendu : l'attribut `title` du
+navigateur ne connaît que le texte. Les notes y sont donc **aplaties** — marques retirées, et
+remplacées par un caractère qui porte le même sens là où il y en a un : `•` pour un tiret, `☐` et
+`☑` pour une case, `│` pour une citation. Sans cela, survoler une barre afficherait la source
+markdown, astérisques comprises, soit une note *moins* lisible qu'avant qu'on ne l'enrichisse. Deux
+infobulles taisaient d'ailleurs les notes et les disent maintenant : celle d'un jalon — c'est
+pourtant là qu'on consigne une décision — et celle du libellé d'un projet, seul endroit de la frise
+où ses notes à lui peuvent se lire, faute de barre qui lui appartienne.
+
+### Le format passe en version 4
+
+Ajouter un champ n'oblige à rien : une version 3 se relit sans encombre. Mais le validateur
+reconstruit chaque projet champ par champ et laisse tomber ce qu'il ne connaît pas. Sans
+incrémenter, un binaire antérieur ouvrirait un fichier plus riche que lui sans broncher et en
+effacerait toutes les notes de projet à la première sauvegarde. Le numéro ne sert qu'à cela : faire
+échouer bruyamment ce qui échouerait silencieusement.
diff --git a/docs/modele-donnees.md b/docs/modele-donnees.md
index e21908a..492d873 100644
--- a/docs/modele-donnees.md
+++ b/docs/modele-donnees.md
@@ -9,7 +9,7 @@ dans un diff git et éditable à la main.
```json
{
- "version": 3,
+ "version": 4,
"projects": [
{
"id": "site-web",
@@ -18,6 +18,7 @@ dans un diff git et éditable à la main.
"tags": ["client", "web"],
"collapsed": false,
"hidden": false,
+ "notes": "## Contexte\n\nPiloté par **Marie D.**\n\n- [x] cadrage validé\n- [ ] recette",
"baselineDate": "2026-06-12",
"phases": [
{
@@ -27,7 +28,7 @@ dans un diff git et éditable à la main.
"end": "2026-08-20",
"status": "done",
"milestone": false,
- "notes": "Ateliers avec les trois pôles.",
+ "notes": "Ateliers avec les trois pôles.\n\nPérimètre arrêté le **8 juillet**.",
"baseline": { "start": "2026-07-25", "end": "2026-08-10" }
},
{
@@ -49,11 +50,17 @@ dans un diff git et éditable à la main.
| Champ | Type | Description |
|---|---|---|
-| `version` | entier | Version du format. Vaut `3`. Sert à détecter un fichier trop ancien au chargement. |
+| `version` | entier | Version du format. Vaut `4`. Sert à détecter un fichier trop ancien au chargement. |
| `projects` | tableau | Les projets, dans l'ordre d'affichage des couloirs. |
-Un fichier en version `2` — celle d'avant les tags — se charge sans rien demander : ses projets
-reçoivent une liste de tags vide, et il est réécrit en version `3` à la première sauvegarde.
+Un fichier plus ancien se charge sans rien demander et est réécrit au format courant à la première
+sauvegarde : une version `2` — celle d'avant les tags — voit ses projets recevoir une liste de tags
+vide, une version `3` — celle d'avant les notes de projet — des notes vides.
+
+Un fichier écrit par une version **plus récente** est en revanche refusé. C'est la raison d'être du
+numéro : le validateur reconstruit chaque projet champ par champ et laisse tomber ce qu'il ne connaît
+pas, si bien qu'un binaire antérieur ouvrirait sans broncher un fichier plus riche que lui et en
+effacerait les champs inconnus à la première sauvegarde.
## Projet
@@ -65,6 +72,7 @@ reçoivent une liste de tags vide, et il est réécrit en version `3` à la prem
| `tags` | tableau | Étiquettes libres servant à filtrer la frise. Trié, sans doublon, éventuellement vide. |
| `collapsed` | booléen | `true` si le couloir est plié. |
| `hidden` | booléen | `true` si le projet est masqué de la frise. Ses données restent intactes. |
+| `notes` | chaîne | Texte libre en markdown, éventuellement vide. Ce que la frise ne sait pas dire : contexte, interlocuteurs, décisions. |
| `baselineDate` | chaîne ou absent | Date à laquelle la référence a été figée. Absent si elle ne l'a jamais été. |
| `phases` | tableau | Les phases, triées par date de début croissante. |
@@ -78,7 +86,7 @@ reçoivent une liste de tags vide, et il est réécrit en version `3` à la prem
| `end` | chaîne | Date de fin **incluse**, `AAAA-MM-JJ`. |
| `status` | chaîne | `todo`, `doing`, `done` ou `blocked`. |
| `milestone` | booléen | `true` pour un jalon. |
-| `notes` | chaîne | Texte libre, éventuellement vide. |
+| `notes` | chaîne | Texte libre en markdown, éventuellement vide. |
| `baseline` | objet ou absent | Dates de référence : `{ "start": …, "end": … }`. Absent tant que la référence n'a pas été figée. |
## Règles de validation
@@ -99,6 +107,9 @@ Appliquées par `js/model.js` et couvertes par `tests/model.test.js`.
- `tags` est un tableau de chaînes. Le tableau lui-même et le type de ses éléments sont vérifiés —
un tag mal typé serait un tag qu'on croit poser et qui ne filtre rien. Leur *contenu*, en revanche,
est normalisé sans erreur (voir ci-dessous).
+- `notes`, de projet comme de phase, est ramenée à `""` si ce n'est pas une chaîne. Le champ n'engage
+ aucun calcul, contrairement aux dates : bloquer le chargement d'un planning entier pour lui serait
+ disproportionné.
## Tags
@@ -130,6 +141,43 @@ temporelle. Voir [decisions.md](decisions.md), section 16.
Un fichier invalide n'est jamais réparé en silence : le chargement échoue avec un message qui pointe
le projet et la phase fautifs, pour que le fichier puisse être corrigé à la main.
+## Notes
+
+Un projet et une phase portent chacun un champ `notes`, en **markdown**. Rien n'est stocké d'autre
+que le texte source : le rendu est refait à l'affichage par `js/markdown.js` (analyse) et
+`js/notes.js` (mise en DOM). Un fichier reste donc lisible et modifiable à la main, et une note
+écrite hors de l'outil s'affiche sans conversion.
+
+Le markdown reconnu est un **sous-ensemble**, choisi pour ce qu'on écrit dans une note de suivi :
+
+| Syntaxe | Rendu |
+|---|---|
+| `# Titre` à `###### Titre` | Titres, décalés de deux niveaux sous celui du panneau |
+| `**gras**`, `*italique*`, `_italique_` | Emphase |
+| `` `code` `` et blocs ```` ``` ```` | Code littéral |
+| `- item`, `* item`, `1. item` | Listes, imbricables à l'indentation |
+| `- [ ] item`, `- [x] item` | Cases à cocher, cliquables dans le panneau |
+| `[texte](https://…)` | Lien |
+| `> cité` | Citation, réanalysée (elle peut contenir une liste) |
+| `---` | Séparateur |
+| `\*` | Le caractère littéral qui suit la contre-oblique |
+
+Trois écarts assumés au markdown standard :
+
+- **Un retour à la ligne en est un.** Le standard recolle les lignes d'un même paragraphe et exige
+ deux espaces en fin de ligne pour un vrai retour ; c'est un piège invisible dans un champ de
+ saisie, où l'on écrit souvent une petite liste à la volée.
+- **Ce qui n'est pas reconnu reste du texte.** `**gras` sans fermeture s'affiche tel quel plutôt que
+ d'avaler la suite : une note à moitié écrite est l'état normal d'une note.
+- **Ni tableaux, ni images, ni HTML brut.** Les deux premiers ne tiennent pas dans un volet latéral ;
+ le troisième rouvrirait la porte à l'injection que la construction du DOM nœud par nœud ferme.
+
+Les **liens** ne sont suivis que si leur schéma est `http`, `https` ou `mailto` — liste blanche, et
+non liste noire. Un planning s'échange, et rien ne garantit que la note affichée a été écrite par
+celui qui la lit. Un lien refusé n'est pas effacé : son libellé redevient du texte.
+
+Voir [decisions.md](decisions.md), section 25.
+
## Dates : conventions
Les dates sont manipulées comme des **chaînes `AAAA-MM-JJ`**, pas comme des objets `Date`. Cela évite
diff --git a/index.html b/index.html
index c900d4d..517c12d 100644
--- a/index.html
+++ b/index.html
@@ -142,10 +142,12 @@
Jalon (une seule date, affichée en losange)
-
+
+