gestion_table_tournante_libre/src/interface/accueil/Demonstrations.svelte
Mathieu Benoit 229a2d4067 [IMP] event list: summary, state filter tiles, table grouped by state
The list gave every event the same weight, so a manager could not see at
a glance what was still in draft or already printed. A header now sums
events and participants; one tile per plan state, in the § 9 order,
filters the table and holds only for the session; rows group by state or
not at all, under grouped column headers. Each « Ouvrir » is named after
its event, and messages take the avis, refus or information tone. The
cards sit under the table: a sidebar does not fit beside nine columns.
Checked: accueil node and browser tests, 8 mutations caught.

--- FR ---

[IMP] liste des événements : résumé, tuiles par état, groupes par état

La liste donnait à chaque événement le même poids : on ne voyait pas
d'un coup d'œil ce qui restait en brouillon ou déjà imprimé. Un en-tête
compte événements et participants ; une tuile par état du plan, dans
l'ordre du § 9, filtre le tableau le temps de la séance ; les lignes se
groupent par état ou pas, sous des en-têtes groupés. Chaque « Ouvrir »
porte le nom de son événement, les messages prennent le ton avis, refus
ou information. Les cartes passent sous le tableau, faute de place.
Vérifié : épreuves node et navigateur de l'accueil, 8 mutations prises.

Assisted-by: Claude Opus 5.5
2026-10-11 00:58:07 -04:00

130 lines
4 KiB
Svelte

<!-- © 2026 TechnoLibre (http://www.technolibre.ca)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
<script>
// Les démonstrations (§ 15), une issue toujours offerte du premier écran :
// une commande par démonstration reçue, dans leur ordre, qui porte son nom
// — une donnée, dans son propre nœud — et data-demonstration, sa clé.
// Charger crée un événement neuf, numéroté, ouvert en lecture ; aucun
// autre n'est touché. Le refus d'un chargement s'affiche sous sa commande.
//
// demonstrations : [{ cle, nom }] ; charger(cle) : le geste de l'accueil ;
// refus : { cle, refus } du dernier échec, ou null.
//
// Un chargement en cours ne se relance pas : tant que charger n'a pas
// rendu la main, chaque démonstration porte aria-disabled, celle qui se
// charge aussi aria-busy, et un clic ne charge rien. Un clic sur une autre
// se dit : sous elle, un message [data-attente-demonstration] nomme celle
// qui se charge, et le bouton le désigne par aria-describedby. Le message
// part avec le chargement.
//
// Les démonstrations sont une carte titrée, l'explication en sous-titre ;
// chacune un bouton secondaire, sur une rangée qui passe à la ligne.
import { getContext } from 'svelte';
import Carte from '../primitives/Carte.svelte';
import Refus from '../primitives/Refus.svelte';
let { demonstrations, charger, refus } = $props();
const t = getContext('t');
const uid = $props.id();
// La clé de la démonstration qui se charge, ou null ; celle d'une autre
// cliquée pendant ce temps, ou null.
let enCours = $state(null);
let attente = $state(null);
const nomDe = (cle) => demonstrations.find((demonstration) => demonstration.cle === cle)?.nom ?? '';
async function choisir(cle) {
if (enCours !== null) {
if (cle !== enCours) attente = cle;
return;
}
enCours = cle;
try {
await charger(cle);
} finally {
enCours = null;
attente = null;
}
}
</script>
<Carte titre={t('accueil.demonstrations.titre')} sousTitre={t('accueil.demonstrations.explication')} icone="demarrer">
<ul>
{#each demonstrations as { cle, nom } (cle)}
<li>
<button
type="button"
data-demonstration={cle}
aria-busy={enCours === cle ? 'true' : undefined}
aria-disabled={enCours !== null ? 'true' : undefined}
aria-describedby={attente === cle ? `${uid}-attente` : undefined}
onclick={() => choisir(cle)}>{nom}</button
>
{#if attente === cle}
<p id={`${uid}-attente`} role="status" data-attente-demonstration
>{t('accueil.demonstrations.enCours', { nom: nomDe(enCours) })}</p
>
{/if}
{#if refus !== null && refus.cle === cle}
<Refus refus={refus.refus} />
{/if}
</li>
{/each}
</ul>
</Carte>
<style>
p {
margin: 0;
}
/* Les démonstrations se rangent en ligne, et passent à la ligne quand la
largeur manque : en colonne, quatre entrées repoussent la dernière
au-dessous d'une fenêtre de 700 px de haut. */
ul {
display: flex;
flex-flow: row wrap;
align-items: flex-start;
gap: var(--espace-2);
margin: 0;
padding: 0;
list-style: none;
}
li {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--espace-1);
}
button {
padding: var(--espace-1) var(--espace-3);
border: 1px solid var(--couleur-page-filet);
border-radius: var(--rayon-2);
background-color: var(--couleur-carte-fond);
color: var(--couleur-page-texte);
font: inherit;
font-size: var(--taille-texte);
text-align: start;
cursor: pointer;
}
button:hover {
background-color: var(--couleur-carte-fond-sourd);
}
button:focus-visible {
outline: 2px solid var(--couleur-focus);
outline-offset: 2px;
}
button[aria-disabled='true'] {
border-style: dashed;
color: var(--couleur-page-texte-secondaire);
cursor: progress;
}
</style>