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:
40
js/app.js
40
js/app.js
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
55
js/mois.js
55
js/mois.js
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user