Ajouter une vue des tâches et jalons groupés par mois

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 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 05:09:18 +02:00
parent faac28172e
commit 8f8707c461
10 changed files with 971 additions and 84 deletions

View File

@@ -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 {