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
135 lines
4.4 KiB
Svelte
135 lines
4.4 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 : le clic répété d'une série
|
|
// — detail supérieur à 1 — est sans effet (§ 8.5). Le clavier n'en porte
|
|
// pas : un appui d'Entrée ou d'Espace que la répétition automatique émet
|
|
// — keydown dont repeat est vrai — voit son action par défaut empêchée, et
|
|
// n'active pas le bouton. Une touche tenue n'active la commande qu'une
|
|
// fois, et ne confirme jamais celle qu'elle vient d'armer.
|
|
//
|
|
// Pendant qu'une exécution qui rend une promesse s'achève, la commande est
|
|
// occupée : un clic ne fait rien, et le bouton porte aria-busy et
|
|
// aria-disabled, dessiné d'un filet tireté. Une exécution sans promesse
|
|
// rend la commande aussitôt.
|
|
//
|
|
// 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 { effetDe } from '../raccourcis.js';
|
|
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 enCours = $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(evenement) {
|
|
const { action } = machine.cliquer({ repete: evenement.detail > 1 });
|
|
armee = machine.armee;
|
|
if (action === 'aucune') return;
|
|
if (action === 'armer') {
|
|
minuterie = horloge.planifier(expirer, DELAI_CONFIRMATION_MS);
|
|
return;
|
|
}
|
|
desarmer();
|
|
refus = null;
|
|
try {
|
|
const rendu = executer();
|
|
if (typeof rendu?.then === 'function') {
|
|
enCours = true;
|
|
await rendu;
|
|
}
|
|
} catch (erreur) {
|
|
if (typeof erreur?.code !== 'string') throw erreur;
|
|
refus = { code: erreur.code, details: erreur.details ?? {} };
|
|
} finally {
|
|
machine.terminer();
|
|
enCours = false;
|
|
}
|
|
}
|
|
|
|
function surTouche(evenement) {
|
|
if (evenement.repeat && effetDe('commande', evenement.key) === 'activer') evenement.preventDefault();
|
|
}
|
|
|
|
onDestroy(desarmer);
|
|
</script>
|
|
|
|
<div class="commande">
|
|
<button
|
|
type="button"
|
|
data-commande={designation}
|
|
aria-busy={enCours ? 'true' : undefined}
|
|
aria-disabled={enCours ? 'true' : undefined}
|
|
onkeydown={surTouche}
|
|
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[aria-disabled='true'] {
|
|
border-style: dashed;
|
|
color: var(--couleur-page-texte-secondaire);
|
|
cursor: progress;
|
|
}
|
|
|
|
button:focus-visible {
|
|
outline: 2px solid var(--couleur-focus);
|
|
outline-offset: 2px;
|
|
}
|
|
</style>
|