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:
118
js/timeline.js
118
js/timeline.js
@@ -328,11 +328,16 @@ function libelleProjet(projet, tagsActifs = []) {
|
||||
const nom = document.createElement('span');
|
||||
nom.className = 'libelle-projet__nom';
|
||||
nom.textContent = projet.name;
|
||||
nom.dataset.action = 'parametres';
|
||||
nom.dataset.projet = projet.id;
|
||||
// Les pastilles rognent le nom quand la colonne se remplit : l'infobulle
|
||||
// reste le seul endroit où les deux se lisent toujours en entier.
|
||||
nom.title = projet.tags.length
|
||||
? `${projet.name}\nTags : ${projet.tags.join(', ')}`
|
||||
: projet.name;
|
||||
nom.title =
|
||||
(projet.tags.length ? `${projet.name}\nTags : ${projet.tags.join(', ')}` : projet.name) +
|
||||
// Le grisé de la ligne dit qu'il se passe quelque chose ; encore faut-il
|
||||
// pouvoir apprendre quoi, et comment revenir en arrière.
|
||||
(projet.hidden ? '\n\nMasqué de la frise — voir le menu ⋯ pour le rétablir.' : '') +
|
||||
'\n\nCliquer pour renommer, changer la couleur et les tags.';
|
||||
ligne.append(nom);
|
||||
|
||||
if (projet.tags.length) ligne.append(pastillesTags(projet.tags, tagsActifs));
|
||||
@@ -349,19 +354,30 @@ function libelleProjet(projet, tagsActifs = []) {
|
||||
ligne.append(etiquette);
|
||||
}
|
||||
|
||||
const oeil = document.createElement('button');
|
||||
oeil.className = 'icone-oeil';
|
||||
oeil.type = 'button';
|
||||
oeil.dataset.action = 'masquer';
|
||||
oeil.dataset.projet = projet.id;
|
||||
oeil.textContent = projet.hidden ? '🚫' : '👁';
|
||||
oeil.setAttribute('aria-label', projet.hidden ? `Afficher ${projet.name}` : `Masquer ${projet.name}`);
|
||||
ligne.append(oeil);
|
||||
// Les commandes sont posées *au-dessus* de la ligne, révélées au survol,
|
||||
// plutôt que rangées dans son flux. Elles y occupaient une soixantaine de
|
||||
// pixels en permanence — de quoi tronquer le nom de tous les projets dans une
|
||||
// colonne de largeur courante. En surimpression, le nom dispose de toute la
|
||||
// colonne et rien ne se déplace à l'approche du curseur.
|
||||
const commandes = document.createElement('span');
|
||||
commandes.className = 'libelle-projet__commandes';
|
||||
|
||||
// Ajouter une phase est de loin l'action la plus fréquente : elle mérite son
|
||||
// bouton, pas une entrée à aller chercher dans un menu.
|
||||
const ajout = document.createElement('button');
|
||||
ajout.className = 'commande-projet';
|
||||
ajout.type = 'button';
|
||||
ajout.dataset.action = 'ajouter';
|
||||
ajout.dataset.projet = projet.id;
|
||||
ajout.textContent = '+';
|
||||
ajout.title = `Ajouter une phase à ${projet.name}`;
|
||||
ajout.setAttribute('aria-label', ajout.title);
|
||||
commandes.append(ajout);
|
||||
|
||||
// Bouton de menu explicite : les actions du projet passaient auparavant par
|
||||
// un clic sur son nom, ce que rien n'indiquait.
|
||||
const menu = document.createElement('button');
|
||||
menu.className = 'menu-projet';
|
||||
menu.className = 'commande-projet';
|
||||
menu.type = 'button';
|
||||
menu.dataset.action = 'menu';
|
||||
menu.dataset.projet = projet.id;
|
||||
@@ -369,7 +385,9 @@ function libelleProjet(projet, tagsActifs = []) {
|
||||
menu.title = `Actions sur ${projet.name}`;
|
||||
menu.setAttribute('aria-label', `Actions sur ${projet.name}`);
|
||||
menu.setAttribute('aria-haspopup', 'menu');
|
||||
ligne.append(menu);
|
||||
commandes.append(menu);
|
||||
|
||||
ligne.append(commandes);
|
||||
|
||||
return ligne;
|
||||
}
|
||||
@@ -381,7 +399,15 @@ function libelleProjet(projet, tagsActifs = []) {
|
||||
* recliquer l'en retire. C'est le geste le plus court pour passer d'un projet
|
||||
* repéré à « montre-moi tous ceux qui lui ressemblent », sans remonter à la
|
||||
* barre de filtres.
|
||||
*
|
||||
* Au-delà de `PASTILLES_VISIBLES`, les tags se résument à un compteur. La
|
||||
* colonne est étroite et disputée : montrer quatre pastilles y revenait à les
|
||||
* rogner toutes jusqu'à l'initiale, ce qui coûtait la place d'un tag sans en
|
||||
* livrer le nom. Les tags masqués restent lisibles dans l'infobulle, et tous
|
||||
* sont cliquables depuis la barre de filtres.
|
||||
*/
|
||||
const PASTILLES_VISIBLES = 2;
|
||||
|
||||
function pastillesTags(tags, tagsActifs) {
|
||||
const conteneur = document.createElement('span');
|
||||
conteneur.className = 'tags';
|
||||
@@ -390,7 +416,16 @@ function pastillesTags(tags, tagsActifs) {
|
||||
// c'est la clé, pas la graphie, qui décide s'il est actif.
|
||||
const clesActives = new Set(tagsActifs.map(cleTag));
|
||||
|
||||
for (const tag of tags) {
|
||||
// Un tag présent dans le filtre courant passe devant : c'est celui qui
|
||||
// explique pourquoi le projet est à l'écran, il ne doit pas tomber dans le
|
||||
// compteur.
|
||||
const ordonnes = [...tags].sort(
|
||||
(a, b) => Number(clesActives.has(cleTag(b))) - Number(clesActives.has(cleTag(a)))
|
||||
);
|
||||
const montres = ordonnes.slice(0, PASTILLES_VISIBLES);
|
||||
const restants = ordonnes.slice(PASTILLES_VISIBLES);
|
||||
|
||||
for (const tag of montres) {
|
||||
const pastille = document.createElement('button');
|
||||
pastille.type = 'button';
|
||||
pastille.className = 'tag';
|
||||
@@ -406,6 +441,14 @@ function pastillesTags(tags, tagsActifs) {
|
||||
conteneur.append(pastille);
|
||||
}
|
||||
|
||||
if (restants.length) {
|
||||
const compteur = document.createElement('span');
|
||||
compteur.className = 'tag--reste';
|
||||
compteur.textContent = `+${restants.length}`;
|
||||
compteur.title = restants.join(', ');
|
||||
conteneur.append(compteur);
|
||||
}
|
||||
|
||||
return conteneur;
|
||||
}
|
||||
|
||||
@@ -421,12 +464,14 @@ function construireCouloirs(projets, echelle, selection) {
|
||||
couloirProjet.className = 'couloir couloir--projet';
|
||||
couloirProjet.style.setProperty('--couleur-projet', projet.color);
|
||||
|
||||
if (projet.collapsed) {
|
||||
const barre = construireBarrePliee(projet, echelle);
|
||||
if (barre) couloirProjet.append(barre);
|
||||
const fantome = construireFantome(projet, echelle);
|
||||
if (fantome) couloirProjet.append(fantome);
|
||||
}
|
||||
// La barre cumulative n'est plus réservée au mode plié : dépliée, elle
|
||||
// coiffe les phases du projet et garde sa forme d'ensemble sous les yeux
|
||||
// pendant qu'on travaille sur le détail. Sans elle, il fallait replier pour
|
||||
// savoir où en était le projet entier, donc perdre la vue qu'on éditait.
|
||||
const barre = construireBarreCumulative(projet, echelle);
|
||||
if (barre) couloirProjet.append(barre);
|
||||
const fantome = construireFantome(projet, echelle);
|
||||
if (fantome) couloirProjet.append(fantome);
|
||||
|
||||
elements.push(couloirProjet);
|
||||
|
||||
@@ -486,12 +531,20 @@ function construirePhase(projet, phase, echelle, selection) {
|
||||
barre.dataset.projet = projet.id;
|
||||
barre.dataset.phase = phase.id;
|
||||
barre.dataset.role = 'barre';
|
||||
barre.textContent = phase.name;
|
||||
barre.title =
|
||||
`${phase.name}\n${formaterDateLongue(phase.start)} → ${formaterDateLongue(phase.end)}` +
|
||||
`\n${LIBELLES_STATUT[phase.status]}` +
|
||||
(phase.notes ? `\n\n${phase.notes}` : '');
|
||||
|
||||
// Le nom vit dans son propre élément plutôt que directement dans la barre :
|
||||
// il est `sticky`, ce qui le maintient visible quand la barre s'enfonce sous
|
||||
// la colonne des libellés. Une phase de plusieurs mois pouvait autrement
|
||||
// occuper tout l'écran sans qu'on lise son nom, parti hors cadre à gauche.
|
||||
const nom = document.createElement('span');
|
||||
nom.className = 'barre__nom';
|
||||
nom.textContent = phase.name;
|
||||
barre.append(nom);
|
||||
|
||||
for (const bord of ['debut', 'fin']) {
|
||||
const poignee = document.createElement('div');
|
||||
poignee.className = `poignee poignee--${bord}`;
|
||||
@@ -505,15 +558,20 @@ function construirePhase(projet, phase, echelle, selection) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Barre unique du mode plié, segmentée par phase.
|
||||
* Lecture seule : aucun `data-role`, donc drag.js l'ignore.
|
||||
* Barre unique du projet, segmentée par phase — sa forme d'ensemble.
|
||||
*
|
||||
* Affichée qu'il soit plié ou déplié. Lecture seule dans les deux cas : aucun
|
||||
* `data-role`, donc drag.js l'ignore (docs/decisions.md, section 8).
|
||||
*/
|
||||
function construireBarrePliee(projet, echelle) {
|
||||
function construireBarreCumulative(projet, echelle) {
|
||||
const bornes = bornesProjet(projet);
|
||||
if (!bornes) return null;
|
||||
|
||||
const conteneur = document.createElement('div');
|
||||
conteneur.className = 'barre-pliee';
|
||||
// Dépliée, elle voisine avec les barres de phases : elle s'amincit pour
|
||||
// qu'on ne la prenne pas pour l'une d'elles et qu'on n'essaie pas de la
|
||||
// glisser. Pliée, elle est seule sur sa ligne et porte toute l'information.
|
||||
conteneur.className = `barre-cumulative${projet.collapsed ? '' : ' barre-cumulative--fine'}`;
|
||||
conteneur.style.left = `${echelle.x(bornes.start)}px`;
|
||||
conteneur.style.width = `${echelle.largeurDe(bornes.start, bornes.end)}px`;
|
||||
|
||||
@@ -522,7 +580,9 @@ function construireBarrePliee(projet, echelle) {
|
||||
`${projet.name}\n${formaterDateLongue(bornes.start)} → ${formaterDateLongue(bornes.end)}` +
|
||||
`\n${projet.phases.length} phase${projet.phases.length > 1 ? 's' : ''}` +
|
||||
(derive ? `\nÉcart à la référence : ${derive}` : '') +
|
||||
'\n\nDéplier le projet pour modifier les dates.';
|
||||
// Déplié, les barres modifiables sont juste en dessous : le dire serait
|
||||
// enfoncer une porte ouverte.
|
||||
(projet.collapsed ? '\n\nDéplier le projet pour modifier les dates.' : '');
|
||||
|
||||
const origine = echelle.x(bornes.start);
|
||||
for (const phase of projet.phases) {
|
||||
@@ -539,13 +599,15 @@ function construireBarrePliee(projet, echelle) {
|
||||
return conteneur;
|
||||
}
|
||||
|
||||
/** Barre fantôme du projet plié, reflétant l'étendue de référence. */
|
||||
/** Barre fantôme du projet, reflétant l'étendue de référence. */
|
||||
function construireFantome(projet, echelle) {
|
||||
const reference = bornesReference(projet);
|
||||
if (!reference) return null;
|
||||
|
||||
const fantome = document.createElement('div');
|
||||
fantome.className = 'fantome';
|
||||
// Le fantôme se cale sous la barre qu'il double : il doit suivre celle-ci
|
||||
// quand elle s'amincit, sans quoi il flotterait sous elle.
|
||||
fantome.className = `fantome${projet.collapsed ? '' : ' fantome--fin'}`;
|
||||
fantome.style.left = `${echelle.x(reference.start)}px`;
|
||||
fantome.style.width = `${echelle.largeurDe(reference.start, reference.end)}px`;
|
||||
fantome.title = `Référence : ${formaterDateLongue(reference.start)} → ${formaterDateLongue(reference.end)}`;
|
||||
|
||||
Reference in New Issue
Block a user