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

@@ -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 `<br>` 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);
}