gestion_table_tournante_libre/src/interface/plan/PanneauPlace.svelte
Mathieu Benoit 13bf16b5be [REF] interface: read every key through the shortcut table
Twelve components still compared key names themselves, so the shortcut
table could drift from what they actually did; a guard listed them as
pending. Each now reads its effect from RACCOURCIS through effetDe, the
pending list and its guard are removed, and the generated shortcut
table is unchanged. The keys the old guard pinned without a behaviour
test (Escape on the plan root with no active element, ArrowLeft in the
grid, and those of the participant and table lists) each gain one.
Checked: 3852 node, 766 browser, 101 long tests, from the index alone.

--- FR ---

[REF] interface : chaque touche lue par la table des raccourcis

Douze composants comparaient encore eux-mêmes les noms de touches : la
table des raccourcis pouvait s'écarter de ce qu'ils faisaient, et une
garde les tenait en attente. Chacun lit désormais son effet dans
RACCOURCIS par effetDe ; la liste d'attente et sa garde disparaissent,
la table engendrée ne change pas. Les touches que l'ancienne garde
figeait sans épreuve de comportement (Échap sur le plan sans élément
actif, flèche gauche dans la grille, listes des personnes et des
tables) en reçoivent chacune une.
Vérifié : 3852 node, 766 navigateur, 101 longues, depuis l'index seul.

Assisted-by: Claude Opus 5.5
2026-10-10 17:25:03 -04:00

243 lines
9.3 KiB
Svelte

<!-- © 2026 TechnoLibre (http://www.technolibre.ca)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
<script>
// Le panneau de la place sélectionnée (§ 7.1, § 2.1 étape 6, § 4, § 5.9) :
// ouvert quand l'état de navigation sélectionne un siège, {table, siege}.
// Il nomme la place et sa ligne, le choix en attente, puis offre les
// actions d'actionsPlace (modele.js) : assigner, réserver — portée au
// choix —, donner un titre, désigner, retirer la réservation, retirer le
// titre, déplacer la réservation ici. Assigner, réserver et désigner
// portent sur la personne choisie (ui.choix), et le panneau invite à la
// choisir tant qu'il n'y en a pas. Assigner donne ce que donne le dépôt
// d'un glissement, par commandeDeDepot (depot.js) : une seule
// implémentation du dépôt. Sous des sièges non attribués, un titre et une
// désignation se posent sans siège : l'application prend le siège (A5).
//
// Toute commande passe par executerSurPlace : son refus se pose sur la
// place du plan, sauf LIBELLE_VIDE, refusé à côté du champ du libellé.
// Le menu de l'occupant suit (MenuPersonne) ; il reste celui de la même
// personne tant que la place reste sélectionnée, même quand elle la quitte
// — une exclusion —, pour qu'elle se réintègre d'où elle a été exclue.
//
// Échap referme le panneau et rend le focus à la racine du plan. Le
// panneau lit 'seance' et 'ui' au contexte ; etatTour, l'état des places
// du tour affiché, que SectionPlan calcule, ou null sans événement, et le
// passe au menu.
import { getContext, untrack } from 'svelte';
import { libellePropose } from '../../application/places.js';
import Commande from '../primitives/Commande.svelte';
import { effetDe } from '../raccourcis.js';
import { commandeDeDepot } from './depot.js';
import { libellePlace } from './gestes.js';
import MenuPersonne from './MenuPersonne.svelte';
import { actionsPlace, executerSurPlace, ligneListe, partiesLigne, textesLigne } from './modele.js';
import { nomDeCharge } from './vue_plan.js';
let { etatTour } = $props();
const t = getContext('t');
const seance = getContext('seance');
const ui = getContext('ui');
const uid = $props.id();
const charge = $derived($seance.charge);
const selection = $derived($ui.selection);
const place = $derived(charge !== null && selection !== null && (selection.siege ?? null) !== null ? selection : null);
const etat = $derived(place === null ? null : etatTour);
const tour = $derived(etatTour === null ? 1 : etatTour.tour);
const etatTable = $derived(etat === null ? undefined : etat.tables.find(({ id }) => id === place.table));
const chaise = $derived(etatTable === undefined ? null : (etatTable.chaises[place.siege - 1] ?? null));
const nomDe = $derived(charge === null ? null : nomDeCharge(charge));
const choix = $derived($ui.choix);
const personne = $derived(choix !== null && choix.genre === 'personne' ? choix : null);
const actions = $derived(chaise === null ? [] : actionsPlace(chaise, choix));
const parties = $derived(chaise === null ? [] : partiesLigne(textesLigne(ligneListe(chaise, nomDe), t)));
const nommee = $derived(etatTable === undefined ? null : libellePlace({ numero: etatTable.numero, siege: place.siege }));
// Le libellé proposé : le titre pressenti de la personne choisie, sinon
// celui de la place, sinon le plus fréquent (libellePropose, A5).
const propose = $derived.by(() => {
if (chaise === null) return '';
if (personne !== null) return libellePropose(charge, personne.participant);
return chaise.titre === null ? libellePropose(charge, 0) : chaise.titre.libelle;
});
// La saisie de l'opérateur, null tant qu'il n'a rien tapé ; la portée
// d'une réservation.
let saisi = $state(null);
let portee = $state('tour');
const libelleSaisi = $derived(saisi ?? propose);
// Le menu : la personne de la place au moment de sa sélection, ou la
// première qui s'y assied ensuite.
const occupant = $derived(chaise === null ? null : chaise.occupant);
let menuDe = $state(null);
let placeDuMenu = null;
$effect(() => {
const lue = place;
const assise = occupant;
untrack(() => {
if (lue !== placeDuMenu) {
placeDuMenu = lue;
menuDe = assise;
saisi = null;
} else if (menuDe === null) {
menuDe = assise;
}
});
});
const dependances = { seance, ui };
const sieges = () => (etat.siegesAttribues ? place.siege : null);
async function assigner() {
if (personne === null) return;
const rendu = commandeDeDepot({
etatPlaces: etat,
tour,
personne: personne.participant,
depuis: personne.depuis,
cible: { table: place.table, siege: place.siege },
surLeSol: false,
});
if (rendu === null) return;
if (Object.hasOwn(rendu, 'refus')) {
ui.poserSurPlace({ place, refus: rendu.refus, avertissements: [] });
return;
}
await executerSurPlace(dependances, place, rendu.commande, rendu.arguments);
}
async function reserver() {
if (personne === null) return;
await executerSurPlace(dependances, place, 'reserver', {
participant: personne.participant,
table: place.table,
siege: place.siege,
portee,
tour: portee === 'tour' ? tour : null,
});
}
async function donnerTitre() {
await executerSurPlace(dependances, place, 'poserTitre', { table: place.table, siege: sieges(), libelle: libelleSaisi }, { locaux: ['LIBELLE_VIDE'] });
}
async function designer() {
if (personne === null) return;
await executerSurPlace(
dependances,
place,
'designer',
{ participant: personne.participant, table: place.table, siege: sieges(), libelle: libelleSaisi },
{ locaux: ['LIBELLE_VIDE'] },
);
}
async function retirerReservation() {
const { participant, portee: sienne, tour: sonTour } = chaise.reservation;
await executerSurPlace(dependances, place, 'retirerReservation', { participant, table: place.table, portee: sienne, tour: sonTour });
}
async function retirerTitre() {
await executerSurPlace(dependances, place, 'retirerTitre', { table: place.table, siege: place.siege });
}
async function deplacerReservationIci() {
await executerSurPlace(dependances, place, 'deplacerReservation', {
participant: choix.participant,
de: choix.de,
vers: { table: place.table, siege: place.siege },
});
}
const EXECUTIONS = { assigner, reserver, donnerTitre, designer, retirerReservation, retirerTitre, deplacerReservationIci };
// Échap referme le panneau et rend le focus à la racine du plan.
function surTouche(evenement) {
if (effetDe('fenetre', evenement.key) !== 'fermer') return;
evenement.preventDefault();
const racine = evenement.currentTarget.closest('[data-section]')?.querySelector('[data-plan]') ?? null;
ui.selectionner(null);
racine?.focus();
}
</script>
{#if chaise !== null}
<!-- Échap remonte des contrôles du panneau jusqu'à lui, qui se referme. -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<section class="panneau" data-panneau="place" tabindex="-1" aria-labelledby="{uid}-titre" onkeydown={surTouche}>
<h3 id="{uid}-titre">{t(nommee.cle, nommee.details)}</h3>
<p class="ligne">
{#each parties as { partie, texte } (partie)}<span class:titre={partie === 'titre'}>{texte}</span>{/each}
</p>
{#if choix !== null}
<p class="choix" role="status">
<span>{t(choix.genre === 'personne' ? 'plan.choix.personne' : 'plan.choix.reservation')}</span><span class="donnee"
>{nomDe(choix.participant)}</span
>
</p>
{:else if actions.includes('assigner')}
<p class="invite">{t('plan.choix.inviter')}</p>
{/if}
{#if actions.includes('reserver')}
<label class="champ">
<span>{t('plan.portee.etiquette')}</span>
<select data-champ="portee" value={portee} onchange={(evenement) => (portee = evenement.currentTarget.value)}>
<option value="tour">{t('plan.portee.tour', { tour })}</option>
<option value="tous">{t('plan.portee.tous')}</option>
</select>
</label>
{/if}
{#if actions.includes('donnerTitre') || actions.includes('designer')}
<label class="champ">
<span>{t('plan.titre.etiquette')}</span>
<input type="text" data-champ="libelle" value={libelleSaisi} oninput={(evenement) => (saisi = evenement.currentTarget.value)} />
</label>
{/if}
<div class="actions">
{#each actions as action (action)}
<Commande cle="plan.action.{action}" nom={action} executer={EXECUTIONS[action]} />
{/each}
{#if choix !== null}
<Commande cle="plan.choix.oublier" nom="oublierChoix" executer={() => ui.choisir(null)} />
{/if}
</div>
{#if menuDe !== null}
<MenuPersonne participant={menuDe} {etatTour} />
{/if}
</section>
{/if}
<style>
.panneau {
display: flex;
flex-direction: column;
gap: var(--espace-2);
}
h3,
p {
margin: 0;
}
h3 {
font-size: var(--taille-texte);
}
.titre {
font-style: italic;
}
.champ {
display: flex;
flex-direction: column;
gap: var(--espace-1);
}
.actions {
display: flex;
flex-wrap: wrap;
gap: var(--espace-2);
}
</style>