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:
13
README.md
13
README.md
@@ -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
|
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.
|
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
|
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
|
chevauche, donc la frise ; c'est là que les dates et les notes se modifient. Seuls les tags et les
|
||||||
restent cliquables, puisque filtrer ne change rien aux données. Le bouton **Aujourd'hui** ramène ici
|
notes y sont cliquables, et ni filtrer ni déplier ne touche aux données. Le bouton **Aujourd'hui**
|
||||||
sur le mois en cours, et la vue affichée n'est pas mémorisée : l'outil s'ouvre toujours sur la
|
ramène ici sur le mois en cours, et la vue affichée n'est pas mémorisée : l'outil s'ouvre toujours
|
||||||
frise.
|
sur la frise.
|
||||||
|
|
||||||
### Filtrer par tag
|
### Filtrer par tag
|
||||||
|
|
||||||
|
|||||||
@@ -1150,7 +1150,7 @@ button[aria-pressed="true"] {
|
|||||||
l'espace disponible se déverse à sa droite plutôt qu'au milieu de la ligne. */
|
l'espace disponible se déverse à sa droite plutôt qu'au milieu de la ligne. */
|
||||||
.mois-entree {
|
.mois-entree {
|
||||||
display: grid;
|
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;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 6px 2px;
|
padding: 6px 2px;
|
||||||
@@ -1161,6 +1161,18 @@ button[aria-pressed="true"] {
|
|||||||
background: var(--surface);
|
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
|
/* 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. */
|
jalon, et les mêmes textures de statut — c'est le même objet vu autrement. */
|
||||||
.mois-entree__marque {
|
.mois-entree__marque {
|
||||||
@@ -1222,6 +1234,48 @@ button[aria-pressed="true"] {
|
|||||||
font-weight: 600;
|
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
|
/* 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
|
pas ce qu'on cherche. Le contraste sépare les deux colonnes de texte mieux
|
||||||
qu'un filet ne le ferait. */
|
qu'un filet ne le ferait. */
|
||||||
|
|||||||
@@ -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
|
frise à arbitrer. Seules les pastilles de tags restent cliquables — filtrer ne change pas les
|
||||||
données, c'est une façon de regarder.
|
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
|
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
|
é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
|
liste, ce qui rend la bascule sûre — on ne se demande jamais si l'une montre plus que l'autre. Les
|
||||||
|
|||||||
40
js/app.js
40
js/app.js
@@ -381,13 +381,45 @@ refs.onglets.addEventListener('click', (evenement) => {
|
|||||||
if (onglet) basculerVue(onglet.dataset.vue);
|
if (onglet) basculerVue(onglet.dataset.vue);
|
||||||
});
|
});
|
||||||
|
|
||||||
// La vue par mois n'a ni barres ni poignées, mais ses pastilles de tags
|
// La vue par mois n'a ni barres ni poignées. Ses deux seuls gestes ne touchent
|
||||||
// filtrent comme celles de la colonne des libellés.
|
// pas au planning : filtrer sur un tag, et déplier les notes d'une phase.
|
||||||
refs.vueMois.addEventListener('click', (evenement) => {
|
refs.vueMois.addEventListener('click', (evenement) => {
|
||||||
const pastille = evenement.target.closest('[data-action="tag"]');
|
const cible = evenement.target.closest('[data-action]');
|
||||||
if (pastille) basculerTag(pastille.dataset.tag);
|
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
|
// 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.className = `mois-entree mois-entree--${phase.status}`;
|
||||||
ligne.style.setProperty('--couleur-projet', projet.color);
|
ligne.style.setProperty('--couleur-projet', projet.color);
|
||||||
if (phase.milestone) ligne.classList.add('mois-entree--jalon');
|
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.title = infobulle(projet, phase);
|
||||||
|
|
||||||
|
ligne.append(triangle(phase));
|
||||||
|
|
||||||
// La marque reprend les formes de la frise — losange pour un jalon, barre
|
// 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 à
|
// pour une tâche — pour qu'on reconnaisse au même dessin ce qu'on a appris à
|
||||||
// lire là-bas.
|
// lire là-bas.
|
||||||
@@ -172,9 +177,59 @@ function construireEntree({ projet, phase }, tagsActifs) {
|
|||||||
statut.textContent = LIBELLES_STATUT[phase.status];
|
statut.textContent = LIBELLES_STATUT[phase.status];
|
||||||
ligne.append(statut);
|
ligne.append(statut);
|
||||||
|
|
||||||
|
if (phase.notes) ligne.append(notes(phase));
|
||||||
|
|
||||||
return ligne;
|
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) {
|
function infobulle(projet, phase) {
|
||||||
const jours = duree(phase);
|
const jours = duree(phase);
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user