La frise répond mal à « qu'est-ce qui arrive le mois prochain » : la question traverse les projets, alors que la frise les range précisément en couloirs séparés. Il fallait balayer une colonne de dates à travers tous les couloirs en gardant en tête ce qu'on avait vu plus haut — d'où le tableur ressorti à côté. Deux boutons dans la barre d'outils basculent vers une liste qui déroule toutes les phases, projets mêlés, dans l'ordre du calendrier. Chaque ligne se lit quand, chez qui, puis quoi : période, projet et ses tags, intitulé, statut. Une phase est rattachée au seul mois de son début. La répéter dans chaque mois traversé aurait donné un tableau de charge — utile, mais c'est une autre vue, et le doublon coûte la lecture en diagonale qui fait tout l'intérêt de celle-ci. Sa fin est écrite sur la ligne, ce qui suffit à voir qu'elle déborde. Les mois sans rien ne sont pas représentés : l'absence se lit déjà dans le saut d'un titre au suivant. La liste ne s'édite pas, et ce n'est pas une lacune à combler : déplacer une phase demande de voir ce qu'elle chevauche et ce qu'elle repousse, or c'est justement ce voisinage que l'ordre chronologique dissout. Seules les pastilles de tags restent cliquables — filtrer ne change pas les données. Le filtre par tags vaut pour les deux vues, sans rien changer à sa mécanique, si bien qu'on ne se demande jamais si l'une montre plus que l'autre. L'œil (hidden) écarte un projet de la liste comme il écarte ses barres. La vue choisie n'est pas enregistrée, contrairement à la largeur de la colonne des libellés : cette largeur est un réglage d'écran, la vue est un geste de consultation. L'outil s'ouvre donc sur la frise, celle où l'on modifie. Le défilement horizontal de la frise, lui, est mémorisé le temps de l'aller-retour, puisque masquer un élément lui fait perdre son scrollLeft. Deux extractions au passage, chacune parce que deux vues sœurs en ont besoin et qu'aucune n'a à importer l'autre : les noms de mois et le formatage des dates rejoignent model.js, les pastilles de tags un module tags.js. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1327 lines
31 KiB
CSS
1327 lines
31 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;
|
|
|
|
--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;
|
|
|
|
--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);
|
|
}
|
|
}
|
|
|
|
* {
|
|
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;
|
|
}
|
|
|
|
/* --- 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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* --- 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);
|
|
}
|
|
|
|
.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. */
|
|
.barre--todo {
|
|
background: color-mix(in srgb, var(--couleur-projet) 28%, var(--surface));
|
|
color: var(--texte);
|
|
border: 1px solid var(--couleur-projet);
|
|
}
|
|
|
|
.barre--doing {
|
|
background: var(--couleur-projet);
|
|
}
|
|
|
|
.barre--done {
|
|
background: color-mix(in srgb, var(--couleur-projet) 55%, #64748b);
|
|
}
|
|
|
|
.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
|
|
);
|
|
}
|
|
|
|
/* 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 {
|
|
opacity: 1;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
.jalon--done {
|
|
background: color-mix(in srgb, var(--couleur-projet) 55%, #64748b);
|
|
}
|
|
|
|
.jalon--blocked {
|
|
background: var(--danger);
|
|
}
|
|
|
|
.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é, et ne se glisse pas.
|
|
Lecture seule : 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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* --- 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: 340px;
|
|
max-width: 90vw;
|
|
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;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
.panneau__corps {
|
|
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 select,
|
|
.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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* --- 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;
|
|
}
|
|
|
|
/* --- 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: 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);
|
|
}
|
|
|
|
/* 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--done .mois-entree__marque {
|
|
background: color-mix(in srgb, var(--couleur-projet) 55%, #64748b);
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
.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;
|
|
}
|