Files
gestion_projets/index.html
Bertrand Benjamin 3092be86ae Piloter la frise au clavier, à la vim
Les flèches décalaient déjà une phase, mais rien ne permettait de la choisir :
`selection` n'était posée qu'au clic, si bien qu'il fallait la souris pour
commencer. Elle devient `curseur`, désigne une ligne — celle d'un projet en est
une, d'où l'on plie, renomme et ajoute — et survit à Échap, sans quoi elle ne
pourrait servir de point de départ au déplacement suivant. `dessiner` le
normalise, la ligne du projet servant de refuge quand la sienne disparaît.

Deux couches qui ne se recouvrent pas : les flèches modifient, les lettres
déplacent et commandent. Aucun raccourci documenté ne change de sens. Les
touches se taisent dès que le clavier est à quelqu'un d'autre — une saisie, un
dialogue, le menu, un glisser —, faute de quoi `a` dans un nom créerait une
phase. AltGr est toléré : sur un clavier français `{` et `}` n'existent qu'avec
lui, et Windows le présente comme Ctrl+Alt.

Les phases étant triées par date, `o` et `O` insèrent dans le temps et non dans
un rang. `P` reste hors de cette famille : un projet naît sans position.

Le curseur se peint dans la colonne des libellés, seul endroit qui porte une
ligne pour chaque position atteignable — projet plié, projet masqué, ligne de
projet. Et `?` ouvre la table des raccourcis, qui ne se devinent pas.

Décision 26.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:36:07 +02:00

378 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Frise multi-projets</title>
<link rel="stylesheet" href="css/style.css" />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect width='16' height='16' rx='3' fill='%233b82f6'/><rect x='3' y='4' width='9' height='2' rx='1' fill='white'/><rect x='5' y='7' width='8' height='2' rx='1' fill='white'/><rect x='3' y='10' width='6' height='2' rx='1' fill='white'/></svg>"
/>
</head>
<body>
<header class="outils">
<h1>Frise multi-projets</h1>
<!--
Deux regards sur les mêmes données : la frise, qui montre les durées et
les chevauchements, et la liste par mois, qui montre l'ordre du
calendrier tous projets mêlés. Le filtre par tags, lui, vaut pour les
deux — d'où sa place inchangée juste en dessous.
-->
<!--
Deux boutons à deux états plutôt que le motif ARIA « tablist » : celui-ci
promet une navigation aux flèches, or les flèches servent déjà à décaler
la phase sélectionnée. Mieux vaut ne rien annoncer qu'annoncer faux.
-->
<div class="onglets" id="onglets" role="group" aria-label="Vue">
<button type="button" data-vue="frise" id="onglet-frise">Frise</button>
<button type="button" data-vue="mois" id="onglet-mois">Par mois</button>
</div>
<div class="outils__groupe">
<button type="button" id="aller-aujourdhui">Aujourd'hui</button>
<button type="button" id="nouveau-projet" class="bouton--principal">
Nouveau projet
</button>
<!--
Des raccourcis d'une lettre ne se devinent pas. Ce bouton est le seul
endroit de l'écran qui dit qu'ils existent : sans lui, la moitié de ce
que l'outil sait faire au clavier resterait dans le README.
-->
<button
type="button"
id="ouvrir-aide"
class="outils__aide"
title="Raccourcis clavier (?)"
aria-label="Raccourcis clavier"
>
?
</button>
</div>
<p class="outils__etat" id="etat-sauvegarde" role="status" aria-live="polite"></p>
</header>
<!--
Barre de filtres, masquée tant qu'aucun projet ne porte de tag : elle
n'apparaît que le jour où elle sert à quelque chose.
-->
<div class="filtres" id="filtres" hidden role="group" aria-label="Filtrer par tag">
<span class="filtres__titre">Filtrer</span>
<div class="tags tags--filtres" id="filtres-liste"></div>
<button type="button" id="effacer-filtres" hidden>Tout afficher</button>
</div>
<!--
Un seul conteneur défilant, découpé en grille 2×2 : l'en-tête de dates est
collé en haut (`sticky top`), la colonne des libellés collée à gauche
(`sticky left`), et le coin aux deux. Le navigateur gère ainsi le
défilement des deux axes sans aucune synchronisation en JavaScript.
-->
<main class="frise" id="frise">
<div class="grille">
<div class="grille__coin">
<!--
Poignée de largeur de la colonne. Placée dans le coin, seul endroit
de la colonne qui reste visible quel que soit le défilement.
-->
<div
class="poignee-colonne"
id="poignee-colonne"
role="separator"
aria-orientation="vertical"
aria-label="Largeur de la colonne des projets"
tabindex="0"
title="Glisser pour élargir la colonne — double-clic pour la réinitialiser"
></div>
</div>
<div class="grille__entete" id="entete"></div>
<div class="grille__libelles" id="libelles"></div>
<div class="grille__couloirs" id="couloirs"></div>
</div>
</main>
<!--
Vue par mois. Elle remplace la frise plutôt que de la flanquer : les deux
veulent toute la largeur, l'une pour dérouler le temps, l'autre pour poser
côte à côte la date, le nom, le projet et le statut.
-->
<section class="mois" id="vue-mois" hidden aria-label="Tâches et jalons par mois"></section>
<p class="vide" id="message-vide" hidden>
Aucun projet pour l'instant.<br />
<button type="button" id="premier-projet" class="bouton--principal">
Créer un premier projet
</button>
</p>
<p class="vide" id="message-filtre" hidden>
Aucun projet ne porte tous les tags sélectionnés.<br />
<button type="button" id="effacer-filtres-vide">Tout afficher</button>
</p>
<!--
Panneau de détail, ouvert au clic sur une phase. Volontairement **non
modal** : la frise reste vivante derrière lui, on passe d'une phase à
l'autre en cliquant sa barre. `tabindex="-1"` permet d'y poser le focus à
l'ouverture sans le placer dans un champ de saisie, ce qui laisse les
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>
</div>
<form class="panneau__corps" id="formulaire-phase">
<label>
Nom
<input type="text" name="name" required autocomplete="off" />
</label>
<div class="champ-double">
<label>
Début
<input type="date" name="start" required />
</label>
<label id="champ-fin">
Fin
<input type="date" name="end" required />
</label>
</div>
<label>
Statut
<select name="status">
<option value="todo">À venir</option>
<option value="doing">En cours</option>
<option value="done">Terminé</option>
<option value="blocked">Bloqué</option>
</select>
</label>
<label class="case">
<input type="checkbox" name="milestone" />
Jalon (une seule date, affichée en losange)
</label>
<!--
Les notes sont bâties par `notes.js` : aperçu markdown, barre d'outils
et zone de saisie, le tout identique dans les deux panneaux. Un seul
point d'ancrage ici plutôt que deux fois la même douzaine de balises.
-->
<section id="notes-phase"></section>
<p class="panneau__erreur" id="erreur-phase" role="alert" hidden></p>
<div class="panneau__actions">
<button type="button" id="supprimer-phase" class="bouton--danger">Supprimer</button>
</div>
</form>
</aside>
<!-- Menu contextuel, positionné à l'ouverture près de son point d'ancrage. -->
<div class="menu" id="menu" hidden role="menu"></div>
<!--
Paramètres d'un projet. Même panneau, mêmes gestes et même édition en
direct que le détail d'une phase : renommer un projet et renommer une
phase sont la même opération à un niveau près, elles n'ont pas à se
présenter différemment.
-->
<aside
class="panneau"
id="panneau-projet"
hidden
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>
</div>
<form class="panneau__corps" id="formulaire-projet">
<label>
Nom
<input type="text" name="name" required autocomplete="off" />
</label>
<fieldset class="couleurs">
<legend>Couleur du couloir</legend>
<div id="panneau-projet-couleurs"></div>
</fieldset>
<label>
Tags, séparés par des virgules
<input type="text" name="tags" autocomplete="off" placeholder="client, refonte" />
</label>
<!-- Masqué tant qu'aucun tag n'existe ailleurs dans le planning. -->
<fieldset class="couleurs" id="panneau-projet-suggestions-bloc" hidden>
<legend>Tags déjà utilisés</legend>
<div class="tags tags--suggestions" id="panneau-projet-suggestions"></div>
</fieldset>
<!--
Notes du projet : ce que la frise ne sait pas dire. Le contexte d'un
engagement, un interlocuteur, une décision et sa date. Elles occupent
le bas du volet, là où celles d'une phase occupent le bas du sien.
-->
<section id="notes-projet"></section>
<p class="panneau__erreur" id="erreur-projet" role="alert" hidden></p>
<div class="panneau__actions">
<button type="button" id="supprimer-projet" class="bouton--danger">
Supprimer le projet
</button>
</div>
</form>
</aside>
<!-- Confirmation d'une action irréversible. -->
<dialog class="dialogue" id="dialogue-confirmation">
<form method="dialog">
<h2 id="dialogue-confirmation-titre">Confirmer</h2>
<p id="dialogue-confirmation-message"></p>
<menu class="dialogue__actions">
<button type="submit" value="annuler">Annuler</button>
<button type="submit" value="ok" class="bouton--principal" id="dialogue-confirmation-ok">
Confirmer
</button>
</menu>
</form>
</dialog>
<!--
Aide des raccourcis, ouverte par `?`.
Un `<dialog>` modal, contrairement aux deux panneaux : celui-ci ne sert
qu'à lire, et le clavier doit y être *rendu* — laisser `j` déplacer le
curseur derrière la table qui explique `j` serait absurde. La modalité
apporte aussi le voile, le piège à focus et la fermeture par Échap.
La table est écrite à la main plutôt que bâtie depuis la table des
séquences de clavier.js : celle-ci associe une touche à un nom d'action, ce
qui ne dit ni ce que l'action fait, ni dans quel ordre la présenter, ni
quelles touches vont par paires. Une génération automatique produirait une
liste exacte et illisible.
-->
<dialog class="dialogue dialogue--aide" id="dialogue-aide" aria-labelledby="aide-titre">
<form method="dialog">
<h2 id="aide-titre">Raccourcis clavier</h2>
<div class="aide__colonnes">
<section>
<h3>Déplacer le curseur</h3>
<dl class="aide">
<dt><kbd>j</kbd> <kbd>k</kbd></dt>
<dd>Ligne suivante, précédente</dd>
<dt><kbd>h</kbd> <kbd>l</kbd></dt>
<dd>Phase précédente, suivante du même projet</dd>
<dt><kbd>{</kbd> <kbd>}</kbd></dt>
<dd>Projet précédent, suivant</dd>
<dt><kbd>gg</kbd> <kbd>G</kbd></dt>
<dd>Première, dernière ligne</dd>
</dl>
</section>
<section>
<h3>Modifier la phase du curseur</h3>
<dl class="aide">
<dt><kbd></kbd> <kbd></kbd></dt>
<dd>Décale d'un jour</dd>
<dt><kbd>Maj</kbd>+<kbd></kbd> <kbd></kbd></dt>
<dd>Allonge, raccourcit d'un jour</dd>
<dt><kbd>Espace</kbd></dt>
<dd>Statut suivant : à venir, en cours, terminé, bloqué</dd>
<dt><kbd>Entrée</kbd> <kbd>e</kbd></dt>
<dd>Ouvre le volet et saisit le nom</dd>
<dt><kbd>dd</kbd></dt>
<dd>Supprime la phase, ou le projet depuis sa ligne</dd>
</dl>
</section>
<section>
<h3>Ajouter</h3>
<dl class="aide">
<dt><kbd>A</kbd></dt>
<dd>Une phase à la fin du projet</dd>
<dt><kbd>a</kbd></dt>
<dd>Une phase au début du projet</dd>
<dt><kbd>o</kbd></dt>
<dd>Une phase juste après celle du curseur</dd>
<dt><kbd>O</kbd></dt>
<dd>Une phase juste avant elle</dd>
<dt><kbd>P</kbd></dt>
<dd>Un projet</dd>
</dl>
</section>
<section>
<h3>Plier, cadrer, naviguer</h3>
<dl class="aide">
<dt><kbd>za</kbd></dt>
<dd>Plie ou déplie le projet</dd>
<dt><kbd>zR</kbd> <kbd>zM</kbd></dt>
<dd>Déplie tout, plie tout</dd>
<dt><kbd>zz</kbd></dt>
<dd>Recentre la frise sur le curseur</dd>
<dt><kbd>w</kbd> <kbd>b</kbd></dt>
<dd>Un mois plus tard, plus tôt</dd>
<dt><kbd>W</kbd> <kbd>B</kbd></dt>
<dd>Un an plus tard, plus tôt</dd>
<dt><kbd>ga</kbd></dt>
<dd>Revient à aujourd'hui</dd>
<dt><kbd>gm</kbd></dt>
<dd>Bascule frise et vue par mois</dd>
<dt><kbd>Maj</kbd>+molette</dt>
<dd>Fait défiler la frise horizontalement</dd>
</dl>
</section>
</div>
<p class="aide__note">
Toutes ces touches se taisent pendant une saisie : dans un champ, une
note ou un dialogue, elles écrivent la lettre qu'elles portent.
<kbd>Échap</kbd> referme le volet ouvert sans déplacer le curseur.
</p>
<menu class="dialogue__actions">
<button type="submit" value="ok" class="bouton--principal">Fermer</button>
</menu>
</form>
</dialog>
<!-- Bandeau d'erreur bloquante : fichier corrompu, serveur injoignable. -->
<div class="alerte" id="alerte" hidden role="alert">
<p id="alerte-message"></p>
</div>
<script type="module" src="js/app.js"></script>
</body>
</html>