[ADD] interface: plan view — zoom, framing, fit, scale, side panels
The drawing becomes a plan the operator can look around. One transform on
the drawing group holds the view, so the screen and the drawing share the
formula of src/geometrie; the wheel zooms about the pointer without
scrolling the page, « + », « − » and « Ajuster » bound the scale, and a
resize keeps the view inside its limits. Only a deliberate gesture saves
the framing, deferred, into reglages_locaux.json — in read mode and on a
blocked plan too. The side panels take their first form.
Checked: 2368 node and 378 browser tests.
--- FR ---
[ADD] interface : vue du plan — zoom, cadrage, ajuster, échelle
Le dessin devient un plan que l'opérateur parcourt. Une seule
transformation du groupe de dessin tient la vue : l'écran et le dessin
partagent la formule de src/geometrie ; la molette zoome autour du
pointeur sans faire défiler la page, « + », « − » et « Ajuster » bornent
l'échelle, et un redimensionnement garde la vue dans ses limites. Seul un
geste voulu enregistre le cadrage, en différé, dans reglages_locaux.json
— en lecture et sur un plan bloqué aussi. Les panneaux latéraux prennent
leur première forme.
Vérifié : 2368 node et 378 navigateur.
Assisted-by: Claude Opus 5.5
2026-10-07 09:09:45 -04:00
|
|
|
<!-- © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
|
|
|
|
|
|
|
|
|
|
<script>
|
[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
|
|
|
// 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, sans propriété.
|
|
|
|
|
import { getContext, untrack } from 'svelte';
|
|
|
|
|
import { etatPlaces, libellePropose } from '../../application/places.js';
|
|
|
|
|
import Commande from '../primitives/Commande.svelte';
|
|
|
|
|
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, 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 selection = $derived($ui.selection);
|
|
|
|
|
const place = $derived(charge !== null && selection !== null && (selection.siege ?? null) !== null ? selection : null);
|
|
|
|
|
const tour = $derived(charge === null ? 1 : tourAffiche($ui.tour, charge.evenement.tours));
|
|
|
|
|
const etat = $derived(place === null ? null : etatPlaces(charge, { tour, proposition: $ui.proposition }));
|
|
|
|
|
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 (evenement.key !== 'Escape') return;
|
|
|
|
|
evenement.preventDefault();
|
|
|
|
|
const racine = evenement.currentTarget.closest('[data-section]')?.querySelector('[data-plan]') ?? null;
|
|
|
|
|
ui.selectionner(null);
|
|
|
|
|
racine?.focus();
|
|
|
|
|
}
|
[ADD] interface: plan view — zoom, framing, fit, scale, side panels
The drawing becomes a plan the operator can look around. One transform on
the drawing group holds the view, so the screen and the drawing share the
formula of src/geometrie; the wheel zooms about the pointer without
scrolling the page, « + », « − » and « Ajuster » bound the scale, and a
resize keeps the view inside its limits. Only a deliberate gesture saves
the framing, deferred, into reglages_locaux.json — in read mode and on a
blocked plan too. The side panels take their first form.
Checked: 2368 node and 378 browser tests.
--- FR ---
[ADD] interface : vue du plan — zoom, cadrage, ajuster, échelle
Le dessin devient un plan que l'opérateur parcourt. Une seule
transformation du groupe de dessin tient la vue : l'écran et le dessin
partagent la formule de src/geometrie ; la molette zoome autour du
pointeur sans faire défiler la page, « + », « − » et « Ajuster » bornent
l'échelle, et un redimensionnement garde la vue dans ses limites. Seul un
geste voulu enregistre le cadrage, en différé, dans reglages_locaux.json
— en lecture et sur un plan bloqué aussi. Les panneaux latéraux prennent
leur première forme.
Vérifié : 2368 node et 378 navigateur.
Assisted-by: Claude Opus 5.5
2026-10-07 09:09:45 -04:00
|
|
|
</script>
|
[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
|
|
|
|
|
|
|
|
{#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} />
|
|
|
|
|
{/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>
|