Glisser la barre résumé décale toutes les phases du même nombre de jours, écarts conservés : un projet engagé glisse souvent, et le faire phase par phase les déformait. Restent en place ce qui est terminé — on ne réécrit pas le passé — et les phases à date imposée, nouveau booléen `fixed` pour ce qui est délégué ou tenu du dehors. D'où le format en version 6 : sans l'incrément, un binaire antérieur effacerait ces dates imposées à la première sauvegarde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
466 lines
18 KiB
HTML
466 lines
18 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>
|
||
|
||
<!--
|
||
Filtre par état, masqué tant que tous les projets sont dans le même : lui
|
||
aussi n'apparaît que le jour où il sert.
|
||
|
||
Il ne se comporte pas comme celui des tags, et c'est pourquoi il ne lui
|
||
ressemble pas. Un projet porte plusieurs tags mais un seul état : cocher un
|
||
tag de plus resserre la sélection, cocher un état de plus l'élargit. D'où
|
||
« Afficher » ici et « Filtrer » en dessous, et deux formes distinctes —
|
||
sans quoi la même case voudrait dire deux choses opposées selon la ligne
|
||
où elle se trouve.
|
||
-->
|
||
<div class="etats" id="etats" hidden role="group" aria-label="Afficher par état">
|
||
<span class="filtres__titre">Afficher</span>
|
||
<div class="etats__liste" id="etats-liste"></div>
|
||
</div>
|
||
|
||
<!--
|
||
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>
|
||
|
||
<!--
|
||
Deux tamis peuvent vider la vue — les tags cochés, les états décochés — et
|
||
celui qui les a resserrés ne sait pas toujours lequel a fini par tout
|
||
emporter. Le bouton relâche donc les deux d'un coup.
|
||
-->
|
||
<p class="vide" id="message-filtre" hidden>
|
||
Aucun projet ne passe les filtres en cours.<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>
|
||
|
||
<!--
|
||
Les deux cases encadrent les dates, chacune du côté où elle agit :
|
||
« Jalon » décide s'il y en a une ou deux, et se lit donc avant ;
|
||
« Date imposée » qualifie celles qu'on vient de saisir, et se lit
|
||
après. Le statut ferme la marche : il parle du travail, pas du
|
||
calendrier.
|
||
-->
|
||
<label class="case">
|
||
<input type="checkbox" name="milestone" />
|
||
Jalon (une seule date, affichée en losange)
|
||
</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>
|
||
|
||
<!--
|
||
Orthogonale au statut, et c'est le point : une phase déléguée avance,
|
||
se termine ou se bloque comme une autre. Seul son ancrage dans le temps
|
||
nous échappe (docs/decisions.md, section 28).
|
||
-->
|
||
<label class="case">
|
||
<input type="checkbox" name="fixed" />
|
||
Date imposée (déléguée, contractuelle ou réglementaire)
|
||
</label>
|
||
|
||
<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>
|
||
|
||
<!--
|
||
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">
|
||
<!--
|
||
Où en est le projet dans son cycle de vie. En lecture seule : l'état
|
||
se déduit des données, il ne se choisit pas dans une liste. Les deux
|
||
qui s'actent — terminer, écarter — sont dans le menu ⋯ du projet, avec
|
||
les autres décisions qu'on ne prend qu'une fois.
|
||
-->
|
||
<p class="panneau__etat" id="panneau-projet-etat"></p>
|
||
|
||
<label>
|
||
Nom
|
||
<input type="text" name="name" required autocomplete="off" />
|
||
</label>
|
||
|
||
<!--
|
||
Horizon : l'ancrage temporel d'un projet envisagé, au mois. Le bloc
|
||
disparaît dès que le projet a une phase — ce sont elles qui commandent
|
||
alors son emprise (docs/decisions.md, section 27).
|
||
-->
|
||
<fieldset class="horizon" id="panneau-projet-horizon" hidden>
|
||
<legend>Horizon</legend>
|
||
<div class="horizon__intervalle">
|
||
<label>
|
||
De
|
||
<input type="month" name="horizonStart" autocomplete="off" />
|
||
</label>
|
||
<label>
|
||
à
|
||
<input type="month" name="horizonEnd" autocomplete="off" />
|
||
</label>
|
||
</div>
|
||
<!--
|
||
Un horizon se repousse par crans et jamais au jour : c'est ce qui
|
||
garde l'imprécision visible. Les mêmes crans dans les deux sens.
|
||
-->
|
||
<div class="horizon__crans" id="panneau-projet-crans">
|
||
<button type="button" data-cran="-3" title="Avancer de trois mois">−3 mois</button>
|
||
<button type="button" data-cran="3" title="Repousser de trois mois">+3 mois</button>
|
||
<button type="button" data-cran="12" title="Repousser d'un an">+1 an</button>
|
||
</div>
|
||
<p class="horizon__aide" id="panneau-projet-horizon-aide"></p>
|
||
</fieldset>
|
||
|
||
<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>
|
||
|
||
<!--
|
||
Les deux actes du cycle de vie. Ils sont aussi dans le menu ⋯, avec les
|
||
autres décisions qu'on ne prend qu'une fois — mais c'est ici qu'on lit
|
||
l'état du projet, donc ici qu'on cherche à en changer.
|
||
-->
|
||
<div class="panneau__actions panneau__actions--cycle">
|
||
<button type="button" id="terminer-projet"></button>
|
||
<button type="button" id="ecarter-projet"></button>
|
||
</div>
|
||
|
||
<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 ligne du curseur</h3>
|
||
<dl class="aide">
|
||
<dt><kbd>←</kbd> <kbd>→</kbd></dt>
|
||
<dd>
|
||
Décale d'un jour — la phase du curseur, ou, sur la ligne d'un
|
||
projet, toutes ses phases non terminées
|
||
</dd>
|
||
<dt><kbd>Maj</kbd>+<kbd>←</kbd> <kbd>→</kbd></dt>
|
||
<dd>Allonge, raccourcit la phase 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>
|