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) - + +
@@ -197,6 +199,13 @@
+ +
+
diff --git a/js/app.js b/js/app.js index 12d8674..e664dba 100644 --- a/js/app.js +++ b/js/app.js @@ -139,6 +139,7 @@ const panneau = creerPanneau( titre: $('panneau-titre'), erreur: $('erreur-phase'), champFin: $('champ-fin'), + notes: $('notes-phase'), supprimer: $('supprimer-phase'), fermer: $('fermer-panneau'), }, @@ -179,6 +180,7 @@ const panneauProjet = creerPanneauProjet( conteneurCouleurs: $('panneau-projet-couleurs'), suggestions: $('panneau-projet-suggestions'), blocSuggestions: $('panneau-projet-suggestions-bloc'), + notes: $('notes-projet'), supprimer: $('supprimer-projet'), fermer: $('fermer-panneau-projet'), }, diff --git a/js/detail.js b/js/detail.js index f00f54b..893d303 100644 --- a/js/detail.js +++ b/js/detail.js @@ -14,9 +14,16 @@ */ import { STATUTS, dateValide, modifierPhase } from './model.js'; +import { creerBlocNotes } from './notes.js'; export function creerPanneau(refs, rappels) { - const { panneau, formulaire, titre, erreur, champFin, supprimer, fermer } = refs; + const { panneau, formulaire, titre, erreur, champFin, notes, supprimer, fermer } = refs; + + // Le bloc pose lui-même un `