É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:
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
|
||||
23
index.html
23
index.html
@@ -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">×</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">×</button>
|
||||
|
||||
175
js/app.js
175
js/app.js
@@ -474,49 +474,150 @@ function restaurerLargeurLibelles() {
|
||||
|
||||
restaurerLargeurLibelles();
|
||||
|
||||
{
|
||||
const poignee = $('poignee-colonne');
|
||||
let depart = null;
|
||||
// ---------------------------------------------------------------------------
|
||||
// Largeur du panneau
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
poignee.addEventListener('pointerdown', (evenement) => {
|
||||
if (evenement.button !== 0) return;
|
||||
depart = { x: evenement.clientX, largeur: largeurLibelles };
|
||||
poignee.setPointerCapture(evenement.pointerId);
|
||||
poignee.dataset.glisse = 'true';
|
||||
document.body.classList.add('glisse-en-cours');
|
||||
evenement.preventDefault();
|
||||
});
|
||||
/**
|
||||
* 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';
|
||||
|
||||
poignee.addEventListener('pointermove', (evenement) => {
|
||||
if (!depart) return;
|
||||
appliquerLargeurLibelles(depart.largeur + (evenement.clientX - depart.x));
|
||||
});
|
||||
/**
|
||||
* 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;
|
||||
|
||||
const terminer = (evenement) => {
|
||||
if (!depart) return;
|
||||
depart = null;
|
||||
poignee.releasePointerCapture?.(evenement.pointerId);
|
||||
delete poignee.dataset.glisse;
|
||||
document.body.classList.remove('glisse-en-cours');
|
||||
};
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
poignee.addEventListener('pointerup', terminer);
|
||||
poignee.addEventListener('pointercancel', terminer);
|
||||
|
||||
poignee.addEventListener('dblclick', () => appliquerLargeurLibelles(LARGEUR_LIBELLES.defaut));
|
||||
|
||||
// Les flèches ajustent aussi la largeur quand la poignée a le focus.
|
||||
// `stopPropagation` est indispensable : sans lui, le gestionnaire posé plus
|
||||
// bas sur `document` décalerait en même temps la phase sélectionnée.
|
||||
poignee.addEventListener('keydown', (evenement) => {
|
||||
const pas = evenement.key === 'ArrowLeft' ? -10 : evenement.key === 'ArrowRight' ? 10 : 0;
|
||||
if (pas === 0) return;
|
||||
evenement.preventDefault();
|
||||
evenement.stopPropagation();
|
||||
appliquerLargeurLibelles(largeurLibelles + pas);
|
||||
});
|
||||
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: largeur() };
|
||||
poignee.setPointerCapture(evenement.pointerId);
|
||||
poignee.dataset.glisse = 'true';
|
||||
document.body.classList.add('glisse-en-cours');
|
||||
evenement.preventDefault();
|
||||
});
|
||||
|
||||
poignee.addEventListener('pointermove', (evenement) => {
|
||||
if (!depart) return;
|
||||
appliquer(depart.largeur + sens * (evenement.clientX - depart.x));
|
||||
});
|
||||
|
||||
const terminer = (evenement) => {
|
||||
if (!depart) return;
|
||||
depart = null;
|
||||
poignee.releasePointerCapture?.(evenement.pointerId);
|
||||
delete poignee.dataset.glisse;
|
||||
document.body.classList.remove('glisse-en-cours');
|
||||
};
|
||||
|
||||
poignee.addEventListener('pointerup', terminer);
|
||||
poignee.addEventListener('pointercancel', terminer);
|
||||
|
||||
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
|
||||
// bas sur `document` décalerait en même temps la phase sélectionnée.
|
||||
poignee.addEventListener('keydown', (evenement) => {
|
||||
const pas = evenement.key === 'ArrowLeft' ? -10 : evenement.key === 'ArrowRight' ? 10 : 0;
|
||||
if (pas === 0) return;
|
||||
evenement.preventDefault();
|
||||
evenement.stopPropagation();
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user