Rendre les notes en markdown, et en donner au projet

Une phase avait quatre lignes de texte brut ; un projet n'avait rien. Les deux
manques se tiennent : la frise ne dit pas qui pilote ni sur quel budget, et ce
contexte finissait dans les notes de la première phase venue, où il ne survit
pas à sa suppression.

Le bloc de notes est le même dans les deux panneaux et prend toute la hauteur
restante : les autres champs ont une taille dictée par leur contenu, une note
fait ce qu'on a à dire. Il s'ouvre sur l'aperçu quand la note existe, sur la
saisie quand elle est vide.

Les cases à cocher se cliquent dans l'aperçu — seul geste de l'aperçu qui touche
aux données. La bascule réécrit la seule ligne visée plutôt que de régénérer la
note, qui verrait sinon sa mise en forme normalisée. Inertes dans la vue par
mois, en lecture seule (décision 24).

La barre d'outils écrit par `document.execCommand('insertText')` et non en
affectant `value`, qui viderait la pile d'annulation du navigateur : `Ctrl+Z`
doit continuer de marcher. Le DOM est bâti nœud par nœud, sans un `innerHTML`.

Les infobulles aplatissent le markdown — `title` ne connaît que le texte —, et
celles d'un jalon et d'un libellé de projet les affichent désormais.

Le format passe en version 4 : le validateur reconstruit chaque projet champ par
champ, un binaire antérieur effacerait les notes de projet à la première
sauvegarde.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 18:35:17 +02:00
parent e04877daae
commit ff147a17b9
16 changed files with 1150 additions and 35 deletions

View File

@@ -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.

View File

@@ -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