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:
321
css/style.css
321
css/style.css
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user