Filtrer la frise par tag

Les tags s'affichent en pastilles colorées dans la colonne des libellés, et une
barre de filtres apparaît sous la barre d'outils dès qu'un tag existe. Cliquer un
tag — dans cette barre ou directement sur la pastille d'un projet — restreint la
frise ; recliquer le retire.

Le filtre est conjonctif : un projet doit porter tous les tags cochés. Cocher un
tag de plus resserre donc toujours la sélection, sans jamais faire se repeupler
la frise. Un message dédié prend la place de la frise quand plus rien ne passe le
filtre, sans quoi elle laisserait croire que les projets ont disparu.

Le filtre est un état de vue, jamais enregistré, au même titre que la fenêtre
temporelle. Il ne fait pas le même travail que le masquage par l'œil, qui reste
enregistré : le filtre retire le projet de la vue entière, l'œil garde son
libellé grisé et n'ôte que les barres.

Deux ajustements de confort : créer un projet sous filtre actif préremplit ses
tags avec ceux du filtre, sinon le nouveau venu naîtrait invisible ; et un tag
que plus aucun projet ne porte est retiré du filtre, sinon il masquerait tout.

Le dialogue d'un projet gagne un champ de saisie et propose en un clic les tags
déjà employés ailleurs, seul garde-fou contre la dérive orthographique.

Dans la colonne des libellés, les pastilles rognent le nom du projet mais lui
laissent quatre caractères, et s'effacent à leur tour plutôt que de repousser
l'œil et le menu hors de la colonne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 05:52:28 +02:00
parent 3be367488c
commit 39ea249cdb
5 changed files with 372 additions and 20 deletions

View File

@@ -135,11 +135,22 @@ function positionner(menu, ancre) {
// ---------------------------------------------------------------------------
/**
* Demande le nom et la couleur d'un projet.
* @returns {Promise<{nom: string, couleur: string} | null>} null si annulé.
* Demande le nom, la couleur et les tags d'un projet.
* @returns {Promise<{nom: string, couleur: string, tags: string[]} | null>} null si annulé.
*/
export function creerDialogueProjet(refs, couleurs) {
const { dialogue, titre, champNom, conteneurCouleurs, erreur, valider } = refs;
export function creerDialogueProjet(refs, couleurs, outilsTags) {
const {
dialogue,
titre,
champNom,
conteneurCouleurs,
champTags,
suggestionsTags,
blocSuggestions,
erreur,
valider,
} = refs;
const { normaliserTags, cleTag, teinteTag } = outilsTags;
let choisie = couleurs[0];
function peindrePalette() {
@@ -159,13 +170,59 @@ export function creerDialogueProjet(refs, couleurs) {
}
}
return function demander({ titreTexte, libelleValider, nom = '', couleur = couleurs[0] }) {
/** Tags saisis dans le champ, tels qu'ils seront enregistrés. */
function tagsSaisis() {
return normaliserTags(champTags.value.split(','));
}
/**
* Tags déjà employés ailleurs dans le planning, proposés en un clic.
*
* Sans eux, chaque projet inventerait sa propre orthographe et le filtre se
* remplirait de « client », « Clients » et « client final ».
*/
function peindreSuggestions(tagsConnus) {
suggestionsTags.replaceChildren();
const posees = new Set(tagsSaisis().map(cleTag));
for (const tag of tagsConnus) {
const bouton = document.createElement('button');
bouton.type = 'button';
bouton.className = 'tag';
bouton.style.setProperty('--teinte-tag', String(teinteTag(tag)));
bouton.textContent = tag;
bouton.setAttribute('aria-pressed', String(posees.has(cleTag(tag))));
bouton.addEventListener('click', () => {
const restants = tagsSaisis().filter((pose) => cleTag(pose) !== cleTag(tag));
const suivants = restants.length === tagsSaisis().length ? [...restants, tag] : restants;
champTags.value = normaliserTags(suivants).join(', ');
peindreSuggestions(tagsConnus);
});
suggestionsTags.append(bouton);
}
blocSuggestions.hidden = tagsConnus.length === 0;
}
return function demander({
titreTexte,
libelleValider,
nom = '',
couleur = couleurs[0],
tags = [],
tagsConnus = [],
}) {
titre.textContent = titreTexte;
valider.textContent = libelleValider;
champNom.value = nom;
champTags.value = tags.join(', ');
choisie = couleur;
erreur.hidden = true;
peindrePalette();
peindreSuggestions(tagsConnus);
const rafraichirSuggestions = () => peindreSuggestions(tagsConnus);
champTags.addEventListener('input', rafraichirSuggestions);
dialogue.showModal();
champNom.focus();
@@ -175,11 +232,12 @@ export function creerDialogueProjet(refs, couleurs) {
dialogue.addEventListener(
'close',
() => {
champTags.removeEventListener('input', rafraichirSuggestions);
// `returnValue` vaut '' quand le dialogue est fermé par Échap.
if (dialogue.returnValue !== 'ok') return resoudre(null);
const saisi = champNom.value.trim();
if (!saisi) return resoudre(null);
resoudre({ nom: saisi, couleur: choisie });
resoudre({ nom: saisi, couleur: choisie, tags: tagsSaisis() });
},
{ once: true }
);