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>
2407 lines
65 KiB
CSS
2407 lines
65 KiB
CSS
/* Frise multi-projets — feuille de style unique.
|
|
*
|
|
* Deux principes structurent ce fichier :
|
|
*
|
|
* 1. Les barres, jalons et cellules d'en-tête sont positionnés en absolu :
|
|
* timeline.js leur calcule un `left` et une `width` en pixels depuis
|
|
* l'échelle temporelle. Les couloirs, eux, restent en flux normal.
|
|
*
|
|
* 2. La frise entière est une grille dans un unique conteneur défilant, dont
|
|
* l'en-tête et la colonne des libellés sont figés par `position: sticky`.
|
|
* Le défilement des deux axes est donc entièrement natif.
|
|
*/
|
|
|
|
:root {
|
|
--fond: #f8fafc;
|
|
--surface: #ffffff;
|
|
--bordure: #e2e8f0;
|
|
--bordure-forte: #cbd5e1;
|
|
--texte: #0f172a;
|
|
--texte-doux: #64748b;
|
|
--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,
|
|
pas imbriqués : la variable doit donc vivre au-dessus des deux. */
|
|
--hauteur-cumulative: 9px;
|
|
--hauteur-ligne: 38px;
|
|
--hauteur-entete: 66px; /* trois bandes de 22 px : année, mois, semaine */
|
|
/* Doit rester en accord avec `LARGEUR_LIBELLES.defaut` (app.js), sur lequel
|
|
retombe le double-clic de la poignée. */
|
|
--largeur-libelles: 300px;
|
|
/* Même règle, pour `LARGEUR_PANNEAU.defaut`. Les deux panneaux la partagent :
|
|
ce qu'on élargit est le volet, pas l'un ou l'autre de ses contenus. */
|
|
--largeur-panneau: 380px;
|
|
|
|
--rayon: 6px;
|
|
--ombre: 0 1px 2px rgb(15 23 42 / 0.08);
|
|
|
|
/* Seule taille de la vue par mois à descendre sous celle du corps (14 px).
|
|
Elle est réservée à ce qui commente la liste — compte d'un mois, repère
|
|
« ce mois-ci », pastilles de tags — et jamais à ce qui la compose. */
|
|
--taille-annotation: 12px;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--fond: #0f172a;
|
|
--surface: #1e293b;
|
|
--bordure: #334155;
|
|
--bordure-forte: #475569;
|
|
--texte: #f1f5f9;
|
|
--texte-doux: #94a3b8;
|
|
--ombre: 0 1px 2px rgb(0 0 0 / 0.3);
|
|
--attention: #fbbf24;
|
|
}
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* Indispensable : plus bas, `.panneau` et `.alerte` déclarent `display: flex`,
|
|
ce qui écrase le `display: none` que le navigateur associe à l'attribut
|
|
`hidden`. Sans cette règle, ces éléments restent des calques `position: fixed`
|
|
étendus sur toute la page, invisibles mais interceptant tous les clics — la
|
|
frise devient inerte à la souris. */
|
|
[hidden] {
|
|
display: none !important;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
color: var(--texte);
|
|
background: var(--fond);
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* --- barre d'outils (préfixe `outils` : `barre` est réservé aux barres de phase) ------------------------------------------------------ */
|
|
|
|
.outils {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 10px 16px;
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--bordure);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.outils h1 {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.outils__groupe {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
|
|
.outils__etat {
|
|
margin: 0 0 0 auto;
|
|
font-size: 12px;
|
|
color: var(--texte-doux);
|
|
min-width: 130px;
|
|
text-align: right;
|
|
}
|
|
|
|
.outils__etat[data-etat="erreur"] {
|
|
color: var(--danger);
|
|
font-weight: 500;
|
|
}
|
|
|
|
button {
|
|
font: inherit;
|
|
padding: 5px 11px;
|
|
border: 1px solid var(--bordure-forte);
|
|
border-radius: var(--rayon);
|
|
background: var(--surface);
|
|
color: var(--texte);
|
|
cursor: pointer;
|
|
}
|
|
|
|
button:hover {
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
button[aria-pressed="true"] {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: #fff;
|
|
}
|
|
|
|
.bouton--principal {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: #fff;
|
|
}
|
|
|
|
.bouton--danger {
|
|
color: var(--danger);
|
|
border-color: var(--danger);
|
|
}
|
|
|
|
.bouton--danger:hover {
|
|
background: var(--danger);
|
|
color: #fff;
|
|
}
|
|
|
|
/* --- onglets de vue ------------------------------------------------------- */
|
|
|
|
/* Deux boutons accolés en un seul bloc, pour qu'ils se lisent comme un choix
|
|
entre deux états et non comme deux commandes indépendantes. */
|
|
.onglets {
|
|
display: flex;
|
|
}
|
|
|
|
.onglets button {
|
|
border-radius: 0;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.onglets button:first-child {
|
|
border-radius: var(--rayon) 0 0 var(--rayon);
|
|
}
|
|
|
|
.onglets button:last-child {
|
|
border-radius: 0 var(--rayon) var(--rayon) 0;
|
|
margin-left: -1px;
|
|
}
|
|
|
|
/* La couleur de l'état pressé vient de la règle générale `button[aria-pressed]`.
|
|
Ne reste ici que l'empilement : sans lui, la bordure du voisin passerait
|
|
par-dessus la sienne à la jointure. */
|
|
.onglets button[aria-pressed="true"] {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* --- barre de filtres ----------------------------------------------------- */
|
|
|
|
.filtres {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 7px 16px;
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--bordure);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.filtres__titre {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--texte-doux);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.filtres button:not(.tag) {
|
|
font-size: 12px;
|
|
padding: 3px 9px;
|
|
margin-left: auto;
|
|
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
|
|
nom du tag (model.js). La saturation et la clarté restent fixes, elles : ce
|
|
sont elles qui garantissent le contraste, quelle que soit la teinte tirée. */
|
|
.tags {
|
|
display: flex;
|
|
gap: 4px;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.tags--filtres,
|
|
.tags--suggestions {
|
|
flex-wrap: wrap;
|
|
overflow: visible;
|
|
}
|
|
|
|
.tag {
|
|
font: inherit;
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
line-height: 1.6;
|
|
padding: 0 6px;
|
|
max-width: 84px;
|
|
border: 1px solid transparent;
|
|
border-radius: 9px;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
background: hsl(var(--teinte-tag) 62% 92%);
|
|
color: hsl(var(--teinte-tag) 55% 27%);
|
|
}
|
|
|
|
.tags--filtres .tag,
|
|
.tags--suggestions .tag {
|
|
font-size: 11px;
|
|
padding: 1px 9px;
|
|
max-width: none;
|
|
border-radius: 11px;
|
|
}
|
|
|
|
.tag:hover {
|
|
border-color: hsl(var(--teinte-tag) 45% 55%);
|
|
}
|
|
|
|
/* Un tag actif s'inverse : le fond prend la teinte pleine. Il faut donc
|
|
neutraliser la règle générale `button[aria-pressed="true"]`, qui repeindrait
|
|
toutes les pastilles de la même couleur d'accent. */
|
|
.tag[aria-pressed="true"] {
|
|
background: hsl(var(--teinte-tag) 52% 38%);
|
|
border-color: hsl(var(--teinte-tag) 52% 38%);
|
|
color: #fff;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.tag {
|
|
background: hsl(var(--teinte-tag) 34% 24%);
|
|
color: hsl(var(--teinte-tag) 62% 76%);
|
|
}
|
|
|
|
.tag[aria-pressed="true"] {
|
|
background: hsl(var(--teinte-tag) 58% 62%);
|
|
border-color: hsl(var(--teinte-tag) 58% 62%);
|
|
color: #0f172a;
|
|
}
|
|
}
|
|
|
|
/* --- structure de la frise ------------------------------------------------ */
|
|
|
|
/* Un seul conteneur défilant, sur les deux axes. L'en-tête et la colonne des
|
|
libellés restent en place grâce à `position: sticky`, ce qui évite toute
|
|
synchronisation de défilement en JavaScript. */
|
|
.frise {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: auto;
|
|
background: var(--fond);
|
|
}
|
|
|
|
.grille {
|
|
display: grid;
|
|
grid-template-columns: var(--largeur-libelles) max-content;
|
|
grid-template-rows: var(--hauteur-entete) max-content;
|
|
width: max-content;
|
|
/* Sans cela, la dernière ligne colle au bas de la fenêtre et la barre de
|
|
défilement horizontale recouvre le dernier couloir. */
|
|
padding-bottom: 24px;
|
|
}
|
|
|
|
/* Le coin cumule les deux ancrages, et doit donc passer au-dessus des deux. */
|
|
.grille__coin {
|
|
position: sticky;
|
|
top: 0;
|
|
left: 0;
|
|
z-index: 6;
|
|
background: var(--surface);
|
|
border-right: 1px solid var(--bordure);
|
|
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;
|
|
z-index: 5;
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--bordure);
|
|
}
|
|
|
|
.grille__libelles {
|
|
position: sticky;
|
|
left: 0;
|
|
z-index: 4;
|
|
background: var(--surface);
|
|
border-right: 1px solid var(--bordure);
|
|
}
|
|
|
|
.grille__couloirs {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* --- en-tête de dates : trois bandes empilées ----------------------------- */
|
|
|
|
.bande {
|
|
position: relative;
|
|
height: calc(var(--hauteur-entete) / 3);
|
|
border-bottom: 1px solid var(--bordure);
|
|
}
|
|
|
|
.bande--semaine {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.bande__cellule {
|
|
position: absolute;
|
|
top: 0;
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-left: 1px solid var(--bordure);
|
|
font-size: 11px;
|
|
color: var(--texte-doux);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.bande--annee .bande__cellule {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--texte);
|
|
border-left-color: var(--bordure-forte);
|
|
background: color-mix(in srgb, var(--texte) 4%, transparent);
|
|
}
|
|
|
|
.bande--mois .bande__cellule {
|
|
font-weight: 500;
|
|
color: var(--texte);
|
|
border-left-color: var(--bordure-forte);
|
|
}
|
|
|
|
/* Les numéros de semaine sont le grain le plus fin : discrets par défaut,
|
|
ils ne doivent pas concurrencer le mois et l'année. */
|
|
.bande--semaine .bande__cellule {
|
|
font-size: 10px;
|
|
letter-spacing: -0.2px;
|
|
}
|
|
|
|
/* --- traits verticaux ----------------------------------------------------- */
|
|
|
|
.trait {
|
|
position: absolute;
|
|
top: 0;
|
|
width: 1px;
|
|
background: var(--bordure);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.trait--majeur {
|
|
background: var(--bordure-forte);
|
|
}
|
|
|
|
.trait--aujourdhui {
|
|
background: var(--aujourdhui);
|
|
width: 2px;
|
|
z-index: 4;
|
|
}
|
|
|
|
.trait--aujourdhui::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 0;
|
|
left: -3px;
|
|
border: 4px solid transparent;
|
|
border-top-color: var(--aujourdhui);
|
|
}
|
|
|
|
/* --- 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;
|
|
height: var(--hauteur-ligne);
|
|
padding: 0 8px 0 6px;
|
|
font-weight: 600;
|
|
border-bottom: 1px solid var(--bordure);
|
|
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 1 auto;
|
|
min-width: 7em;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.libelle-projet .tags {
|
|
flex: 0 1 auto;
|
|
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 {
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
padding: 1px 5px;
|
|
border-radius: 10px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.libelle-projet__derive[data-sens="retard"] {
|
|
background: color-mix(in srgb, var(--danger) 15%, transparent);
|
|
color: var(--danger);
|
|
}
|
|
|
|
.libelle-projet__derive[data-sens="avance"] {
|
|
background: color-mix(in srgb, #16a34a 15%, transparent);
|
|
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;
|
|
padding: 2px 4px;
|
|
font-size: 10px;
|
|
color: var(--texte-doux);
|
|
cursor: pointer;
|
|
transition: transform 0.15s;
|
|
}
|
|
|
|
.plier[aria-expanded="true"] {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
/* `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`. */
|
|
.libelle-phase {
|
|
display: block;
|
|
line-height: var(--hauteur-ligne);
|
|
height: var(--hauteur-ligne);
|
|
padding: 0 8px 0 26px;
|
|
color: var(--texte-doux);
|
|
border-bottom: 1px solid var(--bordure);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.libelle-phase:hover {
|
|
color: var(--texte);
|
|
}
|
|
|
|
.libelle-projet--masque {
|
|
opacity: 0.45;
|
|
}
|
|
|
|
/* --- curseur clavier ------------------------------------------------------ */
|
|
|
|
/* Le curseur des raccourcis vim se marque ici, et pas seulement sur la barre :
|
|
il se pose aussi sur des lignes qui n'ont aucune barre en face — celle d'un
|
|
projet, un projet plié, un projet masqué. Sans cette marque, `j` et `k`
|
|
déplaceraient quelque chose d'invisible.
|
|
|
|
Un trait sur le bord gauche, posé en `box-shadow` : une vraie bordure
|
|
pousserait le contenu de trois pixels à chaque déplacement du curseur.
|
|
|
|
Les deux sortes de ligne prennent aussi une teinte d'accent, mais pas sur le
|
|
même fond : celui d'un projet est déjà coloré à ses couleurs, et le mélange
|
|
part donc de là. Le trait seul ne suffisait pas sur cette ligne-là, où son
|
|
fond propre et le triangle de pli le noyaient. */
|
|
.libelle-projet--curseur,
|
|
.libelle-phase--curseur {
|
|
color: var(--texte);
|
|
box-shadow: inset 3px 0 0 var(--accent);
|
|
}
|
|
|
|
.libelle-phase--curseur {
|
|
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
|
|
}
|
|
|
|
/* On redéfinit `--fond-libelle` au lieu de peindre le fond : le dégradé qui
|
|
donne un fond aux commandes s'appuie sur cette même variable, et un fond posé
|
|
en direct laisserait une couture visible sous elles. La valeur reprend donc
|
|
celle de `.libelle-projet`, teintée d'accent — on ne peut pas se citer
|
|
soi-même dans une variable. */
|
|
.libelle-projet--curseur {
|
|
--fond-libelle: color-mix(
|
|
in srgb,
|
|
var(--accent) 16%,
|
|
color-mix(in srgb, var(--couleur-projet) 8%, var(--surface))
|
|
);
|
|
}
|
|
|
|
/* Le curseur désigne une ligne de projet : ses commandes doivent se voir, sans
|
|
quoi rien ne dirait ce que cette ligne sait faire. */
|
|
.libelle-projet--curseur .libelle-projet__commandes {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* --- couloirs et barres -------------------------------------------------- */
|
|
|
|
.couloir {
|
|
position: relative;
|
|
height: var(--hauteur-ligne);
|
|
border-bottom: 1px solid var(--bordure);
|
|
}
|
|
|
|
.couloir--projet {
|
|
background: color-mix(in srgb, var(--couleur-projet) 5%, transparent);
|
|
}
|
|
|
|
/* Curseur clavier posé sur la ligne d'un projet. Elle n'a que sa barre
|
|
cumulative, trop fine pour porter un contour : c'est le couloir entier qui
|
|
prend la teinte. Après `.couloir--projet`, dont elle remplace le fond. */
|
|
.couloir--curseur {
|
|
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
|
}
|
|
|
|
.barre {
|
|
position: absolute;
|
|
top: calc((var(--hauteur-ligne) - var(--hauteur-barre)) / 2);
|
|
height: var(--hauteur-barre);
|
|
border-radius: 4px;
|
|
background: var(--couleur-projet);
|
|
color: #fff;
|
|
font-size: 12px;
|
|
line-height: var(--hauteur-barre);
|
|
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);
|
|
}
|
|
|
|
.barre--glissee {
|
|
cursor: grabbing;
|
|
z-index: 5;
|
|
filter: brightness(1.12);
|
|
box-shadow: 0 4px 12px rgb(15 23 42 / 0.25);
|
|
}
|
|
|
|
.barre--selectionnee {
|
|
outline: 2px solid var(--texte);
|
|
outline-offset: 1px;
|
|
}
|
|
|
|
/* Le statut se lit à la texture de la barre, pas à sa couleur : la couleur
|
|
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 {
|
|
background: color-mix(in srgb, var(--couleur-projet) 28%, var(--surface));
|
|
color: var(--texte);
|
|
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 {
|
|
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 {
|
|
background: var(--couleur-projet);
|
|
}
|
|
|
|
.barre--blocked {
|
|
background: repeating-linear-gradient(
|
|
45deg,
|
|
var(--couleur-projet),
|
|
var(--couleur-projet) 6px,
|
|
color-mix(in srgb, var(--danger) 75%, var(--couleur-projet)) 6px,
|
|
color-mix(in srgb, var(--danger) 75%, var(--couleur-projet)) 12px
|
|
);
|
|
}
|
|
|
|
/* Une phase dont la date ne nous appartient pas — déléguée, contractuelle,
|
|
réglementaire (docs/decisions.md, section 28).
|
|
|
|
La forme est **cerclée**, tout entière : c'est tenu de partout, et le trait
|
|
suit le contour quel qu'il soit — une barre, un losange. Deux taquets aux
|
|
extrémités disaient la même chose et le disaient mal : sur une barre de deux
|
|
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 ».
|
|
|
|
La marque se cumule avec 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.
|
|
|
|
Le trait est **tireté**, et c'est ce qui le sépare du contour de la sélection,
|
|
plein. Un cerclage plein avait été essayé d'abord : à la taille réelle des
|
|
barres, on ne le distinguait pas d'une phase simplement sélectionnée. Le tiret
|
|
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. */
|
|
.poignee {
|
|
position: absolute;
|
|
top: 0;
|
|
width: 7px;
|
|
height: 100%;
|
|
cursor: ew-resize;
|
|
opacity: 0;
|
|
background: rgb(255 255 255 / 0.5);
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.poignee--fin {
|
|
right: 0;
|
|
border-radius: 0 4px 4px 0;
|
|
}
|
|
|
|
/* --- jalons -------------------------------------------------------------- */
|
|
|
|
.jalon {
|
|
position: absolute;
|
|
top: calc((var(--hauteur-ligne) - 14px) / 2);
|
|
width: 14px;
|
|
height: 14px;
|
|
margin-left: -7px;
|
|
background: var(--couleur-projet);
|
|
transform: rotate(45deg);
|
|
cursor: grab;
|
|
z-index: 3;
|
|
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 {
|
|
background: var(--couleur-projet);
|
|
}
|
|
|
|
.jalon--blocked {
|
|
background: var(--danger);
|
|
}
|
|
|
|
/* Rien ici pour `.jalon--imposee` : son cadre tireté est déclaré avec celui des
|
|
barres, plus haut, à la même épaisseur. C'est la même marque, et un losange
|
|
n'est qu'une autre forme à cerner — son `::after` s'appuie sur le
|
|
`position: absolute` que `.jalon` porte déjà, et tourne avec lui.
|
|
|
|
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 {
|
|
outline: 2px solid var(--texte);
|
|
outline-offset: 1px;
|
|
}
|
|
|
|
/* Le nom du jalon vit hors du losange, pour ne pas subir la rotation. */
|
|
.jalon-nom {
|
|
position: absolute;
|
|
top: calc((var(--hauteur-ligne) - 16px) / 2);
|
|
margin-left: 12px;
|
|
font-size: 11px;
|
|
color: var(--texte-doux);
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
z-index: 3;
|
|
}
|
|
|
|
/* --- barre cumulative d'un projet ---------------------------------------- */
|
|
|
|
/* Barre unique segmentée par phase, affichée que le projet soit plié ou
|
|
déplié, et rendue à l'identique dans les deux cas — c'est le même objet, il
|
|
n'a pas à changer d'aspect quand on ouvre le couloir.
|
|
Volontairement plus mince qu'une barre de phase : elle voisine avec elles
|
|
dès que le projet est déplié.
|
|
Elle se glisse pour repousser le projet : voir docs/decisions.md, section 8. */
|
|
.barre-cumulative {
|
|
position: absolute;
|
|
top: calc((var(--hauteur-ligne) - var(--hauteur-cumulative)) / 2);
|
|
height: var(--hauteur-cumulative);
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
display: flex;
|
|
background: color-mix(in srgb, var(--couleur-projet) 15%, var(--surface));
|
|
cursor: default;
|
|
z-index: 2;
|
|
}
|
|
|
|
/* La prise ne s'annonce que là où elle mène quelque part : sur un projet dont
|
|
tout est terminé, la barre n'a pas de rôle et garde le curseur par défaut —
|
|
rien ne bougerait. */
|
|
.barre-cumulative[data-role='cumul'] {
|
|
cursor: grab;
|
|
}
|
|
|
|
.barre-cumulative[data-role='cumul']:hover {
|
|
background: color-mix(in srgb, var(--couleur-projet) 28%, var(--surface));
|
|
}
|
|
|
|
/* Mince comme elle est, une barre cumulative saisie se repère mal. L'ombre la
|
|
décolle du couloir le temps du geste, sans changer sa géométrie — ce qui
|
|
décalerait ce qu'on est en train de viser. */
|
|
.barre-cumulative.barre--glissee {
|
|
cursor: grabbing;
|
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--couleur-projet) 60%, transparent);
|
|
}
|
|
|
|
.segment {
|
|
position: absolute;
|
|
top: 0;
|
|
height: 100%;
|
|
background: var(--couleur-projet);
|
|
}
|
|
|
|
.segment--todo {
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.segment--doing {
|
|
opacity: 1;
|
|
}
|
|
|
|
.segment--done {
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.segment--blocked {
|
|
background: var(--danger);
|
|
opacity: 0.85;
|
|
}
|
|
|
|
/* --- barre fantôme de référence ------------------------------------------ */
|
|
|
|
.fantome {
|
|
position: absolute;
|
|
top: calc(
|
|
(var(--hauteur-ligne) - var(--hauteur-barre)) / 2 + var(--hauteur-barre) - 3px
|
|
);
|
|
height: 5px;
|
|
border-radius: 3px;
|
|
background: repeating-linear-gradient(
|
|
90deg,
|
|
var(--bordure-forte),
|
|
var(--bordure-forte) 4px,
|
|
transparent 4px,
|
|
transparent 7px
|
|
);
|
|
pointer-events: none;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* Le fantôme suit la barre qu'il double. Sous la barre cumulative, plus mince
|
|
qu'une barre de phase, 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--cumulatif {
|
|
top: calc((var(--hauteur-ligne) + var(--hauteur-cumulative)) / 2);
|
|
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
|
|
l'édition et cliquer une autre barre y bascule la sélection. */
|
|
.panneau {
|
|
position: fixed;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
width: var(--largeur-panneau);
|
|
max-width: 100vw;
|
|
background: var(--surface);
|
|
border-left: 1px solid var(--bordure);
|
|
box-shadow: -4px 0 16px rgb(15 23 42 / 0.12);
|
|
z-index: 20;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* Poignée de largeur, sur le bord gauche. Jumelle de `.poignee-colonne` : même
|
|
épaisseur, même trait d'accent au survol, même curseur. Ce qui change est le
|
|
bord auquel elle s'accroche, puisque le panneau est ancré à droite. */
|
|
.poignee-panneau {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
left: -4px;
|
|
width: 9px;
|
|
cursor: col-resize;
|
|
z-index: 1;
|
|
}
|
|
|
|
.poignee-panneau::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0 4px;
|
|
background: var(--accent);
|
|
opacity: 0;
|
|
transition: opacity 0.12s;
|
|
}
|
|
|
|
.poignee-panneau:hover::after,
|
|
.poignee-panneau:focus-visible::after,
|
|
.poignee-panneau[data-glisse="true"]::after {
|
|
opacity: 1;
|
|
}
|
|
|
|
.poignee-panneau:focus-visible {
|
|
outline: none;
|
|
}
|
|
|
|
/* 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;
|
|
justify-content: space-between;
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid var(--bordure);
|
|
}
|
|
|
|
.panneau__entete h2 {
|
|
margin: 0;
|
|
font-size: 14px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.panneau__entete button {
|
|
border: 0;
|
|
font-size: 20px;
|
|
line-height: 1;
|
|
padding: 0 6px;
|
|
background: none;
|
|
}
|
|
|
|
/* `flex: 1` et `min-height: 0` : le corps prend toute la hauteur sous l'en-tête,
|
|
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
|
|
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 {
|
|
--couleur-projet: var(--accent);
|
|
flex: 1;
|
|
min-height: 0;
|
|
padding: 16px;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
}
|
|
|
|
.panneau__corps label {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--texte-doux);
|
|
}
|
|
|
|
.panneau__corps input[type="text"],
|
|
.panneau__corps input[type="date"],
|
|
.panneau__corps textarea {
|
|
font: inherit;
|
|
padding: 6px 8px;
|
|
border: 1px solid var(--bordure-forte);
|
|
border-radius: var(--rayon);
|
|
background: var(--fond);
|
|
color: var(--texte);
|
|
width: 100%;
|
|
}
|
|
|
|
.panneau__corps textarea {
|
|
resize: vertical;
|
|
}
|
|
|
|
.champ-double {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
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 {
|
|
flex-direction: row !important;
|
|
align-items: center;
|
|
gap: 8px !important;
|
|
font-weight: 400 !important;
|
|
color: var(--texte) !important;
|
|
}
|
|
|
|
.case input {
|
|
width: auto;
|
|
}
|
|
|
|
.panneau__erreur {
|
|
margin: 0;
|
|
padding: 8px 10px;
|
|
border-radius: var(--rayon);
|
|
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
|
color: var(--danger);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.panneau__actions {
|
|
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
|
|
ce qui distingue une note d'un champ : les autres ont une taille dictée par
|
|
ce qu'ils contiennent — une date, un statut —, une note n'en a aucune, et
|
|
quatre lignes fixes étaient un plafond arbitraire qui obligeait à écrire dans
|
|
une meurtrière. Le plancher, lui, reste : sur une fenêtre basse, le corps du
|
|
panneau défile plutôt que d'écraser la note à deux lignes. */
|
|
.notes {
|
|
flex: 1 1 auto;
|
|
min-height: 190px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.notes__entete {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
min-height: 24px;
|
|
}
|
|
|
|
.notes__intitule {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--texte-doux);
|
|
}
|
|
|
|
/* La barre d'outils n'apparaît qu'en saisie : en aperçu elle ne commanderait
|
|
rien, et sa place revient au texte. */
|
|
.notes__outils {
|
|
display: flex;
|
|
gap: 2px;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.notes__outils button {
|
|
width: 24px;
|
|
height: 24px;
|
|
padding: 0;
|
|
border: 1px solid transparent;
|
|
border-radius: var(--rayon);
|
|
background: none;
|
|
color: var(--texte-doux);
|
|
font-size: 13px;
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.notes__outils button:hover {
|
|
border-color: var(--bordure-forte);
|
|
color: var(--texte);
|
|
}
|
|
|
|
.notes__outils [data-outil="gras"] {
|
|
font-weight: 700;
|
|
}
|
|
|
|
.notes__outils [data-outil="italique"] {
|
|
font-style: italic;
|
|
font-family: Georgia, serif;
|
|
}
|
|
|
|
/* Le crayon reste à l'extrémité droite quelle que soit la présence de la barre
|
|
d'outils : c'est le seul bouton toujours là, il lui faut une place fixe. */
|
|
.notes__bascule {
|
|
margin-left: auto;
|
|
width: 26px;
|
|
height: 24px;
|
|
padding: 0;
|
|
border: 1px solid var(--bordure-forte);
|
|
border-radius: var(--rayon);
|
|
background: var(--surface);
|
|
color: var(--texte-doux);
|
|
font-size: 13px;
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.notes__outils:not([hidden]) + .notes__bascule {
|
|
margin-left: 4px;
|
|
}
|
|
|
|
.notes__bascule:hover {
|
|
color: var(--texte);
|
|
border-color: var(--texte-doux);
|
|
}
|
|
|
|
.notes__apercu {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
padding: 8px 10px;
|
|
border: 1px solid var(--bordure);
|
|
border-radius: var(--rayon);
|
|
background: var(--fond);
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
cursor: text;
|
|
}
|
|
|
|
.notes__invite {
|
|
margin: 0;
|
|
color: var(--texte-doux);
|
|
font-style: italic;
|
|
}
|
|
|
|
/* `resize: none` contre le `resize: vertical` des autres textarea du panneau :
|
|
celui-ci occupe la place disponible, une poignée de redimensionnement dans
|
|
son coin ne ferait que se battre avec la poignée du volet. */
|
|
.notes__saisie {
|
|
flex: 1;
|
|
min-height: 0;
|
|
resize: none;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
tab-size: 2;
|
|
}
|
|
|
|
/* --- rendu markdown ------------------------------------------------------- */
|
|
|
|
/* Ces règles servent au panneau comme à la vue par mois : une note doit se lire
|
|
pareil des deux côtés, sans quoi on douterait d'avoir la même sous les yeux.
|
|
Les marges sont serrées — c'est un encart dans un volet, pas un article. */
|
|
/* Le style suit `data-niveau` — le nombre de dièses écrits — et non la balise :
|
|
celle-ci est décalée de deux rangs pour ne pas casser la hiérarchie du
|
|
document, et son plafond à `h6` confond les rangs 4 à 6. Les niveaux se
|
|
séparent surtout par le blanc au-dessus : dans un encart de treize pixels,
|
|
deux corps de titre distants d'un pixel ne se distinguent pas, alors qu'un
|
|
interligne plus large se voit tout de suite. */
|
|
.md-titre {
|
|
margin: 12px 0 4px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.md-titre[data-niveau="1"] {
|
|
font-size: 16px;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.md-titre[data-niveau="2"] {
|
|
font-size: 14px;
|
|
margin-top: 14px;
|
|
}
|
|
|
|
/* Au-delà du troisième rang, le titre ne grossit plus : il s'efface. C'est un
|
|
intertitre dans un encart, pas une section — et une note qui descend à cinq
|
|
niveaux a de toute façon perdu la partie. */
|
|
.md-titre[data-niveau="4"],
|
|
.md-titre[data-niveau="5"],
|
|
.md-titre[data-niveau="6"] {
|
|
color: var(--texte-doux);
|
|
}
|
|
|
|
.md-titre:first-child {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.md-paragraphe {
|
|
margin: 0 0 8px;
|
|
}
|
|
|
|
.md-paragraphe:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.md-liste {
|
|
margin: 0 0 8px;
|
|
padding-left: 20px;
|
|
}
|
|
|
|
.md-liste:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.md-liste li {
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
/* La case *est* la puce : les deux ensemble décaleraient le texte sans rien
|
|
ajouter. */
|
|
.md-liste--taches {
|
|
list-style: none;
|
|
padding-left: 2px;
|
|
}
|
|
|
|
/* Une sous-liste de tâches perdrait sinon toute indentation, son retrait ne
|
|
venant que de la puce qu'on vient de lui retirer. Or l'imbrication est
|
|
justement ce qu'on est venu lire : « variante sombre » dépend de « relancer
|
|
sur les maquettes », et rien d'autre que le décalage ne le dit. */
|
|
.md-liste .md-liste--taches {
|
|
padding-left: 20px;
|
|
}
|
|
|
|
/* `.panneau__corps label` empile ses enfants en colonne et les grise — c'est ce
|
|
qu'il faut d'un champ de formulaire, dont l'intitulé surmonte la saisie. Une
|
|
case à cocher dans une note n'est pas cela : la case précède son texte sur la
|
|
même ligne, et ce texte est du contenu, pas une étiquette. D'où la
|
|
spécificité rehaussée, seule façon de reprendre la main sur une règle qui
|
|
vise tous les labels du panneau. */
|
|
.notes__apercu .md-tache,
|
|
.mois-entree__notes .md-tache {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: flex-start;
|
|
gap: 6px;
|
|
font-size: inherit;
|
|
font-weight: 400;
|
|
color: inherit;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* `width: auto` contre la règle qui étire à 100 % les champs du panneau : une
|
|
case à cocher garde sa taille native. */
|
|
.md-tache input {
|
|
margin: 3px 0 0;
|
|
width: auto;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Une tâche faite s'efface sans disparaître : elle compte encore dans ce qui a
|
|
été décidé, elle ne compte plus dans ce qui reste. */
|
|
.md-tache--faite {
|
|
color: var(--texte-doux);
|
|
text-decoration: line-through;
|
|
}
|
|
|
|
/* Les cases de la vue par mois sont désactivées (lecture seule, décision 24) :
|
|
sans cela, le curseur y promettrait un clic sans effet. */
|
|
.md-tache input:disabled {
|
|
cursor: default;
|
|
}
|
|
|
|
.md-tache:has(input:disabled) {
|
|
cursor: default;
|
|
}
|
|
|
|
.md-citation {
|
|
margin: 0 0 8px;
|
|
padding-left: 10px;
|
|
border-left: 3px solid var(--bordure-forte);
|
|
color: var(--texte-doux);
|
|
}
|
|
|
|
.md-citation > :last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.md-code {
|
|
margin: 0 0 8px;
|
|
padding: 6px 8px;
|
|
overflow-x: auto;
|
|
border-radius: var(--rayon);
|
|
background: color-mix(in srgb, var(--texte) 7%, transparent);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.md-code-ligne {
|
|
padding: 1px 4px;
|
|
border-radius: 3px;
|
|
background: color-mix(in srgb, var(--texte) 7%, transparent);
|
|
font-size: 0.92em;
|
|
}
|
|
|
|
.md-titre + .md-liste,
|
|
.md-titre + .md-paragraphe {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.notes__apercu hr,
|
|
.mois-entree__notes hr {
|
|
margin: 10px 0;
|
|
border: 0;
|
|
border-top: 1px solid var(--bordure-forte);
|
|
}
|
|
|
|
.notes__apercu a,
|
|
.mois-entree__notes a {
|
|
color: var(--accent);
|
|
}
|
|
|
|
/* --- menu contextuel ------------------------------------------------------ */
|
|
|
|
.menu {
|
|
position: fixed;
|
|
z-index: 30;
|
|
min-width: 210px;
|
|
padding: 4px;
|
|
background: var(--surface);
|
|
border: 1px solid var(--bordure-forte);
|
|
border-radius: var(--rayon);
|
|
box-shadow: 0 8px 24px rgb(15 23 42 / 0.18);
|
|
}
|
|
|
|
.menu__entree {
|
|
display: block;
|
|
width: 100%;
|
|
text-align: left;
|
|
padding: 6px 10px;
|
|
border: 0;
|
|
border-radius: 4px;
|
|
background: none;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.menu__entree:hover:not(:disabled),
|
|
.menu__entree:focus-visible {
|
|
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
|
border-color: transparent;
|
|
outline: none;
|
|
}
|
|
|
|
.menu__entree:disabled {
|
|
color: var(--texte-doux);
|
|
opacity: 0.5;
|
|
cursor: default;
|
|
}
|
|
|
|
.menu__entree--danger {
|
|
color: var(--danger);
|
|
}
|
|
|
|
.menu__entree--danger:hover:not(:disabled),
|
|
.menu__entree--danger:focus-visible {
|
|
background: color-mix(in srgb, var(--danger) 14%, transparent);
|
|
}
|
|
|
|
.menu__separateur {
|
|
margin: 4px 6px;
|
|
border: 0;
|
|
border-top: 1px solid var(--bordure);
|
|
}
|
|
|
|
/* Commandes de la ligne d'un projet : ajouter une phase, ouvrir le menu. */
|
|
.commande-projet {
|
|
border: 0;
|
|
background: none;
|
|
padding: 2px 5px;
|
|
cursor: pointer;
|
|
color: var(--texte-doux);
|
|
font-size: 14px;
|
|
line-height: 1;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.commande-projet:hover {
|
|
background: color-mix(in srgb, var(--accent) 16%, transparent);
|
|
color: var(--texte);
|
|
}
|
|
|
|
/* --- 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);
|
|
border-radius: 10px;
|
|
background: var(--surface);
|
|
color: var(--texte);
|
|
max-width: 400px;
|
|
box-shadow: 0 12px 40px rgb(15 23 42 / 0.28);
|
|
}
|
|
|
|
.dialogue::backdrop {
|
|
background: rgb(15 23 42 / 0.45);
|
|
}
|
|
|
|
.dialogue form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
padding: 18px 20px 16px;
|
|
}
|
|
|
|
.dialogue h2 {
|
|
margin: 0;
|
|
font-size: 15px;
|
|
}
|
|
|
|
.dialogue p {
|
|
margin: 0;
|
|
color: var(--texte-doux);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.couleurs {
|
|
margin: 0;
|
|
padding: 0;
|
|
border: 0;
|
|
}
|
|
|
|
.couleurs legend {
|
|
padding: 0 0 6px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--texte-doux);
|
|
}
|
|
|
|
.couleurs div {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 7px;
|
|
}
|
|
|
|
.pastille {
|
|
width: 24px;
|
|
height: 24px;
|
|
padding: 0;
|
|
border: 2px solid transparent;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.pastille[aria-pressed="true"] {
|
|
border-color: var(--texte);
|
|
box-shadow: inset 0 0 0 2px var(--surface);
|
|
}
|
|
|
|
.dialogue__actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
/* --- aide des raccourcis -------------------------------------------------- */
|
|
|
|
/* Le dialogue de confirmation tient en 400 px parce qu'il pose une question ;
|
|
celui-ci est une table de référence qu'on parcourt du regard. Il prend donc
|
|
la largeur qu'il faut, plafonnée par la fenêtre, et ses sections se rangent
|
|
en colonnes tant qu'elles y tiennent. */
|
|
.dialogue--aide {
|
|
max-width: min(920px, calc(100vw - 32px));
|
|
max-height: calc(100vh - 32px);
|
|
}
|
|
|
|
.aide__colonnes {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
gap: 4px 28px;
|
|
}
|
|
|
|
.aide__colonnes h3 {
|
|
margin: 10px 0 6px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: var(--texte-doux);
|
|
}
|
|
|
|
/* Deux colonnes dans un `<dl>` : la colonne des touches se cale sur la plus
|
|
large de la section — fixée en `em`, elle creusait un couloir vide devant les
|
|
raccourcis d'une seule lettre ; laissée libre, elle mettrait les libellés en
|
|
escalier d'une ligne à l'autre. */
|
|
dl.aide {
|
|
display: grid;
|
|
grid-template-columns: max-content 1fr;
|
|
gap: 3px 16px;
|
|
margin: 0;
|
|
font-size: 13px;
|
|
}
|
|
|
|
dl.aide dt {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 3px;
|
|
align-items: baseline;
|
|
}
|
|
|
|
dl.aide dd {
|
|
margin: 0;
|
|
color: var(--texte-doux);
|
|
line-height: 1.45;
|
|
}
|
|
|
|
kbd {
|
|
padding: 1px 5px;
|
|
border: 1px solid var(--bordure-forte);
|
|
border-bottom-width: 2px;
|
|
border-radius: 4px;
|
|
background: var(--fond);
|
|
color: var(--texte);
|
|
font-family: inherit;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.aide__note {
|
|
padding-top: 4px;
|
|
border-top: 1px solid var(--bordure);
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* Le point d'interrogation de la barre d'outils. Rond et discret : c'est un
|
|
recours, pas une commande qu'on utilise en travaillant. */
|
|
.outils__aide {
|
|
width: 28px;
|
|
padding: 0;
|
|
border-radius: 50%;
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* --- vue par mois -------------------------------------------------------- */
|
|
|
|
/* Rien n'est positionné en absolu ici, contrairement à la frise : la liste
|
|
n'a pas d'échelle temporelle, elle empile des lignes dans l'ordre du
|
|
calendrier. Un seul axe de défilement, le vertical.
|
|
`position: relative` fait du conteneur l'`offsetParent` de ses groupes, dont
|
|
`revelerMoisCourant` (mois.js) lit l'`offsetTop`. */
|
|
.mois {
|
|
flex: 1;
|
|
min-height: 0;
|
|
position: relative;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
padding: 0 16px 40px;
|
|
}
|
|
|
|
/* Une largeur de lecture plutôt que toute la page : sur un écran large, une
|
|
ligne étirée sur 2000 px sépare la date de son statut par un désert. */
|
|
.mois-groupe {
|
|
max-width: 1080px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* Le titre reste visible tant qu'on parcourt son mois : sans lui à l'écran,
|
|
une ligne isolée ne dit plus de quel mois elle relève, puisque les dates y
|
|
sont abrégées au quantième. */
|
|
.mois-groupe__titre {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 1;
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 10px;
|
|
margin: 0;
|
|
padding: 18px 2px 7px;
|
|
/* Même taille que les lignes qu'il coiffe : c'est la graisse et le filet qui
|
|
le détachent, pas un corps plus gros. Déclaré tout de même, sinon la taille
|
|
par défaut des `h2` s'appliquerait. */
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
background: var(--fond);
|
|
border-bottom: 1px solid var(--bordure-forte);
|
|
}
|
|
|
|
.mois-groupe__nom {
|
|
text-transform: capitalize;
|
|
}
|
|
|
|
/* Les annotations du titre — le repère du mois courant et le compte — partagent
|
|
la taille des pastilles de tags : ce sont les seuls éléments de la vue à
|
|
descendre sous la taille du corps, parce qu'ils commentent la liste au lieu
|
|
d'en faire partie. */
|
|
.mois-groupe__courant {
|
|
font-size: var(--taille-annotation);
|
|
font-weight: 500;
|
|
padding: 1px 8px;
|
|
border-radius: 9px;
|
|
background: color-mix(in srgb, var(--aujourdhui) 16%, var(--surface));
|
|
color: var(--aujourdhui);
|
|
}
|
|
|
|
.mois-groupe__compte {
|
|
margin-left: auto;
|
|
font-size: var(--taille-annotation);
|
|
font-weight: 400;
|
|
color: var(--texte-doux);
|
|
}
|
|
|
|
.mois-entrees {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
/* Colonnes fixes d'une ligne à l'autre : c'est l'alignement vertical des dates,
|
|
des noms et des statuts qui rend la liste parcourable en diagonale. Les
|
|
colonnes de texte sont en `minmax(0, …)` pour que le rognage se fasse à
|
|
l'intérieur, sans jamais pousser le statut hors de l'écran.
|
|
L'ordre suit celui de la lecture : quand, chez qui, puis quoi. Le ressort
|
|
(`1fr`) est sur l'intitulé de la phase, dernière colonne de texte, pour que
|
|
l'espace disponible se déverse à sa droite plutôt qu'au milieu de la ligne. */
|
|
.mois-entree {
|
|
display: grid;
|
|
grid-template-columns: 14px 16px 124px minmax(0, 200px) 172px minmax(0, 1fr) 86px;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 6px 2px;
|
|
border-bottom: 1px solid var(--bordure);
|
|
}
|
|
|
|
.mois-entree:hover {
|
|
background: var(--surface);
|
|
}
|
|
|
|
/* Toute la ligne déplie ses notes, ce qui doit se voir avant le clic : le
|
|
curseur et le fond qui s'appuie au survol le disent, le triangle de la
|
|
gouttière dit ce qui va se passer. Les lignes sans notes ne réagissent pas —
|
|
promettre un geste qui ne fait rien est pire que ne rien promettre. */
|
|
.mois-entree--commentee {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.mois-entree--commentee:hover {
|
|
background: color-mix(in srgb, var(--accent) 6%, var(--surface));
|
|
}
|
|
|
|
/* Reprise des formes de la frise : une barre pour une tâche, un losange pour un
|
|
jalon, et les mêmes textures de statut — c'est le même objet vu autrement. */
|
|
.mois-entree__marque {
|
|
width: 14px;
|
|
height: 8px;
|
|
border-radius: 2px;
|
|
background: var(--couleur-projet);
|
|
}
|
|
|
|
.mois-entree--todo .mois-entree__marque {
|
|
background: color-mix(in srgb, var(--couleur-projet) 28%, var(--surface));
|
|
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 {
|
|
background: var(--couleur-projet);
|
|
}
|
|
|
|
.mois-entree--blocked .mois-entree__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
|
|
);
|
|
}
|
|
|
|
.mois-entree--jalon .mois-entree__marque {
|
|
width: 10px;
|
|
height: 10px;
|
|
margin-left: 2px;
|
|
border-radius: 0;
|
|
transform: rotate(45deg);
|
|
}
|
|
|
|
.mois-entree--jalon.mois-entree--blocked .mois-entree__marque {
|
|
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
|
|
fait de la colonne une échelle lisible plutôt qu'une pile de textes. */
|
|
.mois-entree__dates {
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--texte-doux);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.mois-entree__projet,
|
|
.mois-entree__nom {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Un jalon se distingue à la lecture, pas seulement à sa marque : sa date est
|
|
un rendez-vous, pas une durée. */
|
|
.mois-entree--jalon .mois-entree__nom {
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Triangle de dépli des notes, en gouttière. C'est le même bouton que celui qui
|
|
plie un projet sur la frise (`.plier`), au même endroit et avec le même
|
|
geste ; il en reprend donc l'aspect, en un peu plus lisible — ici il est seul
|
|
dans sa colonne, là-bas il jouxte le nom du projet.
|
|
Les lignes sans notes gardent la case, vide, pour que les colonnes suivantes
|
|
ne se décalent pas d'une ligne à l'autre. */
|
|
.mois-entree__plier {
|
|
justify-self: center;
|
|
padding: 0;
|
|
border: 0;
|
|
background: none;
|
|
font-size: 12px;
|
|
line-height: 1;
|
|
color: var(--texte-doux);
|
|
transition: transform 0.15s;
|
|
}
|
|
|
|
/* La case des lignes sans notes est un `span` vide : elle réserve la colonne,
|
|
elle ne se clique pas. */
|
|
button.mois-entree__plier {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.mois-entree__plier[aria-expanded="true"] {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
.mois-entree__plier:hover {
|
|
color: var(--texte);
|
|
}
|
|
|
|
/* Les notes prennent une seconde rangée, en retrait de la gouttière et de la
|
|
marque — les colonnes sont nommées par leur rang, ce qui laisse la grille
|
|
faire le calcul, là où un retrait en pixels aurait recopié des largeurs et se
|
|
serait désaccordé à la première retouche. */
|
|
/* Le `pre-wrap` d'autrefois n'a plus lieu d'être : les retours à la ligne sont
|
|
désormais portés par le rendu markdown, qui les traduit en `<br>` et en
|
|
éléments de liste. */
|
|
.mois-entree__notes {
|
|
grid-column: 3 / -1;
|
|
margin: 2px 0 6px;
|
|
color: var(--texte-doux);
|
|
}
|
|
|
|
/* Le projet passe devant la phase, mais reste en retrait : il situe, il n'est
|
|
pas ce qu'on cherche. Le contraste sépare les deux colonnes de texte mieux
|
|
qu'un filet ne le ferait. */
|
|
.mois-entree__projet {
|
|
color: var(--texte-doux);
|
|
}
|
|
|
|
.mois-entree__projet::before {
|
|
content: "";
|
|
display: inline-block;
|
|
width: 7px;
|
|
height: 7px;
|
|
margin-right: 6px;
|
|
border-radius: 50%;
|
|
background: var(--couleur-projet);
|
|
vertical-align: baseline;
|
|
}
|
|
|
|
/* Les tags gardent leur colonne même quand le projet n'en porte aucun : c'est
|
|
une colonne de grille, de largeur fixe, pas un élément qui flotterait à la
|
|
suite du projet. Sans cette fixité, l'intitulé des phases démarrerait à une
|
|
abscisse différente selon le nombre de tags de leur projet. */
|
|
.mois-entree .tags {
|
|
max-width: 100%;
|
|
}
|
|
|
|
/* Les pastilles de la colonne des libellés sont taillées pour une colonne
|
|
étroite (10 px) ; ici la place ne manque pas, et une ligne où seuls les tags
|
|
seraient minuscules se lirait mal. Elles rejoignent donc la taille des
|
|
annotations, comme dans la barre de filtres. */
|
|
.mois-entree .tag {
|
|
font-size: var(--taille-annotation);
|
|
padding: 1px 8px;
|
|
border-radius: 10px;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.mois-entree__statut {
|
|
text-align: right;
|
|
color: var(--texte-doux);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.mois-entree__statut[data-statut="doing"] {
|
|
color: var(--accent);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.mois-entree__statut[data-statut="blocked"] {
|
|
color: var(--danger);
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Une phase terminée reste consultable mais recule d'un plan : ce qui compte
|
|
dans une liste tournée vers ce qui arrive, c'est ce qui n'est pas fait. */
|
|
.mois-entree--done .mois-entree__nom {
|
|
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;
|
|
text-align: center;
|
|
color: var(--texte-doux);
|
|
}
|
|
|
|
/* --- états divers -------------------------------------------------------- */
|
|
|
|
.vide {
|
|
margin: auto;
|
|
text-align: center;
|
|
color: var(--texte-doux);
|
|
line-height: 2.4;
|
|
}
|
|
|
|
.alerte {
|
|
position: fixed;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--fond);
|
|
z-index: 50;
|
|
padding: 24px;
|
|
}
|
|
|
|
.alerte p {
|
|
max-width: 560px;
|
|
padding: 16px 20px;
|
|
border-radius: var(--rayon);
|
|
border: 1px solid var(--danger);
|
|
background: color-mix(in srgb, var(--danger) 8%, var(--surface));
|
|
color: var(--texte);
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
/* Pendant un glisser, on neutralise la sélection de texte sur toute la page :
|
|
sinon le navigateur surligne les libellés au passage du curseur. */
|
|
body.glisse-en-cours {
|
|
user-select: none;
|
|
cursor: grabbing;
|
|
}
|