Rendre les notes d'une phase consultables depuis la vue par mois

Elles n'étaient accessibles que dans l'infobulle de la ligne, où l'on ne pense
pas à aller les chercher et d'où l'on ne peut rien copier. Or une note est
précisément ce qu'on va lire quand on fait le point — « pourquoi cette phase
est-elle bloquée ».

Un triangle en tête de ligne les déplie en dessous, sur les phases qui en
portent. C'est celui qui plie un projet sur la frise, au même endroit et avec
la même rotation : ce qui s'apprend d'un côté vaut de l'autre. La gouttière se
lit ainsi d'un coup d'œil comme la liste de ce qui a quelque chose à dire.

Toute la ligne déplie, pas seulement le triangle. Une première version faisait
de l'intitulé lui-même le bouton, avec un chevron à sa suite : la cible était
trop petite pour un geste aussi banal, et rien ne justifiait cette parcimonie
dans une vue où le clic n'a aucun autre effet. Le triangle reste comme
indicateur plutôt que comme cible, le curseur et le fond au survol disent que
la ligne réagit. Les lignes sans notes, elles, ne réagissent pas : promettre un
geste qui ne fait rien est pire que ne rien promettre.

Il n'en reste pas moins un vrai button : la ligne n'est pas focusable, c'est
donc le seul chemin clavier vers les notes.

Les notes restent repliées par défaut pour deux raisons. La densité d'abord :
trois lignes sous chaque intitulé étalent un mois sur deux écrans et ruinent la
lecture en diagonale. Et parce qu'une phase sur trois seulement en porte, les
afficher d'office donnerait des lignes de hauteurs inégales, ce qui casse
l'alignement des colonnes sur lequel repose tout le balayage du regard.

L'état déplié vit dans le DOM et nulle part ailleurs : il ne survit pas à un
changement de filtre, qui reconstruit la liste. Le remonter dans l'état de
l'application achèterait cher une continuité que personne ne réclame — quand la
liste change, on la relit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 05:40:08 +02:00
parent 8f8707c461
commit a0b8ef9dcf
5 changed files with 178 additions and 9 deletions

View File

@@ -123,11 +123,16 @@ mois —, chez qui — le projet et ses tags —, puis quoi — l'intitulé de l
enfin son statut. Une phase longue n'est **pas** répétée dans les mois qu'elle traverse : elle
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.
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 se modifient. Seules les pastilles de tags y
restent cliquables, puisque filtrer ne change rien aux données. 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.
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**
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.
### Filtrer par tag

View File

@@ -1150,7 +1150,7 @@ button[aria-pressed="true"] {
l'espace disponible se déverse à sa droite plutôt qu'au milieu de la ligne. */
.mois-entree {
display: grid;
grid-template-columns: 16px 124px minmax(0, 200px) 172px minmax(0, 1fr) 86px;
grid-template-columns: 14px 16px 124px minmax(0, 200px) 172px minmax(0, 1fr) 86px;
align-items: center;
gap: 10px;
padding: 6px 2px;
@@ -1161,6 +1161,18 @@ button[aria-pressed="true"] {
background: var(--surface);
}
/* Toute la ligne déplie ses notes, ce qui doit se voir avant le clic : le
curseur et le fond qui s'appuie au survol le disent, le triangle de la
gouttière dit ce qui va se passer. Les lignes sans notes ne réagissent pas —
promettre un geste qui ne fait rien est pire que ne rien promettre. */
.mois-entree--commentee {
cursor: pointer;
}
.mois-entree--commentee:hover {
background: color-mix(in srgb, var(--accent) 6%, var(--surface));
}
/* Reprise des formes de la frise : une barre pour une tâche, un losange pour un
jalon, et les mêmes textures de statut — c'est le même objet vu autrement. */
.mois-entree__marque {
@@ -1222,6 +1234,48 @@ button[aria-pressed="true"] {
font-weight: 600;
}
/* Triangle de dépli des notes, en gouttière. C'est le même bouton que celui qui
plie un projet sur la frise (`.plier`), au même endroit et avec le même
geste ; il en reprend donc l'aspect, en un peu plus lisible — ici il est seul
dans sa colonne, là-bas il jouxte le nom du projet.
Les lignes sans notes gardent la case, vide, pour que les colonnes suivantes
ne se décalent pas d'une ligne à l'autre. */
.mois-entree__plier {
justify-self: center;
padding: 0;
border: 0;
background: none;
font-size: 12px;
line-height: 1;
color: var(--texte-doux);
transition: transform 0.15s;
}
/* La case des lignes sans notes est un `span` vide : elle réserve la colonne,
elle ne se clique pas. */
button.mois-entree__plier {
cursor: pointer;
}
.mois-entree__plier[aria-expanded="true"] {
transform: rotate(90deg);
}
.mois-entree__plier:hover {
color: var(--texte);
}
/* Les notes prennent une seconde rangée, en retrait de la gouttière et de la
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. */
.mois-entree__notes {
grid-column: 3 / -1;
margin: 2px 0 6px;
white-space: pre-wrap;
color: var(--texte-doux);
}
/* Le projet passe devant la phase, mais reste en retrait : il situe, il n'est
pas ce qu'on cherche. Le contraste sépare les deux colonnes de texte mieux
qu'un filet ne le ferait. */

View File

@@ -562,6 +562,29 @@ voir son contexte est une date qu'on modifie à l'aveugle. La vue sert donc à f
frise à arbitrer. Seules les pastilles de tags restent cliquables — filtrer ne change pas les
données, c'est une façon de regarder.
Les **notes** d'une phase, elles, se consultent ici : un triangle en tête de ligne les déplie en
dessous. Lire n'est pas éditer, et une note est précisément ce qu'on va chercher quand on fait le
point — « pourquoi cette phase est-elle bloquée ». Elles restent repliées par défaut, pour deux
raisons. La densité d'abord : trois lignes de notes sous chaque intitulé étalent un mois sur deux
écrans et ruinent la lecture en diagonale. Et parce qu'elles ne sont pas régulières — une phase sur
trois en porte —, les afficher d'office produirait des lignes de hauteurs inégales, ce qui casse
l'alignement des colonnes sur lequel repose tout le balayage du regard.
**Toute la ligne déplie, pas seulement le triangle.** Une première version faisait de l'intitulé
lui-même le bouton, avec un chevron à sa suite : la cible était trop petite pour un geste aussi
banal, et rien ne justifiait cette parcimonie dans une vue où le clic n'a aucun autre effet. Le
triangle reste, mais comme indicateur plutôt que comme cible — c'est lui qui dit ce qui va se
passer, et il reprend le triangle qui plie un projet sur la frise, au même endroit et avec la même
rotation : ce qui s'apprend d'un côté vaut de l'autre. Le curseur et le fond au survol signalent
que la ligne réagit. Les lignes sans notes, elles, ne réagissent pas : promettre un geste qui ne
fait rien est pire que ne rien promettre.
Le triangle n'en reste pas moins un vrai `button` : la ligne, elle, n'est pas focusable, et c'est
donc lui le seul chemin clavier vers les notes. L'état déplié vit dans le DOM et nulle part
ailleurs — il ne survit pas à un changement de filtre, qui reconstruit la liste. Le remonter dans
l'état de l'application pour qu'il y survive serait payer cher une continuité que personne ne
réclame : quand la liste change, on la relit.
Le filtre par tags, lui, vaut pour les deux vues sans rien changer à sa mécanique : c'est le même
état, la même barre, au même endroit. Un filtre posé sur la frise se retrouve en passant à la
liste, ce qui rend la bascule sûre — on ne se demande jamais si l'une montre plus que l'autre. Les

View File

@@ -381,13 +381,45 @@ refs.onglets.addEventListener('click', (evenement) => {
if (onglet) basculerVue(onglet.dataset.vue);
});
// La vue par mois n'a ni barres ni poignées, mais ses pastilles de tags
// filtrent comme celles de la colonne des libellés.
// La vue par mois n'a ni barres ni poignées. Ses deux seuls gestes ne touchent
// pas au planning : filtrer sur un tag, et déplier les notes d'une phase.
refs.vueMois.addEventListener('click', (evenement) => {
const pastille = evenement.target.closest('[data-action="tag"]');
if (pastille) basculerTag(pastille.dataset.tag);
const cible = evenement.target.closest('[data-action]');
if (!cible) return;
if (cible.dataset.action === 'tag') {
basculerTag(cible.dataset.tag);
return;
}
if (cible.dataset.action === 'notes') basculerNotes(cible.closest('.mois-entree'));
});
// Toute la ligne déplie, pas seulement son triangle : viser une cible de dix
// pixels pour lire un commentaire est une exigence inutile, et il n'y a rien
// d'autre à faire d'un clic sur une ligne d'une vue qui ne s'édite pas. Le
// gestionnaire ci-dessus s'occupe des tags, qui eux ont leur propre effet.
refs.vueMois.addEventListener('click', (evenement) => {
if (evenement.target.closest('[data-action]')) return;
const ligne = evenement.target.closest('.mois-entree--commentee');
if (ligne) basculerNotes(ligne);
});
/**
* Ouvre ou referme les notes d'une ligne.
*
* La bascule est appliquée directement au DOM, sans repasser par `dessiner` :
* ce qui est déplié relève de l'écran, pas du planning. Un rendu complet
* replierait d'ailleurs tout le reste au passage.
*/
function basculerNotes(ligne) {
const bouton = ligne.querySelector('.mois-entree__plier');
const deplie = bouton.getAttribute('aria-expanded') !== 'true';
bouton.setAttribute('aria-expanded', String(deplie));
bouton.title = deplie ? 'Masquer les notes' : 'Afficher les notes';
ligne.querySelector('.mois-entree__notes').hidden = !deplie;
}
// ---------------------------------------------------------------------------
// Largeur de la colonne des libellés
// ---------------------------------------------------------------------------

View File

@@ -134,8 +134,13 @@ function construireEntree({ projet, phase }, tagsActifs) {
ligne.className = `mois-entree mois-entree--${phase.status}`;
ligne.style.setProperty('--couleur-projet', projet.color);
if (phase.milestone) ligne.classList.add('mois-entree--jalon');
// C'est cette classe qui rend la ligne entière cliquable (app.js) : sans
// notes, il n'y a rien à déplier et le clic ne doit rien promettre.
if (phase.notes) ligne.classList.add('mois-entree--commentee');
ligne.title = infobulle(projet, phase);
ligne.append(triangle(phase));
// La marque reprend les formes de la frise — losange pour un jalon, barre
// pour une tâche — pour qu'on reconnaisse au même dessin ce qu'on a appris à
// lire là-bas.
@@ -172,9 +177,59 @@ function construireEntree({ projet, phase }, tagsActifs) {
statut.textContent = LIBELLES_STATUT[phase.status];
ligne.append(statut);
if (phase.notes) ligne.append(notes(phase));
return ligne;
}
/**
* Triangle de dépli des notes, en gouttière à gauche de la ligne.
*
* C'est le même objet que le triangle qui plie un projet sur la frise, à la
* même place et avec le même geste : ce qui s'apprend d'un côté vaut de
* l'autre. Il ne paraît que sur les phases qui ont des notes — une phase sur
* trois environ —, si bien que la gouttière se lit d'un coup d'œil comme la
* liste de ce qui a quelque chose à dire.
*
* Les lignes sans notes reçoivent tout de même une case vide : les colonnes de
* la grille sont comptées, et il n'y a pas de raison qu'une phase commentée
* décale toutes les autres.
*/
function triangle(phase) {
if (!phase.notes) {
const vide = document.createElement('span');
vide.className = 'mois-entree__plier';
return vide;
}
const bouton = document.createElement('button');
bouton.type = 'button';
bouton.className = 'mois-entree__plier';
bouton.dataset.action = 'notes';
bouton.setAttribute('aria-expanded', 'false');
// La ligne entière déplie, mais elle n'est pas focusable : ce bouton est le
// seul chemin clavier vers les notes, il lui faut donc un nom qui dise de
// quelle phase il parle.
bouton.setAttribute('aria-label', `Notes de ${phase.name}`);
bouton.title = 'Afficher les notes';
bouton.textContent = '▶';
return bouton;
}
/**
* Les notes occupent une seconde rangée de la grille, sous toutes les colonnes.
* `white-space: pre-wrap` (style.css) leur conserve leurs retours à la ligne :
* elles sont souvent une petite liste, et la mettre à plat en changerait le
* sens.
*/
function notes(phase) {
const bloc = document.createElement('p');
bloc.className = 'mois-entree__notes';
bloc.textContent = phase.notes;
bloc.hidden = true;
return bloc;
}
function infobulle(projet, phase) {
const jours = duree(phase);
return (