From 8f8707c461f1a8ed5f5c5324d3f74ae6d5aa951b Mon Sep 17 00:00:00 2001 From: Bertrand Benjamin Date: Sun, 2 Aug 2026 05:09:18 +0200 Subject: [PATCH] =?UTF-8?q?Ajouter=20une=20vue=20des=20t=C3=A2ches=20et=20?= =?UTF-8?q?jalons=20group=C3=A9s=20par=20mois?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La frise répond mal à « qu'est-ce qui arrive le mois prochain » : la question traverse les projets, alors que la frise les range précisément en couloirs séparés. Il fallait balayer une colonne de dates à travers tous les couloirs en gardant en tête ce qu'on avait vu plus haut — d'où le tableur ressorti à côté. Deux boutons dans la barre d'outils basculent vers une liste qui déroule toutes les phases, projets mêlés, dans l'ordre du calendrier. Chaque ligne se lit quand, chez qui, puis quoi : période, projet et ses tags, intitulé, statut. Une phase est rattachée au seul mois de son début. La répéter dans chaque mois traversé aurait donné un tableau de charge — utile, mais c'est une autre vue, et le doublon coûte la lecture en diagonale qui fait tout l'intérêt de celle-ci. Sa fin est écrite sur la ligne, ce qui suffit à voir qu'elle déborde. Les mois sans rien ne sont pas représentés : l'absence se lit déjà dans le saut d'un titre au suivant. La liste ne s'édite pas, et ce n'est pas une lacune à combler : déplacer une phase demande de voir ce qu'elle chevauche et ce qu'elle repousse, or c'est justement ce voisinage que l'ordre chronologique dissout. Seules les pastilles de tags restent cliquables — filtrer ne change pas les données. Le filtre par tags vaut pour les deux vues, sans rien changer à sa mécanique, si bien qu'on ne se demande jamais si l'une montre plus que l'autre. L'œil (hidden) écarte un projet de la liste comme il écarte ses barres. La vue choisie n'est pas enregistrée, contrairement à la largeur de la colonne des libellés : cette largeur est un réglage d'écran, la vue est un geste de consultation. L'outil s'ouvre donc sur la frise, celle où l'on modifie. Le défilement horizontal de la frise, lui, est mémorisé le temps de l'aller-retour, puisque masquer un élément lui fait perdre son scrollLeft. Deux extractions au passage, chacune parce que deux vues sœurs en ont besoin et qu'aucune n'a à importer l'autre : les noms de mois et le formatage des dates rejoignent model.js, les pastilles de tags un module tags.js. Co-Authored-By: Claude Opus 5 --- README.md | 33 +++++- css/style.css | 256 ++++++++++++++++++++++++++++++++++++++++++++ docs/decisions.md | 47 ++++++++ index.html | 23 ++++ js/app.js | 97 ++++++++++++++++- js/model.js | 104 ++++++++++++++++++ js/mois.js | 220 +++++++++++++++++++++++++++++++++++++ js/tags.js | 71 ++++++++++++ js/timeline.js | 81 +------------- tests/model.test.js | 123 +++++++++++++++++++++ 10 files changed, 971 insertions(+), 84 deletions(-) create mode 100644 js/mois.js create mode 100644 js/tags.js diff --git a/README.md b/README.md index 4e36838..03c438f 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,25 @@ Une fois un projet en place : Faire défiler jusqu'au bord droit élargit la frise vers le futur, et jusqu'au bord gauche vers le passé. Le bouton **Aujourd'hui** ramène la vue sur la date du jour. +### La vue par mois + +Les deux boutons **Frise** et **Par mois**, en haut à gauche, changent de regard sur les mêmes +données. La vue par mois déroule toutes les tâches et tous les jalons du planning en une seule +liste, projets mêlés, groupés par mois — celui où chaque phase **commence**. C'est la réponse à +« qu'est-ce qui arrive le mois prochain », que la frise donne mal parce qu'elle sépare les projets +en couloirs. + +Chaque ligne se lit dans cet ordre : quand — la période, `31 → 13 nov.` quand la phase déborde du +mois —, chez qui — le projet et ses tags —, puis quoi — l'intitulé de la tâche ou du jalon — et +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. + +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. + ### Filtrer par tag Un projet peut porter des **tags** — `client`, `interne`, `R&D` —, saisis séparés par des virgules @@ -119,7 +138,8 @@ existants sont proposés en un clic, et un tag disparaît dès que plus aucun pr Ils s'affichent en petites pastilles colorées dans la colonne de gauche, et une barre de filtres apparaît sous la barre d'outils dès qu'un tag existe. Cliquer un tag — dans la barre de filtres ou directement sur la pastille d'un projet — restreint la frise à ceux qui le portent ; recliquer le -retire. +retire. Le filtre vaut pour les **deux vues** : il ne change pas quand on passe de la frise à la +liste par mois. Cocher plusieurs tags **cumule les conditions** : `client` + `urgent` ne montre que les projets portant les deux. Le filtre n'est pas enregistré, il ne vaut que pour la session en cours. Le bouton @@ -148,7 +168,10 @@ par les champs du panneau de détail. Il n'y a qu'une seule échelle, donc aucun réglage de zoom à comprendre. - **Une frise sans fin** : elle s'élargit d'elle-même quand le défilement approche d'un bord, ce qui permet d'aller planifier dans un futur qu'aucune phase n'occupe encore. -- **Des tags sur les projets**, et un filtre qui restreint la frise à ceux qui les portent tous. +- **Une vue par mois** : toutes les tâches et tous les jalons en une liste, projets mêlés, groupés + par mois de début. Elle se lit, elle ne s'édite pas. +- **Des tags sur les projets**, et un filtre qui restreint les deux vues à ceux qui les portent + tous. - **Repère « aujourd'hui »** et masquage d'un projet. ## Ce que l'outil ne fait pas @@ -208,7 +231,7 @@ go test ./... # le serveur ``` Côté navigateur : validation des dates, bornes d'un projet, calcul de dérive, génération des -identifiants, normalisation et filtrage des tags. Côté serveur : lecture et écriture du planning, +identifiants, normalisation et filtrage des tags, regroupement des phases par mois. Côté serveur : lecture et écriture du planning, refus des corps invalides, rotation des sauvegardes, écriture atomique, résolution du chemin de données. Aucune dépendance de part ni d'autre, les lanceurs intégrés à Node et à Go suffisent. @@ -218,9 +241,11 @@ données. Aucune dépendance de part ni d'autre, les lanceurs intégrés à Node |---|---| | `main.go` | Sert l'interface embarquée, expose `GET`/`PUT` sur `/api/data`, écrit les sauvegardes | | `build.sh` | Compile les binaires des cinq plateformes dans `dist/` | -| `js/model.js` | Données et règles métier : CRUD, validation, bornes, référence, dérive, tags | +| `js/model.js` | Données et règles métier : CRUD, validation, bornes, référence, dérive, tags, regroupement par mois | | `js/storage.js` | Dialogue avec le serveur, sauvegarde debouncée, indicateur d'état | | `js/timeline.js` | Rendu de la frise : échelle, couloirs, barres, jalons, pli/dépli | +| `js/mois.js` | Rendu de la vue par mois : groupes, lignes, ancrage sur le mois courant | +| `js/tags.js` | Pastilles de tags, partagées par les deux vues | | `js/drag.js` | Glisser et redimensionner les barres | | `js/detail.js` | Panneau de détail d'une phase | | `js/projet.js` | Panneau des paramètres d'un projet : nom, couleur, tags | diff --git a/css/style.css b/css/style.css index 152e685..afe4fcf 100644 --- a/css/style.css +++ b/css/style.css @@ -34,6 +34,11 @@ --rayon: 6px; --ombre: 0 1px 2px rgb(15 23 42 / 0.08); + + /* Seule taille de la vue par mois à descendre sous celle du corps (14 px). + Elle est réservée à ce qui commente la liste — compte d'un mois, repère + « ce mois-ci », pastilles de tags — et jamais à ce qui la compose. */ + --taille-annotation: 12px; } @media (prefers-color-scheme: dark) { @@ -145,6 +150,36 @@ button[aria-pressed="true"] { color: #fff; } +/* --- onglets de vue ------------------------------------------------------- */ + +/* Deux boutons accolés en un seul bloc, pour qu'ils se lisent comme un choix + entre deux états et non comme deux commandes indépendantes. */ +.onglets { + display: flex; +} + +.onglets button { + border-radius: 0; + white-space: nowrap; +} + +.onglets button:first-child { + border-radius: var(--rayon) 0 0 var(--rayon); +} + +.onglets button:last-child { + border-radius: 0 var(--rayon) var(--rayon) 0; + margin-left: -1px; +} + +/* La couleur de l'état pressé vient de la règle générale `button[aria-pressed]`. + Ne reste ici que l'empilement : sans lui, la bordure du voisin passerait + par-dessus la sienne à la jointure. */ +.onglets button[aria-pressed="true"] { + position: relative; + z-index: 1; +} + /* --- barre de filtres ----------------------------------------------------- */ .filtres { @@ -1032,6 +1067,227 @@ button[aria-pressed="true"] { padding: 0; } +/* --- vue par mois -------------------------------------------------------- */ + +/* Rien n'est positionné en absolu ici, contrairement à la frise : la liste + n'a pas d'échelle temporelle, elle empile des lignes dans l'ordre du + calendrier. Un seul axe de défilement, le vertical. + `position: relative` fait du conteneur l'`offsetParent` de ses groupes, dont + `revelerMoisCourant` (mois.js) lit l'`offsetTop`. */ +.mois { + flex: 1; + min-height: 0; + position: relative; + overflow-y: auto; + overflow-x: hidden; + padding: 0 16px 40px; +} + +/* Une largeur de lecture plutôt que toute la page : sur un écran large, une + ligne étirée sur 2000 px sépare la date de son statut par un désert. */ +.mois-groupe { + max-width: 1080px; + margin: 0 auto; +} + +/* Le titre reste visible tant qu'on parcourt son mois : sans lui à l'écran, + une ligne isolée ne dit plus de quel mois elle relève, puisque les dates y + sont abrégées au quantième. */ +.mois-groupe__titre { + position: sticky; + top: 0; + z-index: 1; + display: flex; + align-items: baseline; + gap: 10px; + margin: 0; + padding: 18px 2px 7px; + /* Même taille que les lignes qu'il coiffe : c'est la graisse et le filet qui + le détachent, pas un corps plus gros. Déclaré tout de même, sinon la taille + par défaut des `h2` s'appliquerait. */ + font-size: 14px; + font-weight: 600; + background: var(--fond); + border-bottom: 1px solid var(--bordure-forte); +} + +.mois-groupe__nom { + text-transform: capitalize; +} + +/* Les annotations du titre — le repère du mois courant et le compte — partagent + la taille des pastilles de tags : ce sont les seuls éléments de la vue à + descendre sous la taille du corps, parce qu'ils commentent la liste au lieu + d'en faire partie. */ +.mois-groupe__courant { + font-size: var(--taille-annotation); + font-weight: 500; + padding: 1px 8px; + border-radius: 9px; + background: color-mix(in srgb, var(--aujourdhui) 16%, var(--surface)); + color: var(--aujourdhui); +} + +.mois-groupe__compte { + margin-left: auto; + font-size: var(--taille-annotation); + font-weight: 400; + color: var(--texte-doux); +} + +.mois-entrees { + list-style: none; + margin: 0; + padding: 0; +} + +/* Colonnes fixes d'une ligne à l'autre : c'est l'alignement vertical des dates, + des noms et des statuts qui rend la liste parcourable en diagonale. Les + colonnes de texte sont en `minmax(0, …)` pour que le rognage se fasse à + l'intérieur, sans jamais pousser le statut hors de l'écran. + L'ordre suit celui de la lecture : quand, chez qui, puis quoi. Le ressort + (`1fr`) est sur l'intitulé de la phase, dernière colonne de texte, pour que + 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; + align-items: center; + gap: 10px; + padding: 6px 2px; + border-bottom: 1px solid var(--bordure); +} + +.mois-entree:hover { + background: 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 { + width: 14px; + height: 8px; + border-radius: 2px; + background: var(--couleur-projet); +} + +.mois-entree--todo .mois-entree__marque { + background: color-mix(in srgb, var(--couleur-projet) 28%, var(--surface)); + border: 1px solid var(--couleur-projet); +} + +.mois-entree--done .mois-entree__marque { + background: color-mix(in srgb, var(--couleur-projet) 55%, #64748b); +} + +.mois-entree--blocked .mois-entree__marque { + background: repeating-linear-gradient( + 45deg, + var(--couleur-projet), + var(--couleur-projet) 3px, + color-mix(in srgb, var(--danger) 75%, var(--couleur-projet)) 3px, + color-mix(in srgb, var(--danger) 75%, var(--couleur-projet)) 6px + ); +} + +.mois-entree--jalon .mois-entree__marque { + width: 10px; + height: 10px; + margin-left: 2px; + border-radius: 0; + transform: rotate(45deg); +} + +.mois-entree--jalon.mois-entree--blocked .mois-entree__marque { + background: var(--danger); +} + +/* Chiffres tabulaires : les quantièmes s'alignent d'une ligne à l'autre, ce qui + fait de la colonne une échelle lisible plutôt qu'une pile de textes. */ +.mois-entree__dates { + font-variant-numeric: tabular-nums; + color: var(--texte-doux); + white-space: nowrap; +} + +.mois-entree__projet, +.mois-entree__nom { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Un jalon se distingue à la lecture, pas seulement à sa marque : sa date est + un rendez-vous, pas une durée. */ +.mois-entree--jalon .mois-entree__nom { + font-weight: 600; +} + +/* 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. */ +.mois-entree__projet { + color: var(--texte-doux); +} + +.mois-entree__projet::before { + content: ""; + display: inline-block; + width: 7px; + height: 7px; + margin-right: 6px; + border-radius: 50%; + background: var(--couleur-projet); + vertical-align: baseline; +} + +/* Les tags gardent leur colonne même quand le projet n'en porte aucun : c'est + une colonne de grille, de largeur fixe, pas un élément qui flotterait à la + suite du projet. Sans cette fixité, l'intitulé des phases démarrerait à une + abscisse différente selon le nombre de tags de leur projet. */ +.mois-entree .tags { + max-width: 100%; +} + +/* Les pastilles de la colonne des libellés sont taillées pour une colonne + étroite (10 px) ; ici la place ne manque pas, et une ligne où seuls les tags + seraient minuscules se lirait mal. Elles rejoignent donc la taille des + annotations, comme dans la barre de filtres. */ +.mois-entree .tag { + font-size: var(--taille-annotation); + padding: 1px 8px; + border-radius: 10px; + max-width: 100%; +} + +.mois-entree__statut { + text-align: right; + color: var(--texte-doux); + white-space: nowrap; +} + +.mois-entree__statut[data-statut="doing"] { + color: var(--accent); + font-weight: 500; +} + +.mois-entree__statut[data-statut="blocked"] { + color: var(--danger); + font-weight: 500; +} + +/* Une phase terminée reste consultable mais recule d'un plan : ce qui compte + dans une liste tournée vers ce qui arrive, c'est ce qui n'est pas fait. */ +.mois-entree--done .mois-entree__nom { + color: var(--texte-doux); +} + +.mois__vide { + max-width: 1080px; + margin: 48px auto; + text-align: center; + color: var(--texte-doux); +} + /* --- états divers -------------------------------------------------------- */ .vide { diff --git a/docs/decisions.md b/docs/decisions.md index dd8f00c..3c6a1a4 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -531,3 +531,50 @@ planning dans git, ce que son format texte indenté rend parfaitement lisible (v Ce changement a rendu concurrentes deux étapes qui ne l'étaient pas — la copie et le remplacement dépendent maintenant d'un état partagé, la date de la dernière sauvegarde. Un verrou les sérialise, ce qui protège au passage du cas où deux onglets enregistrent en même temps. + +## 24. Une vue par mois, en lecture seule, à côté de la frise + +La frise répond très bien à « comment ce projet-là s'étale-t-il », et très mal à « qu'est-ce qui +arrive le mois prochain ». La seconde question traverse les projets, alors que la frise les range +précisément en couloirs séparés : il faut balayer une colonne de dates à travers tous les couloirs, +en gardant en tête ce qu'on a vu plus haut. Avec cinq projets et une échelle de six pixels par +jour, c'est déjà pénible ; c'est la raison pour laquelle on ressortait un tableur à côté. + +D'où une seconde vue, atteinte par deux boutons dans la barre d'outils. Trois arbitrages la +définissent. + +**Une phase est rattachée au mois de son début, et à lui seul.** Une phase de trois mois n'apparaît +donc qu'une fois. La répéter dans chaque mois traversé aurait donné un tableau de charge — +« qu'est-ce qui tourne en octobre » —, qui est une question légitime mais une autre vue : le +doublon coûte la lecture en diagonale qui fait tout l'intérêt de celle-ci, et brouille le compte +affiché en tête de mois. La fin est écrite sur la ligne (« 31 → 13 nov. »), ce qui suffit à voir +qu'une phase déborde et jusqu'où. + +**Les mois sans rien ne sont pas représentés.** Un planning à horizon de deux ans a des trous ; +intercaler « novembre 2026 — rien » ferait défiler du vide, alors que l'absence se lit déjà dans le +saut d'un titre au suivant. + +**La liste ne s'édite pas.** Aucune poignée, aucun glisser, et un clic sur une ligne n'ouvre pas le +panneau de détail. Ce n'est pas une lacune à combler plus tard : déplacer une phase demande de voir +ce qu'elle chevauche et ce qu'elle repousse, c'est-à-dire le voisinage que la frise montre et que +la liste, ordonnée par date et non par projet, a justement dissous. Une date qu'on modifie sans +voir son contexte est une date qu'on modifie à l'aveugle. La vue sert donc à faire le point, la +frise à arbitrer. Seules les pastilles de tags restent cliquables — filtrer ne change pas les +données, c'est une façon de regarder. + +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 +deux tamis de la frise s'appliquent d'ailleurs à l'identique : l'œil (`hidden`) écarte un projet de +la liste comme il écarte ses barres. + +La vue choisie n'est pas enregistrée, contrairement à la largeur de la colonne des libellés +(décision 18). Cette largeur est un réglage d'écran, la vue est un geste de consultation : on passe +par la liste pour faire le point, on revient à la frise pour travailler. L'outil s'ouvre donc +toujours sur la frise, celle où l'on modifie. Le défilement horizontal de la frise, lui, est +mémorisé le temps de l'aller-retour : masquer un élément lui fait perdre son `scrollLeft`, et +revenir des années avant ce qu'on regardait serait une punition pour avoir consulté la liste. + +Deux conséquences dans le code, toutes deux de bon aloi : les noms de mois et `formaterDateLongue` +sont remontés de `timeline.js` vers `model.js`, et les pastilles de tags dans `tags.js`. Deux vues +sœurs les emploient, aucune n'a à importer l'autre pour écrire « septembre ». diff --git a/index.html b/index.html index 2e89473..c900d4d 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,22 @@

Frise multi-projets

+ + +
+ + +
+
+ + +