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>
264 lines
10 KiB
HTML
264 lines
10 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>
|
||
</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>
|
||
|
||
<!-- 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>
|