The working screens of an evening. The home screen lists the events of the working folder, asks its question, loads demos, copies, renames, imports and deletes, and shows what the trash holds. An open event frames its mode, offers Modifier, Débloquer and a confirmed lock takeover, and shows its warnings. Participants get a grid, a form and a file; an import goes through a preview and a report; tables, the default and tours have their screens; the plan draws tables, chairs, empty seats and names. Checked: 2342 node, 356 browser, 60 node-long; the Electron start check. --- FR --- [ADD] interface : liste, événement, participants, import, tables, plan Les écrans de travail d'une soirée. L'accueil liste les événements du dossier de travail, pose sa question, charge les démonstrations, copie, renomme, importe et supprime, et montre ce que tient la corbeille. Un événement ouvert encadre son mode, offre Modifier, Débloquer et une reprise de verrou confirmée, et montre ses avertissements. Les participants ont une grille, un formulaire et une fiche ; un import passe par un aperçu et un rapport ; tables, défaut et tours ont leurs écrans ; le plan dessine tables, chaises, sièges vides et noms. Vérifié : 2342 node, 356 navigateur, 60 node-long ; démarrage d'Electron. Assisted-by: Claude Opus 5.5
221 lines
6.9 KiB
Svelte
221 lines
6.9 KiB
Svelte
<!-- © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
|
|
|
|
<script>
|
|
// Le formulaire d'ajout d'une personne (§ 4, § 10.2) : un champ par
|
|
// colonne de texte, seul le nom requis. Entrée, ou « Ajouter », exécute
|
|
// ajouterParticipant par la séance ; la commande lit chaque saisie — NFC,
|
|
// blancs de bord retirés, vide valant null — et refuse un nom vide,
|
|
// NOM_REQUIS, que le formulaire pose contre le champ du nom. Un autre
|
|
// refus — le mode, l'état du plan — se pose contre « Ajouter ».
|
|
//
|
|
// L'appartenance est collante : après un ajout, chaque champ se vide sauf
|
|
// elle. Une appartenance qui ne diffère d'une existante que par la casse,
|
|
// les accents ou les blancs (orthographeExistante, grille.js) est signalée
|
|
// contre son champ, avec l'orthographe existante à reprendre d'un clic ;
|
|
// rien ne fusionne seul.
|
|
//
|
|
// Une saisie non envoyée — un champ rempli, l'appartenance exceptée — est
|
|
// un geste ouvert (geste.js), qui suspend le compte d'inactivité.
|
|
import { getContext, onDestroy, tick } from 'svelte';
|
|
import { cleNormalisee } from '../../application/csv.js';
|
|
import { creerGeste } from '../primitives/geste.js';
|
|
import Refus from '../primitives/Refus.svelte';
|
|
import { cleDeColonne, orthographeExistante } from './grille.js';
|
|
|
|
let { participants } = $props();
|
|
|
|
const t = getContext('t');
|
|
const seance = getContext('seance');
|
|
const uid = $props.id();
|
|
const geste = creerGeste(seance);
|
|
|
|
// Les champs du formulaire, dans l'ordre des colonnes de la grille.
|
|
const CHAMPS = ['nom', 'prenom', 'appartenance', 'courriel', 'titrePressenti', 'notes'];
|
|
// Ceux qu'un ajout vide : tous, l'appartenance exceptée.
|
|
const EPHEMERES = CHAMPS.filter((cle) => cle !== 'appartenance');
|
|
|
|
let valeurs = $state({ nom: '', prenom: '', appartenance: '', courriel: '', titrePressenti: '', notes: '' });
|
|
let refusNom = $state.raw(null);
|
|
let refusAjout = $state.raw(null);
|
|
let entreeNom = $state(null);
|
|
|
|
const orthographe = $derived(orthographeExistante(valeurs.appartenance, participants, cleNormalisee));
|
|
const idRefusNom = `${uid}-refus-nom`;
|
|
const idOrthographe = `${uid}-orthographe`;
|
|
|
|
$effect(() => {
|
|
geste.tenir(EPHEMERES.some((cle) => valeurs[cle] !== ''));
|
|
});
|
|
|
|
// Une frappe dans le nom retire le refus qu'il portait.
|
|
function surSaisieNom() {
|
|
refusNom = null;
|
|
}
|
|
|
|
async function ajouter(evenement) {
|
|
evenement.preventDefault();
|
|
refusNom = null;
|
|
refusAjout = null;
|
|
const champs = {
|
|
nom: valeurs.nom,
|
|
prenom: valeurs.prenom,
|
|
appartenance: valeurs.appartenance,
|
|
courriel: valeurs.courriel,
|
|
titrePressenti: valeurs.titrePressenti,
|
|
notes: valeurs.notes,
|
|
};
|
|
try {
|
|
await seance.executer('ajouterParticipant', { champs });
|
|
} catch (erreur) {
|
|
if (typeof erreur?.code !== 'string') throw erreur;
|
|
const refus = { code: erreur.code, details: erreur.details ?? {} };
|
|
if (refus.code === 'NOM_REQUIS') refusNom = refus;
|
|
else refusAjout = refus;
|
|
return;
|
|
}
|
|
valeurs = { nom: '', prenom: '', appartenance: valeurs.appartenance, courriel: '', titrePressenti: '', notes: '' };
|
|
await tick();
|
|
entreeNom?.focus();
|
|
}
|
|
|
|
function reprendre() {
|
|
valeurs.appartenance = orthographe;
|
|
}
|
|
|
|
onDestroy(() => geste.liberer());
|
|
</script>
|
|
|
|
<form aria-labelledby={`${uid}-titre`} onsubmit={ajouter}>
|
|
<h3 id={`${uid}-titre`}>{t('participants.formulaire.titre')}</h3>
|
|
<div class="champs">
|
|
{#each CHAMPS as cle (cle)}
|
|
<div class="champ">
|
|
<label for={`${uid}-${cle}`}>{t(cleDeColonne(cle))}</label>
|
|
{#if cle === 'nom'}
|
|
<input
|
|
id={`${uid}-${cle}`}
|
|
type="text"
|
|
data-champ={cle}
|
|
bind:value={valeurs.nom}
|
|
bind:this={entreeNom}
|
|
aria-invalid={refusNom === null ? undefined : 'true'}
|
|
aria-describedby={refusNom === null ? undefined : idRefusNom}
|
|
oninput={surSaisieNom}
|
|
/>
|
|
{#if refusNom !== null}
|
|
<Refus id={idRefusNom} refus={refusNom} lever={() => (refusNom = null)} />
|
|
{/if}
|
|
{:else if cle === 'appartenance'}
|
|
<input
|
|
id={`${uid}-${cle}`}
|
|
type="text"
|
|
data-champ={cle}
|
|
bind:value={valeurs.appartenance}
|
|
aria-describedby={orthographe === null ? undefined : idOrthographe}
|
|
/>
|
|
{#if orthographe !== null}
|
|
<div class="signal" id={idOrthographe} role="status" data-avertissement="orthographe">
|
|
<p><span>{t('participants.formulaire.orthographe')}</span> <span class="donnee">{orthographe}</span></p>
|
|
<button type="button" data-commande="reprendreOrthographe" onclick={reprendre}>
|
|
{t('participants.formulaire.reprendre')}
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
{:else}
|
|
<input id={`${uid}-${cle}`} type="text" data-champ={cle} bind:value={valeurs[cle]} />
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
<div class="issue">
|
|
<button type="submit" data-commande="ajouter">{t('participants.formulaire.ajouter')}</button>
|
|
{#if refusAjout !== null}
|
|
<Refus refus={refusAjout} lever={() => (refusAjout = null)} />
|
|
{/if}
|
|
</div>
|
|
</form>
|
|
|
|
<style>
|
|
form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--espace-2);
|
|
}
|
|
|
|
h3 {
|
|
margin: 0;
|
|
font-size: var(--taille-texte);
|
|
}
|
|
|
|
.champs {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: flex-start;
|
|
gap: var(--espace-3);
|
|
}
|
|
|
|
.champ {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--espace-1);
|
|
max-width: 18rem;
|
|
}
|
|
|
|
input {
|
|
padding: var(--espace-1) var(--espace-2);
|
|
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);
|
|
}
|
|
|
|
input[aria-invalid='true'] {
|
|
border-color: var(--couleur-refus);
|
|
}
|
|
|
|
input:focus-visible,
|
|
button:focus-visible {
|
|
outline: 2px solid var(--couleur-focus);
|
|
outline-offset: 1px;
|
|
}
|
|
|
|
.signal {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: var(--espace-1);
|
|
padding: var(--espace-1) var(--espace-2);
|
|
border-left: 3px solid var(--couleur-avis);
|
|
font-size: var(--taille-texte);
|
|
}
|
|
|
|
.signal p {
|
|
margin: 0;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.donnee {
|
|
font-weight: bold;
|
|
}
|
|
|
|
.issue {
|
|
display: 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;
|
|
}
|
|
</style>
|