Dessiner l'horizon sur la frise, l'éditer et le glisser

La barre d'un projet envisagé est pâle et hachurée, aux bords fondus.
Elle se glisse et se redimensionne comme une phase, mais accrochée au
mois : le geste existe, il ne permet pas de poser une date qu'on n'a
pas. Le panneau ajoute deux champs de mois et trois crans.

Une barre « Afficher » filtre par état, disjonctive là où celle des
tags est conjonctive — d'où des boutons carrés et non des pastilles.
Les écartés en sont décochés au départ.

Engager démarre au premier jour de l'horizon, et clore un projet
demande confirmation quand il reste des phases à écraser.

Au passage : releasePointerCapture peut lever quand le pointeur a
disparu, ce qui sautait l'application du geste. Le nettoyage en est
maintenant isolé.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 14:58:05 +02:00
parent 8404f12656
commit 60a91ba5b2
6 changed files with 1018 additions and 40 deletions

View File

@@ -21,6 +21,10 @@
--accent: #3b82f6;
--danger: #dc2626;
--aujourdhui: #ef4444;
/* Réservé à ce qui réclame un arbitrage sans être une erreur : un horizon
dépassé n'est pas une faute, c'est une décision à reprendre. D'où une teinte
distincte de `--danger`, qui signale ce qui ne va pas. */
--attention: #b45309;
--hauteur-barre: 26px;
/* Barre cumulative d'un projet, et fantôme qui la double. Ils sont frères,
@@ -53,6 +57,7 @@
--texte: #f1f5f9;
--texte-doux: #94a3b8;
--ombre: 0 1px 2px rgb(0 0 0 / 0.3);
--attention: #fbbf24;
}
}
@@ -209,6 +214,68 @@ button[aria-pressed="true"] {
white-space: nowrap;
}
/* --- filtre par état ------------------------------------------------------ */
/* Même bandeau que le filtre par tags, mais des boutons carrés et non des
pastilles rondes : les deux filtres jouent en sens contraire — cocher un tag
resserre, cocher un état élargit — et rien ne serait plus trompeur que deux
commandes identiques aux effets opposés. */
.etats {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 16px;
background: var(--surface);
border-bottom: 1px solid var(--bordure);
flex-shrink: 0;
}
.etats__liste {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.etat-filtre {
font-size: 12px;
padding: 3px 10px;
border: 1px solid var(--bordure-forte);
border-radius: var(--rayon);
background: var(--surface);
color: var(--texte-doux);
cursor: pointer;
white-space: nowrap;
}
/* Un état affiché est plein, un état masqué est creux. La différence se lit au
remplissage et pas seulement à la teinte : la barre en porte quatre côte à
côte, et la couleur seule n'y suffirait pas. */
.etat-filtre[aria-pressed="true"] {
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
border-color: var(--accent);
color: var(--accent);
font-weight: 500;
}
.etat-filtre:hover:not(:disabled) {
border-color: var(--accent);
}
/* Le dernier état affiché ne se décoche pas : tout masquer reviendrait à vider
la frise sans qu'aucun geste ne l'ait demandé. */
.etat-filtre:disabled {
cursor: default;
opacity: 0.75;
}
/* Les écartés se distinguent des trois états vivants, cochés ou non : c'est le
seul dont l'ouverture change la nature de ce qu'on regarde. */
.etat-filtre[data-etat="discarded"][aria-pressed="true"] {
background: color-mix(in srgb, var(--attention) 12%, var(--surface));
border-color: var(--attention);
color: var(--attention);
}
/* --- tags ----------------------------------------------------------------- */
/* Chaque pastille porte sa teinte dans `--teinte-tag`, calculée par hachage du
@@ -539,6 +606,48 @@ button[aria-pressed="true"] {
color: #16a34a;
}
/* Un projet envisagé dont l'horizon est passé — ou qui n'en a jamais eu. Même
forme que la dérive, dont c'est le pendant : l'une dit qu'un projet engagé a
glissé, l'autre qu'un projet envisagé attend qu'on tranche.
Volontairement sobre : ce signal doit rester lisible sur une colonne qui en
porte plusieurs, sans devenir l'alerte qu'on apprend à ignorer. */
.libelle-projet__adater {
flex: 0 0 auto;
font-size: 11px;
font-weight: 500;
padding: 1px 5px;
border-radius: 10px;
white-space: nowrap;
background: color-mix(in srgb, var(--attention) 15%, transparent);
color: var(--attention);
cursor: default;
}
/* --- états du cycle de vie ------------------------------------------------ */
/* Un projet terminé **reste** sur la frise : elle est notre mémoire, et l'en
retirer perdrait le bilan (décision 27). Il s'éteint donc au lieu de
disparaître — assez pour que l'œil aille d'abord à ce qui est vivant, pas
assez pour qu'on ne puisse plus le lire.
L'écarté va plus loin : il n'est visible que si on l'a demandé au filtre, et
le gris dit qu'il ne se passera plus rien. */
.libelle-projet[data-etat="completed"],
.couloir--projet[data-etat="completed"] {
opacity: 0.65;
}
.libelle-projet[data-etat="discarded"],
.couloir--projet[data-etat="discarded"] {
opacity: 0.5;
filter: grayscale(0.85);
}
/* Le nom d'un projet écarté se barre : l'opacité seule dirait « en retrait »,
là où il faut lire « on ne le fera pas ». */
.libelle-projet[data-etat="discarded"] .libelle-projet__nom {
text-decoration: line-through;
}
.plier {
border: 0;
background: none;
@@ -731,10 +840,17 @@ button[aria-pressed="true"] {
background: rgb(255 255 255 / 0.5);
}
.barre:hover .poignee {
.barre:hover .poignee,
.barre-horizon:hover .poignee {
opacity: 1;
}
/* Sur fond pâle et hachuré, un voile blanc à demi transparent ne se voit pas :
les poignées d'un horizon prennent la teinte de son projet. */
.barre-horizon .poignee {
background: color-mix(in srgb, var(--couleur-projet) 55%, transparent);
}
.poignee--debut {
left: 0;
border-radius: 4px 0 0 4px;
@@ -858,6 +974,67 @@ button[aria-pressed="true"] {
height: 4px;
}
/* --- horizon d'un projet envisagé ---------------------------------------- */
/* La barre d'un projet qui n'a pas encore de phase. Tout, ici, sert à dire que
les dates ne sont pas fermes (décision 27) :
- les **bords fondus** (`mask-image`), qui suppriment le trait net qu'on
pourrait prendre pour une date de début ou de fin ;
- les **hachures obliques**, qui la distinguent au premier coup d'œil d'une
barre de phase, y compris pour qui ne perçoit pas la nuance de teinte.
Elle se glisse et se redimensionne comme une barre de phase, mais accrochée au
**mois** plutôt qu'au lundi : le geste est possible, il ne permet simplement
pas de poser une date qu'on n'a pas.
Sa largeur porte à elle seule l'incertitude : un mois pour un projet mûr, une
large plage pour un projet vague. */
.barre-horizon {
position: absolute;
top: calc((var(--hauteur-ligne) - var(--hauteur-barre)) / 2);
height: var(--hauteur-barre);
border-radius: 4px;
color: var(--texte-doux);
font-size: 12px;
font-style: italic;
line-height: var(--hauteur-barre);
cursor: grab;
user-select: none;
z-index: 2;
background:
repeating-linear-gradient(
-45deg,
color-mix(in srgb, var(--couleur-projet) 26%, transparent),
color-mix(in srgb, var(--couleur-projet) 26%, transparent) 5px,
transparent 5px,
transparent 10px
),
color-mix(in srgb, var(--couleur-projet) 12%, var(--surface));
mask-image: linear-gradient(
90deg,
transparent,
#000 14px,
#000 calc(100% - 14px),
transparent
);
}
/* Un horizon dépassé se teinte, en écho à la pastille « à dater » de la colonne.
La marque est portée deux fois — dans la colonne et sur la frise — parce que
les deux se lisent séparément : on parcourt l'une, on balaie l'autre. */
.barre-horizon--depasse {
background:
repeating-linear-gradient(
-45deg,
color-mix(in srgb, var(--attention) 30%, transparent),
color-mix(in srgb, var(--attention) 30%, transparent) 5px,
transparent 5px,
transparent 10px
),
color-mix(in srgb, var(--attention) 12%, var(--surface));
}
/* --- panneau de détail --------------------------------------------------- */
/* Non modal : aucun voile derrière lui, la frise reste manipulable pendant
@@ -1011,6 +1188,106 @@ button[aria-pressed="true"] {
margin-top: 4px;
}
/* Les deux actes du cycle de vie, côte à côte et à parts égales : aucun des deux
n'est plus courant que l'autre, et les mettre l'un sous l'autre suggérerait
une progression qui n'existe pas — on termine *ou* on écarte. */
.panneau__actions--cycle {
display: flex;
gap: 8px;
}
.panneau__actions--cycle button {
flex: 1 1 0;
white-space: nowrap;
}
/* --- cycle de vie dans le panneau ---------------------------------------- */
/* Où en est le projet. Une ligne de texte et non un sélecteur : l'état se
déduit des données (décision 27), il n'y a rien à y choisir. */
.panneau__etat {
margin: 0;
font-size: var(--taille-annotation);
font-weight: 600;
letter-spacing: 0.02em;
color: var(--texte-doux);
}
.panneau__etat[data-etat="discarded"] {
color: var(--attention);
}
.horizon {
border: 1px solid var(--bordure);
border-radius: var(--rayon);
padding: 8px 10px 10px;
margin: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.horizon > legend {
font-size: var(--taille-annotation);
color: var(--texte-doux);
padding: 0 4px;
}
/* Les deux bornes côte à côte : c'est un intervalle, et le lire sur deux lignes
ferait perdre ce que sa largeur veut dire. */
.horizon__intervalle {
display: flex;
gap: 8px;
}
.horizon__intervalle label {
flex: 1 1 0;
display: flex;
flex-direction: column;
gap: 3px;
font-size: var(--taille-annotation);
color: var(--texte-doux);
}
.horizon__crans {
display: flex;
gap: 6px;
}
.horizon__crans button {
flex: 1 1 0;
padding: 4px 6px;
font-size: var(--taille-annotation);
border: 1px solid var(--bordure-forte);
border-radius: var(--rayon);
background: var(--surface);
color: var(--texte);
cursor: pointer;
}
.horizon__crans button:hover:not(:disabled) {
border-color: var(--accent);
color: var(--accent);
}
.horizon__crans button:disabled {
opacity: 0.45;
cursor: default;
}
.horizon__aide {
margin: 0;
font-size: var(--taille-annotation);
line-height: 1.35;
color: var(--texte-doux);
}
/* Un horizon passé le dit ici comme il le dit sur la frise et dans la colonne :
c'est le même signal, vu depuis les trois endroits où l'on travaille. */
.horizon__aide[data-alerte="true"] {
color: var(--attention);
}
/* --- bloc de notes -------------------------------------------------------- */
/* Le bloc prend toute la hauteur que les champs au-dessus lui laissent. C'est
@@ -1791,6 +2068,48 @@ button.mois-entree__plier {
color: var(--texte-doux);
}
/* --- ligne d'un projet envisagé ------------------------------------------ */
/* Mêmes colonnes qu'une ligne de phase — la liste reste une grille qu'on lit en
diagonale —, mais tout y dit que rien n'est arrêté : l'italique de l'intitulé
et les hachures de la marque, qui reprennent celles de la barre sur la frise.
Ce qu'on a appris à lire d'un côté vaut de l'autre. */
.mois-entree--envisage .mois-entree__nom {
font-style: italic;
color: var(--texte-doux);
}
.mois-entree--envisage .mois-entree__marque {
background: repeating-linear-gradient(
-45deg,
color-mix(in srgb, var(--couleur-projet) 60%, transparent),
color-mix(in srgb, var(--couleur-projet) 60%, transparent) 3px,
transparent 3px,
transparent 6px
);
border: 1px solid color-mix(in srgb, var(--couleur-projet) 45%, transparent);
}
.mois-entree__statut[data-statut="considered"],
.mois-entree__statut[data-statut="discarded"] {
font-style: italic;
}
/* Un projet écarté qui n'a jamais été engagé passe aussi par cette ligne — il
n'a que son horizon pour se situer. Il s'y lit comme sur la frise : éteint et
barré, pour qu'on ne le confonde pas avec ce qui va arriver. */
.mois-entree--envisage[data-etat="discarded"] {
opacity: 0.55;
}
.mois-entree--envisage[data-etat="discarded"] .mois-entree__projet {
text-decoration: line-through;
}
.mois-entree--envisage[data-etat="discarded"] .mois-entree__statut {
color: var(--attention);
}
.mois__vide {
max-width: 1080px;
margin: 48px auto;