Elle s'épaississait en mode plié, au motif qu'elle y était seule sur sa ligne. Mais plier un couloir ne change rien à ce que cette barre représente : la voir changer d'aspect au pli laissait croire à deux objets différents. Elle garde donc partout la hauteur de sa version dépliée, plus mince qu'une barre de phase — puisqu'elle voisine avec elles dès que le projet est déplié, et qu'elle ne se glisse pas. `--hauteur-cumulative` remonte dans `:root` : la barre et le fantôme qui la double sont frères et non imbriqués, la variable doit donc vivre au-dessus des deux. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1071 lines
24 KiB
CSS
1071 lines
24 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);
|
|
}
|
|
|
|
@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;
|
|
}
|
|
|
|
/* --- 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;
|
|
}
|
|
|
|
/* --- é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;
|
|
}
|