Élargir le panneau en glissant son bord

Trois cent quarante pixels suffisaient à des champs de date ; ils ne suffisent
pas à une liste imbriquée, où chaque niveau mange une indentation.

Le panneau reçoit donc la même poignée que la colonne des libellés (décision
18) : même geste, même double-clic de remise à zéro, même mémorisation dans
`localStorage`. Le code de glisser, jusque-là écrit pour la seule colonne, est
paramétré par le sens et la largeur qu'il pilote plutôt que dupliqué.

Une largeur unique pour les deux panneaux : ce qu'on élargit est le volet, pas
l'un ou l'autre de ses contenus. La largeur voulue et la largeur appliquée sont
deux variables distinctes, faute de quoi réduire un instant la fenêtre ferait
perdre le réglage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 18:36:07 +02:00
parent ff147a17b9
commit 7ff1329a86
5 changed files with 214 additions and 41 deletions

View File

@@ -97,6 +97,7 @@ Une fois un projet en place :
|---|---|
| Glisser le corps d'une barre | Décale la phase, durée conservée |
| Glisser le bord d'une barre | Change la date de début ou de fin |
| Glisser le bord gauche du panneau | Change sa largeur (double-clic : d'origine) |
| Clic sur une barre ou son nom | Ouvre le panneau de détail |
| Clic sur une autre barre, panneau ouvert | Bascule le panneau sur cette phase |
| Clic sur un nom de phase hors écran | Ramène la frise sur elle |
@@ -132,6 +133,10 @@ décision à quelle date. Elles se lisent aussi au survol du nom du projet dans
et celles d'une phase au survol de sa barre : l'infobulle les affiche sans les marques de mise en
forme, puces et cases dessinées comprises.
Le **panneau se redimensionne** en glissant son bord gauche, comme la colonne des projets ; un
double-clic revient à la largeur d'origine. Cette largeur est propre au navigateur, elle n'est pas
enregistrée dans le planning.
### La vue par mois
Les deux boutons **Frise** et **Par mois**, en haut à gauche, changent de regard sur les mêmes

View File

@@ -31,6 +31,9 @@
/* 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);
@@ -814,8 +817,8 @@ button[aria-pressed="true"] {
top: 0;
right: 0;
bottom: 0;
width: 340px;
max-width: 90vw;
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);
@@ -824,6 +827,38 @@ button[aria-pressed="true"] {
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. */

View File

@@ -691,9 +691,18 @@ Une seule écriture y échappe : cocher une case depuis l'aperçu, où le champ
pas prendre le focus. Le geste vide donc la pile — sans grande conséquence, puisqu'on n'était pas en
train d'y taper.
### Les infobulles s'aplatissent
### Le volet s'élargit, les infobulles s'aplatissent
Les **infobulles** de la frise ne peuvent pas montrer de rendu : l'attribut `title` du
Trois cent quarante pixels suffisaient à des champs de date ; ils ne suffisent pas à une liste
imbriquée, où chaque niveau mange une indentation. Le panneau reçoit donc la **même poignée** que la
colonne des libellés (décision 18) : même geste, même double-clic de remise à zéro, même
mémorisation dans `localStorage` — c'est un réglage d'écran, pas une donnée à synchroniser. Une
largeur unique pour les deux panneaux, parce que ce qu'on élargit est le volet, pas l'un ou l'autre
de ses contenus. Elle est plafonnée à la fenêtre sans être écrasée pour autant : la largeur voulue
et la largeur appliquée sont deux variables distinctes, faute de quoi réduire un instant la fenêtre
ferait perdre le réglage.
Les **infobulles** de la frise, elles, ne peuvent pas montrer de rendu : l'attribut `title` du
navigateur ne connaît que le texte. Les notes y sont donc **aplaties** — marques retirées, et
remplacées par un caractère qui porte le même sens là où il y en a un : `•` pour un tiret, `☐` et
`☑` pour une case, `│` pour une citation. Sans cela, survoler une barre afficherait la source

View File

@@ -106,6 +106,20 @@
flèches piloter la phase sélectionnée.
-->
<aside class="panneau" id="panneau" hidden tabindex="-1" aria-label="Détail de la phase">
<!--
Poignée de largeur, sur le bord gauche. Même geste que celle de la
colonne des libellés, et même largeur pour les deux panneaux : ce qu'on
élargit, c'est le volet, pas l'un ou l'autre de ses contenus.
-->
<div
class="poignee-panneau"
data-poignee-panneau
role="separator"
aria-orientation="vertical"
aria-label="Largeur du panneau"
tabindex="0"
title="Glisser pour élargir le panneau — double-clic pour le réinitialiser"
></div>
<div class="panneau__entete">
<h2 id="panneau-titre">Phase</h2>
<button type="button" id="fermer-panneau" aria-label="Fermer">&times;</button>
@@ -173,6 +187,15 @@
tabindex="-1"
aria-label="Paramètres du projet"
>
<div
class="poignee-panneau"
data-poignee-panneau
role="separator"
aria-orientation="vertical"
aria-label="Largeur du panneau"
tabindex="0"
title="Glisser pour élargir le panneau — double-clic pour le réinitialiser"
></div>
<div class="panneau__entete">
<h2 id="panneau-projet-titre">Projet</h2>
<button type="button" id="fermer-panneau-projet" aria-label="Fermer">&times;</button>

113
js/app.js
View File

@@ -474,13 +474,90 @@ function restaurerLargeurLibelles() {
restaurerLargeurLibelles();
{
const poignee = $('poignee-colonne');
// ---------------------------------------------------------------------------
// Largeur du panneau
// ---------------------------------------------------------------------------
/**
* Le panneau porte désormais les notes, qui prennent toute sa hauteur restante
* et se rédigent en markdown. Trois cent quarante pixels suffisaient à des
* champs de date et à quatre lignes de texte ; ils ne suffisent plus à une liste
* à puces imbriquée, où chaque niveau mange une indentation.
*
* D'où la même poignée qu'à la colonne des libellés (décision 18) : même geste,
* même double-clic de remise à zéro, même mémorisation dans `localStorage`. Une
* largeur unique pour les deux panneaux, parce que ce qu'on élargit est le
* volet — l'espace où l'on écrit —, pas l'un ou l'autre de ses contenus.
*/
// `defaut` doit rester en accord avec `--largeur-panneau` dans style.css.
const LARGEUR_PANNEAU = { min: 300, max: 900, defaut: 380 };
const CLE_LARGEUR_PANNEAU = 'frise.largeurPanneau';
/**
* Deux largeurs plutôt qu'une : celle que l'utilisateur a demandée, et celle
* qui tient réellement dans la fenêtre.
*
* Le panneau ne peut pas couvrir tout l'écran — il laisse la frise vivante
* derrière lui (décision 17) —, et une largeur choisie sur un 27 pouces doit
* donc être rabotée sur un portable. Mais seulement rabotée : sans cette
* distinction, réduire un instant la fenêtre écraserait la largeur mémorisée
* par sa valeur contrainte, et l'agrandir de nouveau ne la rendrait pas.
*/
let largeurPanneauVoulue = LARGEUR_PANNEAU.defaut;
let largeurPanneau = LARGEUR_PANNEAU.defaut;
function appliquerLargeurPanneau(largeur, { memoriser = true } = {}) {
if (memoriser) {
largeurPanneauVoulue = Math.min(
LARGEUR_PANNEAU.max,
Math.max(LARGEUR_PANNEAU.min, Math.round(largeur))
);
try {
localStorage.setItem(CLE_LARGEUR_PANNEAU, String(largeurPanneauVoulue));
} catch {
// Mode privé, quota saturé : la largeur vaut pour la séance, sans plus.
}
}
const plafond = Math.max(LARGEUR_PANNEAU.min, window.innerWidth - 80);
largeurPanneau = Math.min(largeurPanneauVoulue, plafond);
document.documentElement.style.setProperty('--largeur-panneau', `${largeurPanneau}px`);
return largeurPanneau;
}
function restaurerLargeurPanneau() {
let memorisee = null;
try {
memorisee = localStorage.getItem(CLE_LARGEUR_PANNEAU);
} catch {
// Lecture refusée : on garde la valeur de la feuille de style.
}
const largeur = Number(memorisee);
if (Number.isFinite(largeur) && largeur > 0) appliquerLargeurPanneau(largeur);
}
restaurerLargeurPanneau();
/**
* Installe une poignée de redimensionnement.
*
* Deux réglages emploient le même geste, à deux détails près : le sens dans
* lequel tirer élargit, et l'endroit où l'on range le résultat. Le reste — la
* capture du pointeur, la classe qui fige le curseur pendant le glisser, le
* double-clic qui remet à la valeur d'usine, les flèches au clavier — est
* strictement le même, et n'a pas à être écrit deux fois.
*
* @param {object} options
* - sens : +1 si tirer vers la droite élargit (la colonne, ancrée à gauche),
* 1 sinon (le panneau, ancré à droite).
*/
function installerPoignee({ poignees, sens, largeur, appliquer, defaut }) {
for (const poignee of poignees) {
let depart = null;
poignee.addEventListener('pointerdown', (evenement) => {
if (evenement.button !== 0) return;
depart = { x: evenement.clientX, largeur: largeurLibelles };
depart = { x: evenement.clientX, largeur: largeur() };
poignee.setPointerCapture(evenement.pointerId);
poignee.dataset.glisse = 'true';
document.body.classList.add('glisse-en-cours');
@@ -489,7 +566,7 @@ restaurerLargeurLibelles();
poignee.addEventListener('pointermove', (evenement) => {
if (!depart) return;
appliquerLargeurLibelles(depart.largeur + (evenement.clientX - depart.x));
appliquer(depart.largeur + sens * (evenement.clientX - depart.x));
});
const terminer = (evenement) => {
@@ -503,7 +580,7 @@ restaurerLargeurLibelles();
poignee.addEventListener('pointerup', terminer);
poignee.addEventListener('pointercancel', terminer);
poignee.addEventListener('dblclick', () => appliquerLargeurLibelles(LARGEUR_LIBELLES.defaut));
poignee.addEventListener('dblclick', () => appliquer(defaut));
// Les flèches ajustent aussi la largeur quand la poignée a le focus.
// `stopPropagation` est indispensable : sans lui, le gestionnaire posé plus
@@ -513,10 +590,34 @@ restaurerLargeurLibelles();
if (pas === 0) return;
evenement.preventDefault();
evenement.stopPropagation();
appliquerLargeurLibelles(largeurLibelles + pas);
appliquer(largeur() + sens * pas);
});
}
}
installerPoignee({
poignees: [$('poignee-colonne')],
sens: 1,
largeur: () => largeurLibelles,
appliquer: appliquerLargeurLibelles,
defaut: LARGEUR_LIBELLES.defaut,
});
installerPoignee({
poignees: document.querySelectorAll('[data-poignee-panneau]'),
sens: -1,
largeur: () => largeurPanneau,
appliquer: appliquerLargeurPanneau,
defaut: LARGEUR_PANNEAU.defaut,
});
// Rétrécir la fenêtre sous la largeur du panneau le laisserait déborder : on
// rejoue le plafond sans toucher à la largeur mémorisée, qu'on retrouvera
// intacte en rouvrant la fenêtre en grand.
window.addEventListener('resize', () =>
appliquerLargeurPanneau(largeurPanneauVoulue, { memoriser: false })
);
// ---------------------------------------------------------------------------
// Fenêtre temporelle et défilement sans fin
// ---------------------------------------------------------------------------