gestion_table_tournante_libre/src/interface/plan/Reserve.svelte
Mathieu Benoit 811cd16b76 [ADD] interface: plan keyboard, tooltip, side panels, person menu, lists
Every gesture the pointer offers on the plan is reachable from the
keyboard (§ 7.3): one stop enters the plan, arrows move between tables and
seats, and the table and place panels choose a seat and keep the focus.
A tooltip names who sits where; a per-person menu moves, assigns or
reserves without dragging; the reserve and the work lists choose a person
to designate on the plan. Escape, a tour change and the executed command
forget the choice, and a click never moves the view.
Checked: 2537 node, 466 browser (one expected failure kept), 60 node-long.

--- FR ---

[ADD] interface : clavier du plan, infobulle, panneaux, menu, listes

Chaque geste que le pointeur offre sur le plan s'atteint au clavier
(§ 7.3) : un seul arrêt entre dans le plan, les flèches vont de table en
table et de siège en siège, et les panneaux de table et de place
choisissent un siège en gardant le focus. Une infobulle nomme qui est
assis où ; un menu par personne déplace, assigne ou réserve sans glisser ;
la réserve et les listes de travail choisissent une personne à désigner
sur le plan. Échap, un changement de tour et la commande exécutée
oublient le choix, et un clic ne déplace jamais la vue.
Vérifié : 2537 node, 466 navigateur (un échec attendu gardé), 60 node-long.

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

93 lines
3 KiB
Svelte

<!-- © 2026 TechnoLibre (http://www.technolibre.ca)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
<script>
// La réserve visible (§ 7.1, § 5.8) : les personnes en attente au tour
// affiché, celles de l'état des places (EtatPlaces.reserve, A5) — avant
// toute proposition, chaque personne qu'aucune réservation n'assied à ce
// tour —, par identifiant croissant. Chacune est un bouton
// [data-participant] qui en fait la personne choisie (ui.choix), puis
// l'oublie ; aria-pressed dit la personne choisie. Aucun glisser ne part
// d'une liste vers le plan : la place se choisit ensuite sur le plan. Un
// changement de tour change la liste et oublie le choix (etat_ui.js) ;
// Échap, sur la réserve ou l'un de ses boutons, l'oublie aussi.
// Le panneau lit 'seance' et 'ui' au contexte, sans propriété.
import { getContext } from 'svelte';
import { etatPlaces } from '../../application/places.js';
import { nomDeCharge, tourAffiche } from './vue_plan.js';
const t = getContext('t');
const seance = getContext('seance');
const ui = getContext('ui');
const uid = $props.id();
const charge = $derived($seance.charge);
const tour = $derived(charge === null ? 1 : tourAffiche($ui.tour, charge.evenement.tours));
const reserve = $derived(charge === null ? [] : etatPlaces(charge, { tour, proposition: $ui.proposition }).reserve);
const nomDe = $derived(charge === null ? null : nomDeCharge(charge));
const choix = $derived($ui.choix);
const choisie = (id) => choix !== null && choix.genre === 'personne' && choix.participant === id;
function basculer(id) {
ui.choisir(choisie(id) ? null : { genre: 'personne', participant: id, depuis: null });
}
function surTouche(evenement) {
if (evenement.key !== 'Escape' || choix === null) return;
evenement.preventDefault();
ui.choisir(null);
}
</script>
<!-- Échap remonte des boutons de la réserve jusqu'à elle, qui oublie le choix. -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<section data-reserve={tour} aria-labelledby="{uid}-titre" onkeydown={surTouche}>
<h3 id="{uid}-titre">{t('plan.reserve.titre')}</h3>
{#if charge !== null}
<p>{t('plan.reserve.tour', { tour })}</p>
{#if reserve.length === 0}
<p>{t('plan.reserve.vide')}</p>
{:else}
<ul>
{#each reserve as id (id)}
<li>
<button type="button" data-participant={id} aria-pressed={choisie(id)} onclick={() => basculer(id)}>{nomDe(id)}</button>
</li>
{/each}
</ul>
{/if}
{/if}
</section>
<style>
section {
display: flex;
flex-direction: column;
gap: var(--espace-1);
}
h3,
p {
margin: 0;
}
h3 {
font-size: var(--taille-texte);
}
ul {
display: flex;
flex-wrap: wrap;
gap: var(--espace-1);
margin: 0;
padding: 0;
list-style: none;
}
/* La personne choisie se dit par aria-pressed et par sa forme : un trait
épais et du gras. */
button[aria-pressed='true'] {
border-width: 3px;
font-weight: 700;
}
</style>