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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user