[IMP] interface: full-width event screen, no mode frame, fewer columns

The 6 px mode frame around the event screen read as a dashed box that
narrowed the work surface, and the screen stopped at the 87rem page
width. The event screen now spans the window; the mode band under the
app header, the header label and the pill beside « Modifier » carry the
mode. The event list drops both « Collisions cumulées » columns, which
repeated a measure the proposals screen already shows: 7 columns, not 9.
Checked: node 4009, browser 872, long 105; full width at 1366 and 1920.

--- FR ---

[IMP] interface : événement pleine fenêtre, sans cadre, liste allégée

Le cadre de mode de 6 px autour de l'écran d'événement se lisait comme
un encadré pointillé qui rétrécissait la surface, et l'écran s'arrêtait
à 87rem. L'écran de l'événement occupe la fenêtre ; la bande sous le
bandeau, le libellé du bandeau et la pastille près de « Modifier »
portent le mode. La liste des événements perd ses deux colonnes
« Collisions cumulées », déjà montrées par les propositions : 7
colonnes au lieu de 9. Vérifié : node 4009, navigateur 872, long 105 ;
pleine largeur à 1366 et 1920.

Assisted-by: Claude Opus 5.5
This commit is contained in:
Mathieu Benoit 2026-10-11 02:44:40 -04:00
parent 40bdfc7d77
commit 8b0d5a9097
32 changed files with 444 additions and 517 deletions

View file

@ -23,8 +23,9 @@ fichiers, lisez `GUIDE-WINDOWS.md`.
## Avant de commencer : lecture, écriture, et trois garanties
**Tout événement s’ouvre en lecture.** Rien ne s’y modifie tant que vous n’avez
pas cliqué sur « Modifier », toujours à la même place, en haut de l’écran. Un
cadre autour du plan et le bandeau disent dans quel mode vous êtes. Après un
pas cliqué sur « Modifier », toujours à la même place, en haut de l’écran. La
bande de couleur sous le bandeau, le libellé à côté de « Modifier » et le
bandeau disent dans quel mode vous êtes. Après un
moment d’inactivité, le logiciel revient seul en lecture, entre deux gestes et
jamais au milieu de l’un d’eux.
@ -71,10 +72,10 @@ s’affiche à côté du bouton, et propose « Modifier ».
![Un ajout de table refusé en lecture, et la commande « Modifier »](doc/captures/01b-passer-en-ecriture.png)
*Le refus apparaît là où vous avez cliqué, sans fenêtre. Le mot « Lecture »
figure dans le bandeau et sur le cadre de l’événement.*
figure dans le bandeau, dans la bande de mode et à côté de « Modifier ».*
Cliquez sur « Modifier » : le cadre passe en écriture, et le même geste
réussit. « Revenir en lecture » fait l’inverse.
Cliquez sur « Modifier » : la bande et le libellé passent en écriture, et le
même geste réussit. « Revenir en lecture » fait l’inverse.
## Étape 2 — Importer la liste des participants
@ -306,8 +307,8 @@ réservations imposent de ce que la recherche a choisi.
proposition alors que le retenu a été ajusté à la main détruit ces ajustements :
une fenêtre le dit avant.
La liste des événements résume ensuite le retenu par ses collisions cumulées et
par l’écart de sa somme de redondance d’appartenance à la cible de la soirée.
La liste des événements résume ensuite le retenu par l’écart de sa somme de
redondance d’appartenance à la cible de la soirée.
Retenir n’est pas obligatoire, mais tout ce qui s’imprime sans placement retenu
porte la mention BROUILLON (étape 12).
@ -329,9 +330,10 @@ Les indicateurs du tableau des propositions suivent l’ajustement.
En lecture, aucun de ces gestes ne passe :
![Le plan en lecture : le cadre, son libellé, et un geste refusé](doc/captures/20-mode-lecture.png)
![Le plan en lecture : la bande de mode, le libellé, et un geste refusé](doc/captures/20-mode-lecture.png)
*Le cadre et le libellé « Lecture » entourent le plan, et le bandeau le répète.
*La bande de mode et le libellé « Lecture » le disent au-dessus du plan, et le
bandeau le répète.
Glisser une personne ou une table, tirer une poignée, poser une réservation :
chaque geste est refusé avec « Modifier » pour remède, et rien ne change sur le
disque.*

95
spec.md
View file

@ -178,8 +178,8 @@ propositions sur le même axe — celui du **manque** (§ 12.10) —, retient ce
qu'il préfère. Le logiciel sépare ce que le moteur a décidé de ce que les
réservations imposent (§ 5.4). Retenir fait passer le plan à l'état *Retenu*
(§ 9) ; les propositions non retenues restent disponibles. Le retenu se résume
ensuite dans la liste des événements, par ses collisions cumulées et la
différence absolue de sa somme de redondance à la cible de l'événement (§ 8.1).
ensuite dans la liste des événements, par la différence absolue de sa somme de
redondance à la cible de l'événement (§ 8.1).
**11. Ajuster à la main.** *Facultative, reprenable.* Le même geste qu'à
l'étape 7 déplace maintenant une personne réelle dans le placement retenu. **Le
@ -1265,24 +1265,24 @@ pastille de l'état et son compte —, ou ne les groupe pas, au choix ; ses
en-têtes de colonnes se groupent en « Événement », « Placement retenu » et
« Meilleure proposition du classement », et le nom de chaque ligne en est
l'en-tête, la date et le chemin en petite ligne dessous. Sous le tableau, des
cartes offrent la création, le dossier de travail et les démonstrations. Deux
colonnes résument en plus le **placement retenu** : « Collisions cumulées (paire
et tour) », les collisions cumulées du retenu (§ 5.4), et « Diff somme
d'appartenance », |cible − Σ r(p)|, où Σ r(p) est la somme de redondance du
retenu — le nombre de la colonne « Redondance d'appartenance, somme Σ r(p) » du
tableau des propositions — et la cible celle de l'événement (§ 8.9), 1000 par
défaut, réglable dans la section des propositions. Les deux se lisent dans la
même mesure que le tableau, sans rien écrire ; un événement sans retenu, ou dont
le retenu est fautif ou en dérive (§ 8.9, § 9), y porte « — ». Chaque ligne
porte aussi la **« Meilleure proposition du classement »**, sous les libellés du
bloc que la section de la génération montre en tête : l'identifiant de la
proposition au rang 1 du classement (§ 5.7) et ses collisions cumulées — la même
proposition et le même nombre que ce bloc et que la première ligne du tableau
des propositions, retenue ou non ; une proposition fautive, que l'ouverture
écarte (§ 8.9, point 3), n'y entre pas. Sans proposition classée — aucune, ou
aucune qui décrive encore la configuration —, l'un et l'autre valent « — ».
Chaque intitulé s'explique dans une infobulle, posée sur l'en-tête de sa
colonne.
cartes offrent la création, le dossier de travail et les démonstrations. Une
colonne résume en plus le **placement retenu** : « Diff somme d'appartenance »,
|cible − Σ r(p)|, où Σ r(p) est la somme de redondance du retenu — le nombre de
la colonne « Redondance d'appartenance, somme Σ r(p) » du tableau des
propositions — et la cible celle de l'événement (§ 8.9), 1000 par défaut,
réglable dans la section des propositions. Elle se lit dans la même mesure que
le tableau, sans rien écrire ; un événement sans retenu, ou dont le retenu est
fautif ou en dérive (§ 8.9, § 9), y porte « — ». Chaque ligne porte aussi la
**« Meilleure proposition du classement »**, sous les libellés du bloc que la
section de la génération montre en tête : l'identifiant de la proposition au
rang 1 du classement (§ 5.7) — la même proposition que ce bloc et que la
première ligne du tableau des propositions, retenue ou non ; une proposition
fautive, que l'ouverture écarte (§ 8.9, point 3), n'y entre pas. Sans
proposition classée — aucune, ou aucune qui décrive encore la configuration —,
elle vaut « — ». La liste ne montre les collisions cumulées ni du retenu ni de
la meilleure proposition : le tableau des propositions et le bloc de la
génération les portent. Chaque intitulé s'explique dans une infobulle, posée
sur l'en-tête de sa colonne.
**La liste paraît avant ses colonnes calculées** : ses lignes s'affichent dès le
relevé du dossier, et les mesures du retenu et la meilleure proposition, qui
évaluent chaque événement, se calculent ensuite, un événement après l'autre, la
@ -1406,30 +1406,31 @@ ou refusé, une saisie validée ou abandonnée, une fenêtre à confirmer referm
et tout appui relâché sur le plan — un clic qui n'exécute rien et un déplacement
de vue compris. Regarder sans appuyer n'en est pas une : zoomer à la molette,
une commande de vue, changer de tour affiché ne relancent rien. Le retour
automatique se voit au cadre, au libellé, à la bande de mode et au bandeau, et
un avertissement non modal le dit jusqu'au passage en écriture suivant.
automatique se voit à la bande de mode, au libellé de mode et au bandeau, et un
avertissement non modal le dit jusqu'au passage en écriture suivant.
**Le mode est visible sur la surface de travail elle-même** — un cadre et un
libellé permanents autour du plan — et non dans un coin de barre d'outils :
l'opérateur regarde le plan.
**Le mode est visible sans quitter la surface de travail des yeux**, et non dans
un coin de barre d'outils : l'opérateur regarde le plan. Sous le bandeau, quand
un plan est ouvert, une **bande de mode** court de bord à bord de la fenêtre et
dit le mode : son icône, son libellé, et une phrase qui dit ce qu'il permet en
nommant la commande de la place fixe qui en sort — « Modifier » en lecture,
« Revenir en lecture » en écriture, « Débloquer » sur un plan bloqué, et aucune
devant un fichier d'un format plus récent (§ 8.8), qui n'en offre pas —, sur le
fond de sa couleur. D'une hauteur constante, elle le dit dès l'entrée dans
l'écran. Le **libellé de mode**, une pastille à côté de la place fixe, le redit
là où la main agit. **Aucun cadre n'entoure la surface de travail** : l'écran de
l'événement prend **toute la largeur de la fenêtre**, à son retrait près, sans
la largeur de page qui borne la liste des événements, et le plan garde chaque
pixel que la bande et la barre lui laissent.
S'y ajoute un **bandeau d'application présent sur tout écran**, qui porte la
version et la provenance de la construction (§ 18.6) et, **quand un plan est
ouvert**, le libellé de mode. Les deux ne font pas double emploi : le cadre
signale le mode là où la main agit ; le bandeau porte la version jusque sur la
liste des événements, qui est le premier écran d'un signalement sur deux. Le
bandeau porte aussi la marque du logiciel, le réglage de thème et « À propos » ;
il garde une hauteur d'une ligne de commande, de sorte que la surface de travail
ne perd rien.
Sous le bandeau, quand un plan est ouvert, une **bande de mode** court de bord à
bord de la fenêtre et redit le mode : son icône, son libellé, et une phrase qui
dit ce qu'il permet en nommant la commande de la place fixe qui en sort —
« Modifier » en lecture, « Revenir en lecture » en écriture, « Débloquer » sur
un plan bloqué, et aucune devant un fichier d'un format plus récent (§ 8.8), qui
n'en offre pas —, sur le fond de sa couleur. D'une hauteur constante, elle
s'ajoute au cadre sans le remplacer : le cadre reste là où la main agit, la
bande le dit dès l'entrée dans l'écran.
ouvert**, le libellé de mode. Ils ne font pas double emploi : la bande et le
libellé de la barre signalent le mode dans l'écran de l'événement ; le bandeau
porte la version jusque sur la liste des événements, qui est le premier écran
d'un signalement sur deux. Le bandeau porte aussi la marque du logiciel, le
réglage de thème et « À propos » ; il garde une hauteur d'une ligne de commande,
de sorte que la surface de travail ne perd rien.
**Le passage en écriture est une étape nommée du parcours** (§ 2.1, étape 1b),
et non un sous-entendu : huit des douze étapes sont modifiantes, et toutes sont
@ -3357,7 +3358,7 @@ des fonctions pures du module de géométrie.
| tenir hors ligne | se compile en JavaScript ordinaire ; Vite est une dépendance de développement qui ne survit pas à la construction |
| rester rapide | compile des mises à jour fines, sans arbre virtuel ni mémoïsation à poser à la main |
| se tester sans navigateur | n'impose d'écrire aucun calcul dans un composant ; le composant ne contient que le câblage |
| être joli sans y passer des semaines | un fichier de jetons — couleurs, rayons, ombres, échelle typographique, cible principale, largeur de page — décliné en deux thèmes ; ses familles de couleur : la page, le bandeau, la carte, l'action principale, les quatre états du plan, le neutre, les appartenances, la bande et le cadre de mode, les messages (avis, refus, information), le plan, le papier |
| être joli sans y passer des semaines | un fichier de jetons — couleurs, rayons, ombres, échelle typographique, cible principale, largeur de page — décliné en deux thèmes ; ses familles de couleur : la page, le bandeau, la carte, l'action principale, les quatre états du plan, le neutre, les appartenances, la bande de mode, les messages (avis, refus, information), le plan, le papier |
**Les candidates écartées.** Vue 3 est le second choix défendable, écarté de peu
sur le poids et sur la discipline qu'exige sa réactivité. React est écarté sur la
@ -4934,10 +4935,10 @@ complément.** Un signalement arrive le plus souvent sous forme d'image, et une
capture faite à la touche d'impression d'écran, ou cadrée sur le défaut, ne
contient pas toujours la barre de titre du système — elle ne la contient jamais sur
la plateforme `web`, où le titre est celui d'un onglet. Un bandeau dessiné par
l'application est dans l'image quel que soit le cadrage. Le § 8.4 impose en plus un
cadre permanent **autour du plan** pour le mode : les deux coexistent, le cadre
signalant le mode là où la main agit, le bandeau portant la version jusque sur la
liste des événements.
l'application est dans l'image quel que soit le cadrage. Le § 8.4 impose en plus une
bande de mode **sous le bandeau** et un libellé de mode à côté de la place fixe : ils
coexistent, la bande et le libellé signalant le mode dans l'écran de l'événement, le
bandeau portant la version jusque sur la liste des événements.
**Le bandeau annonce aussi la version de format du fichier ouvert** dès qu'elle
diffère de celle que le binaire écrit : plus ancienne, « ce fichier se convertira à
@ -5088,7 +5089,7 @@ n'interviennent qu'aux étapes où une configuration **déjà tendue** est le pr
| fichier | configuration | ce que la capture d'écran montre | ce que son étape affirme |
|---|---|---|---|
| `01-premier-ecran.png` | répertoire neuf | le répertoire vide : la commande de création et les configurations de démonstration offertes, **jamais une liste vide** (§ 8.1) | chaque configuration que le catalogue déclare est nommée et cliquable, et **leur nombre est lu dans le catalogue, jamais écrit dans le scénario** |
| `01b-passer-en-ecriture.png` | idem | la commande « Modifier » à place fixe, et le cadre de mode autour du plan (§ 8.4) | avant ce geste, une tentative de modification est refusée ; après, elle passe |
| `01b-passer-en-ecriture.png` | idem | la commande « Modifier » à place fixe, la bande de mode et le libellé de mode, sans cadre autour du plan (§ 8.4) | avant ce geste, une tentative de modification est refusée ; après, elle passe |
| `02-import-apercu.png` | `participants_cas_limites.csv` | l'aperçu obligatoire (§ 10.1) : encodage et séparateur détectés et **nommés**, association par en-tête, nombre de lignes refusées | l'encodage et le séparateur affichés sont ceux que le module d'import a retenus ; l'aperçu n'est pas contournable |
| `02b-import-cp1252.png` | `participants_cas_limites_cp1252.csv` | l'aperçu du même contenu en windows-1252, séparé par des virgules : l'encodage détecté et **nommé**, les cellules accentuées intactes (§ 10.1) | l'encodage affiché est celui que le module d'import a retenu ; les cellules accentuées affichées sont celles que rend le décodage du fichier, sans caractère parasite |
| `03-import-rapport.png` | idem | le rapport : lignes valides, orthographes d'appartenance fondues avec leur compte, lignes refusées avec leur motif | au moins un groupe d'orthographes fondu et au moins une ligne refusée sont visibles |
@ -5111,7 +5112,7 @@ n'interviennent qu'aux étapes où une configuration **déjà tendue** est le pr
| `17-comparaison-manque.png` | **variante sans exception** (§ 15.1) | les deux profils de manque qui se croisent, l'axe commun, le zéro libellé, les trois effectifs de rangs (§ 12.10.6) | la page écrit sa question en toutes lettres et les trois effectifs ; aucune échelle propre à une courbe |
| `18-comparaison-escalier.png` | idem | l'escalier du manque, jumeau tabulaire de la vue précédente | les effectifs cumulés du tableau reproduisent la courbe |
| `19-ajustement.png` | l'événement retenu | le déplacement d'une personne après la retenue, l'interface disant **lequel des deux gestes** est en cours (§ 5.8) | le libellé de l'état des données est visible |
| `20-mode-lecture.png` | idem | le cadre et le libellé permanents autour du plan, et le bandeau d'application (§ 8.4) | un geste de chaque famille — glisser une personne, glisser une table, tirer une poignée, poser une réservation — est tenté, et l'empreinte d'état est inchangée après les quatre |
| `20-mode-lecture.png` | idem | la bande de mode, le libellé de mode à côté de la place fixe, et le bandeau d'application (§ 8.4) | un geste de chaque famille — glisser une personne, glisser une table, tirer une poignée, poser une réservation — est tenté, et l'empreinte d'état est inchangée après les quatre |
| `21-historique.png` | idem | la liste des instants, le fil courant distingué des fils abandonnés, un jalon nommé (§ 8.3) | au moins un fil abandonné est présent et marqué comme tel |
| `22-plan-brouillon.png` / `23-plan-retenu.png` | idem | l'**aperçu d'impression** du plan (§ 11.7) avec son cartouche, son échelle graphique et son horodatage, **avec** puis **sans** le filigrane BROUILLON | **deux images, parce qu'une absence ne se photographie pas** : le filigrane présent sur l'un, absent sur l'autre, et la paire est le seul exposé lisible de la règle |
| `24-badges-controle.png` / `25-badges-planche.png` | idem | la **feuille de contrôle** avec son échelle de 100 mm et ses comptes, puis une **planche** de quatre A6 et ses trois filets de coupe | voir § 19.6 — ces deux images ne viennent pas du navigateur. La paire est nécessaire : la feuille de contrôle est ce que l'opérateur mesure sous une règle avant d'engager la rame, et une planche seule ne le montre pas |

View file

@ -1013,7 +1013,7 @@ describe('le bandeau de la maquette (§ 8.4, § 18.6)', () => {
});
describe('la page de la maquette : fond, largeur bornée, styles de base', () => {
test('au-delà de la largeur de page, l’écran se centre dans --largeur-page ; en deçà, il prend la fenêtre', async () => {
test('au-delà de la largeur de page, la liste se centre dans --largeur-page ; en deçà, elle prend la fenêtre ; l’événement prend la fenêtre à toute largeur', async () => {
const avant = { largeur: innerWidth, hauteur: innerHeight };
try {
await fixerCadre(1800, 900);
@ -1027,6 +1027,14 @@ describe('la page de la maquette : fond, largeur bornée, styles de base', () =>
await fixerCadre(1366, 700);
flushSync();
expect(main.getBoundingClientRect().width).toBeCloseTo(document.documentElement.clientWidth, 0);
for (const largeur of [1800, 1366]) {
await fixerCadre(largeur, 900);
const ouvert = await avecEvenementOuvert();
const principal = ecran(ouvert).parentElement;
expect(ecran(ouvert).dataset.ecran, `${largeur}`).toBe('evenement');
expect(principal.getBoundingClientRect().left, `${largeur}`).toBeCloseTo(0, 0);
expect(principal.getBoundingClientRect().width, `${largeur}`).toBeCloseTo(document.documentElement.clientWidth, 0);
}
} finally {
await fixerCadre(avant.largeur, avant.hauteur);
}

View file

@ -38,7 +38,9 @@
//
// La page porte le fond de la page ; l'écran — l'élément <main>, parent
// direct de chaque [data-ecran] — se centre dans --largeur-page au-delà
// de cette largeur et prend la fenêtre en deçà. App pose les styles de
// de cette largeur et prend la fenêtre en deçà ; l'écran de l'événement
// retire cette borne et prend la fenêtre à toute largeur
// (Evenement.svelte). App pose les styles de
// base d'un titre de premier niveau, des cellules d'un tableau et des
// champs de saisie de l'écran, de spécificité nulle : le style propre d'un
// composant l'emporte toujours, et le document imprimé (§ 11.7), sous

View file

@ -1238,10 +1238,9 @@ describe('les mesures du retenu dans la liste (§ 5.4, § 8.1, § 8.9)', () => {
return mesurer(instance, indexerPlan(instance, planDepuisFichier(charge.retenu)));
}
test('un événement retenu : ses collisions cumulées et |1000 \u{2212} Σ r(p)| ; sans retenu : « — » ; rien d’écrit', async () => {
test('un événement retenu : |1000 \u{2212} Σ r(p)|, sans colonne des collisions cumulées ; sans retenu : « — » ; rien d’écrit', async () => {
const charge = chargeRetenue();
const mesures = mesuresDe(charge);
expect(mesures.collisionsCumulees).toBeGreaterThan(0);
const m = await monter({
fichiers: [fichier(`retenue${SUFFIXES.etat}`, etat(charge)), fichier(`club${SUFFIXES.etat}`, texteRenomme('Club des Merles'))],
});
@ -1249,21 +1248,17 @@ describe('les mesures du retenu dans la liste (§ 5.4, § 8.1, § 8.9)', () => {
await chargerResumes(m);
const avant = ecritures(m);
const retenue = ligneDe(m, 'retenue');
expect(colonne(retenue, 'collisionsCumulees')).toBe(format.nombre(mesures.collisionsCumulees));
expect(colonne(retenue, 'diffRedondance')).toBe(format.nombre(Math.abs(1000 - mesures.totalRedondance)));
const club = ligneDe(m, 'club');
expect(colonne(club, 'collisionsCumulees')).toBe(TIRET);
expect(colonne(club, 'diffRedondance')).toBe(TIRET);
// Chaque intitulé est un en-tête de colonne, une seule fois pour toute la
// liste.
for (const [nom, cle] of [
['collisionsCumulees', 'accueil.colonne.collisionsCumulees'],
['diffRedondance', 'accueil.colonne.diffRedondance'],
]) {
expect(tous(m, `[data-terme="${nom}"]`).length, nom).toBe(1);
expect(terme(accueil(m), nom).closest('thead'), nom).not.toBeNull();
expect(terme(accueil(m), nom).textContent).toBe(t(cle));
}
// L'intitulé est un en-tête de colonne, une seule fois pour toute la
// liste ; aucune colonne ni aucun intitulé ne nomme les collisions
// cumulées.
expect(tous(m, '[data-terme="diffRedondance"]').length).toBe(1);
expect(terme(accueil(m), 'diffRedondance').closest('thead')).not.toBeNull();
expect(terme(accueil(m), 'diffRedondance').textContent).toBe(t('accueil.colonne.diffRedondance'));
expect(tous(m, '[data-colonne="collisionsCumulees"], [data-terme="collisionsCumulees"]')).toEqual([]);
expect(tous(m, '[data-ecran="accueil"] thead th').filter((th) => th.textContent.includes(t('propositions.colonne.collisionsCumulees')))).toEqual([]);
expect(visible(retenue.querySelector('[data-colonne="diffRedondance"]'))).toBe(true);
expect(ecritures(m)).toBe(avant);
});
@ -1271,10 +1266,7 @@ describe('les mesures du retenu dans la liste (§ 5.4, § 8.1, § 8.9)', () => {
test('chaque intitulé s’explique : le survol puis le focus ouvrent son infobulle, Échap et la sortie la ferment', async () => {
const m = await monter({ fichiers: [fichier(`retenue${SUFFIXES.etat}`, etat(chargeRetenue()))] });
await vi.waitFor(() => expect(lignes(m).length).toBe(1));
await eprouverTermes(accueil(m), [
['collisionsCumulees', 'accueil.explication.collisionsCumulees'],
['diffRedondance', 'accueil.explication.diffRedondance'],
]);
await eprouverTermes(accueil(m), [['diffRedondance', 'accueil.explication.diffRedondance']]);
});
test('la cible changée dans les propositions, puis la liste : la différence se lit contre la nouvelle cible', async () => {
@ -1301,7 +1293,6 @@ describe('les mesures du retenu dans la liste (§ 5.4, § 8.1, § 8.9)', () => {
await revenirALaListe(m, 1);
await resumeRecu('retenue');
expect(colonne(ligneDe(m, 'retenue'), 'diffRedondance')).toBe(format.nombre(Math.abs(25 - totalRedondance)));
expect(colonne(ligneDe(m, 'retenue'), 'collisionsCumulees')).toBe(format.nombre(mesuresDe(charge).collisionsCumulees));
});
});
@ -1323,12 +1314,9 @@ describe('la meilleure proposition du classement dans la liste (§ 5.4, § 5.7,
return appliquerCommande('retenirProposition', charge, { id: autre.id }).charge;
}
// Ce que la ligne, ou le bloc de la génération, écrit de la meilleure
// proposition.
const lu = (bloc) => ({
id: bloc.querySelector('[data-meilleure-id]')?.textContent ?? null,
collisions: bloc.querySelector('[data-meilleure-collisions]')?.textContent ?? null,
});
// Ce que la ligne, ou le bloc de la génération, écrit de l'identifiant de
// la meilleure proposition.
const lu = (bloc) => ({ id: bloc.querySelector('[data-meilleure-id]')?.textContent ?? null });
// L'en-tête du groupe de colonnes de la meilleure proposition, et celui de
// sa colonne d'identifiant.
const groupeMeilleure = (m) => terme(accueil(m), 'meilleure').closest('th');
@ -1342,7 +1330,7 @@ describe('la meilleure proposition du classement dans la liste (§ 5.4, § 5.7,
};
const blocGeneration = (m) => dans(m, '[data-section="generation"] [data-meilleure]');
test('un événement classé : le rang 1, son identifiant et ses collisions cumulées, à part du retenu ; sans proposition, « — » ; rien d’écrit', async () => {
test('un événement classé : le rang 1 et son identifiant, à part du retenu, sans ses collisions cumulées ; sans proposition, « — » ; rien d’écrit', async () => {
const charge = chargeClassee();
const table = evaluer(charge);
const [premiere] = table.propositions;
@ -1355,35 +1343,29 @@ describe('la meilleure proposition du classement dans la liste (§ 5.4, § 5.7,
await chargerResumes(m);
const avant = ecritures(m);
const classee = ligneDe(m, 'classee');
expect(lu(classee)).toEqual({ id: format.nombre(premiere.id), collisions: format.nombre(premiere.mesures.collisionsCumulees) });
expect(colonne(classee, 'collisionsCumulees')).toBe(format.nombre(table.retenu.mesures.collisionsCumulees));
expect(lu(classee)).toEqual({ id: format.nombre(premiere.id) });
// Les intitulés sont ceux du bloc de la génération, en en-têtes de
// colonnes : le titre sur le groupe, l'identifiant et les collisions sur
// ses deux colonnes ; chaque valeur se tient sous son groupe.
// colonnes : le titre sur le groupe, l'identifiant sur sa seule colonne ;
// la valeur se tient sous son groupe. Aucune cellule ni aucun intitulé
// des collisions de la meilleure.
expect(terme(accueil(m), 'meilleure').textContent).toBe(t('generation.meilleure.titre'));
expect(groupeMeilleure(m).getAttribute('scope')).toBe('colgroup');
expect(groupeMeilleure(m).colSpan).toBe(2);
expect(groupeMeilleure(m).colSpan).toBe(1);
expect(enteteProposition(m)).toBeDefined();
expect(terme(accueil(m), 'meilleureCollisions').textContent).toBe(t('propositions.colonne.collisionsCumulees'));
for (const valeur of ['[data-meilleure-id]', '[data-meilleure-collisions]']) {
const cellule = classee.querySelector(valeur);
expect(visible(cellule), valeur).toBe(true);
expect(atteint(cellule), valeur).toBe(true);
expect(sous(cellule, groupeMeilleure(m)), valeur).toBe(true);
}
expect(sous(classee.querySelector('[data-meilleure-id]'), enteteProposition(m))).toBe(true);
expect(sous(classee.querySelector('[data-meilleure-collisions]'), terme(accueil(m), 'meilleureCollisions').closest('th'))).toBe(true);
expect(lu(ligneDe(m, 'club'))).toEqual({ id: TIRET, collisions: TIRET });
const cellule = classee.querySelector('[data-meilleure-id]');
expect(visible(cellule)).toBe(true);
expect(atteint(cellule)).toBe(true);
expect(sous(cellule, groupeMeilleure(m))).toBe(true);
expect(sous(cellule, enteteProposition(m))).toBe(true);
expect(tous(m, '[data-ecran="accueil"] [data-meilleure-collisions], [data-terme="meilleureCollisions"]')).toEqual([]);
expect(lu(ligneDe(m, 'club'))).toEqual({ id: TIRET });
expect(ecritures(m)).toBe(avant);
});
test('le titre et l’intitulé des collisions s’expliquent : le survol puis le focus ouvrent l’infobulle, Échap et la sortie la ferment', async () => {
test('le titre s’explique : le survol puis le focus ouvrent l’infobulle, Échap et la sortie la ferment', async () => {
const m = await monter({ fichiers: [fichier(`classee${SUFFIXES.etat}`, etat(chargeClassee()))] });
await vi.waitFor(() => expect(lignes(m).length).toBe(1));
await eprouverTermes(accueil(m), [
['meilleure', 'accueil.explication.meilleure'],
['meilleureCollisions', 'propositions.explication.collisionsCumulees'],
]);
await eprouverTermes(accueil(m), [['meilleure', 'accueil.explication.meilleure']]);
});
test('la ligne écrit ce que le bloc de la génération écrit du même événement, sous les mêmes libellés, et de nouveau au retour', async () => {
@ -1392,20 +1374,16 @@ describe('la meilleure proposition du classement dans la liste (§ 5.4, § 5.7,
await chargerResumes(m);
const deLaListe = lu(ligneDe(m, 'classee'));
expect(deLaListe.id).toMatch(/\d/);
const libelles = [terme(accueil(m), 'meilleure'), enteteProposition(m), terme(accueil(m), 'meilleureCollisions')].map(
(element) => element.textContent.trim(),
);
const libelles = [terme(accueil(m), 'meilleure'), enteteProposition(m)].map((element) => element.textContent.trim());
await ouvrirLigne(m, 'classee');
await attendreEvenement(m);
m.ui.allerA('generation');
await vi.waitFor(() => expect(blocGeneration(m)?.querySelector('[data-meilleure-id]')).not.toBeNull());
const bloc = blocGeneration(m);
expect(lu(bloc)).toEqual(deLaListe);
expect(
[bloc.querySelector('h3'), bloc.querySelector('[data-meilleure-libelle-id]'), bloc.querySelector('[data-terme]')].map(
(element) => element.textContent,
),
).toEqual(libelles);
expect([bloc.querySelector('h3'), bloc.querySelector('[data-meilleure-libelle-id]')].map((element) => element.textContent)).toEqual(
libelles,
);
// Le même résumé, mais reçu de la liste relue au retour : celui d'avant
// l'ouverture écrirait les mêmes valeurs sans rien prouver.
const resumeRecu = suivreResumes(m);
@ -1423,7 +1401,7 @@ describe('la meilleure proposition du classement dans la liste (§ 5.4, § 5.7,
const m = await monter({ fichiers: [fichier(`horsclassement${SUFFIXES.etat}`, etat(charge))] });
await vi.waitFor(() => expect(lignes(m).length).toBe(1));
await chargerResumes(m);
expect(lu(ligneDe(m, 'horsclassement'))).toEqual({ id: TIRET, collisions: TIRET });
expect(lu(ligneDe(m, 'horsclassement'))).toEqual({ id: TIRET });
await ouvrirLigne(m, 'horsclassement');
await attendreEvenement(m);
m.ui.allerA('generation');
@ -1473,16 +1451,15 @@ describe('le chargement des résumés de la liste (§ 8.1, § 14.5, § 19.2)', (
return appliquerCommande('retenirProposition', charge, { id: charge.propositions[1].id }).charge;
}
// Les quatre valeurs de résumé d'une ligne, dans l'ordre du rendu.
const CELLULES = ['[data-colonne="collisionsCumulees"]', '[data-colonne="diffRedondance"]', '[data-meilleure-id]', '[data-meilleure-collisions]'];
// Les deux valeurs de résumé d'une ligne, dans l'ordre du rendu.
const CELLULES = ['[data-colonne="diffRedondance"]', '[data-meilleure-id]'];
const cellules = (ligne) => CELLULES.map((selecteur) => ligne.querySelector(selecteur));
// Ce que la ligne d'une charge écrit, par la table d'évaluation sollicitée
// une seconde fois (§ 19.1).
function attendues(charge) {
const table = evaluer(charge);
const [premiere] = table.propositions;
const { collisionsCumulees, totalRedondance } = table.retenu.mesures;
return [collisionsCumulees, Math.abs(1000 - totalRedondance), premiere.id, premiere.mesures.collisionsCumulees].map((n) => format.nombre(n));
return [Math.abs(1000 - table.retenu.mesures.totalRedondance), premiere.id].map((n) => format.nombre(n));
}
// Chaque valeur de la ligne attend : la cellule se dit occupée, porte un
// seul signe, muet pour l'accessibilité (aria-hidden, sans rôle ni nom),
@ -1528,12 +1505,12 @@ describe('le chargement des résumés de la liste (§ 8.1, § 14.5, § 19.2)', (
expect(colonne(ligne, 'participants')).toBe(format.nombre(charges[ligne.dataset.evenement].participants.length));
enAttente(ligne);
}
expect(signes(m).length).toBe(8);
expect(signes(m).length).toBe(4);
const region = regionEtat(m);
expect(region.textContent).toBe(t('accueil.chargement'));
for (let image = 0; image < 3; image += 1) await uneImage();
flushSync();
expect(signes(m).length).toBe(8);
expect(signes(m).length).toBe(4);
m.horloge.avancer(0);
await vi.waitFor(() => resolue(ligneDe(m, 'alpha'), attendues(charges.alpha)));
enAttente(ligneDe(m, 'beta'));
@ -1551,7 +1528,7 @@ describe('le chargement des résumés de la liste (§ 8.1, § 14.5, § 19.2)', (
test('démontée avant le premier pas, l’application arrête le flux des résumés : aucun état ne se relit ensuite', async () => {
const charges = { alpha: chargeResumee('evt-alpha'), beta: chargeResumee('evt-beta') };
const m = await monter({ fichiers: Object.entries(charges).map(([base, charge]) => fichier(`${base}${SUFFIXES.etat}`, etat(charge))) });
expect(signes(m).length).toBe(8);
expect(signes(m).length).toBe(4);
const relus = [];
const lireTexte = m.fs.lireTexte;
m.fs.lireTexte = (racine, chemin) => {
@ -1568,7 +1545,7 @@ describe('le chargement des résumés de la liste (§ 8.1, § 14.5, § 19.2)', (
expect(relus).toEqual([]);
});
test('un état qui ne se relit pas pendant le chargement : « — » dans ses quatre valeurs, et l’autre ligne résolue', async () => {
test('un état qui ne se relit pas pendant le chargement : « — » dans ses deux valeurs, et l’autre ligne résolue', async () => {
const charges = { alpha: chargeResumee('evt-alpha'), beta: chargeResumee('evt-beta') };
const m = await monter({ fichiers: Object.entries(charges).map(([base, charge]) => fichier(`${base}${SUFFIXES.etat}`, etat(charge))) });
const lireTexte = m.fs.lireTexte;
@ -1577,7 +1554,7 @@ describe('le chargement des résumés de la liste (§ 8.1, § 14.5, § 19.2)', (
? Promise.reject(new ErreurStockage('LECTURE', { chemin, cause: 'EIO' }))
: lireTexte.call(m.fs, racine, chemin);
await chargerResumes(m);
resolue(ligneDe(m, 'alpha'), [TIRET, TIRET, TIRET, TIRET]);
resolue(ligneDe(m, 'alpha'), [TIRET, TIRET]);
resolue(ligneDe(m, 'beta'), attendues(charges.beta));
});
@ -1611,7 +1588,7 @@ describe('le chargement des résumés de la liste (§ 8.1, § 14.5, § 19.2)', (
test('le signe se voit : il tient 3:1 sur ce qui l’entoure dans les deux thèmes, sans atténuation', async () => {
const m = await monter({ fichiers: [fichier(`alpha${SUFFIXES.etat}`, etat(chargeResumee('evt-alpha')))] });
const traces = tous(m, '[data-chargement] path');
expect(traces.length).toBe(4);
expect(traces.length).toBe(2);
for (const theme of ['clair', 'sombre']) {
await sousTheme(theme, async () => {
for (const trace of traces) {
@ -1822,7 +1799,7 @@ describe('la liste en tableau : en-tête, tuiles, groupes et cartes (§ 8.1, §
await chargerResumes(m);
const entetes = tous(m, '[data-ecran="accueil"] thead th[scope="colgroup"]');
expect(entetes.map(texte)).toEqual([t('accueil.entete.evenement'), t('accueil.entete.retenu'), t('generation.meilleure.titre')]);
expect(entetes.map((th) => th.colSpan)).toEqual([4, 2, 2]);
expect(entetes.map((th) => th.colSpan)).toEqual([4, 1, 1]);
expect(fond(entetes[1])).toEqual(jeton('--couleur-etat-retenu-fond'));
expect(fond(entetes[2])).toEqual(jeton('--couleur-etat-propose-fond'));
// Le groupe porte le signe de son état, de la marque de l'état.
@ -1835,8 +1812,8 @@ describe('la liste en tableau : en-tête, tuiles, groupes et cartes (§ 8.1, §
};
const ligne = ligneDe(m, 'gala');
for (const nom of ['nom', 'etat', 'participants', 'modification']) expect(dans2(ligne.querySelector(`[data-colonne="${nom}"]`), entetes[0]), nom).toBe(true);
for (const nom of ['collisionsCumulees', 'diffRedondance']) expect(dans2(ligne.querySelector(`[data-colonne="${nom}"]`), entetes[1]), nom).toBe(true);
for (const valeur of ['[data-meilleure-id]', '[data-meilleure-collisions]']) expect(dans2(ligne.querySelector(valeur), entetes[2]), valeur).toBe(true);
expect(dans2(ligne.querySelector('[data-colonne="diffRedondance"]'), entetes[1])).toBe(true);
expect(dans2(ligne.querySelector('[data-meilleure-id]'), entetes[2])).toBe(true);
// Le nom est l'en-tête de sa ligne ; la date et le chemin le suivent,
// dessous, en plus petit.
const nom = ligne.querySelector('[data-colonne="nom"]');

View file

@ -14,13 +14,13 @@
//
// Les en-têtes de colonnes sont groupés : l'événement — nom, état,
// participants, dernière modification —, le placement retenu, à la teinte
// de Retenu — ses deux mesures (COLONNES_DU_RETENU) —, la meilleure
// proposition du classement, à la teinte de Proposé, sous le titre et les
// libellés du bloc de la génération (MEILLEURE) — son identifiant et ses
// collisions cumulées —, puis les actions. Chaque valeur est dans son
// propre nœud : data-colonne, data-meilleure-id, data-meilleure-collisions,
// « — » sans valeur. Le nom est l'en-tête de sa ligne ; la date et le
// chemin le suivent en petite ligne, chacun précédé de son intitulé, que
// de Retenu — sa différence à la cible (COLONNES_DU_RETENU) —, la
// meilleure proposition du classement, à la teinte de Proposé, sous le
// titre et le libellé du bloc de la génération (MEILLEURE) — son
// identifiant —, puis les actions. Chaque valeur est dans son propre
// nœud : data-colonne, data-meilleure-id, « — » sans valeur. Le nom est
// l'en-tête de sa ligne ; la date et le chemin le suivent en petite
// ligne, chacun précédé de son intitulé, que
// lisent les technologies d'assistance et que l'œil ne voit pas, puis ses
// signalements traduits — les bases qu'un doublon nomme suivent son
// texte, chacune dans son propre nœud (data-doublon). L'état est une
@ -33,7 +33,7 @@
//
// lignes : LigneListe[] de la façade ; resumes : les résumés reçus de
// facade.resumer, par base — une ligne sans le sien attend, et chacune de
// ses quatre valeurs de résumé porte le signe du chargement (Chargement),
// ses deux valeurs de résumé porte le signe du chargement (Chargement),
// décoratif, dans une cellule qui se dit occupée (aria-busy), jusqu'à ce
// qu'il arrive. La région d'état qui suit le tableau
// (data-chargement-liste) existe tant que la liste a des lignes, et écrit
@ -44,10 +44,9 @@
// du dernier échec, ou null ; apresOperation() : à appeler quand une
// opération a changé le dossier.
//
// L'intitulé de chaque mesure du retenu, le titre de la meilleure
// proposition et l'intitulé de ses collisions s'expliquent dans
// l'infobulle de la liste, une seule (Infobulle, primitive) : chaque terme
// (data-terme, que pose le fragment termeExplique), un en-tête de colonne,
// L'intitulé de la mesure du retenu et le titre de la meilleure
// proposition s'expliquent dans l'infobulle de la liste, une seule
// (Infobulle, primitive) : chaque terme (data-terme, que pose le fragment termeExplique), un en-tête de colonne,
// se focalise au clavier, son survol ou son focus ouvre l'explication, le
// quitter la referme quand elle est encore la sienne, Échap aussi. Le
// terme expliqué porte aria-describedby vers elle, et lui seul.
@ -73,9 +72,9 @@
const enChargement = $derived(lignes.some((ligne) => ligneAffichee(ligne, format, resumes.get(ligne.base)).enAttente));
const groupes = $derived(groupesDeLaListe(lignes, vue.filtre, vue.groupement));
// Le nombre de colonnes du tableau, que l'intitulé d'un groupe couvre :
// les quatre de l'événement, les deux du retenu, les deux de la meilleure
// les quatre de l'événement, celle du retenu, celle de la meilleure
// proposition, les actions.
const COLONNES = 9;
const COLONNES = 7;
// Le terme expliqué : {ancre, terme, cle, details} — son élément, son nom
// et la clé de son explication — ; null sans infobulle.
@ -137,18 +136,18 @@
<div class="defilement">
<table>
<colgroup span="4"></colgroup>
<colgroup span="2"></colgroup>
<colgroup span="2"></colgroup>
<colgroup></colgroup>
<colgroup></colgroup>
<colgroup></colgroup>
<thead>
<tr>
<th scope="colgroup" colspan="4" class="groupe">{t('accueil.entete.evenement')}</th>
<th scope="colgroup" colspan="2" class="groupe retenu"
<th scope="colgroup" class="groupe retenu"
><span class="entete-groupe"
><span class="signe"><Icone nom="coche" taille={14} /></span><span>{t('accueil.entete.retenu')}</span></span
></th
>
<th scope="colgroup" colspan="2" class="groupe propose"
<th scope="colgroup" class="groupe propose"
><span class="entete-groupe"
><span class="signe"><Icone nom="listePoints" taille={14} /></span>{@render termeExplique(
'meilleure',
@ -168,9 +167,6 @@
<th scope="col" class="nombre">{@render termeExplique(colonne, cle, explication)}</th>
{/each}
<th scope="col" class="nombre">{t(MEILLEURE.proposition)}</th>
<th scope="col" class="nombre"
>{@render termeExplique('meilleureCollisions', MEILLEURE.collisions, MEILLEURE.explicationCollisions)}</th
>
</tr>
</thead>
{#each groupes as groupe, rangGroupe (groupe.groupe ?? '')}
@ -231,9 +227,6 @@
<td class="nombre" data-colonne={colonne} aria-busy={occupee}>{@render resume(affichee, affichee[colonne])}</td>
{/each}
<td class="nombre" data-meilleure-id aria-busy={occupee}>{@render resume(affichee, affichee.meilleure.id)}</td>
<td class="nombre" data-meilleure-collisions aria-busy={occupee}
>{@render resume(affichee, affichee.meilleure.collisions)}</td
>
<td class="actions">
<div class="commandes-ligne">
<button

View file

@ -4,15 +4,14 @@
// Le modèle de l'accueil, pur (§ 8.1, § 8.6, § 8.8) : ce que les composants
// de la liste des événements affichent, sans arithmétique ni texte. Une
// ligne de la liste (LigneListe de la façade, evenements.js) devient ses six
// informations et ses signalements, mis en forme, puis, son résumé reçu, les
// deux mesures de son retenu et la meilleure proposition de son
// classement ; les lignes de la liste, les tuiles de ses filtres par état
// du plan, le résumé de son en-tête et les groupes de son tableau ; une
// question du dossier de travail, ses réponses ; un état illisible, ses
// secours ; un refus de la façade, le refus affiché sur la liste. Chaque clé
// rendue est écrite ici en entier : aucun composant ne compose une clé.
import { COLONNE_ANNONCEE } from '../generation/meilleure.js';
import { COLONNES } from '../propositions/modele.js';
// informations et ses signalements, mis en forme, puis, son résumé reçu, la
// différence à la cible de son retenu et l'identifiant de la meilleure
// proposition de son classement ; les lignes de la liste, les tuiles de ses
// filtres par état du plan, le résumé de son en-tête et les groupes de son
// tableau ; une question du dossier de travail, ses réponses ; un état
// illisible, ses secours ; un refus de la façade, le refus affiché sur la
// liste. Chaque clé rendue est écrite ici en entier : aucun composant ne
// compose une clé.
// Les états du plan (§ 9) et la clé de leur nom, dans l'ordre du § 9.
const CLES_ETATS = new Map([
@ -26,34 +25,29 @@ const CLES_ETATS = new Map([
export const ETATS = Object.freeze([...CLES_ETATS.keys()]);
/**
* Les deux mesures du placement retenu qu'une ligne montre (§ 5.4, § 8.9),
* dans l'ordre d'affichage : chacune { colonne, cle, explication } — le nom
* de sa valeur dans ligneAffichee et son data-colonne, la clé de son
* intitulé, celle de son explication, que l'infobulle de l'intitulé montre.
* La mesure du placement retenu qu'une ligne montre (§ 5.4, § 8.9) : la
* différence à la cible, { colonne, cle, explication } — le nom de sa valeur
* dans ligneAffichee et son data-colonne, la clé de son intitulé, celle de
* son explication, que l'infobulle de l'intitulé montre. Les collisions
* cumulées du retenu ne se montrent pas dans la liste.
*/
export const COLONNES_DU_RETENU = Object.freeze(
['collisionsCumulees', 'diffRedondance'].map((colonne) =>
['diffRedondance'].map((colonne) =>
Object.freeze({ colonne, cle: `accueil.colonne.${colonne}`, explication: `accueil.explication.${colonne}` }),
),
);
// La colonne du tableau des propositions que le bloc de la génération
// annonce : son intitulé et son explication.
const COLONNE_MEILLEURE = COLONNES.find(({ nom }) => nom === COLONNE_ANNONCEE);
/**
* Les clés du bloc de la meilleure proposition du classement qu'une ligne
* montre (§ 5.4, § 5.7), celles du bloc de la génération : titre,
* explication du titre — ce que désigne le rang 1, et quand le bloc écrit
* « — » —, libellé de l'identifiant, intitulé des collisions cumulées et
* leur explication, celles de la colonne du tableau des propositions.
* Les clés de la meilleure proposition du classement qu'une ligne montre
* (§ 5.4, § 5.7) : titre et libellé de l'identifiant, ceux du bloc de la
* génération, et explication du titre — ce que désigne le rang 1, et quand
* la ligne écrit « — ». Ses collisions cumulées ne se montrent pas dans la
* liste.
*/
export const MEILLEURE = Object.freeze({
titre: 'generation.meilleure.titre',
explication: 'accueil.explication.meilleure',
proposition: 'generation.meilleure.proposition',
collisions: COLONNE_MEILLEURE.cle,
explicationCollisions: COLONNE_MEILLEURE.explication,
});
// La clé de la valeur absente, celle de la mise en forme.
@ -133,9 +127,9 @@ export function signalementsDe(ligne) {
}
/**
* Les six informations d'une ligne (§ 8.1), les deux mesures de son retenu
* et la meilleure proposition de son classement, mises en forme, et ses
* signalements. nom : celui inscrit dans le fichier, ou la base quand l'état
* Les six informations d'une ligne (§ 8.1), la différence à la cible de son
* retenu et l'identifiant de la meilleure proposition de son classement,
* mis en forme, et ses signalements. nom : celui inscrit dans le fichier, ou la base quand l'état
* ne se lit pas ; date, participants et dernière modification par format,
* la valeur absente quand ils manquent — la dernière modification est
* l'horodatage de la dernière entrée du journal, sur l'horloge de
@ -143,13 +137,12 @@ export function signalementsDe(ligne) {
* clé de son nom, que le composant traduit ; chemin : tel que la façade
* l'affiche, en entier.
*
* Les quatre valeurs de résumé lisent resume, ce que facade.resumer a rendu
* pour la ligne : collisionsCumulees et diffRedondance celles de
* mesuresDuRetenu, par format, la valeur absente sans retenu mesurable — la
* différence se lit telle quelle, jamais refaite ; meilleure, l'identifiant
* et les collisions cumulées de meilleureDuClassement, par format, la valeur
* absente l'un et l'autre sans proposition classée. Sans resume, la ligne
* est enAttente : son résumé se calcule encore, et les quatre valeurs
* Les deux valeurs de résumé lisent resume, ce que facade.resumer a rendu
* pour la ligne : diffRedondance, celle de mesuresDuRetenu, par format, la
* valeur absente sans retenu mesurable — elle se lit telle quelle, jamais
* refaite ; meilleure.id, l'identifiant de meilleureDuClassement, par
* format, la valeur absente sans proposition classée. Sans resume, la
* ligne est enAttente : son résumé se calcule encore, et les deux valeurs
* valent la valeur absente, que le composant remplace par le signe du
* chargement.
*
@ -158,8 +151,7 @@ export function signalementsDe(ligne) {
* @param {{mesuresDuRetenu: Object|null, meilleureDuClassement: Object|null}} [resume]
* @returns {{nom: string, date: string, etat: string, participants: string,
* modification: string, chemin: string, enAttente: boolean,
* collisionsCumulees: string, diffRedondance: string,
* meilleure: {id: string, collisions: string},
* diffRedondance: string, meilleure: {id: string},
* signalements: Array<{cle: string, details: Object, noms: string[]}>}}
*/
export function ligneAffichee(ligne, format, resume) {
@ -171,12 +163,8 @@ export function ligneAffichee(ligne, format, resume) {
modification: ligne.derniereModification === null ? format.nombre(null) : format.horodatage(ligne.derniereModification),
chemin: ligne.chemin,
enAttente: resume === undefined,
collisionsCumulees: format.nombre(resume?.mesuresDuRetenu?.collisionsCumulees ?? null),
diffRedondance: format.nombre(resume?.mesuresDuRetenu?.diffRedondance ?? null),
meilleure: {
id: format.nombre(resume?.meilleureDuClassement?.id ?? null),
collisions: format.nombre(resume?.meilleureDuClassement?.collisionsCumulees ?? null),
},
meilleure: { id: format.nombre(resume?.meilleureDuClassement?.id ?? null) },
signalements: signalementsDe(ligne),
};
}

View file

@ -19,9 +19,8 @@ import { texteLivre } from '../../demo/livrees.js';
import { rechercher } from '../../moteur/recherche.js';
import { analyser, configurationDepuisCharge } from '../../stockage/document.js';
import { VERSION } from '../../version.genere.js';
import { COLONNE_ANNONCEE, meilleureProposition } from '../generation/meilleure.js';
import { meilleureProposition } from '../generation/meilleure.js';
import { TABLES_INTERFACE } from '../libelles.js';
import { COLONNES, texteCellule } from '../propositions/modele.js';
import {
COLONNES_DU_RETENU,
ETATS,
@ -81,7 +80,6 @@ describe('ligneAffichee : les six informations (§ 8.1)', () => {
const affichee = ligneAffichee(ligne(), format);
assert.deepEqual(Object.keys(affichee).sort(), [
'chemin',
'collisionsCumulees',
'date',
'diffRedondance',
'enAttente',
@ -337,29 +335,28 @@ describe('saisieDe : la saisie d’un nom pour copier ou renommer (§ 8.7)', ()
});
describe('ligneAffichee : les mesures du retenu (§ 5.4, § 8.1, § 8.9)', () => {
test('les collisions cumulées et la différence à la cible, celles de la façade, mises en forme ; aucune ne se calcule ici', () => {
const mesuresDuRetenu = { collisionsCumulees: 1234, totalRedondance: 1500, cibleRedondance: 1000, diffRedondance: 500 };
test('la différence à la cible, celle de la façade, mise en forme, sans calcul ici ; les collisions cumulées du retenu ne se montrent pas', () => {
const mesuresDuRetenu = { collisionsCumulees: 8765, totalRedondance: 1500, cibleRedondance: 1000, diffRedondance: 500 };
const affichee = ligneAffichee(ligne(), format, { ...AUCUN, mesuresDuRetenu });
assert.equal(affichee.collisionsCumulees, format.nombre(1234));
assert.equal(affichee.diffRedondance, format.nombre(500));
assert.ok(!Object.values(affichee).includes(format.nombre(8765)));
// La différence se lit telle que la façade la rend, jamais refaite
// depuis la cible et la somme.
const telleQuelle = ligneAffichee(ligne(), format, { ...AUCUN, mesuresDuRetenu: { ...mesuresDuRetenu, diffRedondance: 7 } });
assert.equal(telleQuelle.diffRedondance, format.nombre(7));
});
test('sans retenu mesurable, et pour une ligne illisible, les deux valent « — »', () => {
test('sans retenu mesurable, et pour une ligne illisible, la différence vaut « — »', () => {
for (const champs of [{}, { nom: null, etat: null, participants: null, illisible: { raison: 'VIDE', chemin: null } }]) {
const affichee = ligneAffichee(ligne(champs), format, AUCUN);
assert.equal(affichee.collisionsCumulees, VIDE);
assert.equal(affichee.diffRedondance, VIDE);
}
});
test('COLONNES_DU_RETENU : les deux colonnes, dans leur ordre, chacune avec son intitulé et son explication, qui existent', () => {
test('COLONNES_DU_RETENU : la seule différence à la cible, avec son intitulé et son explication, qui existent ; aucune clé des collisions cumulées dans la liste', () => {
assert.deepEqual(
COLONNES_DU_RETENU.map(({ colonne }) => colonne),
['collisionsCumulees', 'diffRedondance'],
['diffRedondance'],
);
assert.ok(Object.isFrozen(COLONNES_DU_RETENU));
for (const { colonne, cle, explication } of COLONNES_DU_RETENU) {
@ -368,30 +365,29 @@ describe('ligneAffichee : les mesures du retenu (§ 5.4, § 8.1, § 8.9)', () =>
assert.ok(t(cle).length > 0);
assert.ok(t(explication).length > 0);
}
assert.equal(t('accueil.colonne.collisionsCumulees'), 'Collisions cumulées (paire et tour)');
assert.equal(t('accueil.colonne.diffRedondance'), "Diff somme d'appartenance");
for (const cle of ['accueil.colonne.collisionsCumulees', 'accueil.explication.collisionsCumulees']) assert.throws(() => t(cle), RangeError, cle);
});
});
describe('ligneAffichee : les résumés en attente (§ 8.1)', () => {
test('sans résumé reçu, la ligne est en attente et ses quatre valeurs de résumé ne disent rien ; reçu, elle ne l’est plus', () => {
test('sans résumé reçu, la ligne est en attente et ses deux valeurs de résumé ne disent rien ; reçu, elle ne l’est plus', () => {
const attente = ligneAffichee(ligne(), format);
assert.equal(attente.enAttente, true);
assert.equal(attente.collisionsCumulees, VIDE);
assert.equal(attente.diffRedondance, VIDE);
assert.deepEqual(attente.meilleure, { id: VIDE, collisions: VIDE });
assert.deepEqual(attente.meilleure, { id: VIDE });
assert.equal(ligneAffichee(ligne(), format, AUCUN).enAttente, false);
const recu = ligneAffichee(ligne(), format, {
mesuresDuRetenu: { collisionsCumulees: 12, totalRedondance: 990, cibleRedondance: 1000, diffRedondance: 10 },
meilleureDuClassement: { id: 3, collisionsCumulees: 9 },
});
assert.equal(recu.enAttente, false);
assert.equal(recu.collisionsCumulees, format.nombre(12));
assert.deepEqual(recu.meilleure, { id: format.nombre(3), collisions: format.nombre(9) });
assert.equal(recu.diffRedondance, format.nombre(10));
assert.deepEqual(recu.meilleure, { id: format.nombre(3) });
});
test('les six informations ne dépendent pas des résumés : en attente ou non, elles sont les mêmes', () => {
const sans = ({ enAttente, collisionsCumulees, diffRedondance, meilleure, ...reste }) => reste;
const sans = ({ enAttente, diffRedondance, meilleure, ...reste }) => reste;
assert.deepEqual(sans(ligneAffichee(ligne(), format)), sans(ligneAffichee(ligne(), format, AUCUN)));
});
});
@ -405,29 +401,25 @@ describe('ligneAffichee : la meilleure proposition du classement (§ 5.4, § 5.7
return executer(charge, 'enregistrerGeneration', { propositions, configuration, produitVersion: VERSION.affichee });
}
test("l'identifiant et les collisions cumulées que rend la façade, mis en forme ; aucun ne se calcule ici", () => {
test("l'identifiant que rend la façade, mis en forme, sans calcul ici ; ses collisions cumulées ne se montrent pas", () => {
const affichee = ligneAffichee(ligne(), format, { ...AUCUN, meilleureDuClassement: { id: 1234, collisionsCumulees: 5678 } });
assert.deepEqual(affichee.meilleure, { id: format.nombre(1234), collisions: format.nombre(5678) });
assert.deepEqual(affichee.meilleure, { id: format.nombre(1234) });
});
test('sans proposition classée, et pour une ligne illisible, les deux valent « — »', () => {
test('sans proposition classée, et pour une ligne illisible, l’identifiant vaut « — »', () => {
for (const champs of [{}, { nom: null, etat: null, participants: null, illisible: { raison: 'VIDE', chemin: null } }]) {
assert.deepEqual(ligneAffichee(ligne(champs), format, AUCUN).meilleure, { id: VIDE, collisions: VIDE });
assert.deepEqual(ligneAffichee(ligne(champs), format, AUCUN).meilleure, { id: VIDE });
}
});
test('MEILLEURE : les libellés du bloc de la génération, son explication des collisions, et l’explication du titre, qui existent', () => {
const colonne = COLONNES.find(({ nom }) => nom === COLONNE_ANNONCEE);
test('MEILLEURE : le titre et le libellé de l’identifiant du bloc de la génération, et l’explication du titre, qui existent', () => {
assert.ok(Object.isFrozen(MEILLEURE));
assert.deepEqual(MEILLEURE, {
titre: 'generation.meilleure.titre',
explication: 'accueil.explication.meilleure',
proposition: 'generation.meilleure.proposition',
collisions: colonne.cle,
explicationCollisions: colonne.explication,
});
assert.equal(t(MEILLEURE.titre), 'Meilleure proposition du classement');
assert.equal(t(MEILLEURE.collisions), 'Collisions cumulées (paire et tour)');
for (const cle of Object.values(MEILLEURE)) assert.ok(t(cle).length > 0, cle);
});
@ -441,11 +433,11 @@ describe('ligneAffichee : la meilleure proposition du classement (§ 5.4, § 5.7
const affichee = ligneAffichee(ligne(), format, { ...AUCUN, meilleureDuClassement: resumeDuMeilleur(charge) });
const bloc = meilleureProposition(charge);
if (bloc === null) {
assert.deepEqual(affichee.meilleure, { id: VIDE, collisions: VIDE });
assert.deepEqual(affichee.meilleure, { id: VIDE });
continue;
}
classees += 1;
assert.deepEqual(affichee.meilleure, { id: format.nombre(bloc.id), collisions: texteCellule(bloc.cellule, { t, format }) });
assert.deepEqual(affichee.meilleure, { id: format.nombre(bloc.id) });
}
assert.equal(classees, 2);
});

View file

@ -3,30 +3,26 @@
<script>
// La bande de mode (§ 8.4) : sous le bandeau, de bord à bord de la
// fenêtre, l'icône du mode, son libellé — celui du cadre et du
// bandeau — en gras, puis la phrase qui dit ce que le mode permet et la
// commande qui en sort (bandeDeMode, modele.js). Elle s'ajoute au cadre de
// mode, qui reste autour de la surface de travail, et ne le remplace pas.
// fenêtre, l'icône du mode, son libellé — celui du bandeau et de la barre
// de l'événement — en gras, puis la phrase qui dit ce que le mode permet
// et la commande qui en sort (bandeDeMode, modele.js). Avec ces deux
// libellés, elle porte seule le mode : aucun trait n'entoure la surface de
// travail.
//
// Son fond et son filet sont ceux du mode, son icône de la teinte du
// cadre, son texte de l'encre de la page (marques.js : bandeMode et les
// signes bandeLecture, bandeEcriture, bandeBloque). Région d'état, elle
// annonce le mode quand il change.
// Son fond et son filet sont ceux du mode, son icône de la teinte du mode
// (--couleur-cadre-…), son texte de l'encre de la page (marques.js :
// bandeMode et les signes bandeLecture, bandeEcriture, bandeBloque).
// Région d'état, elle annonce le mode quand il change.
//
// Sa marge haute négative vaut le retrait de l'écran (App.svelte) : la
// bande touche le bandeau. Ses marges latérales négatives valent la
// distance du contenu de la colonne au bord de la fenêtre : le retrait de
// l'écran tant que la fenêtre ne dépasse pas --largeur-page, ce retrait
// plus la moitié de l'excédent au-delà, où la colonne se centre. La bande
// va donc d'un bord à l'autre de la fenêtre à toute largeur, et son retrait
// intérieur, de même valeur, aligne son texte sur la colonne. 100vw égale
// la largeur utile de la fenêtre parce que l'écran de l'événement tient
// dans sa hauteur (Evenement.svelte) : aucune barre de défilement
// verticale n'en retranche, et la page ne défile pas en largeur. Elle tient en
// une ligne aux largeurs usuelles, de même hauteur dans les trois modes :
// changer de mode ne déplace pas la place fixe du cadre, sous elle. Mince
// — une ligne de petit corps —, elle prend au plan à peine plus que le
// retrait de l'écran qu'elle remplace.
// Ses marges négatives valent le retrait de l'écran (App.svelte), que
// l'écran de l'événement garde à toute largeur de fenêtre sans être borné
// par --largeur-page : la bande touche le bandeau et va d'un bord à
// l'autre de la fenêtre, et son retrait intérieur, de même valeur, aligne
// son texte sur l'écran. Elle tient en une ligne aux largeurs usuelles, de
// même hauteur dans les trois modes : changer de mode ne déplace pas la
// place fixe de la barre, sous elle. Mince — une ligne de petit corps —,
// elle prend au plan à peine plus que le retrait de l'écran qu'elle
// remplace.
import { getContext } from 'svelte';
import Icone from '../primitives/Icone.svelte';
import { bandeDeMode } from './modele.js';
@ -52,8 +48,8 @@
flex-wrap: wrap;
align-items: center;
gap: var(--espace-1) var(--espace-2);
margin: calc(-1 * var(--espace-4)) calc(-1 * max(var(--espace-4), (100vw - var(--largeur-page)) / 2 + var(--espace-4))) 0;
padding: 0.0625rem max(var(--espace-4), (100vw - var(--largeur-page)) / 2 + var(--espace-4));
margin: calc(-1 * var(--espace-4)) calc(-1 * var(--espace-4)) 0;
padding: 0.0625rem var(--espace-4);
border-bottom: 1px solid var(--couleur-mode-lecture-filet);
background-color: var(--couleur-mode-lecture-fond);
color: var(--couleur-page-texte);

View file

@ -2,33 +2,34 @@
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
<script>
// Le cadre de mode (§ 8.4, § 8.5, § 14.5) : un trait autour de la surface
// de travail, dont la forme dit le mode — tirets en lecture, plein en
// écriture, double sur un plan bloqué —, et une barre qui porte, de gauche
// à droite, la place fixe, le libellé de mode, puis « Défaire » et
// « Refaire » (§ 8.3), puis les outils, outils — la suite des états du
// plan —, et les actions, actions — « Imprimer… », calée à droite —, quand
// ils sont donnés. Outils et actions suivent la place fixe et ne la
// déplacent donc jamais : un refus posé à côté de la place fixe les
// repousse sur la ligne suivante, à côté du libellé, sans ligne de plus.
// Une barre de moins de 80rem — la fenêtre de 1 366 px lui en donne
// davantage — passe les outils sous les commandes et les actions, sur une
// ligne à eux : la barre garde deux lignes là où, dans l'ordre du
// document, elle en prendrait trois.
// La barre, la tête et la surface de travail de l'événement (§ 8.4, § 8.5,
// § 14.5), sans trait autour d'elles : le mode se lit dans la bande de
// mode (BandeMode.svelte), dans le libellé de mode du bandeau, et dans
// celui de la barre, à côté de la place fixe. data-mode porte le mode —
// lecture, ecriture ou bloque. La barre porte, de gauche à droite, la
// place fixe, le libellé de mode, puis « Défaire » et « Refaire » (§ 8.3),
// puis les outils, outils — la suite des états du plan —, et les actions,
// actions — « Imprimer… », calée à droite —, quand ils sont donnés.
// Outils et actions suivent la place fixe et ne la déplacent donc jamais :
// un refus posé à côté de la place fixe les repousse sur la ligne
// suivante, à côté du libellé, sans ligne de plus. Une barre de moins de
// 80rem — la fenêtre de 1 366 px lui en donne davantage — passe les outils
// sous les commandes et les actions, sur une ligne à eux : la barre garde
// deux lignes là où, dans l'ordre du document, elle en prendrait trois.
//
// Sous la barre vient la tête, tete, quand elle est donnée — l'en-tête de
// l'événement et la navigation des sections —, puis les annonces,
// annonces, quand il y en a — support rompu et avertissements —, puis la
// surface, children. La barre ouvre le cadre, et seule la bande de mode,
// de hauteur constante, le précède dans l'écran de l'événement : un refus
// posé dans la barre ou la tête, ou un nom qui s'étend sur plusieurs
// surface, children. La barre ouvre le composant, et seule la bande de
// mode, de hauteur constante, le précède dans l'écran de l'événement : un
// refus posé dans la barre ou la tête, ou un nom qui s'étend sur plusieurs
// lignes, allonge la barre ou la tête sans déplacer la place fixe
// (§ 8.4). Tête, annonces et surface défilent
// chacune dans sa zone : des avertissements nombreux ne poussent pas la
// surface hors du cadre, ni le cadre hors de la fenêtre. La surface porte
// le plan, que l'opérateur regarde : elle réclame les trois quarts du
// cadre avant que les annonces ne dépassent leur première ligne, sauf
// devant un support rompu, dont les issues restent en vue.
// (§ 8.4). Tête, annonces et surface défilent chacune dans sa zone : des
// avertissements nombreux ne poussent pas la surface hors du composant, ni
// le composant hors de la fenêtre. La surface porte le plan, que
// l'opérateur regarde : elle réclame les trois quarts du composant avant
// que les annonces ne dépassent leur première ligne, sauf devant un
// support rompu, dont les issues restent en vue.
//
// Sous une tête, et hors d'un support rompu, les annonces se posent à
// côté d'elle, dans la place que sa navigation laisse à droite. Leur zone
@ -69,26 +70,23 @@
//
// La barre et la tête forment la carte d'en-tête de l'événement : la
// barre sur le fond sourd de la carte, la tête sur son fond, séparées et
// closes par le filet de la carte. Le cadre, lui, reste sur le fond de la
// page, que son trait contraste (marques.js).
// closes par le filet de la carte. La surface reste sur le fond de la
// page.
import { getContext } from 'svelte';
import Commande from '../primitives/Commande.svelte';
import { CONFIRMATION_DEBLOCAGE } from '../primitives/commande.js';
import Refus from '../primitives/Refus.svelte';
import { cadreDeMode, commandeFixe, repriseOfferte } from './modele.js';
import { bandeDeMode, commandeFixe, repriseOfferte } from './modele.js';
let { children, tete = null, annonces = null, outils = null, actions = null } = $props();
const t = getContext('t');
const seance = getContext('seance');
// Sans événement — l'instant où l'écran se retire —, le cadre garde la
// forme de la lecture, et n'a pas de libellé.
const cadre = $derived(cadreDeMode($seance));
const forme = $derived(cadre?.forme ?? 'tirets');
// Le trait du cadre est une marque (§ 14.5) : data-marque nomme l'entrée
// du registre (marques.js) qui porte le jeton de sa couleur.
const MARQUE_DE_LA_FORME = { tirets: 'cadreLecture', plein: 'cadreEcriture', double: 'cadreBloque' };
// Le mode et la clé de son libellé, ceux de la bande. Sans événement —
// l'instant où l'écran se retire —, le mode est la lecture, sans libellé.
const libelle = $derived(bandeDeMode($seance));
const mode = $derived(libelle?.mode ?? 'lecture');
const commande = $derived(commandeFixe($seance));
const verrou = $derived($seance.mode === 'lecture' ? $seance.verrouAutre : null);
const reprise = $derived(repriseOfferte($seance));
@ -96,7 +94,7 @@
const aCote = $derived(tete !== null && annonces !== null && $seance.supportRompu === null);
</script>
<div class="cadre" data-mode={forme} data-marque={MARQUE_DE_LA_FORME[forme]} class:tirets={forme === 'tirets'} class:double={forme === 'double'}>
<div class="corps" data-mode={mode}>
<div class="barre" role="group" aria-label={t('evenement.cadre.commandes')}>
<div class="ligne">
<div class="fixe" data-place-fixe>
@ -115,10 +113,10 @@
<Commande cle="communs.commande.lecture" nom="lecture" executer={() => seance.passerEnLecture()} />
{/if}
</div>
{#if cadre !== null}
{#if libelle !== null}
<span class="porte-libelle">
<span class="libelle" class:plein={forme === 'plein'} class:double={forme === 'double'} data-mode-libelle="cadre"
>{t(cadre.cle)}</span
<span class="libelle" class:ecriture={mode === 'ecriture'} class:bloque={mode === 'bloque'} data-mode-libelle="cadre"
>{t(libelle.cle)}</span
>
</span>
{/if}
@ -187,29 +185,16 @@
</div>
<style>
/* Le trait du cadre : sa forme dit le mode, sa couleur le redit. */
/* Une même épaisseur dans les trois formes : la place fixe ne bouge pas
d'un mode à l'autre. */
.cadre {
/* Sans trait : rien autour de la surface ne change d'un mode à l'autre,
et la place fixe ne bouge pas. */
.corps {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
border: 6px solid var(--couleur-cadre-ecriture);
border-radius: var(--rayon-2);
}
.cadre.tirets {
border-style: dashed;
border-color: var(--couleur-cadre-lecture);
}
.cadre.double {
border-style: double;
border-color: var(--couleur-cadre-bloque);
}
/* La barre ne prend jamais toute la hauteur du cadre : au-delà, ses
/* La barre ne prend jamais toute la hauteur du composant : au-delà, ses
messages défilent, la place fixe en tête. */
.barre {
display: flex;
@ -284,12 +269,12 @@
font-weight: bold;
}
.libelle.plein {
.libelle.ecriture {
border-color: var(--couleur-mode-ecriture-filet);
background-color: var(--couleur-mode-ecriture-fond);
}
.libelle.double {
.libelle.bloque {
border-color: var(--couleur-mode-bloque-filet);
background-color: var(--couleur-mode-bloque-fond);
}
@ -320,7 +305,7 @@
font-weight: bold;
}
/* La tête ne prend jamais plus de 35 % du cadre : au-delà, elle défile,
/* La tête ne prend jamais plus de 35 % du composant : au-delà, elle défile,
et la surface garde sa place. Elle n'a pas de retrait en bas : le trait
des onglets de la navigation se pose sur son filet. */
.tete {
@ -397,7 +382,7 @@
retrait compris : 7,25rem tiennent entier, dans la place de 36 % d'une
fenêtre de 1 366 px, un avertissement de trois lignes de texte et son
action dessous (Avertissements.svelte). Au-delà, la zone défile, et la
tête, qui ne grandit pas plus, laisse le reste du cadre à la surface. */
tête, qui ne grandit pas plus, laisse le reste du composant à la surface. */
.place > .annonces {
box-sizing: border-box;
max-height: 7.25rem;
@ -416,7 +401,7 @@
}
/* Seules — sans tête, en plein écran, ou devant un support rompu —, les
annonces ne prennent jamais plus du quart du cadre, et cèdent d'abord
annonces ne prennent jamais plus du quart du composant, et cèdent d'abord
leur hauteur à la surface : leur facteur de rétrécissement, mille fois
celui de la surface, leur fait absorber presque tout le manque de
place, jusqu'à leur hauteur minimale : un avertissement d'une ligne en
@ -430,13 +415,13 @@
/* Le support rompu, en tête des annonces, porte les seules issues de
l'opérateur : tant qu'il est là, les annonces ne cèdent rien à la
surface, et gardent leur hauteur jusqu'au quart du cadre ; les
surface, et gardent leur hauteur jusqu'au quart du composant ; les
avertissements qui le suivent défilent sous lui. */
.annonces.rompu {
flex-shrink: 0;
}
/* La surface part des trois quarts du cadre, s'étend à ce qui reste, et
/* La surface part des trois quarts du composant, s'étend à ce qui reste, et
ne rétrécit, après les annonces, que pour la barre et la tête. Sa
hauteur ne dépend pas de son contenu : elle est un conteneur de taille,
dont une section borne un panneau à la hauteur visible (unités cqb). */

View file

@ -2,8 +2,8 @@
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
<script>
// L'en-tête de l'événement ouvert, en tête de la tête du cadre : son nom,
// puis, sur la même ligne tant qu'elle a la place, son résumé — la date
// L'en-tête de l'événement ouvert, en tête de la tête (CadreMode.svelte) :
// son nom, puis, sur la même ligne tant qu'elle a la place, son résumé — la date
// quand il en a une, les participants, les tables et leurs sièges, les
// tours (resumeEvenement, modele.js) —, d'encre secondaire. Le nom se
// coupe où il le faut, même sans espace : un nom long de glyphes larges

View file

@ -146,11 +146,11 @@ async function enManqueDe(m, manque) {
// --- Les épreuves -----------------------------------------------------------------
describe('la bande de mode (§ 8.4)', () => {
// 1 920 px dépasse --largeur-page : la colonne de la page n'y touche plus
// les bords de la fenêtre, la bande si.
describe('la bande de mode et la pleine fenêtre (§ 8.4)', () => {
// 1 920 px dépasse --largeur-page, qui borne la liste des événements :
// l'écran de l'événement n'y est pas borné.
for (const largeur of [1366, 1920]) {
test(`à ${largeur} px, sous le bandeau, de bord à bord de la fenêtre sans défilement en largeur, avant le cadre : l’icône, le libellé du cadre et son explication, aux couleurs de chaque mode, à la même hauteur`, async () => {
test(`à ${largeur} px : la bande de bord à bord sous le bandeau, aux couleurs de chaque mode, à hauteur constante ; l’écran en pleine fenêtre, sans trait`, async () => {
await fixerCadre(largeur, 700);
const m = await neuf();
const hauteurs = [];
@ -163,6 +163,20 @@ describe('la bande de mode (§ 8.4)', () => {
expect([b.left, b.right], mode).toEqual([0, innerWidth]);
expect(document.documentElement.scrollWidth, mode).toBeLessThanOrEqual(innerWidth);
expect(b.bottom, mode).toBeLessThanOrEqual(boite(cadre(m)).top);
// L'écran va d'un bord à l'autre de la fenêtre, au retrait de
// l'écran près, et la barre, la tête et la surface avec lui, sans
// trait autour d'elles.
const retrait = Number.parseFloat(style(ecran(m).parentElement).paddingLeft);
expect(retrait, mode).toBeGreaterThan(0);
expect(retrait, mode).toBeLessThanOrEqual(32);
for (const element of [ecran(m), cadre(m)]) {
const e = boite(element);
expect(e.left, mode).toBeCloseTo(retrait, 0);
expect(e.right, mode).toBeCloseTo(innerWidth - retrait, 0);
}
for (const cote of ['Top', 'Right', 'Bottom', 'Left']) {
expect(Number.parseFloat(style(cadre(m))[`border${cote}Width`]), `${mode} ${cote}`).toBe(0);
}
expect(region.textContent, mode).toContain(t(cle));
expect(region.textContent, mode).toContain(t(explication));
expect(region.textContent, mode).toContain(dans(m, '[data-mode-libelle="cadre"]').textContent);

View file

@ -2,14 +2,13 @@
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
<script>
// La suite des états du plan (§ 9), dans la barre du cadre : sous le nom
// « État du plan », que redit un intertitre en petites capitales tant que
// la barre a la place, les
// quatre états dans l'ordre du § 9 (etapesEtat, modele.js), séparés de
// chevrons cachés à l'assistance. Chaque étape est la Commande
// changerEtat de son état, restylée en pilule, avec l'icône de sa pastille
// d'état posée sur le bouton, à gauche de son nom, de la marque de sa
// teinte. L'état courant porte en plus le fond de sa teinte, un filet
// La suite des états du plan (§ 9), dans la barre de l'événement : sous
// le nom « État du plan », que redit un intertitre en petites capitales
// tant que la barre a la place, les quatre états dans l'ordre du § 9
// (etapesEtat, modele.js), séparés de chevrons cachés à l'assistance.
// Chaque étape est la Commande changerEtat de son état, restylée en
// pilule, avec l'icône de sa pastille d'état posée sur le bouton, à
// gauche de son nom, de la marque de sa teinte. L'état courant porte en plus le fond de sa teinte, un filet
// plein et le gras ; les autres, un filet tireté, et restent cliquables
// sous le même nom.
//

View file

@ -4,7 +4,8 @@
// L'événement ouvert dans un vrai moteur de rendu (§ 2.1, § 8.3 à § 8.9,
// § 9, § 14.5) : aucune section n'en exige une autre ; la place fixe de
// « Modifier », de « Débloquer » et de « Revenir en lecture », plein écran
// compris ; le cadre de mode et son libellé ; l'inactivité ; l'état bloqué
// compris ; la surface de travail sans cadre, le mode porté par la bande et
// le libellé ; l'inactivité ; l'état bloqué
// et son déblocage confirmé en place ; les états du plan ; défaire et
// refaire ; les annonces d'ouverture, du journal et de la dérive ; le verrou
// d'une autre séance et sa reprise ; le support rompu ; le format plus
@ -13,7 +14,7 @@
// qu'atteint un point, le style calculé, le texte — jamais une classe.
//
// Les éléments s'atteignent par leurs attributs data-… : data-place-fixe, la
// place fixe du cadre ; data-etat, un état du plan de l'en-tête ;
// place fixe de la barre ; data-etat, un état du plan de l'en-tête ;
// data-verrou, le verrou d'une autre séance ; data-support-rompu, le message
// du support rompu. Le texte attendu vient de la même traduction que celle
// de l'application ; les noms d'épreuve sont inventés.
@ -67,6 +68,7 @@ afterEach(() => {
const dans = (m, selecteur) => m.racine.querySelector(selecteur);
const tous = (m, selecteur) => [...m.racine.querySelectorAll(selecteur)];
const cadre = (m) => dans(m, '[data-mode]');
const bande = (m) => dans(m, '[data-ecran="evenement"] > [role="status"]');
const fixe = (m, nom) => dans(m, `[data-place-fixe] [data-commande="${nom}"]`);
const historique = (m, nom) => dans(m, `[data-mode] [data-historique] [data-commande="${nom}"]`);
const boutonEtat = (m, etat) => dans(m, `[data-etat="${etat}"] [data-commande="etat"]`);
@ -327,39 +329,39 @@ describe('passer en écriture (§ 8.4, § 19.3 01b)', () => {
});
});
describe('le cadre de mode (§ 8.4, § 14.5)', () => {
test('il contient la surface, sa bordure n’est pas nulle, son libellé se voit ; texte et trait diffèrent en lecture, en écriture et bloqué ; même boîte après défilement', async () => {
describe('le mode sans cadre (§ 8.4, § 14.5)', () => {
test('aucun trait autour de la surface ; le mode se lit dans la bande, le libellé de la barre et le bandeau, distincts en lecture, en écriture et bloqué ; même boîte après défilement', async () => {
const m = await neuf();
const releves = [];
const relever = () => {
const relever = (mode) => {
flushSync();
const boite = cadre(m).getBoundingClientRect();
const surface = dans(m, '[data-section]').getBoundingClientRect();
expect(boite.left).toBeLessThanOrEqual(surface.left);
expect(boite.top).toBeLessThanOrEqual(surface.top);
expect(boite.right).toBeGreaterThanOrEqual(surface.right);
expect(boite.bottom).toBeGreaterThanOrEqual(surface.bottom);
expect(boite.left, mode).toBeLessThanOrEqual(surface.left);
expect(boite.top, mode).toBeLessThanOrEqual(surface.top);
expect(boite.right, mode).toBeGreaterThanOrEqual(surface.right);
expect(boite.bottom, mode).toBeGreaterThanOrEqual(surface.bottom);
expect(cadre(m).dataset.mode, mode).toBe(mode);
expect(cadre(m).hasAttribute('data-marque'), mode).toBe(false);
const style = getComputedStyle(cadre(m));
expect(Number.parseFloat(style.borderTopWidth)).toBeGreaterThan(0);
expect(style.borderTopStyle).not.toBe('none');
for (const cote of ['Top', 'Right', 'Bottom', 'Left']) {
expect(Number.parseFloat(style[`border${cote}Width`]), `${mode} ${cote}`).toBe(0);
}
const libelle = libelleCadre(m);
expect(libelle.checkVisibility()).toBe(true);
expect(visible(libelle)).toBe(true);
expect(libelle.contains(auCentre(libelle))).toBe(true);
expect(libelle.textContent).toBe(libelleBandeau(m).textContent);
releves.push({ texte: libelle.textContent, trait: style.borderTopStyle });
expect(visible(bande(m)), mode).toBe(true);
expect(bande(m).textContent, mode).toContain(libelle.textContent);
releves.push(libelle.textContent);
};
relever();
relever('lecture');
await m.seance.passerEnEcriture();
relever();
relever('ecriture');
await m.seance.executer('changerEtat', { etat: 'bloque' });
relever();
expect(releves.map(({ texte }) => texte)).toEqual([
t('coquille.mode.lecture'),
t('coquille.mode.ecriture'),
t('coquille.mode.bloque'),
]);
expect(new Set(releves.map(({ trait }) => trait)).size).toBe(3);
relever('bloque');
expect(releves).toEqual([t('coquille.mode.lecture'), t('coquille.mode.ecriture'), t('coquille.mode.bloque')]);
// Une surface plus haute que la fenêtre : la page et la surface
// défilées jusqu'en bas, le libellé garde sa boîte.
const avant = libelleCadre(m).getBoundingClientRect().toJSON();

View file

@ -3,18 +3,18 @@
<script>
// L'événement ouvert (§ 2.1, § 8.4) : la bande de mode, sous le bandeau,
// puis le cadre de mode, qui entoure la surface de travail de toute
// section. Sa barre porte la place fixe, le libellé, Défaire et Refaire,
// puis les outils — la suite des états du plan — et les actions —
// « Imprimer… », l'action principale de l'en-tête. Sous la barre, la tête
// de l'événement : son en-tête — nom et résumé — et la navigation des
// sections, dans l'ordre du § 2.1. La barre et la tête forment la carte
// d'en-tête. Seule la bande précède le cadre dans l'écran, à hauteur
// constante : ce que la tête montre ne déplace pas la place fixe. Aucune
// section n'en exige une autre : la navigation les offre toutes, et la
// séance refuse elle-même ce qui ne peut pas se faire. Le plein écran du
// plan efface les outils, les actions et la tête, jamais la bande, le
// cadre ni sa place fixe.
// puis la barre, la tête et la surface de travail de toute section
// (CadreMode.svelte), sans trait autour d'elles. La barre porte la place
// fixe, le libellé de mode, Défaire et Refaire, puis les outils — la
// suite des états du plan — et les actions — « Imprimer… », l'action
// principale de l'en-tête. Sous la barre, la tête de l'événement : son
// en-tête — nom et résumé — et la navigation des sections, dans l'ordre
// du § 2.1. La barre et la tête forment la carte d'en-tête. Seule la
// bande précède la barre dans l'écran, à hauteur constante : ce que la
// tête montre ne déplace pas la place fixe. Aucune section n'en exige une
// autre : la navigation les offre toutes, et la séance refuse elle-même
// ce qui ne peut pas se faire. Le plein écran du plan efface les outils,
// les actions et la tête, jamais la bande, la barre ni sa place fixe.
//
// « Imprimer… » ouvre l'aperçu avant impression (§ 11.7) sur le plan, au
// tour montré : toute impression passe par lui. Vivante dans tout mode,
@ -29,18 +29,21 @@
// de l'action.
//
// L'écran de l'événement tient dans la fenêtre, sous le bandeau : la page
// ne défile pas, la surface de travail défile dans le cadre, et le cadre,
// son libellé et sa place fixe restent où ils sont (§ 8.4). La règle
// globale ci-dessous donne pour cela une hauteur à l'ascendance de l'écran
// — le conteneur de l'application et son <main> — tant qu'il porte cet
// écran, et à lui seul.
// ne défile pas, la surface de travail défile dans sa zone, et la barre,
// son libellé et sa place fixe restent où ils sont (§ 8.4). Il prend
// aussi toute la largeur de la fenêtre, au retrait de l'écran près, sans
// la borne --largeur-page de la liste des événements. Les règles
// globales ci-dessous donnent pour cela une hauteur à l'ascendance de
// l'écran — le conteneur de l'application et son <main> — et retirent la
// borne de largeur de son <main>, tant qu'il porte cet écran, et à lui
// seul.
//
// Le support rompu et les avertissements se posent dans la zone des
// annonces du cadre, qui défile seule au-delà de son plafond, et non
// au-dessus de la section : la section ne cède à la zone que ce dont elle
// dépasse la tête, quel que soit le nombre des avertissements, et le
// support rompu, en tête de la zone, garde ses issues en vue
// (CadreMode.svelte).
// annonces, à côté de la tête ou sous elle, qui défile seule au-delà de
// son plafond, et non au-dessus de la section : la section ne cède à la
// zone que ce dont elle dépasse la tête, quel que soit le nombre des
// avertissements, et le support rompu, en tête de la zone, garde ses
// issues en vue (CadreMode.svelte).
import { getContext } from 'svelte';
import SectionBadges from '../badges/SectionBadges.svelte';
import Diagnostic from '../diagnostic/Diagnostic.svelte';
@ -79,7 +82,7 @@
const Section = $derived(COMPOSANTS.get($ui.section));
// La zone des annonces n'existe que lorsqu'elle a quelque chose à dire :
// sans annonce, le cadre ne lui garde ni hauteur ni trait.
// sans annonce, aucune hauteur ni aucun trait ne lui est gardé.
const annoncer = $derived($seance.supportRompu !== null || avertissementsOrdonnes($seance).length > 0);
</script>
@ -155,9 +158,11 @@
min-height: 0;
}
/* La classe de l'écran (App.svelte) porte la spécificité de son retrait,
que cette règle doit dépasser. */
/* La classe de l'écran (App.svelte) porte la spécificité de son retrait
et de sa borne de largeur, que cette règle doit dépasser : l'écran de
l'événement prend la largeur de la fenêtre. */
:global(main.ecran:has(> [data-ecran='evenement'])) {
max-width: none;
padding-bottom: var(--espace-2);
}

View file

@ -2,8 +2,8 @@
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Le modèle de l'événement ouvert, pur : ce que ses composants montrent de
// l'état observable de la séance (§ 8.4, § 8.5, § 8.8, § 8.9, § 9). Le cadre
// de mode et sa forme, la commande de la place fixe, les états du plan et
// l'état observable de la séance (§ 8.4, § 8.5, § 8.8, § 8.9, § 9). La
// commande de la place fixe, les états du plan et
// leurs clés, la reprise de verrou que l'écran offre, et les avertissements
// dans l'ordre de la séance, chacun avec son remède ; puis ce que montre
// l'enveloppe de l'événement : le résumé chiffré de son en-tête, les étapes
@ -14,29 +14,6 @@ import { cleDeMode } from '../mode.js';
import { modelePastille } from '../primitives/pastille.js';
import { etatTable } from '../tables/modele.js';
// La forme du cadre pour chaque clé du libellé de mode : un trait différent
// par mode, pour qu'aucun ne se distingue par la seule couleur (§ 14.5).
const FORMES = new Map([
['coquille.mode.lecture', 'tirets'],
['coquille.mode.ecriture', 'plein'],
['coquille.mode.bloque', 'double'],
]);
/**
* Libellé et forme du cadre de mode : cle, celle de cleDeMode (mode.js) — le
* libellé du bandeau — ; forme : lecture (tirets), écriture (trait plein),
* bloqué (trait double), quel que soit le mode d'un plan bloqué. null sans
* événement.
*
* @param {Object} etatSeance EtatSeance
* @returns {null|{cle: string, forme: 'tirets'|'plein'|'double'}}
*/
export function cadreDeMode(etatSeance) {
const cle = cleDeMode(etatSeance);
if (cle === null) return null;
return { cle, forme: FORMES.get(cle) };
}
/**
* La commande de la place fixe (§ 8.4, § 8.5, § 8.8) : 'format' quand le
* refus courant est celui d'un format plus récent — l'explication remplace
@ -84,18 +61,20 @@ export function etapesEtat(etatSeance) {
return ETATS_PLAN.map((etat) => ({ etat, cle: cleEtat(etat), icone: modelePastille('etat', etat, null).icone, courant: etat === courant }));
}
// Le mode de la bande, son explication et son icône, pour chaque forme du
// cadre : la bande dit ce que le cadre dit.
// Le mode de la bande, son explication et son icône, pour chaque clé du
// libellé de mode : une icône et une phrase différentes par mode, pour
// qu'aucun ne se distingue par la seule couleur (§ 14.5).
const BANDES = new Map([
['tirets', { mode: 'lecture', explication: 'evenement.bande.lecture', icone: 'oeil' }],
['plein', { mode: 'ecriture', explication: 'evenement.bande.ecriture', icone: 'stylo' }],
['double', { mode: 'bloque', explication: 'evenement.bande.bloque', icone: 'cadenas' }],
['coquille.mode.lecture', { mode: 'lecture', explication: 'evenement.bande.lecture', icone: 'oeil' }],
['coquille.mode.ecriture', { mode: 'ecriture', explication: 'evenement.bande.ecriture', icone: 'stylo' }],
['coquille.mode.bloque', { mode: 'bloque', explication: 'evenement.bande.bloque', icone: 'cadenas' }],
]);
/**
* La bande de mode, sous le bandeau (§ 8.4) : mode, 'lecture', 'ecriture' ou
* 'bloque' — un plan bloqué l'est quel que soit le mode — ; cle, le libellé
* du cadre (cadreDeMode) ; explication, la clé de la phrase qui le suit ;
* 'bloque' — un plan bloqué l'est quel que soit le mode — ; cle, celle du
* libellé de mode (cleDeMode, mode.js), que le bandeau et la barre de
* l'événement montrent aussi ; explication, la clé de la phrase qui le suit ;
* icone, celle des primitives qui le précède. La phrase nomme la commande
* de la place fixe (commandeFixe) ; devant un format plus récent, qui n'en
* offre aucune, plan bloqué compris, elle est evenement.bande.lectureSeule,
@ -105,11 +84,11 @@ const BANDES = new Map([
* @returns {null|{mode: 'lecture'|'ecriture'|'bloque', cle: string, explication: string, icone: string}}
*/
export function bandeDeMode(etatSeance) {
const cadre = cadreDeMode(etatSeance);
if (cadre === null) return null;
const { mode, explication, icone } = BANDES.get(cadre.forme);
const cle = cleDeMode(etatSeance);
if (cle === null) return null;
const { mode, explication, icone } = BANDES.get(cle);
const phrase = commandeFixe(etatSeance) === 'format' ? 'evenement.bande.lectureSeule' : explication;
return { mode, cle: cadre.cle, explication: phrase, icone };
return { mode, cle, explication: phrase, icone };
}
/**

View file

@ -1,8 +1,8 @@
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Le modèle de l'événement ouvert (§ 8.4, § 8.5, § 8.8, § 8.9, § 9) : le cadre
// de mode et sa forme, la commande de la place fixe, les états du plan et
// Le modèle de l'événement ouvert (§ 8.4, § 8.5, § 8.8, § 8.9, § 9) : la
// commande de la place fixe, les états du plan et
// leurs clés, la reprise de verrou offerte, et les avertissements ordonnés,
// chacun avec son remède ; puis l'enveloppe : le résumé chiffré de
// l'en-tête et ses textes, les étapes des états, la bande de mode. Chaque
@ -13,11 +13,11 @@ import { describe, test } from '../../../test/lanceur.js';
import { TABLES_APPLICATION } from '../../application/libelles.js';
import { creerTraduction } from '../../application/traduction.js';
import { TABLES_INTERFACE } from '../libelles.js';
import { cleDeMode } from '../mode.js';
import {
ETATS_PLAN,
avertissementsOrdonnes,
bandeDeMode,
cadreDeMode,
cleEtat,
commandeFixe,
etapesEtat,
@ -53,25 +53,6 @@ function etat({
};
}
describe('cadreDeMode (§ 8.4, § 14.5)', () => {
test('null sans événement', () => {
assert.equal(cadreDeMode(etat({ plan: null })), null);
});
test('lecture : tirets ; écriture : trait plein ; bloqué : trait double, quel que soit le mode', () => {
assert.deepEqual(cadreDeMode(etat({ mode: 'lecture' })), { cle: 'coquille.mode.lecture', forme: 'tirets' });
assert.deepEqual(cadreDeMode(etat({ mode: 'ecriture' })), { cle: 'coquille.mode.ecriture', forme: 'plein' });
assert.deepEqual(cadreDeMode(etat({ mode: 'lecture', plan: 'bloque' })), { cle: 'coquille.mode.bloque', forme: 'double' });
assert.deepEqual(cadreDeMode(etat({ mode: 'ecriture', plan: 'bloque' })), { cle: 'coquille.mode.bloque', forme: 'double' });
});
test('trois formes distinctes pour trois libellés distincts, chaque libellé traduit', () => {
const cadres = [etat(), etat({ mode: 'ecriture' }), etat({ plan: 'bloque' })].map(cadreDeMode);
assert.equal(new Set(cadres.map(({ forme }) => forme)).size, 3);
assert.equal(new Set(cadres.map(({ cle }) => t(cle))).size, 3);
});
});
describe('commandeFixe (§ 8.4, § 8.5, § 8.8)', () => {
test('null sans événement ; modifier en lecture ; lecture en écriture ; debloquer sur un plan bloqué', () => {
assert.equal(commandeFixe(etat({ plan: null })), null);
@ -316,7 +297,7 @@ describe('bandeDeMode : la bande sous le bandeau', () => {
assert.equal(bandeDeMode(etat({ plan: null })), null);
});
test('lecture, écriture, plan bloqué — quel que soit le mode : le libellé du cadre, une explication, une icône', () => {
test('lecture, écriture, plan bloqué — quel que soit le mode : le libellé de mode, une explication, une icône', () => {
assert.deepEqual(bandeDeMode(etat({ mode: 'lecture' })), {
mode: 'lecture',
cle: 'coquille.mode.lecture',
@ -339,9 +320,10 @@ describe('bandeDeMode : la bande sous le bandeau', () => {
}
});
test('le libellé est celui du cadre ; trois explications traduites, distinctes, qui citent la commande qui en sort', () => {
test('le libellé est celui du bandeau ; trois libellés et trois explications traduits, distincts ; chaque explication cite la commande qui en sort', () => {
const etats = [etat(), etat({ mode: 'ecriture' }), etat({ plan: 'bloque' })];
for (const e of etats) assert.equal(bandeDeMode(e).cle, cadreDeMode(e).cle);
for (const e of etats) assert.equal(bandeDeMode(e).cle, cleDeMode(e));
assert.equal(new Set(etats.map((e) => t(bandeDeMode(e).cle))).size, 3);
const explications = etats.map((e) => t(bandeDeMode(e).explication));
for (const texte of explications) assert.notEqual(texte.trim(), '');
assert.equal(new Set(explications).size, 3);
@ -349,7 +331,7 @@ describe('bandeDeMode : la bande sous le bandeau', () => {
explications.forEach((texte, rang) => assert.ok(texte.includes(`« ${t(commandes[rang])} »`), texte));
});
test('un format plus récent, plan bloqué ou non : le mode du cadre, et une explication qui ne cite aucune commande', () => {
test('un format plus récent, plan bloqué ou non : son mode, et une explication qui ne cite aucune commande', () => {
const attendus = [
[etat({ refus: FORMAT_REFUS }), { mode: 'lecture', cle: 'coquille.mode.lecture', icone: 'oeil' }],
[etat({ plan: 'bloque', refus: FORMAT_REFUS }), { mode: 'bloque', cle: 'coquille.mode.bloque', icone: 'cadenas' }],

View file

@ -40,7 +40,7 @@
import { estBrouillon, piedDePage, refusImpression } from '../../application/sorties.js';
import { constante } from '../../geometrie/constantes.js';
import { DECOUPES, VUES_IMPRESSION } from '../etat_ui.js';
import { cadreDeMode } from '../evenement/modele.js';
import { cleDeMode } from '../mode.js';
import Commande from '../primitives/Commande.svelte';
import Papier from '../sorties/Papier.svelte';
import FeuillesTables from './FeuillesTables.svelte';
@ -74,7 +74,7 @@
);
const feuille = $derived(feuilleDe(pied, estBrouillon(charge, { source: sortie.source, tour: sortie.tour })));
const Vue = $derived(VUES.get(options.vue));
const cadre = $derived(cadreDeMode($seance));
const cleMode = $derived(cleDeMode($seance));
const tours = $derived(toursOfferts(charge.evenement.tours));
// La bande mesurée sur le gabarit, null avant la première mesure ; ce qui
@ -113,8 +113,8 @@
<div class="barre" role="group" aria-label={t('impression.barre')}>
<div class="tete">
<h2>{t('impression.titre')}</h2>
{#if cadre !== null}
<span class="mode" data-mode-libelle="cadre">{t(cadre.cle)}</span>
{#if cleMode !== null}
<span class="mode" data-mode-libelle="cadre">{t(cleMode)}</span>
{/if}
</div>
<div class="groupe vues" role="group" aria-label={t('impression.vue.titre')}>

View file

@ -70,8 +70,8 @@
lignes d'un tableau — ; l'action principale — son fond, son texte, sa
teinte claire et son encre, celle d'un lien — ; les quatre états du plan,
le neutre et les appartenances, chacun un fond teinté et une marque, que
lisent les pastilles ; la bande de mode, un fond et un filet par mode, sa
marque étant la teinte du cadre de mode ; les messages — avis, refus,
lisent les pastilles ; la bande de mode, un fond, un filet et une
teinte par mode, celle de son icône ; les messages — avis, refus,
information —, chacun son encre, un fond, un filet et une marque.
Le texte d'une pastille ou de la bande de mode garde l'encre de la page
@ -203,8 +203,8 @@ textarea {
--couleur-appartenance-6-fond: #f3f0fc;
--couleur-appartenance-6-marque: #8f6dd5;
/* Cadre de mode (§ 8.4), et bande de mode : fond et filet ; sa marque est
la teinte du cadre. */
/* Bande de mode (§ 8.4) : la teinte de chaque mode, que porte son icône,
puis son fond et son filet. */
--couleur-cadre-lecture: #5c6878;
--couleur-cadre-ecriture: #2e7d32;
--couleur-cadre-bloque: #7b3fa0;

View file

@ -74,18 +74,10 @@ export const TABLE = new Map([
['accueil.colonne.participants', 'Participants'],
['accueil.colonne.modification', 'Dernière modification'],
['accueil.colonne.chemin', 'Fichier'],
// Les deux mesures du placement retenu (§ 5.4, § 8.9), et l'explication
// que l'infobulle de leur intitulé donne : ce que compte le nombre, d'où
// il vient, et quand il vaut « — ».
['accueil.colonne.collisionsCumulees', 'Collisions cumulées (paire et tour)'],
// La mesure du placement retenu (§ 5.4, § 8.9), et l'explication que
// l'infobulle de son intitulé donne : ce que compte le nombre, d'où il
// vient, et quand il vaut « — ».
['accueil.colonne.diffRedondance', "Diff somme d'appartenance"],
[
'accueil.explication.collisionsCumulees',
'Celles du placement retenu : chaque fois que deux personnes de la même appartenance partagent une table, ' +
'compté une fois par paire et par tour ; deux collègues ensemble aux quatre tours font 4. Le même nombre que la ' +
"colonne « Collisions cumulées » du tableau des propositions. Plus il est bas, mieux c'est. " +
"« \u{2014} » quand aucun placement n'est retenu, ou que le retenu ne décrit plus la configuration.",
],
[
'accueil.explication.diffRedondance',
'|cible \u{2212} Σ r(p)| : la différence, sans signe, entre la cible de la soirée et la somme Σ r(p) du placement ' +
@ -96,13 +88,13 @@ export const TABLE = new Map([
],
// La meilleure proposition du classement de chaque événement (§ 5.4,
// § 5.7) : son titre, l'identifiant et les collisions s'écrivent sous les
// libellés du bloc de la génération ; l'explication du titre est propre à
// la liste, qui l'écrit « — » quand rien n'est classé.
// § 5.7) : son titre et son identifiant s'écrivent sous les libellés du
// bloc de la génération ; l'explication du titre est propre à la liste,
// qui l'écrit « — » quand rien n'est classé.
[
'accueil.explication.meilleure',
"La proposition au rang 1 du classement de l'événement, celle que la section de la génération annonce en tête : " +
'son identifiant et ses collisions cumulées. Le classement range les propositions comparables, retenue comprise ; ' +
'son identifiant. Le classement range les propositions comparables, retenue comprise ; ' +
'le placement retenu se lit à part, sous ses propres intitulés. ' +
"« \u{2014} » quand aucune proposition n'est classée : aucune proposition, ou aucune qui décrive encore la " +
'configuration.',

View file

@ -2,7 +2,7 @@
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Les libellés de l'événement ouvert : son en-tête et son résumé chiffré,
// ses états, la bande et la place fixe de son cadre de mode, les phases de
// ses états, la bande de mode et la place fixe de sa barre, les phases de
// sa navigation, la reprise d'un verrou, ses avertissements, le support
// rompu (§ 8.3 à § 8.9, § 9, § 14.6). Chaque clé commence par
// « evenement. ». Une donnée qui désigne — un numéro, une séance, un
@ -53,7 +53,7 @@ export const TABLE = new Map([
['evenement.phase.placer', 'Placer'],
['evenement.phase.produire', 'Produire'],
// --- Le cadre de mode et sa place fixe (§ 8.3, § 8.4, § 8.5) -------------
// --- La barre de l'événement et sa place fixe (§ 8.3, § 8.4, § 8.5) ------
['evenement.cadre.commandes', 'Commandes de l\u{2019}événement'],
['evenement.commande.defaire', 'Défaire'],
['evenement.commande.refaire', 'Refaire'],

View file

@ -94,8 +94,8 @@ const TEINTES_DE_PASTILLE = [
/** Les signes, même forme que MARQUES, seuil 3 : l'icône ou le point d'une
* pastille sur son fond teinté et sur les surfaces de contenu ; l'icône
* d'un message sur son fond ; l'icône de la bande de mode, de la teinte du
* cadre de mode, sur le fond de la bande ; le fond de l'action principale,
* d'un message sur son fond ; l'icône de la bande de mode, de la teinte de
* son mode, sur le fond de la bande ; le fond de l'action principale,
* qui délimite le bouton, sur les surfaces. */
export const SIGNES = Object.freeze([
...TEINTES_DE_PASTILLE.map(([nom, teinte]) => marque(nom, `${teinte}-marque`, [`${teinte}-fond`, ...SURFACES])),
@ -134,10 +134,6 @@ export const MARQUES = Object.freeze([
// La sélection marque aussi une colonne du tableau des propositions, sur la
// carte, son fond sourd et la ligne du retenu.
marque('selection', 'selection', ['page-fond', 'carte-fond', 'carte-fond-sourd', 'etat-retenu-fond', 'sol', 'table-fond', ...REMPLISSAGES_CHAISE]),
// Le cadre de mode (§ 8.4) entoure le contenu de l'événement, sur la page.
marque('cadreLecture', 'cadre-lecture', ['page-fond']),
marque('cadreEcriture', 'cadre-ecriture', ['page-fond']),
marque('cadreBloque', 'cadre-bloque', ['page-fond']),
marque('focus', 'focus', ['page-fond', 'bandeau-fond', 'carte-fond', 'carte-fond-sourd', 'etat-retenu-fond', 'action-teinte', 'sol', 'table-fond', ...REMPLISSAGES_CHAISE]),
// Le mobilier sur le sol : le trait de la table, celui de la chaise, qui
// touche aussi le plateau.

View file

@ -17,7 +17,7 @@ const THEMES = ['clair', 'sombre'];
const NOMS_MARQUES = [
'cadenasFerme', 'cadenasOuvert', 'anneau', 'hachure', 'remplissagePlein',
'contourConflit', 'trameAppartenance', 'trameVoisins', 'trameTable', 'pastille', 'poignee', 'gel', 'miseEnEvidence',
'selection', 'cadreLecture', 'cadreEcriture', 'cadreBloque', 'focus',
'selection', 'focus',
'contourTable', 'contourChaise', 'echelle', 'fantome', 'chargement', 'filigrane',
];
@ -39,7 +39,7 @@ const FONDS_REQUIS = {
const parNom = (registre, nom) => registre.find((e) => e.nom === nom);
describe('registre des marques', () => {
test('les vingt-quatre marques du contrat, noms uniques, seuil 3, au moins un fond chacune', () => {
test('les vingt et une marques du contrat, noms uniques, seuil 3, au moins un fond chacune', () => {
assert.equal(MARQUES.length, NOMS_MARQUES.length);
assert.deepEqual(MARQUES.map((m) => m.nom), NOMS_MARQUES);
assert.equal(new Set(MARQUES.map((m) => m.nom)).size, MARQUES.length);
@ -204,7 +204,8 @@ describe('les signes des pastilles, des messages, de la bande de mode et de l’
assert.equal(signe.jeton, `--couleur-${teinte}-marque`, nom);
assert.deepEqual(signe.fonds, [`--couleur-${teinte}-fond`, '--couleur-carte-fond', '--couleur-carte-fond-sourd', '--couleur-page-fond'], nom);
}
// La bande de mode reprend la teinte du cadre de mode, sur son propre fond.
// L'icône de la bande de mode porte la teinte de son mode, sur le fond de
// la bande.
for (const mode of ['Ecriture', 'Lecture', 'Bloque']) {
const signe = parNom(SIGNES, `bande${mode}`);
assert.deepEqual([signe.jeton, ...signe.fonds], [`--couleur-cadre-${mode.toLowerCase()}`, `--couleur-mode-${mode.toLowerCase()}-fond`]);

View file

@ -3,7 +3,7 @@
// Ce que le bandeau dit de l'événement ouvert, en clés de la table de la
// coquille : le libellé de mode (§ 8.4, § 8.5), calculé ici une fois pour le
// bandeau et pour le cadre de mode, et l'annonce de la version de format du
// bandeau, la bande de mode et la barre de l'événement, et l'annonce de la version de format du
// fichier (§ 18.6). Les deux se lisent dans l'état observable de la séance.
// La clé de chaque annonce de format de la séance.

View file

@ -1,9 +1,9 @@
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// La clé du libellé de mode (§ 8.4, § 8.5), que lisent le bandeau et le
// cadre de mode : aucune sans événement ; le plan bloqué avant le mode, qu'il
// force en lecture ; sinon le mode de la séance. Et la clé de l'annonce de
// La clé du libellé de mode (§ 8.4, § 8.5), que lisent le bandeau, la bande
// de mode et la barre de l'événement : aucune sans événement ; le plan
// bloqué avant le mode, qu'il force en lecture ; sinon le mode de la séance. Et la clé de l'annonce de
// format du bandeau (§ 18.6), une par annonce de la séance.
import assert from 'node:assert/strict';
import { describe, test } from '../../test/lanceur.js';

View file

@ -215,6 +215,24 @@ const base = () => etat().evenement.base;
const plat = (texte) => texte.replace(/\s+/g, ' ').trim();
const texteDe = (element) => plat(element.textContent);
// Le mode de l'événement ouvert, sans cadre : la surface de travail est en
// ce mode et ne porte aucun trait ; la bande de mode, visible, et le libellé
// de la barre, à côté de la place fixe, le nomment, comme le bandeau.
function exigerModeSansCadre(mode) {
const surface = dans('[data-mode]');
expect(surface.dataset.mode).toBe(mode);
expect(surface.hasAttribute('data-marque')).toBe(false);
const style = getComputedStyle(surface);
for (const cote of ['Top', 'Right', 'Bottom', 'Left']) expect(Number.parseFloat(style[`border${cote}Width`]), cote).toBe(0);
const bande = dans('[data-ecran="evenement"] > [role="status"]');
const libelle = dans('[data-mode-libelle="cadre"]');
expect(visible(bande)).toBe(true);
expect(visible(libelle)).toBe(true);
expect(texteDe(libelle).length).toBeGreaterThan(0);
expect(texteDe(bande)).toContain(texteDe(libelle));
expect(texteDe(dans('[data-mode-libelle="bandeau"]'))).toBe(texteDe(libelle));
}
// Le texte de l'état et du journal de l'événement ouvert, et sa révision.
function fichiers() {
return {
@ -832,10 +850,10 @@ describe('le parcours par gestes, depuis un répertoire neuf', { timeout: DUREE
expect(charge().participants).toHaveLength(0);
});
test('01b une modification refusée en lecture, à côté de son geste, avec « Modifier » pour remède ; le cadre de lecture', async () => {
test('01b une modification refusée en lecture, à côté de son geste, avec « Modifier » pour remède ; la lecture, sans cadre', async () => {
await ouvrirSection('tables');
const avant = fichiers();
expect(dans('[data-mode]').dataset.marque).toBe('cadreLecture');
exigerModeSansCadre('lecture');
await cliquer('[data-section="tables"] [data-commande="creerTableRonde"]');
await refusPres(() => dans('[data-commande="creerTableRonde"]').parentElement, 'MODE_LECTURE', 'modifier', 'créer une table');
expect(document.querySelectorAll(FENETRE)).toHaveLength(0);
@ -846,16 +864,11 @@ describe('le parcours par gestes, depuis un répertoire neuf', { timeout: DUREE
});
});
test('01b « Modifier » : l’écriture et son cadre ; la même modification passe', async () => {
test('01b « Modifier » : l’écriture, sans cadre ; la même modification passe', async () => {
await cliquer('[data-place-fixe] [data-commande="modifier"]');
await attendre('l’écriture', () => etat().mode === 'ecriture' && dans('[data-place-fixe] [data-commande="lecture"]'));
const cadre = dans('[data-mode]');
expect(cadre.dataset.marque).toBe('cadreEcriture');
expect(visible(cadre)).toBe(true);
const libelle = dans('[data-mode-libelle="cadre"]');
expect(visible(libelle)).toBe(true);
expect(texteDe(libelle).length).toBeGreaterThan(0);
expect(texteDe(dans('[data-mode-libelle="bandeau"]'))).toBe(texteDe(libelle));
expect(visible(dans('[data-mode]'))).toBe(true);
exigerModeSansCadre('ecriture');
const r = revision();
await cliquer('[data-section="tables"] [data-commande="creerTableRonde"]');
@ -1407,11 +1420,10 @@ describe('le parcours par gestes, depuis un répertoire neuf', { timeout: DUREE
});
// Glisser une personne, une table, tirer une poignée, poser une réservation.
test('20 la lecture : quatre gestes du plan, empreinte inchangée ; le cadre, son libellé et le bandeau', async () => {
test('20 la lecture : quatre gestes du plan, empreinte inchangée ; la bande, le libellé de mode et le bandeau, sans cadre', async () => {
await cliquer('[data-place-fixe] [data-commande="lecture"]');
await attendre('la lecture', () => etat().mode === 'lecture');
expect(dans('[data-mode]').dataset.marque).toBe('cadreLecture');
expect(visible(dans('[data-mode-libelle="cadre"]'))).toBe(true);
exigerModeSansCadre('lecture');
expect(dans('[data-bandeau]').textContent).toContain(MARQUE_PROVENANCE);
const empreinte = serialiserCharge(charge());
const avant = fichiers();
@ -2095,7 +2107,7 @@ describe('lecture et bloqué : tout geste modifiant refusé, rien d’écrit', {
await cliquer('[data-etat="bloque"] [data-commande="etat"]');
await uneEntree(r, 'bloquer');
expect(charge().evenement.etat).toBe('bloque');
expect(dans('[data-mode]').dataset.marque).toBe('cadreBloque');
exigerModeSansCadre('bloque');
const empreinte = serialiserCharge(charge());
const avant = fichiers();
const rang = m.trace.length;

View file

@ -2,8 +2,8 @@
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
<script>
// Le bandeau du plan (§ 7.5), au-dessus du <svg data-plan> et dans le
// cadre de mode : vide tant qu'une table au moins est saine ; quand toutes
// Le bandeau du plan (§ 7.5), au-dessus du <svg data-plan> et dans la
// surface de travail de l'événement : vide tant qu'une table au moins est saine ; quand toutes
// sont en conflit, la phrase, puis ce que le diagnostic prouve — chaque
// entrée {cle, details} du modèle (modele.js), traduite. L'appartenance
// d'un plancher de groupe est une donnée : elle occupe son propre nœud,

View file

@ -21,8 +21,8 @@
// pas rognée par son défilement.
//
// En plein écran, le titre et les panneaux s'effacent : le plan prend la
// surface que le cadre de mode lui laisse sous le bandeau (§ 18.6), et le
// cadre l'entoure toujours (§ 8.4).
// surface de travail que la bande de mode et la barre de l'événement lui
// laissent sous le bandeau (§ 8.4, § 18.6).
import { getContext } from 'svelte';
import { diagnostiquerCharge } from '../../application/diagnostic.js';
import { etatPlaces } from '../../application/places.js';
@ -75,8 +75,8 @@
</section>
<style>
/* La section prend la hauteur de la surface du cadre de mode : le plan
s'y étend, et c'est la surface du cadre qui défile, jamais la page. */
/* La section prend la hauteur de la surface de travail de l'événement :
le plan s'y étend, et c'est cette surface qui défile, jamais la page. */
.section-plan {
display: flex;
flex-direction: column;

View file

@ -833,7 +833,7 @@ describe('le plein écran (§ 8.4, § 18.6)', () => {
expect(boite(surface(m))).toEqual(avant);
});
test('le cadre de mode garde sa boîte après un zoom', async () => {
test('la barre, la tête et la surface de l’événement gardent leur boîte après un zoom', async () => {
const m = await petite();
await deuxImages();
const avant = boite(dans(m, '[data-mode]'));

View file

@ -460,13 +460,17 @@ function releverEtats(m) {
return etats;
}
// Le cadre de mode : sa signature, son libellé compris.
function signatureCadre(racine) {
const cadre = racine.querySelector('[data-mode]');
if (cadre === null) return null;
const s = getComputedStyle(cadre);
const libelle = racine.querySelector('[data-mode-libelle="cadre"]')?.textContent.trim() ?? '';
return `bords ${s.borderTopStyle} ${s.borderTopWidth} | ${libelle}`;
// Le mode de l'événement : sig, la signature de la bande de mode et du
// libellé de mode de la barre ; trait, la somme des épaisseurs des bords de
// la surface de travail, qui n'en porte aucun. null hors d'un événement.
function signatureMode(racine) {
const surface = racine.querySelector('[data-mode]');
const bande = racine.querySelector('[data-ecran="evenement"] > [role="status"]');
const libelle = racine.querySelector('[data-mode-libelle="cadre"]');
if (surface === null || bande === null || libelle === null) return null;
const s = getComputedStyle(surface);
const trait = ['Top', 'Right', 'Bottom', 'Left'].reduce((somme, cote) => somme + Number.parseFloat(s[`border${cote}Width`]), 0);
return { sig: signature([bande, libelle]), trait };
}
/** Fautes d'une paire d'états : chaque élément du premier dont la signature
@ -494,7 +498,7 @@ function dansLaFenetre(element) {
/** Fautes du bandeau d'un écran (§ 8.4, § 18.6) : unique, visible, portant
* la version et la provenance ; le libellé de mode absent sans événement,
* présent et égal à celui du cadre sinon. */
* présent et égal à celui de la barre de l'événement sinon. */
export function fautesBandeau(racine, mode, { version = VERSION.affichee, provenance = provenanceVisible(PROVENANCE, tTemoin) } = {}) {
const bandeaux = racine.querySelectorAll('[data-bandeau]');
if (bandeaux.length !== 1) return [`${bandeaux.length} bandeaux`];
@ -513,12 +517,12 @@ export function fautesBandeau(racine, mode, { version = VERSION.affichee, proven
if (libelle !== null) fautes.push(`libellé de mode « ${libelle.textContent.trim()} » sans événement ouvert`);
return fautes;
}
const cadre = racine.querySelector('[data-mode-libelle="cadre"]');
const barre = racine.querySelector('[data-mode-libelle="cadre"]');
if (libelle === null) fautes.push(`libellé de mode absent du bandeau (${mode})`);
else if (voir(libelle) !== true) fautes.push(`libellé de mode du bandeau non visible : ${voir(libelle)}`);
if (cadre === null) fautes.push(`libellé de mode absent du cadre (${mode})`);
if (libelle !== null && cadre !== null && libelle.textContent.trim() !== cadre.textContent.trim()) {
fautes.push(`bandeau « ${libelle.textContent.trim()} », cadre « ${cadre.textContent.trim()} »`);
if (barre === null) fautes.push(`libellé de mode absent de la barre (${mode})`);
if (libelle !== null && barre !== null && libelle.textContent.trim() !== barre.textContent.trim()) {
fautes.push(`bandeau « ${libelle.textContent.trim()} », barre « ${barre.textContent.trim()} »`);
}
return fautes;
}
@ -603,7 +607,7 @@ beforeAll(async () => {
releve.refus = releverRefus(racine).map((r) => ({ ...r, restes: horsMarques(r.texte).filter((x) => !estDonnee(x, connues)) }));
releve.etats = releverEtats(m);
releve.listes = releverListes(racine);
releve.cadre = signatureCadre(racine);
releve.signatureMode = signatureMode(racine);
releve.clair = releverTheme(racine);
await cliquer(m, '[data-bandeau] [data-commande="theme"][data-theme-choix="sombre"]');
await attendre('le thème sombre', () => document.documentElement.getAttribute('data-theme') === 'sombre');
@ -984,7 +988,7 @@ describe('chaque écran de l’itération (§ 14.5, § 14.6, § 19.11)', () => {
aucuneFaute([...new Set(fautes)], 'atténuations');
});
test('jamais la seule couleur : table saine et en conflit, les trois sortes de conflit, place libre, réservée et à titre non pourvu, portée tour et tous, cadre en lecture, en écriture et bloqué', () => {
test('jamais la seule couleur : table saine et en conflit, les trois sortes de conflit, place libre, réservée et à titre non pourvu, portée tour et tous, mode en lecture, en écriture et bloqué, sans trait autour de la surface', () => {
const plan = atteints().find((r) => r.nom === 'evenement.plan.ecriture');
expect(plan, 'le plan en écriture relevé').toBeDefined();
const e = plan.etats;
@ -1000,13 +1004,14 @@ describe('chaque écran de l’itération (§ 14.5, § 14.6, § 19.11)', () => {
expect(trois, 'le plan des trois sortes de conflit relevé').toBeDefined();
const [a, b, c] = CONFLITS.map(({ marque }) => trois.etats.trames[marque]);
fautes.push(...fautesPaire('trame appartenance / voisins', a, b), ...fautesPaire('trame appartenance / table', a, c), ...fautesPaire('trame voisins / table', b, c));
const cadres = ['lecture', 'ecriture', 'bloque'].map((mode) => {
const modes = ['lecture', 'ecriture', 'bloque'].map((mode) => {
const r = atteints().find((x) => x.nom === `evenement.plan.${mode}`);
return { ou: `cadre ${mode}`, sig: r?.cadre ?? null };
return { ou: `mode ${mode}`, sig: r?.signatureMode?.sig ?? null, trait: r?.signatureMode?.trait ?? null };
});
for (const c of cadres) if (c.sig === null) fautes.push(`${c.ou} : absent`);
for (let i = 0; i < cadres.length; i += 1) {
for (let j = i + 1; j < cadres.length; j += 1) fautes.push(...fautesPaire(`${cadres[i].ou} / ${cadres[j].ou}`, [cadres[i]], [cadres[j]]));
for (const c of modes) if (c.sig === null) fautes.push(`${c.ou} : absent`);
for (const c of modes) if (c.trait !== null && c.trait !== 0) fautes.push(`${c.ou} : un trait de ${c.trait} px autour de la surface`);
for (let i = 0; i < modes.length; i += 1) {
for (let j = i + 1; j < modes.length; j += 1) fautes.push(...fautesPaire(`${modes[i].ou} / ${modes[j].ou}`, [modes[i]], [modes[j]]));
}
aucuneFaute(fautes, 'jamais la seule couleur');
});
@ -1024,7 +1029,7 @@ describe('chaque écran de l’itération (§ 14.5, § 14.6, § 19.11)', () => {
aucuneFaute(atteints().flatMap((r) => r.emoji.fautes.map((f) => `${r.nom} — ${f}`)), 'emoji');
});
test('le bandeau sur tout écran : visible, version et provenance ; libellé de mode absent de la liste, égal à celui du cadre sur un événement', () => {
test('le bandeau sur tout écran : visible, version et provenance ; libellé de mode absent de la liste, égal à celui de la barre sur un événement', () => {
expect(atteints().some((r) => r.mode === null) && atteints().some((r) => r.mode !== null)).toBe(true);
aucuneFaute(atteints().flatMap((r) => r.bandeau.map((f) => `${r.nom} — ${f}`)), 'bandeau');
});

View file

@ -107,13 +107,13 @@ const ENTREES = [
['data-liste-travail', 'plan : une liste de travail', ['pressentis', 'titres']],
['data-marque', 'une marque porteuse d’information, par son nom au registre', null],
['data-meilleure', 'la meilleure proposition : accueil ou génération', ['']],
['data-meilleure-collisions', 'les collisions de la meilleure proposition', ['']],
['data-meilleure-collisions', 'génération : les collisions de la meilleure proposition', ['']],
['data-meilleure-id', 'l’identifiant de la meilleure proposition', ['']],
['data-meilleure-libelle-id', 'génération : le libellé de l’identifiant de la meilleure', ['']],
['data-meilleure-vide', 'génération : la meilleure absente', ['']],
['data-mention', 'aperçu : une mention d’une page du plan imprimé — disposition relative, listes hors d’échelle', ['listes', 'relative']],
['data-menu', 'plan : le menu d’une personne', null],
['data-mode', 'événement : le cadre de mode, par sa forme', null],
['data-mode', 'événement : la barre, la tête et la surface de l’événement, par son mode', null],
['data-mode-libelle', 'le libellé de mode', ['bandeau', 'cadre']],
['data-motif', 'import : le motif d’une ligne refusée', ['']],
['data-multiligne', 'import : une ligne sur plusieurs lignes de texte', null],
@ -130,7 +130,7 @@ const ENTREES = [
['data-pastille', 'plan : la pastille d’une table', null],
['data-phrase', 'une phrase : diagnostic ou défaut des tables', ['']],
['data-pied', 'aperçu : le pied d’une feuille imprimée', ['']],
['data-place-fixe', 'événement : la place fixe du cadre de mode', ['']],
['data-place-fixe', 'événement : la place fixe de la barre de l’événement', ['']],
['data-place-vide', 'aperçu : une place vide de la feuille par table', ['']],
['data-plan', 'plan : le <svg> du plan', ['']],
['data-poignee', 'plan : la poignée d’une table', null],

View file

@ -424,7 +424,7 @@ export const ECRANS = Object.freeze([
// « chargement ».
const m = await contexte.monter({ fichiers: [fichierEtat(chargePrincipale()), fichierEtat(chargeGelee(), 'gelee')] });
await attendre('le signe du chargement dans chaque valeur de résumé', () =>
[BASE, 'gelee'].every((base) => m.racine.querySelectorAll(`[data-evenement="${base}"] [aria-busy="true"] [data-chargement]`).length === 4),
[BASE, 'gelee'].every((base) => m.racine.querySelectorAll(`[data-evenement="${base}"] [aria-busy="true"] [data-chargement]`).length === 2),
);
return m.racine;
},
@ -433,8 +433,8 @@ export const ECRANS = Object.freeze([
nom: 'accueil.mesuresRetenu',
mode: null,
async atteindre(contexte) {
// La principale, sa proposition retenue : la ligne montre les deux
// mesures du retenu et la meilleure proposition du classement, et le
// La principale, sa proposition retenue : la ligne montre la
// différence du retenu et la meilleure proposition du classement, et le
// pointeur réel sur l'intitulé de la différence — un en-tête de
// colonne du tableau, une fois pour la liste — ouvre son infobulle.
const principale = chargePrincipale();
@ -442,12 +442,8 @@ export const ECRANS = Object.freeze([
const m = await contexte.monter({ fichiers: [fichierEtat(charge)] });
const ligne = `[data-evenement="${BASE}"]`;
await chargerResumes(m);
await attendre('les mesures du retenu et la meilleure proposition', () =>
[
...['collisionsCumulees', 'diffRedondance'].map((colonne) => `[data-colonne="${colonne}"]`),
'[data-meilleure-id]',
'[data-meilleure-collisions]',
].every((valeur) => /\d/.test(m.racine.querySelector(`${ligne} ${valeur}`)?.textContent ?? '')),
await attendre('la différence du retenu et la meilleure proposition', () =>
['[data-colonne="diffRedondance"]', '[data-meilleure-id]'].every((valeur) => /\d/.test(m.racine.querySelector(`${ligne} ${valeur}`)?.textContent ?? '')),
);
const { x, y } = await centre(await element(m, '[data-ecran="accueil"] thead [data-terme="diffRedondance"]'));
await pointer('bouger', x, y);