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>
378 lines
14 KiB
HTML
378 lines
14 KiB
HTML
<!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">×</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">×</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>
|