gestion_table_tournante_libre/src/interface/primitives/Commande.svelte
Mathieu Benoit 876ea0f2ce [ADD] interface: primitives — field, refusal, confirmation, command
Every screen of the plan edits, refuses and confirms the same way, so
these behaviours live once. A field validates on leaving it and shows a
refused value as refused, restoring the stored one when a session refusal
clears. A refusal is shown where the gesture happened, with its remedy as
an executable command. The confirmation window only opens for a gesture
that destroys data, and stays busy — no key closes it — until the
confirmed action ends.
Checked: 2152 node and 163 browser tests.

--- FR ---

[ADD] interface : primitives — champ, refus, confirmation, commande

Chaque écran du plan saisit, refuse et confirme de la même façon : ces
comportements n'existent qu'une fois. Un champ se valide à sa sortie et
montre une valeur refusée comme refusée, rendant la valeur enregistrée
quand un refus de séance s'efface. Un refus se pose là où le geste a eu
lieu, son remède en commande exécutable. La fenêtre à confirmer ne s'ouvre
que pour un geste qui détruit des données, et reste occupée — aucune touche
ne la ferme — jusqu'à la fin de l'action confirmée.
Vérifié : 2152 node et 163 navigateur.

Assisted-by: Claude Opus 5.5
2026-10-07 07:21:20 -04:00

98 lines
3 KiB
Svelte

<!-- © 2026 TechnoLibre (http://www.technolibre.ca)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
<script>
// Une commande (§ 2.1, § 8.5) : un bouton dont le libellé est la clé cle,
// traduite, et qui appelle executer au clic. Avec confirmer, la clé du
// libellé de confirmation, un premier clic arme la commande — son libellé
// devient celui de confirmer — et seul un second exécute ; armée, elle se
// désarme au terme de DELAI_CONFIRMATION_MS, par l'horloge du contexte.
// La machine est celle de commande.js.
//
// Un refus — une erreur qui porte un code — s'affiche à côté du bouton,
// avec son remède, et le bouton reste actif : jamais un bouton grisé muet.
// Une erreur sans code n'est pas un refus et remonte.
//
// data-commande porte nom, ou à défaut le dernier segment de la clé.
// confirmer se lit au montage : une commande qui change de confirmation se
// remonte.
import { getContext, onDestroy, untrack } from 'svelte';
import { DELAI_CONFIRMATION_MS, creerMachineCommande } from './commande.js';
import Refus from './Refus.svelte';
let { cle, executer, confirmer = null, details = {}, nom = null, gestes = {} } = $props();
const t = getContext('t');
const horloge = getContext('horloge');
const machine = creerMachineCommande({ confirmer: untrack(() => confirmer) });
let armee = $state(false);
let refus = $state(null);
let minuterie = null;
const libelle = $derived(armee ? t(confirmer) : t(cle, details));
const designation = $derived(nom ?? cle.slice(cle.lastIndexOf('.') + 1));
function desarmer() {
if (minuterie === null) return;
horloge.annuler(minuterie);
minuterie = null;
}
function expirer() {
minuterie = null;
machine.expirer();
armee = machine.armee;
}
async function cliquer() {
const { action } = machine.cliquer();
armee = machine.armee;
if (action === 'armer') {
minuterie = horloge.planifier(expirer, DELAI_CONFIRMATION_MS);
return;
}
desarmer();
refus = null;
try {
await executer();
} catch (erreur) {
if (typeof erreur?.code !== 'string') throw erreur;
refus = { code: erreur.code, details: erreur.details ?? {} };
}
}
onDestroy(desarmer);
</script>
<div class="commande">
<button type="button" data-commande={designation} onclick={cliquer}>{libelle}</button>
{#if refus !== null}
<Refus {refus} lever={() => (refus = null)} {gestes} />
{/if}
</div>
<style>
.commande {
display: inline-flex;
flex-wrap: wrap;
align-items: flex-start;
gap: var(--espace-2);
}
button {
padding: var(--espace-1) var(--espace-3);
border: 1px solid var(--couleur-page-filet);
border-radius: var(--rayon-1);
background-color: var(--couleur-page-fond);
color: var(--couleur-page-texte);
font: inherit;
font-size: var(--taille-texte);
cursor: pointer;
}
button:focus-visible {
outline: 2px solid var(--couleur-focus);
outline-offset: 2px;
}
</style>