Revoir l'ergonomie des panneaux et de la colonne des projets

Les panneaux d'édition étaient modaux et la colonne de gauche tronquait
tout ce qu'elle portait. Sept corrections, mesurées sur data/exemple.json.

Panneaux
- Le voile qui recouvrait la page disparaît : la frise reste manipulable
  pendant l'édition, et cliquer une autre barre y bascule la sélection.
  Il rendait aussi le raccourci flèches inatteignable, le focus partant
  dans le champ « Nom » sans qu'on puisse cliquer ailleurs pour en sortir.
- Un clic dans une zone inerte referme le panneau : ce que rendait le
  voile, sans le calque qui figeait la frise.
- Les paramètres d'un projet quittent le <dialog> modal pour le même
  panneau qu'une phase, avec la même édition au fil de la saisie.
  « Nouveau projet » crée donc le projet aussitôt, comme le fait déjà
  l'ajout d'une phase : plus aucune création ne passe par un formulaire.

Colonne des projets
- Le nom ne disposait que de 56 px sur 250, et « client » s'affichait
  « c. ». Les commandes passent en surimpression au survol, les tags
  cèdent en bloc derrière un compteur, et le nom cède en dernier.
- La colonne se redimensionne, largeur conservée dans localStorage.
- L'œil disparaît : il faisait doublon avec l'entrée « Masquer » du menu.
  Un « + » le remplace, l'ajout d'une phase étant l'action la plus
  fréquente, et le nom du projet devient cliquable — il portait déjà un
  curseur « main » sans aucune action derrière.
- Plus de commande « Ajouter un jalon » : un jalon est une phase dont on
  coche la case, pas un autre objet.

Frise
- Le nom d'une phase reste accroché au bord visible quand sa barre
  s'enfonce sous la colonne.
- Cliquer un nom de phase hors écran ramène la frise sur elle.
- La barre cumulative d'un projet s'affiche aussi quand il est déplié,
  amincie pour ne pas se faire passer pour une barre de phase.

Arbitrages consignés dans docs/decisions.md, sections 8 et 17 à 21.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 10:42:35 +02:00
parent a2a5c5cb9c
commit eaca2f7813
9 changed files with 1032 additions and 333 deletions

View File

@@ -25,7 +25,9 @@
--hauteur-barre: 26px;
--hauteur-ligne: 38px;
--hauteur-entete: 66px; /* trois bandes de 22 px : année, mois, semaine */
--largeur-libelles: 250px;
/* Doit rester en accord avec `LARGEUR_LIBELLES.defaut` (app.js), sur lequel
retombe le double-clic de la poignée. */
--largeur-libelles: 300px;
--rayon: 6px;
--ombre: 0 1px 2px rgb(15 23 42 / 0.08);
@@ -268,6 +270,37 @@ button[aria-pressed="true"] {
border-bottom: 1px solid var(--bordure);
}
/* Zone de saisie large de 9 px pour un trait qui n'en fait qu'un : viser une
bordure d'un pixel à la souris est un exercice de patience. */
.poignee-colonne {
position: absolute;
top: 0;
bottom: 0;
right: -4px;
width: 9px;
cursor: col-resize;
z-index: 7;
}
.poignee-colonne::after {
content: "";
position: absolute;
inset: 0 4px;
background: var(--accent);
opacity: 0;
transition: opacity 0.12s;
}
.poignee-colonne:hover::after,
.poignee-colonne:focus-visible::after,
.poignee-colonne[data-glisse="true"]::after {
opacity: 1;
}
.poignee-colonne:focus-visible {
outline: none;
}
.grille__entete {
position: sticky;
top: 0;
@@ -369,6 +402,9 @@ button[aria-pressed="true"] {
/* --- libellés ------------------------------------------------------------ */
.libelle-projet {
--fond-libelle: color-mix(in srgb, var(--couleur-projet) 8%, var(--surface));
position: relative;
display: flex;
align-items: center;
gap: 6px;
@@ -376,25 +412,72 @@ button[aria-pressed="true"] {
padding: 0 8px 0 6px;
font-weight: 600;
border-bottom: 1px solid var(--bordure);
background: color-mix(in srgb, var(--couleur-projet) 8%, var(--surface));
background: var(--fond-libelle);
}
/* Commandes secondaires, en surimpression sur le bord droit de la ligne. Le
dégradé leur donne un fond opaque sans trancher : le nom qu'elles recouvrent
s'estompe au lieu d'être coupé net. */
.libelle-projet__commandes {
position: absolute;
top: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
gap: 2px;
padding: 0 6px 0 18px;
opacity: 0;
background: linear-gradient(to right, transparent, var(--fond-libelle) 18px);
transition: opacity 0.12s;
}
.libelle-projet:hover .libelle-projet__commandes,
.libelle-projet:focus-within .libelle-projet__commandes {
opacity: 1;
}
/* Un projet masqué garde ses commandes visibles : le menu est le seul chemin
pour le rétablir, il ne doit pas falloir le deviner sous le curseur. */
.libelle-projet--masque .libelle-projet__commandes {
opacity: 1;
}
/* Le nom est ce qui identifie le projet : il cède en dernier, et jamais
au-delà d'une largeur où il reste reconnaissable. Sous l'ancienne règle
(`min-width: 4em`), les tags et l'étiquette de dérive le réduisaient à
« Refon… » dans une colonne pourtant deux fois trop large pour lui. */
.libelle-projet__nom {
flex: 1;
/* Le nom cède la place aux pastilles, mais jamais entièrement : sous quatre
caractères il ne resterait qu'un « … » qui n'identifie plus rien. */
min-width: 4em;
flex: 1 1 auto;
min-width: 7em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
/* Les pastilles rognent le nom, pas l'inverse — mais elles s'effacent à leur
tour plutôt que de repousser l'œil et le menu hors de la colonne. */
.libelle-projet .tags {
flex: 0 1 auto;
max-width: 45%;
max-width: 40%;
}
/* Une pastille écrasée à « c. » occupe la place d'un tag sans en donner
l'information. Elle garde donc une largeur minimale et cède en bloc :
`pastillesTags` n'en affiche qu'un nombre fixe, suivies d'un compteur. */
.libelle-projet .tag {
flex: 0 0 auto;
min-width: 3.4em;
text-align: center;
}
.tag--reste {
flex: 0 0 auto;
align-self: center;
font-size: 10px;
font-weight: 500;
color: var(--texte-doux);
white-space: nowrap;
cursor: default;
}
.libelle-projet__derive {
@@ -429,19 +512,6 @@ button[aria-pressed="true"] {
transform: rotate(90deg);
}
.icone-oeil {
border: 0;
background: none;
padding: 2px 4px;
cursor: pointer;
opacity: 0.45;
font-size: 12px;
}
.icone-oeil:hover {
opacity: 1;
}
/* `display: block` et non `flex` : `text-overflow: ellipsis` ne s'applique pas
au texte d'un conteneur flex, et le nom serait coupé net. Le centrage
vertical passe donc par `line-height`. */
@@ -487,16 +557,36 @@ button[aria-pressed="true"] {
color: #fff;
font-size: 12px;
line-height: var(--hauteur-barre);
padding: 0 7px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: grab;
box-shadow: var(--ombre);
user-select: none;
z-index: 2;
}
/* Le nom se décale pour rester au bord de la vue quand la barre s'enfonce sous
la colonne des libellés — le cas d'une phase longue dont on ne voit que la
fin. Trois conditions le rendent possible, et aucune n'est décorative :
- `sticky` s'ancre au conteneur défilant le plus proche. Un `overflow:
hidden` sur `.barre` en ferait ce conteneur, et le décalage n'aurait plus
lieu : le rognage du texte trop long est donc porté ici, pas sur la barre.
- le décalage est plafonné par la boîte du parent, donc le nom ne sort jamais
de sa barre — d'où `inline-block`, qui laisse au texte la marge de manœuvre
qu'un bloc pleine largeur n'aurait pas.
- `left` vaut la largeur de la colonne des libellés : c'est là que commence
la portion réellement visible de la frise. */
.barre__nom {
position: sticky;
left: var(--largeur-libelles);
display: inline-block;
max-width: 100%;
padding: 0 7px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: top;
pointer-events: none;
}
.barre:hover {
filter: brightness(1.08);
}
@@ -604,11 +694,11 @@ button[aria-pressed="true"] {
z-index: 3;
}
/* --- projet plié --------------------------------------------------------- */
/* --- barre cumulative d'un projet ---------------------------------------- */
/* Barre unique segmentée par phase. Lecture seule : voir docs/decisions.md,
section 8. */
.barre-pliee {
/* Barre unique segmentée par phase, affichée que le projet soit plié ou
déplié. Lecture seule : voir docs/decisions.md, section 8. */
.barre-cumulative {
position: absolute;
top: calc((var(--hauteur-ligne) - var(--hauteur-barre)) / 2);
height: var(--hauteur-barre);
@@ -645,6 +735,17 @@ button[aria-pressed="true"] {
opacity: 0.85;
}
/* Dépliée, la barre cumulative coiffe les phases du projet : elle s'amincit
pour ne pas se faire passer pour l'une d'elles — elle ne se glisse pas. */
.barre-cumulative--fine {
--hauteur-cumulative: 9px;
top: calc((var(--hauteur-ligne) - var(--hauteur-cumulative)) / 2);
height: var(--hauteur-cumulative);
border-radius: 3px;
box-shadow: none;
}
/* --- barre fantôme de référence ------------------------------------------ */
.fantome {
@@ -665,8 +766,18 @@ button[aria-pressed="true"] {
z-index: 1;
}
/* Le fantôme suit la barre qu'il double : sous une barre cumulative amincie,
il flotterait sinon dans le vide. Déclaré après `.fantome`, dont il partage
la spécificité — l'ordre est ce qui le fait gagner. */
.fantome--fin {
top: calc((var(--hauteur-ligne) + 9px) / 2);
height: 4px;
}
/* --- panneau de détail --------------------------------------------------- */
/* Non modal : aucun voile derrière lui, la frise reste manipulable pendant
l'édition et cliquer une autre barre y bascule la sélection. */
.panneau {
position: fixed;
top: 0;
@@ -682,6 +793,13 @@ button[aria-pressed="true"] {
flex-direction: column;
}
/* Le panneau reçoit le focus à l'ouverture pour que le clavier le suive, mais
il n'est pas un contrôle : un anneau de focus autour de tout le panneau
n'apprendrait rien et attirerait l'œil loin du champ à remplir. */
.panneau:focus {
outline: none;
}
.panneau__entete {
display: flex;
align-items: center;
@@ -771,13 +889,6 @@ button[aria-pressed="true"] {
margin-top: 4px;
}
.voile {
position: fixed;
inset: 0;
background: rgb(15 23 42 / 0.15);
z-index: 15;
}
/* --- menu contextuel ------------------------------------------------------ */
.menu {
@@ -830,7 +941,8 @@ button[aria-pressed="true"] {
border-top: 1px solid var(--bordure);
}
.menu-projet {
/* Commandes de la ligne d'un projet : ajouter une phase, ouvrir le menu. */
.commande-projet {
border: 0;
background: none;
padding: 2px 5px;
@@ -841,13 +953,16 @@ button[aria-pressed="true"] {
border-radius: 4px;
}
.menu-projet:hover {
.commande-projet:hover {
background: color-mix(in srgb, var(--accent) 16%, transparent);
color: var(--texte);
}
/* --- dialogues ------------------------------------------------------------ */
/* --- dialogue de confirmation --------------------------------------------- */
/* Seul `<dialog>` restant : les paramètres d'un projet ont rejoint un panneau
(voir `.panneau`). Modal ici à dessein — interrompre est le but quand une
action est irréversible. */
.dialogue {
padding: 0;
border: 1px solid var(--bordure-forte);
@@ -880,24 +995,6 @@ button[aria-pressed="true"] {
line-height: 1.5;
}
.dialogue label {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
font-weight: 500;
color: var(--texte-doux);
}
.dialogue input[type="text"] {
font: inherit;
padding: 7px 9px;
border: 1px solid var(--bordure-forte);
border-radius: var(--rayon);
background: var(--fond);
color: var(--texte);
}
.couleurs {
margin: 0;
padding: 0;
@@ -931,11 +1028,6 @@ button[aria-pressed="true"] {
box-shadow: inset 0 0 0 2px var(--surface);
}
.dialogue__erreur {
color: var(--danger) !important;
font-size: 12px;
}
.dialogue__actions {
display: flex;
justify-content: flex-end;