diff --git a/README.md b/README.md index 03c438f..abc78aa 100644 --- a/README.md +++ b/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 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 diff --git a/css/style.css b/css/style.css index afe4fcf..b4ebc67 100644 --- a/css/style.css +++ b/css/style.css @@ -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. */ diff --git a/docs/decisions.md b/docs/decisions.md index 3c6a1a4..6c509d4 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -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 diff --git a/js/app.js b/js/app.js index a794164..12d8674 100644 --- a/js/app.js +++ b/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 // --------------------------------------------------------------------------- diff --git a/js/mois.js b/js/mois.js index f3eb936..84e29c6 100644 --- a/js/mois.js +++ b/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 (