Rendre lisibles le statut et la date imposée
All checks were successful
Build / build (push) Successful in 1m2s
All checks were successful
Build / build (push) Successful in 1m2s
Statut : quatre boutons peints de leur texture, si bien que le panneau sert enfin de légende. « En cours » se raye et « terminé » redevient vif — la trame les distingue, la teinte n'a plus à le faire. Date imposée : un cadre tireté cerne la forme entière. Les taquets tombaient sous les poignées de redimensionnement et disparaissaient sur une barre courte. Son libellé passe dans le modèle, ce qui la fait apparaître dans la vue par mois, qui la taisait. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
23
README.md
23
README.md
@@ -95,7 +95,18 @@ survol de la ligne, sur son bord droit :
|
|||||||
|
|
||||||
Il n'y a pas de commande séparée pour créer un **jalon** ni pour marquer une **date imposée** : ce
|
Il n'y a pas de commande séparée pour créer un **jalon** ni pour marquer une **date imposée** : ce
|
||||||
sont deux cases du panneau de la phase, indépendantes l'une de l'autre comme du statut. Cocher
|
sont deux cases du panneau de la phase, indépendantes l'une de l'autre comme du statut. Cocher
|
||||||
« Jalon » ramène la fin sur le début. Les deux bascules jouent dans les deux sens à tout moment.
|
« Jalon » ramène la fin sur le début, cocher « Date imposée » encadre les deux champs de date d'un
|
||||||
|
trait tireté — le même qu'on verra sur la barre. Les deux bascules jouent dans les deux sens à tout
|
||||||
|
moment.
|
||||||
|
|
||||||
|
Le **statut** se choisit sur quatre boutons, chacun peint de la texture qu'il donne à la barre. Deux
|
||||||
|
traits suffisent à les lire : c'est **plein** quand rien ne bouge et **rayé** quand quelque chose se
|
||||||
|
passe ; c'est la **couleur pleine** du projet dès qu'une phase a commencé, et une teinte **diluée**
|
||||||
|
tant qu'elle n'a pas commencé. D'où : à venir pâle et bordé, en cours plein et rayé, terminé plein et
|
||||||
|
nu, bloqué rayé de rouge — seule teinte étrangère, parce que c'est la seule chose qui ne parle pas
|
||||||
|
d'avancement. La couleur reste celle du projet : elle distingue les couloirs, jamais l'avancement. Le
|
||||||
|
panneau sert ainsi de légende — il n'y en a pas d'autre, et il n'y a rien à apprendre avant de s'en
|
||||||
|
servir.
|
||||||
|
|
||||||
La colonne se **redimensionne** en glissant la ligne qui la sépare de la frise, en haut à
|
La colonne se **redimensionne** en glissant la ligne qui la sépare de la frise, en haut à
|
||||||
gauche : un double-clic revient à la largeur d'origine. Cette largeur est propre au navigateur,
|
gauche : un double-clic revient à la largeur d'origine. Cette largeur est propre au navigateur,
|
||||||
@@ -138,8 +149,10 @@ va. Un projet dont tout est terminé n'offre aucune prise, et son infobulle le d
|
|||||||
|
|
||||||
**Ce dont on ne décide pas non plus.** Une phase peut porter une **date imposée** : elle est
|
**Ce dont on ne décide pas non plus.** Une phase peut porter une **date imposée** : elle est
|
||||||
déléguée à quelqu'un d'autre, ou tenue par un contrat, un créneau, une échéance réglementaire. La
|
déléguée à quelqu'un d'autre, ou tenue par un contrat, un créneau, une échéance réglementaire. La
|
||||||
case se coche dans son panneau, sous « Jalon », et la barre reçoit alors deux petits taquets à ses
|
case se coche dans son panneau, sous « Jalon », et sa forme se cerne alors d'un **trait tireté** —
|
||||||
extrémités — un cercle pour un jalon. Repousser le projet ne la repousse pas : si tout le reste
|
une barre comme un losange, et jusqu'aux deux champs de date du panneau. À ne pas confondre avec le
|
||||||
|
contour plein de la phase sélectionnée : celui-ci est dedans et discontinu, celui-là dehors et
|
||||||
|
franc. Repousser le projet ne la repousse pas : si tout le reste
|
||||||
glisse, c'est le créneau qu'on rate, et le chevauchement qui apparaît est justement ce qu'on voulait
|
glisse, c'est le créneau qu'on rate, et le chevauchement qui apparaît est justement ce qu'on voulait
|
||||||
voir. Elle reste glissable seule, elle : « imposée » veut dire *indépendante du projet*, pas
|
voir. Elle reste glissable seule, elle : « imposée » veut dire *indépendante du projet*, pas
|
||||||
*immuable*.
|
*immuable*.
|
||||||
@@ -297,6 +310,10 @@ mois —, chez qui — le projet et ses tags —, puis quoi — l'intitulé de l
|
|||||||
enfin son statut. Une phase longue n'est **pas** répétée dans les mois qu'elle traverse : elle
|
enfin son statut. Une phase longue n'est **pas** répétée dans les mois qu'elle traverse : elle
|
||||||
n'apparaît qu'à son mois de départ, avec sa date de fin écrite en clair.
|
n'apparaît qu'à son mois de départ, avec sa date de fin écrite en clair.
|
||||||
|
|
||||||
|
La petite marque en tête de ligne reprend les formes de la frise — barre pour une tâche, losange pour
|
||||||
|
un jalon, texture du statut, cadre tireté pour une **date imposée**. C'est ici qu'elle compte le
|
||||||
|
plus : une liste d'échéances doit dire lesquelles ne vous appartiennent pas.
|
||||||
|
|
||||||
Les phases qui portent des **notes** le signalent par un triangle en tête de ligne, le même que
|
Les phases qui portent des **notes** le signalent par un triangle en tête de ligne, le même que
|
||||||
celui qui plie un projet sur la frise. Un clic **n'importe où sur la ligne** déplie la note en
|
celui qui plie un projet sur la frise. Un clic **n'importe où sur la ligne** déplie la note en
|
||||||
dessous, un second la referme. Elle s'y affiche rendue, comme dans le panneau. Rien n'est affiché
|
dessous, un second la referme. Elle s'y affiche rendue, comme dans le panneau. Rien n'est affiché
|
||||||
|
|||||||
257
css/style.css
257
css/style.css
@@ -804,19 +804,52 @@ button[aria-pressed="true"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Le statut se lit à la texture de la barre, pas à sa couleur : la couleur
|
/* Le statut se lit à la texture de la barre, pas à sa couleur : la couleur
|
||||||
appartient au projet, elle sert à distinguer les couloirs entre eux. */
|
appartient au projet, elle sert à distinguer les couloirs entre eux.
|
||||||
|
|
||||||
|
Une seule grammaire pour les quatre, et elle tient en deux traits :
|
||||||
|
|
||||||
|
- **plein** pour un état stable, **rayé** pour ce qui bouge ou coince ;
|
||||||
|
- la couleur du projet **pleine** dès qu'une phase a commencé, **diluée** tant
|
||||||
|
qu'elle n'a pas commencé.
|
||||||
|
|
||||||
|
D'où : à venir dilué et bordé, en cours plein et rayé, terminé plein et nu,
|
||||||
|
bloqué rayé de rouge — seule teinte étrangère, parce que c'est la seule chose
|
||||||
|
qui ne parle pas d'avancement.
|
||||||
|
|
||||||
|
« Terminé » a longtemps été assourdi, d'abord d'un gris fixe puis d'une
|
||||||
|
dilution. Les deux disaient « c'est derrière nous » en donnant une frise
|
||||||
|
morte, où la majorité des barres d'un projet avancé sont éteintes. C'est la
|
||||||
|
rayure d'« en cours » qui a permis de le rendre vif : la trame distingue
|
||||||
|
désormais ces deux-là, la teinte n'a plus à s'en charger. */
|
||||||
.barre--todo {
|
.barre--todo {
|
||||||
background: color-mix(in srgb, var(--couleur-projet) 28%, var(--surface));
|
background: color-mix(in srgb, var(--couleur-projet) 28%, var(--surface));
|
||||||
color: var(--texte);
|
color: var(--texte);
|
||||||
border: 1px solid var(--couleur-projet);
|
border: 1px solid var(--couleur-projet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Rayée, et non pleine : posée à côté d'une barre terminée — un aplat de la même
|
||||||
|
teinte —, une couleur pleine ne se distinguait pas assez pour qu'on lise
|
||||||
|
l'avancement d'un couloir en diagonale. La rayure lui donne ce que les trois
|
||||||
|
autres textures ont déjà : une trame, et pas seulement une nuance. Elle mêle
|
||||||
|
la couleur du projet à elle-même diluée, là où celle d'une phase bloquée y
|
||||||
|
mêle du rouge : le motif est le même, la teinte dit lequel des deux on
|
||||||
|
regarde. */
|
||||||
.barre--doing {
|
.barre--doing {
|
||||||
background: var(--couleur-projet);
|
background: repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--couleur-projet),
|
||||||
|
var(--couleur-projet) 6px,
|
||||||
|
color-mix(in srgb, var(--couleur-projet) 50%, var(--surface)) 6px,
|
||||||
|
color-mix(in srgb, var(--couleur-projet) 50%, var(--surface)) 12px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* La couleur du projet, pleine et nue — c'est la seule des quatre à n'avoir
|
||||||
|
aucune marque, et c'est bien ce qu'on veut dire : il n'y a plus rien à en
|
||||||
|
suivre. Déclarée quand même, et pas laissée au fond de `.barre` : ce qui vaut
|
||||||
|
pour un statut doit se lire à l'endroit où on lit les statuts. */
|
||||||
.barre--done {
|
.barre--done {
|
||||||
background: color-mix(in srgb, var(--couleur-projet) 55%, #64748b);
|
background: var(--couleur-projet);
|
||||||
}
|
}
|
||||||
|
|
||||||
.barre--blocked {
|
.barre--blocked {
|
||||||
@@ -832,19 +865,37 @@ button[aria-pressed="true"] {
|
|||||||
/* Une phase dont la date ne nous appartient pas — déléguée, contractuelle,
|
/* Une phase dont la date ne nous appartient pas — déléguée, contractuelle,
|
||||||
réglementaire (docs/decisions.md, section 28).
|
réglementaire (docs/decisions.md, section 28).
|
||||||
|
|
||||||
Deux **taquets** verticaux à ses extrémités, comme les butées d'une pièce
|
La forme est **cerclée**, tout entière : c'est tenu de partout, et le trait
|
||||||
qu'on ne peut plus faire coulisser. La marque se cumule avec les quatre
|
suit le contour quel qu'il soit — une barre, un losange. Deux taquets aux
|
||||||
textures de statut au lieu de leur disputer la place : c'est bien une phase à
|
extrémités disaient la même chose et le disaient mal : sur une barre de deux
|
||||||
venir, en cours ou bloquée, qui se trouve en plus tenue par le dehors.
|
jours ils l'avalaient, sur une barre à venir ils se lisaient comme une bordure
|
||||||
|
épaissie, et ils occupaient la place où le survol révèle les poignées de
|
||||||
|
redimensionnement — le signe « cette date ne vous appartient pas » tombait
|
||||||
|
exactement là où l'outil dit « tirez ici pour la changer ».
|
||||||
|
|
||||||
En `inset`, donc sans toucher à la géométrie : la barre garde exactement la
|
La marque se cumule avec les quatre textures de statut au lieu de leur
|
||||||
largeur de ses dates, et l'ombre portée reste déclarée à côté — un
|
disputer la place : c'est bien une phase à venir, en cours ou bloquée, qui se
|
||||||
`box-shadow` ne se cumule pas d'une règle à l'autre, il se remplace. */
|
trouve en plus tenue par le dehors.
|
||||||
.barre--imposee {
|
|
||||||
box-shadow:
|
Le trait est **tireté**, et c'est ce qui le sépare du contour de la sélection,
|
||||||
var(--ombre),
|
plein. Un cerclage plein avait été essayé d'abord : à la taille réelle des
|
||||||
inset 3px 0 0 var(--texte),
|
barres, on ne le distinguait pas d'une phase simplement sélectionnée. Le tiret
|
||||||
inset -3px 0 0 var(--texte);
|
dit d'ailleurs ce qu'il faut — ce n'est pas nous qui traçons cette ligne.
|
||||||
|
|
||||||
|
Il vit sur un `::after` plutôt que sur la barre : un élément n'a qu'un
|
||||||
|
`outline`, et c'est la sélection qui le prend. Les deux marques se superposent
|
||||||
|
donc sans se gêner sur une phase imposée qu'on vient de sélectionner — un
|
||||||
|
cadre tireté dedans, un contour plein dehors. */
|
||||||
|
.barre--imposee::after,
|
||||||
|
.jalon--imposee::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border: 2px dashed var(--texte);
|
||||||
|
border-radius: inherit;
|
||||||
|
/* Le losange tourne, son cadre avec lui : le pseudo-élément hérite de la
|
||||||
|
rotation du jalon, et n'a donc rien à en savoir. */
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Poignées de redimensionnement, révélées au survol de la barre. */
|
/* Poignées de redimensionnement, révélées au survol de la barre. */
|
||||||
@@ -894,23 +945,35 @@ button[aria-pressed="true"] {
|
|||||||
box-shadow: var(--ombre);
|
box-shadow: var(--ombre);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mêmes textures que les barres, hachures resserrées : le losange fait 14px de
|
||||||
|
côté, une rayure de 6px n'y tiendrait pas deux fois. */
|
||||||
|
.jalon--doing {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--couleur-projet),
|
||||||
|
var(--couleur-projet) 3px,
|
||||||
|
color-mix(in srgb, var(--couleur-projet) 50%, var(--surface)) 3px,
|
||||||
|
color-mix(in srgb, var(--couleur-projet) 50%, var(--surface)) 6px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.jalon--done {
|
.jalon--done {
|
||||||
background: color-mix(in srgb, var(--couleur-projet) 55%, #64748b);
|
background: var(--couleur-projet);
|
||||||
}
|
}
|
||||||
|
|
||||||
.jalon--blocked {
|
.jalon--blocked {
|
||||||
background: var(--danger);
|
background: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Un losange n'a pas d'extrémités où poser les taquets d'une barre imposée : il
|
/* Rien ici pour `.jalon--imposee` : son cadre tireté est déclaré avec celui des
|
||||||
se cercle. Déclarée après `.jalon`, dont elle partage la spécificité — c'est
|
barres, plus haut, à la même épaisseur. C'est la même marque, et un losange
|
||||||
l'ordre qui la fait gagner, un `box-shadow` se remplaçant en bloc. */
|
n'est qu'une autre forme à cerner — son `::after` s'appuie sur le
|
||||||
.jalon--impose {
|
`position: absolute` que `.jalon` porte déjà, et tourne avec lui.
|
||||||
box-shadow:
|
|
||||||
var(--ombre),
|
|
||||||
inset 0 0 0 2px var(--texte);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Le trait avait été affiné à 1px, au motif que la longueur des tirets suit
|
||||||
|
l'épaisseur et qu'un losange de 14px n'en porte que quelques-uns. Mais c'est
|
||||||
|
la lisibilité qui tranche : à 1px sur une forme de cette taille, la marque
|
||||||
|
s'efface. Mieux vaut peu de tirets qu'on voit que beaucoup qu'on devine. */
|
||||||
.jalon--selectionne {
|
.jalon--selectionne {
|
||||||
outline: 2px solid var(--texte);
|
outline: 2px solid var(--texte);
|
||||||
outline-offset: 1px;
|
outline-offset: 1px;
|
||||||
@@ -1167,7 +1230,14 @@ button[aria-pressed="true"] {
|
|||||||
et le second garde le droit de rétrécir sous la taille de son contenu — sans
|
et le second garde le droit de rétrécir sous la taille de son contenu — sans
|
||||||
lui, un flex item refuse de descendre sous sa hauteur intrinsèque et c'est la
|
lui, un flex item refuse de descendre sous sa hauteur intrinsèque et c'est la
|
||||||
page entière qui déborderait au lieu du corps qui défile. */
|
page entière qui déborderait au lieu du corps qui défile. */
|
||||||
|
/* `--couleur-projet` se déclare ici, sur l'élément même que detail.js peint en
|
||||||
|
style inline à l'ouverture : le style inline l'emporte sur cette règle, et
|
||||||
|
l'accent ne sert que de repli tant qu'aucune phase n'est ouverte — sans quoi
|
||||||
|
les `color-mix` des textures de statut se peindraient dans le vide. La
|
||||||
|
déclarer plus bas, sur le groupe de boutons, ferait l'inverse : une valeur
|
||||||
|
posée sur un descendant gagne toujours contre celle dont il hérite. */
|
||||||
.panneau__corps {
|
.panneau__corps {
|
||||||
|
--couleur-projet: var(--accent);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
@@ -1188,7 +1258,6 @@ button[aria-pressed="true"] {
|
|||||||
|
|
||||||
.panneau__corps input[type="text"],
|
.panneau__corps input[type="text"],
|
||||||
.panneau__corps input[type="date"],
|
.panneau__corps input[type="date"],
|
||||||
.panneau__corps select,
|
|
||||||
.panneau__corps textarea {
|
.panneau__corps textarea {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
@@ -1209,6 +1278,122 @@ button[aria-pressed="true"] {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Le cerclage de la barre imposée, reporté sur les deux champs que la case
|
||||||
|
qualifie : cocher « date imposée » n'avait jusque-là aucun effet visible dans
|
||||||
|
le panneau, alors que la conséquence porte précisément sur ces dates-là.
|
||||||
|
|
||||||
|
Le padding est repris par une marge négative de même valeur : les champs ne
|
||||||
|
bougent pas d'un pixel quand la case change d'état, seul le contour apparaît.
|
||||||
|
Il déborde de 8px dans les 16px de padding du corps, donc sans jamais sortir
|
||||||
|
du panneau. */
|
||||||
|
.champ-double--imposee {
|
||||||
|
padding: 6px 8px;
|
||||||
|
margin: -6px -8px;
|
||||||
|
border-radius: var(--rayon);
|
||||||
|
outline: 2px dashed var(--texte);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- statut, en quatre boutons ------------------------------------------- */
|
||||||
|
|
||||||
|
/* Chacun peint de la texture qu'il donne à la barre sur la frise : voir
|
||||||
|
index.html pour le pourquoi. La couleur, elle, vient du formulaire — cf. le
|
||||||
|
repli déclaré sur `.panneau__corps`. */
|
||||||
|
.statuts {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statuts legend {
|
||||||
|
padding: 0 0 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--texte-doux);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statuts__choix {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Le label *est* le bouton. Le radio natif s'efface sans quitter le DOM : c'est
|
||||||
|
lui qui porte la valeur, réunit le groupe et répond aux flèches. */
|
||||||
|
.panneau__corps .statut {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid var(--bordure-forte);
|
||||||
|
border-radius: var(--rayon);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--texte);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statut input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statut:hover {
|
||||||
|
background: color-mix(in srgb, var(--accent) 6%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statut:has(input:checked) {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Le focus se pose sur un radio devenu invisible : c'est au bouton de le
|
||||||
|
montrer, sans quoi la navigation aux flèches se ferait à l'aveugle. */
|
||||||
|
.statut:has(input:focus-visible) {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Les quatre textures, reprises telles quelles des barres — hachures resserrées
|
||||||
|
à l'échelle de la marque, comme celles de la vue par mois. */
|
||||||
|
.statut__marque {
|
||||||
|
flex: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--couleur-projet);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statut--todo .statut__marque {
|
||||||
|
background: color-mix(in srgb, var(--couleur-projet) 28%, var(--surface));
|
||||||
|
border: 1px solid var(--couleur-projet);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statut--doing .statut__marque {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--couleur-projet),
|
||||||
|
var(--couleur-projet) 3px,
|
||||||
|
color-mix(in srgb, var(--couleur-projet) 50%, var(--surface)) 3px,
|
||||||
|
color-mix(in srgb, var(--couleur-projet) 50%, var(--surface)) 6px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statut--done .statut__marque {
|
||||||
|
background: var(--couleur-projet);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statut--blocked .statut__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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.case {
|
.case {
|
||||||
flex-direction: row !important;
|
flex-direction: row !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1964,8 +2149,18 @@ kbd {
|
|||||||
border: 1px solid var(--couleur-projet);
|
border: 1px solid var(--couleur-projet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mois-entree--doing .mois-entree__marque {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--couleur-projet),
|
||||||
|
var(--couleur-projet) 3px,
|
||||||
|
color-mix(in srgb, var(--couleur-projet) 50%, var(--surface)) 3px,
|
||||||
|
color-mix(in srgb, var(--couleur-projet) 50%, var(--surface)) 6px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.mois-entree--done .mois-entree__marque {
|
.mois-entree--done .mois-entree__marque {
|
||||||
background: color-mix(in srgb, var(--couleur-projet) 55%, #64748b);
|
background: var(--couleur-projet);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mois-entree--blocked .mois-entree__marque {
|
.mois-entree--blocked .mois-entree__marque {
|
||||||
@@ -1990,6 +2185,16 @@ kbd {
|
|||||||
background: var(--danger);
|
background: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Le même cadre tireté que sur la frise, à l'échelle de la marque : 1px sur huit
|
||||||
|
de haut plutôt que 2 sur vingt-six. En `border` et non sur un `::after` — la
|
||||||
|
marque n'a pas de sélection à qui laisser la place, et `border-box` lui garde
|
||||||
|
sa taille. Déclaré après les quatre textures de statut, dont il ne prend pas
|
||||||
|
la place : c'est une phase à venir, en cours ou bloquée qui se trouve en plus
|
||||||
|
tenue par le dehors. */
|
||||||
|
.mois-entree--imposee .mois-entree__marque {
|
||||||
|
border: 1px dashed var(--texte);
|
||||||
|
}
|
||||||
|
|
||||||
/* Chiffres tabulaires : les quantièmes s'alignent d'une ligne à l'autre, ce qui
|
/* 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. */
|
fait de la colonne une échelle lisible plutôt qu'une pile de textes. */
|
||||||
.mois-entree__dates {
|
.mois-entree__dates {
|
||||||
|
|||||||
@@ -1150,15 +1150,14 @@ entièrement clos ; il n'a pas eu à changer, seule la liste des phases emporté
|
|||||||
|
|
||||||
### Ce que ça donne à l'œil
|
### Ce que ça donne à l'œil
|
||||||
|
|
||||||
Deux **taquets** verticaux aux extrémités de la barre, comme les butées d'une pièce qui ne coulisse
|
> Deux **taquets** verticaux aux extrémités de la barre, comme les butées d'une pièce qui ne coulisse
|
||||||
plus ; un **cercle** pour un jalon, qui n'a pas d'extrémités où en poser. La marque se cumule avec
|
> plus ; un **cercle** pour un jalon, qui n'a pas d'extrémités où en poser.
|
||||||
les quatre textures de statut au lieu de leur disputer la place — c'est bien une phase à venir, en
|
|
||||||
cours ou bloquée, qui se trouve en plus tenue par le dehors.
|
|
||||||
|
|
||||||
Elle se pose en `box-shadow` interne, donc sans toucher à la géométrie : la barre garde exactement la
|
C'est ce qui avait été retenu ici, et c'est ce que la section 29 a défait : la marque était trop
|
||||||
largeur de ses dates. Sur une phase de deux jours, les taquets occupent presque toute la barre et la
|
faible et mal placée. Elle est aujourd'hui un **cadre tireté** qui cerne la forme entière, barre ou
|
||||||
texture de statut disparaît ; c'est accepté, une fenêtre de maintenance de deux jours se lit de toute
|
losange. Le reste du raisonnement tient toujours — elle se cumule avec les quatre textures de statut
|
||||||
façon à son infobulle.
|
au lieu de leur disputer la place, et ne touche pas à la géométrie : la barre garde exactement la
|
||||||
|
largeur de ses dates.
|
||||||
|
|
||||||
### Le format passe en version 6
|
### Le format passe en version 6
|
||||||
|
|
||||||
@@ -1176,3 +1175,115 @@ comprenne pourquoi.
|
|||||||
Au passage, `donneesInitiales` dans `main.go` a été remis d'aplomb : le serveur écrivait encore
|
Au passage, `donneesInitiales` dans `main.go` a été remis d'aplomb : le serveur écrivait encore
|
||||||
`version 4` pour un planning vide, deux versions en retard. Sans conséquence — le validateur accepte
|
`version 4` pour un planning vide, deux versions en retard. Sans conséquence — le validateur accepte
|
||||||
un fichier plus ancien et le remonte — mais un fichier neuf se déclarait d'avant le cycle de vie.
|
un fichier plus ancien et le remonte — mais un fichier neuf se déclarait d'avant le cycle de vie.
|
||||||
|
|
||||||
|
## 29. Rendre lisibles le statut et la date imposée
|
||||||
|
|
||||||
|
La section 28 a posé la date imposée ; l'usage a montré que ni elle ni le statut ne se lisaient bien
|
||||||
|
là où on les cherche. Quatre corrections, de la plus factuelle à la plus discutable.
|
||||||
|
|
||||||
|
### Le libellé d'une phase vit dans le modèle
|
||||||
|
|
||||||
|
« En cours — date imposée, non emportée par un décalage du projet » se recomposait sur quatre sites :
|
||||||
|
les trois infobulles de la frise, et celle de la vue par mois. La quatrième avait oublié la mention,
|
||||||
|
si bien que la vue des échéances — celle où une date déléguée compte le plus — taisait ce que la
|
||||||
|
frise annonçait sur chaque barre.
|
||||||
|
|
||||||
|
D'où `libelleStatut(phase)` dans `model.js`, à côté de la table dont il se sert. Le mettre dans une
|
||||||
|
vue aurait reposé la même question à la suivante ; le modèle est le seul endroit que les deux vues
|
||||||
|
connaissent déjà. La vue par mois a reçu du même coup la marque d'une date imposée sur sa puce de
|
||||||
|
ligne, qui reprend déjà les formes de la frise : le vocabulaire visuel est le même des deux côtés, on
|
||||||
|
ne l'apprend qu'une fois.
|
||||||
|
|
||||||
|
### Le statut passe de la liste déroulante à quatre boutons
|
||||||
|
|
||||||
|
Une liste déroulante montre une valeur sur quatre, n'en dessine aucune, et demande deux clics. Les
|
||||||
|
quatre boutons montrent les quatre, chacun peint de la **texture qu'il donne à la barre**, et le
|
||||||
|
panneau devient ainsi la légende que l'outil n'avait nulle part — au seul moment où on en a besoin,
|
||||||
|
celui du choix, et sans coûter d'espace permanent au planning.
|
||||||
|
|
||||||
|
Une légende posée sur la frise avait été envisagée d'abord. Elle occupe de la place en permanence
|
||||||
|
pour une chose qui s'apprend une fois, et elle se lit loin de l'objet qu'elle explique.
|
||||||
|
|
||||||
|
Les textures se peignent dans la **couleur du projet qu'on édite** : `detail.js` pose
|
||||||
|
`--couleur-projet` sur le formulaire à l'ouverture. Le repli se déclare sur `.panneau__corps`, donc
|
||||||
|
sur l'élément même que le style inline surcharge — le déclarer sur le groupe de boutons faisait
|
||||||
|
l'inverse, une valeur posée sur un descendant gagnant toujours contre celle dont il hérite. C'est le
|
||||||
|
genre de détail qui se paie en minutes de recherche : la teinte du repli est une couleur plausible,
|
||||||
|
et rien ne signale qu'elle n'est pas celle du projet.
|
||||||
|
|
||||||
|
### Les quatre textures sur une seule échelle
|
||||||
|
|
||||||
|
`doing` était un aplat de la couleur du projet, `done` le même aplat assourdi d'un gris fixe. Deux
|
||||||
|
nuances de la même teinte, qu'on ne distinguait pas en parcourant un couloir en diagonale — alors que
|
||||||
|
*ce qui avance / ce qui est fait* est la distinction qu'on lit le plus souvent.
|
||||||
|
|
||||||
|
Deux corrections, et la seconde n'a été possible que grâce à la première.
|
||||||
|
|
||||||
|
**`doing` reçoit une rayure**, comme `blocked` en a une : le motif est le même, la teinte dit lequel
|
||||||
|
des deux on regarde — du rouge pour une phase bloquée, la couleur du projet mêlée à elle-même diluée
|
||||||
|
pour une phase qui avance.
|
||||||
|
|
||||||
|
**`done` redevient vif.** Le gris qui l'assourdissait n'était là que pour le séparer de `doing` ; la
|
||||||
|
rayure s'en charge désormais, et la teinte n'a plus à s'en mêler. C'est un gain net : sur un projet
|
||||||
|
avancé, la majorité des barres sont terminées, et les éteindre toutes donnait une frise morte où le
|
||||||
|
travail accompli avait l'air d'un brouillon. Une dilution à mi-chemin avait été essayée d'abord —
|
||||||
|
même famille de couleur, mais toujours délavée, donc toujours le même reproche.
|
||||||
|
|
||||||
|
La grammaire tient maintenant en deux traits, et c'est cette économie qui la rend lisible :
|
||||||
|
|
||||||
|
- **plein** pour un état stable, **rayé** pour ce qui bouge ou coince ;
|
||||||
|
- la couleur du projet **pleine** dès qu'une phase a commencé, **diluée** tant qu'elle n'a pas
|
||||||
|
commencé.
|
||||||
|
|
||||||
|
D'où : à venir dilué et bordé, en cours plein et rayé, terminé plein et nu, bloqué rayé de rouge —
|
||||||
|
seule teinte étrangère, parce que c'est la seule chose qui ne parle pas d'avancement. Aucune des
|
||||||
|
quatre ne se lit à une nuance qu'il faudrait comparer à une autre pour la reconnaître.
|
||||||
|
|
||||||
|
Le contraste de la rayure a été resserré au passage — la bande claire descend à 50% de dilution
|
||||||
|
plutôt que 65% : face à un `done` désormais plein, une rayure trop douce se serait lue comme un aplat
|
||||||
|
légèrement texturé.
|
||||||
|
|
||||||
|
La barre cumulative garde ses opacités : elle fait neuf pixels de haut, une rayure y serait du bruit,
|
||||||
|
et elle dit la forme d'ensemble d'un projet, pas le détail d'une phase.
|
||||||
|
|
||||||
|
### La date imposée se marque d'un cadre tireté
|
||||||
|
|
||||||
|
Les deux taquets de la section 28 ne tenaient pas leurs promesses, pour trois raisons qui se
|
||||||
|
cumulaient :
|
||||||
|
|
||||||
|
- sur une barre de deux jours, ils l'avalaient — la section 28 l'avait prévu et accepté ; à l'usage
|
||||||
|
ce n'est pas acceptable, une fenêtre de maintenance est *typiquement* courte, c'est le cas central
|
||||||
|
et non le cas limite ;
|
||||||
|
- sur une barre à venir, qui porte déjà une bordure, ils se lisaient comme une bordure épaissie aux
|
||||||
|
bouts, c'est-à-dire comme rien ;
|
||||||
|
- surtout, ils occupaient exactement la place où le survol révèle les **poignées de
|
||||||
|
redimensionnement**. Le signe « cette date ne vous appartient pas » tombait là où l'outil dit
|
||||||
|
« tirez ici pour la changer ».
|
||||||
|
|
||||||
|
La forme se cerne donc maintenant tout entière, d'un trait **tireté** — une barre, un losange, et
|
||||||
|
jusqu'au bloc des deux champs de date dans le panneau, qui ne montrait rien quand on cochait la case
|
||||||
|
alors que la conséquence porte précisément sur ces dates-là. Un seul signe partout, là où le losange
|
||||||
|
se cerclait et la barre se taquetait : c'était deux signes pour une même chose.
|
||||||
|
|
||||||
|
Le tiret n'est pas décoratif. Un cerclage **plein** a été essayé d'abord, et à la taille réelle des
|
||||||
|
barres on ne le distinguait pas d'une phase simplement sélectionnée. Discontinu, il ne se confond
|
||||||
|
plus avec rien — et il dit ce qu'il faut : ce n'est pas nous qui traçons cette ligne.
|
||||||
|
|
||||||
|
Sur la frise, le cadre vit sur un `::after` : un élément n'a qu'un `outline`, et c'est la sélection
|
||||||
|
qui le prend. Les deux marques se superposent donc sans se gêner sur une phase imposée qu'on vient de
|
||||||
|
sélectionner — un cadre tireté dedans, un contour plein dehors. Le losange l'affine à 1px : la
|
||||||
|
longueur des tirets suit l'épaisseur du trait, et 2px en posait de trop longs pour 14px de côté.
|
||||||
|
|
||||||
|
Dans le panneau, le padding qui porte le cadre est repris par une marge négative de même valeur : les
|
||||||
|
champs ne bougent pas d'un pixel quand la case change d'état, seul le trait apparaît.
|
||||||
|
|
||||||
|
### Ce que `detail.js` a appris de la section 28
|
||||||
|
|
||||||
|
Ajouter `fixed` y avait demandé quatre retouches dispersées — remplissage, application, rafraîchissement,
|
||||||
|
comparaison — qu'on ne savait liées qu'en les cherchant. Les champs sont maintenant une liste unique
|
||||||
|
que ces fonctions parcourent. `notes` en reste dehors : son contrôle est bâti par `notes.js` et se
|
||||||
|
remplit par lui, jamais en écrivant dans le `textarea`, sans quoi l'aperçu markdown ne suivrait pas.
|
||||||
|
|
||||||
|
Le groupe de radios a demandé un détour : `formulaire.status` n'est plus un contrôle mais la liste des
|
||||||
|
quatre, et n'est donc jamais l'élément focalisé. Sans `aLeFocus()`, le panneau réécrirait le statut
|
||||||
|
sous les flèches de qui est en train de le choisir.
|
||||||
|
|||||||
59
index.html
59
index.html
@@ -177,7 +177,13 @@
|
|||||||
Jalon (une seule date, affichée en losange)
|
Jalon (une seule date, affichée en losange)
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="champ-double">
|
<!--
|
||||||
|
Le bloc se cerne du cadre tireté d'une date imposée quand la case
|
||||||
|
ci-dessous est cochée : elle agit sur ces deux champs-là, sa
|
||||||
|
conséquence doit se voir là où elle joue. Même dessin que sur la barre
|
||||||
|
de la frise — ce qui s'apprend d'un côté se relit de l'autre.
|
||||||
|
-->
|
||||||
|
<div class="champ-double" id="champ-dates">
|
||||||
<label>
|
<label>
|
||||||
Début
|
Début
|
||||||
<input type="date" name="start" required />
|
<input type="date" name="start" required />
|
||||||
@@ -198,15 +204,48 @@
|
|||||||
Date imposée (déléguée, contractuelle ou réglementaire)
|
Date imposée (déléguée, contractuelle ou réglementaire)
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<!--
|
||||||
Statut
|
Quatre boutons plutôt qu'une liste déroulante, chacun peint de la
|
||||||
<select name="status">
|
texture qu'il donne à la barre sur la frise. Le panneau devient ainsi
|
||||||
<option value="todo">À venir</option>
|
sa propre légende, au seul moment où l'on en a besoin — celui du choix
|
||||||
<option value="doing">En cours</option>
|
—, et sans coûter d'espace permanent au planning. Une liste déroulante
|
||||||
<option value="done">Terminé</option>
|
cachait trois valeurs sur quatre, n'en montrait aucune texture, et
|
||||||
<option value="blocked">Bloqué</option>
|
demandait deux clics là où il en faut un.
|
||||||
</select>
|
|
||||||
</label>
|
Les quatre `input` partagent le nom `status` : `formulaire.status`
|
||||||
|
reste ce qu'il était pour detail.js, une valeur qui se lit et s'écrit.
|
||||||
|
-->
|
||||||
|
<fieldset class="statuts">
|
||||||
|
<legend>Statut</legend>
|
||||||
|
<!--
|
||||||
|
Les quatre choix vivent dans un `div` et non directement sous le
|
||||||
|
`fieldset` : un `legend` n'est pas un enfant comme les autres, et les
|
||||||
|
navigateurs le sortent du flux de son parent — le mettre en grille
|
||||||
|
aurait fait dépendre la mise en page d'un cas particulier.
|
||||||
|
-->
|
||||||
|
<div class="statuts__choix">
|
||||||
|
<label class="statut statut--todo">
|
||||||
|
<input type="radio" name="status" value="todo" />
|
||||||
|
<span class="statut__marque"></span>
|
||||||
|
À venir
|
||||||
|
</label>
|
||||||
|
<label class="statut statut--doing">
|
||||||
|
<input type="radio" name="status" value="doing" />
|
||||||
|
<span class="statut__marque"></span>
|
||||||
|
En cours
|
||||||
|
</label>
|
||||||
|
<label class="statut statut--done">
|
||||||
|
<input type="radio" name="status" value="done" />
|
||||||
|
<span class="statut__marque"></span>
|
||||||
|
Terminé
|
||||||
|
</label>
|
||||||
|
<label class="statut statut--blocked">
|
||||||
|
<input type="radio" name="status" value="blocked" />
|
||||||
|
<span class="statut__marque"></span>
|
||||||
|
Bloqué
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Les notes sont bâties par `notes.js` : aperçu markdown, barre d'outils
|
Les notes sont bâties par `notes.js` : aperçu markdown, barre d'outils
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ const panneau = creerPanneau(
|
|||||||
formulaire: $('formulaire-phase'),
|
formulaire: $('formulaire-phase'),
|
||||||
titre: $('panneau-titre'),
|
titre: $('panneau-titre'),
|
||||||
erreur: $('erreur-phase'),
|
erreur: $('erreur-phase'),
|
||||||
|
champDates: $('champ-dates'),
|
||||||
champFin: $('champ-fin'),
|
champFin: $('champ-fin'),
|
||||||
notes: $('notes-phase'),
|
notes: $('notes-phase'),
|
||||||
supprimer: $('supprimer-phase'),
|
supprimer: $('supprimer-phase'),
|
||||||
|
|||||||
115
js/detail.js
115
js/detail.js
@@ -16,8 +16,31 @@
|
|||||||
import { STATUTS, dateValide, modifierPhase } from './model.js';
|
import { STATUTS, dateValide, modifierPhase } from './model.js';
|
||||||
import { creerBlocNotes } from './notes.js';
|
import { creerBlocNotes } from './notes.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Les champs de la phase, et la forme de leur contrôle.
|
||||||
|
*
|
||||||
|
* Une seule liste, parcourue par tout ce qui traite les champs en bloc :
|
||||||
|
* l'affichage, le rafraîchissement et la comparaison. Ajouter `fixed` avait
|
||||||
|
* demandé quatre retouches dispersées, dont on ne savait qu'en les cherchant
|
||||||
|
* qu'elles allaient ensemble ; il n'y a plus qu'une ligne à ajouter ici, et le
|
||||||
|
* cas particulier à écrire s'il y en a un.
|
||||||
|
*
|
||||||
|
* `notes` n'y est pas : son contrôle est bâti par `notes.js` et se remplit par
|
||||||
|
* lui, jamais en écrivant directement dans le `textarea` — l'aperçu markdown ne
|
||||||
|
* suivrait pas. Il reste comparé à part, pour la même raison.
|
||||||
|
*/
|
||||||
|
const CHAMPS = [
|
||||||
|
{ nom: 'name' },
|
||||||
|
{ nom: 'start' },
|
||||||
|
{ nom: 'end' },
|
||||||
|
{ nom: 'status' },
|
||||||
|
{ nom: 'milestone', case: true },
|
||||||
|
{ nom: 'fixed', case: true },
|
||||||
|
];
|
||||||
|
|
||||||
export function creerPanneau(refs, rappels) {
|
export function creerPanneau(refs, rappels) {
|
||||||
const { panneau, formulaire, titre, erreur, champFin, notes, supprimer, fermer } = refs;
|
const { panneau, formulaire, titre, erreur, champDates, champFin, notes, supprimer, fermer } =
|
||||||
|
refs;
|
||||||
|
|
||||||
// Le bloc pose lui-même un `<textarea name="notes">` dans le formulaire, et
|
// Le bloc pose lui-même un `<textarea name="notes">` dans le formulaire, et
|
||||||
// émet un `input` à chaque changement — bouton de la barre d'outils ou case
|
// émet un `input` à chaque changement — bouton de la barre d'outils ou case
|
||||||
@@ -35,20 +58,51 @@ export function creerPanneau(refs, rappels) {
|
|||||||
|
|
||||||
function remplir(projet, phase) {
|
function remplir(projet, phase) {
|
||||||
titre.textContent = `${projet.name} — ${phase.name}`;
|
titre.textContent = `${projet.name} — ${phase.name}`;
|
||||||
formulaire.name.value = phase.name;
|
for (const champ of CHAMPS) ecrire(champ, phase);
|
||||||
formulaire.start.value = phase.start;
|
|
||||||
formulaire.end.value = phase.end;
|
|
||||||
formulaire.status.value = STATUTS.includes(phase.status) ? phase.status : 'todo';
|
|
||||||
formulaire.milestone.checked = phase.milestone;
|
|
||||||
formulaire.fixed.checked = phase.fixed;
|
|
||||||
// Ouverture sur l'aperçu si la phase est commentée, sur la saisie sinon :
|
// Ouverture sur l'aperçu si la phase est commentée, sur la saisie sinon :
|
||||||
// on ouvre une phase bloquée pour relire pourquoi, une phase vierge pour
|
// on ouvre une phase bloquée pour relire pourquoi, une phase vierge pour
|
||||||
// y écrire quelque chose.
|
// y écrire quelque chose.
|
||||||
blocNotes.definir(phase.notes, { reinitialiserMode: true });
|
blocNotes.definir(phase.notes, { reinitialiserMode: true });
|
||||||
|
|
||||||
|
peindre(projet);
|
||||||
|
accorderLesChamps(phase);
|
||||||
|
afficherErreur(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Donne au formulaire la couleur du projet qu'il édite : les textures des
|
||||||
|
* quatre boutons de statut s'y peignent, comme la barre qu'on regarde
|
||||||
|
* derrière le panneau. Rejoué au rafraîchissement, faute de quoi recolorer un
|
||||||
|
* projet laisserait le panneau à sa teinte d'ouverture.
|
||||||
|
*/
|
||||||
|
function peindre(projet) {
|
||||||
|
formulaire.style.setProperty('--couleur-projet', projet.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Écrit un champ du modèle dans son contrôle, quelle que soit sa forme. */
|
||||||
|
function ecrire(champ, phase) {
|
||||||
|
const controle = formulaire[champ.nom];
|
||||||
|
if (champ.case) controle.checked = phase[champ.nom];
|
||||||
|
// Les quatre boutons de statut portent le même nom : leur affecter une
|
||||||
|
// valeur inconnue les décocherait tous, et le panneau n'afficherait plus
|
||||||
|
// rien là où le modèle, lui, retombe sur « à venir ».
|
||||||
|
else if (champ.nom === 'status') controle.value = statutAffiche(phase);
|
||||||
|
else controle.value = phase[champ.nom];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ce que le formulaire montre en plus de ses valeurs : les champs qui n'ont
|
||||||
|
* pas lieu d'être, et la marque de ceux qu'on ne décide pas.
|
||||||
|
*
|
||||||
|
* Rejoué à chaque frappe et pas seulement à l'ouverture : les deux cases
|
||||||
|
* changent ce que le reste du panneau donne à voir, et l'effet doit suivre le
|
||||||
|
* clic qui le déclenche.
|
||||||
|
*/
|
||||||
|
function accorderLesChamps(phase) {
|
||||||
// Un jalon tient sur une seule date : le champ « fin » n'a rien à dire.
|
// Un jalon tient sur une seule date : le champ « fin » n'a rien à dire.
|
||||||
champFin.hidden = phase.milestone;
|
champFin.hidden = phase.milestone;
|
||||||
afficherErreur(null);
|
// Le cadre tireté de la frise, reporté sur les dates que la case qualifie.
|
||||||
|
champDates.classList.toggle('champ-double--imposee', phase.fixed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Lit le formulaire, applique les invariants et remonte le changement. */
|
/** Lit le formulaire, applique les invariants et remonte le changement. */
|
||||||
@@ -94,7 +148,7 @@ export function creerPanneau(refs, rappels) {
|
|||||||
afficherErreur(null);
|
afficherErreur(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
champFin.hidden = jalon;
|
accorderLesChamps(apres);
|
||||||
phaseCourante = apres;
|
phaseCourante = apres;
|
||||||
|
|
||||||
if (!sontEgales(avant, apres)) {
|
if (!sontEgales(avant, apres)) {
|
||||||
@@ -164,16 +218,15 @@ export function creerPanneau(refs, rappels) {
|
|||||||
const actif = document.activeElement;
|
const actif = document.activeElement;
|
||||||
phaseCourante = phase;
|
phaseCourante = phase;
|
||||||
titre.textContent = `${projet.name} — ${phase.name}`;
|
titre.textContent = `${projet.name} — ${phase.name}`;
|
||||||
for (const champ of ['name', 'start', 'end', 'status']) {
|
for (const champ of CHAMPS) {
|
||||||
if (formulaire[champ] !== actif) formulaire[champ].value = phase[champ];
|
if (!aLeFocus(formulaire[champ.nom], actif)) ecrire(champ, phase);
|
||||||
}
|
}
|
||||||
// Le mode n'est pas réinitialisé : basculer en aperçu sous les doigts de
|
// Le mode n'est pas réinitialisé : basculer en aperçu sous les doigts de
|
||||||
// qui écrit, parce qu'un glisser sur la frise a rafraîchi le panneau,
|
// qui écrit, parce qu'un glisser sur la frise a rafraîchi le panneau,
|
||||||
// serait le pire moment pour changer d'état.
|
// serait le pire moment pour changer d'état.
|
||||||
if (formulaire.notes !== actif) blocNotes.definir(phase.notes);
|
if (formulaire.notes !== actif) blocNotes.definir(phase.notes);
|
||||||
if (formulaire.milestone !== actif) formulaire.milestone.checked = phase.milestone;
|
peindre(projet);
|
||||||
if (formulaire.fixed !== actif) formulaire.fixed.checked = phase.fixed;
|
accorderLesChamps(phase);
|
||||||
champFin.hidden = phase.milestone;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
fermer() {
|
fermer() {
|
||||||
@@ -195,13 +248,29 @@ export function creerPanneau(refs, rappels) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sontEgales(a, b) {
|
function sontEgales(a, b) {
|
||||||
return (
|
return CHAMPS.every((champ) => a[champ.nom] === b[champ.nom]) && a.notes === b.notes;
|
||||||
a.name === b.name &&
|
}
|
||||||
a.start === b.start &&
|
|
||||||
a.end === b.end &&
|
/**
|
||||||
a.status === b.status &&
|
* Le statut que le formulaire doit montrer.
|
||||||
a.milestone === b.milestone &&
|
*
|
||||||
a.fixed === b.fixed &&
|
* Le modèle ramène un statut inconnu à « à venir » sans bloquer le chargement
|
||||||
a.notes === b.notes
|
* (model.js) ; le panneau fait le même choix, faute de quoi il n'afficherait
|
||||||
);
|
* rien là où la frise, elle, dessine une barre.
|
||||||
|
*/
|
||||||
|
function statutAffiche(phase) {
|
||||||
|
return STATUTS.includes(phase.status) ? phase.status : 'todo';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ce contrôle a-t-il le focus ?
|
||||||
|
*
|
||||||
|
* Les quatre boutons de statut partagent un nom, si bien que `formulaire.status`
|
||||||
|
* est la liste des quatre et non l'un d'eux : elle n'est jamais l'élément
|
||||||
|
* focalisé, et sans ce détour le panneau réécrirait le statut sous les flèches
|
||||||
|
* de qui est en train de le choisir.
|
||||||
|
*/
|
||||||
|
function aLeFocus(controle, actif) {
|
||||||
|
if (controle instanceof RadioNodeList) return [...controle].includes(actif);
|
||||||
|
return controle === actif;
|
||||||
}
|
}
|
||||||
|
|||||||
18
js/model.js
18
js/model.js
@@ -51,6 +51,24 @@ export const LIBELLES_STATUT = {
|
|||||||
blocked: 'Bloqué',
|
blocked: 'Bloqué',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ce qu'une phase est aujourd'hui, en une ligne : son statut, et la mention de
|
||||||
|
* sa date imposée quand elle en porte une.
|
||||||
|
*
|
||||||
|
* Les deux se lisent ensemble — « en cours, date imposée » — et occupent le même
|
||||||
|
* registre, d'où une seule phrase plutôt qu'une ligne chacun.
|
||||||
|
*
|
||||||
|
* Ce libellé vit ici, et non dans la vue qui l'affiche, parce que les deux vues
|
||||||
|
* l'écrivent : la frise sur ses barres, ses jalons et ses libellés, la vue par
|
||||||
|
* mois sur ses lignes. Recomposé sur place, il avait fini par diverger — la vue
|
||||||
|
* par mois taisait la date imposée que la frise annonçait.
|
||||||
|
*/
|
||||||
|
export function libelleStatut(phase) {
|
||||||
|
const statut = LIBELLES_STATUT[phase.status] ?? LIBELLES_STATUT.todo;
|
||||||
|
if (!phase.fixed) return statut;
|
||||||
|
return `${statut} — date imposée, non emportée par un décalage du projet`;
|
||||||
|
}
|
||||||
|
|
||||||
/** Palette par défaut, parcourue à la création de chaque nouveau projet. */
|
/** Palette par défaut, parcourue à la création de chaque nouveau projet. */
|
||||||
export const COULEURS = [
|
export const COULEURS = [
|
||||||
'#3b82f6', '#10b981', '#f59e0b', '#ef4444',
|
'#3b82f6', '#10b981', '#f59e0b', '#ef4444',
|
||||||
|
|||||||
11
js/mois.js
11
js/mois.js
@@ -29,6 +29,7 @@ import {
|
|||||||
formaterHorizon,
|
formaterHorizon,
|
||||||
formaterMois,
|
formaterMois,
|
||||||
formaterPeriode,
|
formaterPeriode,
|
||||||
|
libelleStatut,
|
||||||
moisDe,
|
moisDe,
|
||||||
projetFiltre,
|
projetFiltre,
|
||||||
projetFiltreEtat,
|
projetFiltreEtat,
|
||||||
@@ -213,6 +214,10 @@ function construireEntreePhase({ projet, phase }, tagsActifs) {
|
|||||||
ligne.className = `mois-entree mois-entree--${phase.status}`;
|
ligne.className = `mois-entree mois-entree--${phase.status}`;
|
||||||
ligne.style.setProperty('--couleur-projet', projet.color);
|
ligne.style.setProperty('--couleur-projet', projet.color);
|
||||||
if (phase.milestone) ligne.classList.add('mois-entree--jalon');
|
if (phase.milestone) ligne.classList.add('mois-entree--jalon');
|
||||||
|
// La vue par mois est celle où l'on parcourt les échéances : c'est
|
||||||
|
// exactement là qu'une date qu'on ne décide pas doit se voir. Elle ne le
|
||||||
|
// faisait pas, alors que la frise l'annonçait sur chaque barre.
|
||||||
|
if (phase.fixed) ligne.classList.add('mois-entree--imposee');
|
||||||
// C'est cette classe qui rend la ligne entière cliquable (app.js) : sans
|
// C'est cette classe qui rend la ligne entière cliquable (app.js) : sans
|
||||||
// notes, il n'y a rien à déplier et le clic ne doit rien promettre.
|
// notes, il n'y a rien à déplier et le clic ne doit rien promettre.
|
||||||
if (phase.notes) ligne.classList.add('mois-entree--commentee');
|
if (phase.notes) ligne.classList.add('mois-entree--commentee');
|
||||||
@@ -221,8 +226,8 @@ function construireEntreePhase({ projet, phase }, tagsActifs) {
|
|||||||
ligne.append(triangle(phase.notes, phase.name));
|
ligne.append(triangle(phase.notes, phase.name));
|
||||||
|
|
||||||
// La marque reprend les formes de la frise — losange pour un jalon, barre
|
// La marque reprend les formes de la frise — losange pour un jalon, barre
|
||||||
// pour une tâche — pour qu'on reconnaisse au même dessin ce qu'on a appris à
|
// pour une tâche, cadre tireté pour une date imposée — pour qu'on reconnaisse
|
||||||
// lire là-bas.
|
// au même dessin ce qu'on a appris à lire là-bas.
|
||||||
const marque = document.createElement('span');
|
const marque = document.createElement('span');
|
||||||
marque.className = 'mois-entree__marque';
|
marque.className = 'mois-entree__marque';
|
||||||
ligne.append(marque);
|
ligne.append(marque);
|
||||||
@@ -327,7 +332,7 @@ function infobulle(projet, phase) {
|
|||||||
? `Jalon du ${formaterDateLongue(phase.start)}`
|
? `Jalon du ${formaterDateLongue(phase.start)}`
|
||||||
: `${formaterDateLongue(phase.start)} → ${formaterDateLongue(phase.end)}` +
|
: `${formaterDateLongue(phase.start)} → ${formaterDateLongue(phase.end)}` +
|
||||||
` (${jours} jour${jours > 1 ? 's' : ''})`) +
|
` (${jours} jour${jours > 1 ? 's' : ''})`) +
|
||||||
`\n${LIBELLES_STATUT[phase.status]}` +
|
`\n${libelleStatut(phase)}` +
|
||||||
// Aplati : un `title` ne connaît que le texte. Sans cela, survoler afficherait
|
// Aplati : un `title` ne connaît que le texte. Sans cela, survoler afficherait
|
||||||
// la source markdown, astérisques comprises — une note *moins* lisible
|
// la source markdown, astérisques comprises — une note *moins* lisible
|
||||||
// qu'avant qu'on ne l'enrichisse.
|
// qu'avant qu'on ne l'enrichisse.
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
LIBELLES_ETAT,
|
LIBELLES_ETAT,
|
||||||
LIBELLES_STATUT,
|
|
||||||
MOIS_COURTS,
|
MOIS_COURTS,
|
||||||
MOIS_LONGS,
|
MOIS_LONGS,
|
||||||
ajouterJours,
|
ajouterJours,
|
||||||
@@ -28,6 +27,7 @@ import {
|
|||||||
formaterDateLongue,
|
formaterDateLongue,
|
||||||
formaterHorizon,
|
formaterHorizon,
|
||||||
formulerDerive,
|
formulerDerive,
|
||||||
|
libelleStatut,
|
||||||
lundiDeLaSemaine,
|
lundiDeLaSemaine,
|
||||||
phasesDecalables,
|
phasesDecalables,
|
||||||
projetADater,
|
projetADater,
|
||||||
@@ -308,7 +308,7 @@ function construireLibelles(projets, tagsActifs, curseur) {
|
|||||||
const ligne = document.createElement('div');
|
const ligne = document.createElement('div');
|
||||||
ligne.className = `libelle-phase${sousCurseur ? ' libelle-phase--curseur' : ''}`;
|
ligne.className = `libelle-phase${sousCurseur ? ' libelle-phase--curseur' : ''}`;
|
||||||
ligne.textContent = phase.name;
|
ligne.textContent = phase.name;
|
||||||
ligne.title = `${phase.name} — ${LIBELLES_STATUT[phase.status]}${mentionImposee(phase)}`;
|
ligne.title = `${phase.name} — ${libelleStatut(phase)}`;
|
||||||
ligne.dataset.projet = projet.id;
|
ligne.dataset.projet = projet.id;
|
||||||
ligne.dataset.phase = phase.id;
|
ligne.dataset.phase = phase.id;
|
||||||
elements.push(ligne);
|
elements.push(ligne);
|
||||||
@@ -507,16 +507,14 @@ function construirePhase(projet, phase, echelle, curseur) {
|
|||||||
elements.push(fantome);
|
elements.push(fantome);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Une date imposée se lit sur la barre, et pas seulement dans le panneau : un
|
|
||||||
// planning se parcourt des yeux, et savoir ce qui suivra un décalage doit se
|
|
||||||
// voir avant de faire le geste, pas après.
|
|
||||||
const impose = phase.fixed ? ' barre--imposee' : '';
|
|
||||||
|
|
||||||
if (phase.milestone) {
|
if (phase.milestone) {
|
||||||
const losange = document.createElement('div');
|
const losange = document.createElement('div');
|
||||||
losange.className =
|
losange.classList.add('jalon', `jalon--${phase.status}`);
|
||||||
`jalon jalon--${phase.status}${phase.fixed ? ' jalon--impose' : ''}` +
|
// Une date imposée se lit sur le losange, et pas seulement dans le panneau :
|
||||||
(selectionnee ? ' jalon--selectionne' : '');
|
// un planning se parcourt des yeux, et savoir ce qui suivra un décalage doit
|
||||||
|
// se voir avant de faire le geste, pas après.
|
||||||
|
if (phase.fixed) losange.classList.add('jalon--imposee');
|
||||||
|
if (selectionnee) losange.classList.add('jalon--selectionne');
|
||||||
losange.style.left = `${echelle.x(phase.start)}px`;
|
losange.style.left = `${echelle.x(phase.start)}px`;
|
||||||
losange.dataset.projet = projet.id;
|
losange.dataset.projet = projet.id;
|
||||||
losange.dataset.phase = phase.id;
|
losange.dataset.phase = phase.id;
|
||||||
@@ -526,7 +524,7 @@ function construirePhase(projet, phase, echelle, curseur) {
|
|||||||
// infobulle les taisait, alors que celle d'une barre les montrait.
|
// infobulle les taisait, alors que celle d'une barre les montrait.
|
||||||
losange.title =
|
losange.title =
|
||||||
`${phase.name} — ${formaterDateLongue(phase.start)}` +
|
`${phase.name} — ${formaterDateLongue(phase.start)}` +
|
||||||
`\n${LIBELLES_STATUT[phase.status]}${mentionImposee(phase)}` +
|
`\n${libelleStatut(phase)}` +
|
||||||
(phase.notes ? `\n\n${aplatirMarkdown(phase.notes)}` : '');
|
(phase.notes ? `\n\n${aplatirMarkdown(phase.notes)}` : '');
|
||||||
elements.push(losange);
|
elements.push(losange);
|
||||||
|
|
||||||
@@ -545,8 +543,11 @@ function construirePhase(projet, phase, echelle, curseur) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const barre = document.createElement('div');
|
const barre = document.createElement('div');
|
||||||
barre.className =
|
barre.classList.add('barre', `barre--${phase.status}`);
|
||||||
`barre barre--${phase.status}${impose}` + (selectionnee ? ' barre--selectionnee' : '');
|
// Voir le losange plus haut : c'est la même marque, un cadre tireté qui cerne
|
||||||
|
// la forme — laquelle, le CSS s'en arrange.
|
||||||
|
if (phase.fixed) barre.classList.add('barre--imposee');
|
||||||
|
if (selectionnee) barre.classList.add('barre--selectionnee');
|
||||||
barre.style.left = `${echelle.x(phase.start)}px`;
|
barre.style.left = `${echelle.x(phase.start)}px`;
|
||||||
barre.style.width = `${echelle.largeurDe(phase.start, phase.end)}px`;
|
barre.style.width = `${echelle.largeurDe(phase.start, phase.end)}px`;
|
||||||
barre.dataset.projet = projet.id;
|
barre.dataset.projet = projet.id;
|
||||||
@@ -554,7 +555,7 @@ function construirePhase(projet, phase, echelle, curseur) {
|
|||||||
barre.dataset.role = 'barre';
|
barre.dataset.role = 'barre';
|
||||||
barre.title =
|
barre.title =
|
||||||
`${phase.name}\n${formaterDateLongue(phase.start)} → ${formaterDateLongue(phase.end)}` +
|
`${phase.name}\n${formaterDateLongue(phase.start)} → ${formaterDateLongue(phase.end)}` +
|
||||||
`\n${LIBELLES_STATUT[phase.status]}${mentionImposee(phase)}` +
|
`\n${libelleStatut(phase)}` +
|
||||||
// Aplati : l'infobulle native ne connaît que le texte, et y déverser la
|
// Aplati : l'infobulle native ne connaît que le texte, et y déverser la
|
||||||
// source markdown rendrait la note moins lisible qu'avant qu'on ne
|
// source markdown rendrait la note moins lisible qu'avant qu'on ne
|
||||||
// l'enrichisse. `aplatirMarkdown` retire les marques et remplace celles qui
|
// l'enrichisse. `aplatirMarkdown` retire les marques et remplace celles qui
|
||||||
@@ -625,17 +626,6 @@ function construireBarreCumulative(projet, echelle) {
|
|||||||
return conteneur;
|
return conteneur;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Ce qui suit le statut dans l'infobulle d'une phase à date imposée.
|
|
||||||
*
|
|
||||||
* Accolé au statut, et non sur sa propre ligne : les deux se lisent ensemble —
|
|
||||||
* « en cours, date imposée » — et occupent le même registre, celui de ce que la
|
|
||||||
* phase est aujourd'hui.
|
|
||||||
*/
|
|
||||||
function mentionImposee(phase) {
|
|
||||||
return phase.fixed ? ' — date imposée, non emportée par un décalage du projet' : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ce que dit l'infobulle du résumé sur le geste qu'il offre.
|
* Ce que dit l'infobulle du résumé sur le geste qu'il offre.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
formaterMois,
|
formaterMois,
|
||||||
formaterPeriode,
|
formaterPeriode,
|
||||||
formulerDerive,
|
formulerDerive,
|
||||||
|
libelleStatut,
|
||||||
libererReference,
|
libererReference,
|
||||||
lundiDeLaSemaine,
|
lundiDeLaSemaine,
|
||||||
modifierPhase,
|
modifierPhase,
|
||||||
@@ -924,6 +925,26 @@ describe('modifications', () => {
|
|||||||
|
|
||||||
// --- décalage d'un projet ---------------------------------------------------
|
// --- décalage d'un projet ---------------------------------------------------
|
||||||
|
|
||||||
|
// --- libellé d'une phase ----------------------------------------------------
|
||||||
|
|
||||||
|
describe('libelleStatut', () => {
|
||||||
|
test('rend le statut seul quand la date nous appartient', () => {
|
||||||
|
assert.equal(libelleStatut(phase({ status: 'doing' })), 'En cours');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accole la date imposée au statut, sans le remplacer', () => {
|
||||||
|
const libelle = libelleStatut(phase({ status: 'blocked', fixed: true }));
|
||||||
|
assert.match(libelle, /^Bloqué — date imposée/);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Le validateur ramène un statut inconnu à « à venir » plutôt que de refuser
|
||||||
|
// le fichier : le libellé fait le même choix, faute de quoi une vue afficherait
|
||||||
|
// « undefined » là où la frise dessine une barre.
|
||||||
|
test('retombe sur « à venir » pour un statut inconnu', () => {
|
||||||
|
assert.equal(libelleStatut(phase({ status: 'zzz' })), 'À venir');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('décaler un projet', () => {
|
describe('décaler un projet', () => {
|
||||||
const enCours = projet({
|
const enCours = projet({
|
||||||
phases: [
|
phases: [
|
||||||
|
|||||||
Reference in New Issue
Block a user