Frise multi-projets : outil de planification macro

Une frise chronologique unique où plusieurs projets s'empilent en couloirs
pliables, pour voir d'un coup d'œil où en est chacun et comment ils se situent
les uns par rapport aux autres. Ce n'est pas un outil de suivi quotidien : une
phase se compte en semaines, et il n'y a ni sous-tâches, ni tickets, ni
dépendances entre phases.

- Manipulation directe des barres (glisser, redimensionner), accrochées au lundi
- Couloirs pliables : plié, un projet devient une barre segmentée par phase
- Planning de référence figeable, avec barre fantôme et calcul de dérive
- En-tête à trois bandes : année, mois, numéro de semaine ISO 8601
- Frise qui s'élargit au défilement, pour planifier dans un futur encore vide
- Micro-serveur Python (bibliothèque standard) exposant GET/PUT sur /api/data,
  avec sauvegarde horodatée avant chaque écriture
- Zéro build : modules ES natifs, aucune dépendance à installer

50 tests unitaires sur la logique métier (node --test, sans dépendance).
Vérifié dans Chromium et Firefox.

Les arbitrages de conception et surtout leurs raisons sont consignés dans
docs/decisions.md — notamment l'abandon des dépendances entre phases, de la
File System Access API, du SVG et des niveaux de zoom.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 06:53:57 +02:00
commit 98ae311545
16 changed files with 4766 additions and 0 deletions

158
index.html Normal file
View File

@@ -0,0 +1,158 @@
<!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>
<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>
<!--
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"></div>
<div class="grille__entete" id="entete"></div>
<div class="grille__libelles" id="libelles"></div>
<div class="grille__couloirs" id="couloirs"></div>
</div>
</main>
<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>
<!-- Panneau de détail, ouvert au clic sur une phase. -->
<aside class="panneau" id="panneau" hidden aria-label="Détail de la phase">
<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>
<label>
Notes
<textarea name="notes" rows="4"></textarea>
</label>
<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>
<div class="voile" id="voile" hidden></div>
<!-- Menu contextuel, positionné à l'ouverture près de son point d'ancrage. -->
<div class="menu" id="menu" hidden role="menu"></div>
<!-- Création et renommage d'un projet. -->
<dialog class="dialogue" id="dialogue-projet">
<form method="dialog">
<h2 id="dialogue-projet-titre">Nouveau projet</h2>
<label>
Nom
<input type="text" id="dialogue-projet-nom" required autocomplete="off" />
</label>
<fieldset class="couleurs">
<legend>Couleur du couloir</legend>
<div id="dialogue-projet-couleurs"></div>
</fieldset>
<p class="dialogue__erreur" id="dialogue-projet-erreur" role="alert" hidden></p>
<menu class="dialogue__actions">
<!--
`formnovalidate` est indispensable : le champ « Nom » est `required`,
et sans lui la validation HTML refuserait la soumission — donc la
fermeture du dialogue — tant qu'aucun nom n'est saisi. On se
retrouverait incapable d'annuler une création de projet.
-->
<button type="submit" value="annuler" formnovalidate>Annuler</button>
<button type="submit" value="ok" class="bouton--principal" id="dialogue-projet-ok">
Créer
</button>
</menu>
</form>
</dialog>
<!-- 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>