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.