Files
gestion_projets/index.html
Bertrand Benjamin 815e144d9a
All checks were successful
Build / build (push) Successful in 1m2s
Rendre lisibles le statut et la date imposée
Statut : quatre boutons peints de leur texture, si bien que le panneau
sert enfin de légende. « En cours » se raye et « terminé » redevient vif
— la trame les distingue, la teinte n'a plus à le faire.

Date imposée : un cadre tireté cerne la forme entière. Les taquets
tombaient sous les poignées de redimensionnement et disparaissaient sur
une barre courte. Son libellé passe dans le modèle, ce qui la fait
apparaître dans la vue par mois, qui la taisait.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 19:17:15 +02:00

505 lines
20 KiB
HTML
Raw Permalink 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>
<!--
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">&times;</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>
<!--
Le bloc se cerne du cadre tireté d'une date imposée quand la case
ci-dessous est cochée : elle agit sur ces deux champs-là, sa
conséquence doit se voir là où elle joue. Même dessin que sur la barre
de la frise — ce qui s'apprend d'un côté se relit de l'autre.
-->
<div class="champ-double" id="champ-dates">
<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>
<!--
Quatre boutons plutôt qu'une liste déroulante, chacun peint de la
texture qu'il donne à la barre sur la frise. Le panneau devient ainsi
sa propre légende, au seul moment où l'on en a besoin — celui du choix
—, et sans coûter d'espace permanent au planning. Une liste déroulante
cachait trois valeurs sur quatre, n'en montrait aucune texture, et
demandait deux clics là où il en faut un.
Les quatre `input` partagent le nom `status` : `formulaire.status`
reste ce qu'il était pour detail.js, une valeur qui se lit et s'écrit.
-->
<fieldset class="statuts">
<legend>Statut</legend>
<!--
Les quatre choix vivent dans un `div` et non directement sous le
`fieldset` : un `legend` n'est pas un enfant comme les autres, et les
navigateurs le sortent du flux de son parent — le mettre en grille
aurait fait dépendre la mise en page d'un cas particulier.
-->
<div class="statuts__choix">
<label class="statut statut--todo">
<input type="radio" name="status" value="todo" />
<span class="statut__marque"></span>
À venir
</label>
<label class="statut statut--doing">
<input type="radio" name="status" value="doing" />
<span class="statut__marque"></span>
En cours
</label>
<label class="statut statut--done">
<input type="radio" name="status" value="done" />
<span class="statut__marque"></span>
Terminé
</label>
<label class="statut statut--blocked">
<input type="radio" name="status" value="blocked" />
<span class="statut__marque"></span>
Bloqué
</label>
</div>
</fieldset>
<!--
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">
<!--
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&nbsp;mois</button>
<button type="button" data-cran="3" title="Repousser de trois mois">+3&nbsp;mois</button>
<button type="button" data-cran="12" title="Repousser d'un an">+1&nbsp;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>