[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 ## 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 **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 pas cliqué sur « Modifier », toujours à la même place, en haut de l’écran. La
cadre autour du plan et le bandeau disent dans quel mode vous êtes. Après un 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 moment d’inactivité, le logiciel revient seul en lecture, entre deux gestes et
jamais au milieu de l’un d’eux. 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) ![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 » *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 Cliquez sur « Modifier » : la bande et le libellé passent en écriture, et le
réussit. « Revenir en lecture » fait l’inverse. même geste réussit. « Revenir en lecture » fait l’inverse.
## Étape 2 — Importer la liste des participants ## É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 : proposition alors que le retenu a été ajusté à la main détruit ces ajustements :
une fenêtre le dit avant. une fenêtre le dit avant.
La liste des événements résume ensuite le retenu par ses collisions cumulées et La liste des événements résume ensuite le retenu par l’écart de sa somme de
par l’écart de sa somme de redondance d’appartenance à la cible de la soirée. redondance d’appartenance à la cible de la soirée.
Retenir n’est pas obligatoire, mais tout ce qui s’imprime sans placement retenu Retenir n’est pas obligatoire, mais tout ce qui s’imprime sans placement retenu
porte la mention BROUILLON (étape 12). 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 : 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 : 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 chaque geste est refusé avec « Modifier » pour remède, et rien ne change sur le
disque.* 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 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* 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 (§ 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 ensuite dans la liste des événements, par la différence absolue de sa somme de
différence absolue de sa somme de redondance à la cible de l'événement (§ 8.1). redondance à la cible de l'événement (§ 8.1).
**11. Ajuster à la main.** *Facultative, reprenable.* Le même geste qu'à **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 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 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 « 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 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 cartes offrent la création, le dossier de travail et les démonstrations. Une
colonnes résument en plus le **placement retenu** : « Collisions cumulées (paire colonne résume en plus le **placement retenu** : « Diff somme d'appartenance »,
et tour) », les collisions cumulées du retenu (§ 5.4), et « Diff somme |cible − Σ r(p)|, où Σ r(p) est la somme de redondance du retenu — le nombre de
d'appartenance », |cible − Σ r(p)|, où Σ r(p) est la somme de redondance du la colonne « Redondance d'appartenance, somme Σ r(p) » du tableau des
retenu — le nombre de la colonne « Redondance d'appartenance, somme Σ r(p) » du propositions — et la cible celle de l'événement (§ 8.9), 1000 par défaut,
tableau des propositions — et la cible celle de l'événement (§ 8.9), 1000 par réglable dans la section des propositions. Elle se lit dans la même mesure que
défaut, réglable dans la section des propositions. Les deux se lisent dans la le tableau, sans rien écrire ; un événement sans retenu, ou dont le retenu est
même mesure que le tableau, sans rien écrire ; un événement sans retenu, ou dont fautif ou en dérive (§ 8.9, § 9), y porte « — ». Chaque ligne porte aussi la
le retenu est fautif ou en dérive (§ 8.9, § 9), y porte « — ». Chaque ligne **« Meilleure proposition du classement »**, sous les libellés du bloc que la
porte aussi la **« Meilleure proposition du classement »**, sous les libellés du section de la génération montre en tête : l'identifiant de la proposition au
bloc que la section de la génération montre en tête : l'identifiant de la rang 1 du classement (§ 5.7) — la même proposition que ce bloc et que la
proposition au rang 1 du classement (§ 5.7) et ses collisions cumulées — la même première ligne du tableau des propositions, retenue ou non ; une proposition
proposition et le même nombre que ce bloc et que la première ligne du tableau fautive, que l'ouverture écarte (§ 8.9, point 3), n'y entre pas. Sans
des propositions, retenue ou non ; une proposition fautive, que l'ouverture proposition classée — aucune, ou aucune qui décrive encore la configuration —,
écarte (§ 8.9, point 3), n'y entre pas. Sans proposition classée — aucune, ou elle vaut « — ». La liste ne montre les collisions cumulées ni du retenu ni de
aucune qui décrive encore la configuration —, l'un et l'autre valent « — ». la meilleure proposition : le tableau des propositions et le bloc de la
Chaque intitulé s'explique dans une infobulle, posée sur l'en-tête de sa génération les portent. Chaque intitulé s'explique dans une infobulle, posée
colonne. sur l'en-tête de sa colonne.
**La liste paraît avant ses colonnes calculées** : ses lignes s'affichent dès le **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 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 é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 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, 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 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 automatique se voit à la bande de mode, au libellé de mode et au bandeau, et un
un avertissement non modal le dit jusqu'au passage en écriture suivant. 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 **Le mode est visible sans quitter la surface de travail des yeux**, et non dans
libellé permanents autour du plan — et non dans un coin de barre d'outils : un coin de barre d'outils : l'opérateur regarde le plan. Sous le bandeau, quand
l'opérateur regarde le plan. 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 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 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 ouvert**, le libellé de mode. Ils ne font pas double emploi : la bande et le
signale le mode là où la main agit ; le bandeau porte la version jusque sur la libellé de la barre signalent le mode dans l'écran de l'événement ; le bandeau
liste des événements, qui est le premier écran d'un signalement sur deux. Le porte la version jusque sur la liste des événements, qui est le premier écran
bandeau porte aussi la marque du logiciel, le réglage de thème et « À propos » ; d'un signalement sur deux. Le bandeau porte aussi la marque du logiciel, le
il garde une hauteur d'une ligne de commande, de sorte que la surface de travail réglage de thème et « À propos » ; il garde une hauteur d'une ligne de commande,
ne perd rien. 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.
**Le passage en écriture est une étape nommée du parcours** (§ 2.1, étape 1b), **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 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 | | 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 | | 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 | | 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 **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 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 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 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 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 l'application est dans l'image quel que soit le cadrage. Le § 8.4 impose en plus une
cadre permanent **autour du plan** pour le mode : les deux coexistent, le cadre bande de mode **sous le bandeau** et un libellé de mode à côté de la place fixe : ils
signalant le mode là où la main agit, le bandeau portant la version jusque sur la coexistent, la bande et le libellé signalant le mode dans l'écran de l'événement, le
liste des événements. 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 **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 à 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 | | 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** | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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', () => { 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 }; const avant = { largeur: innerWidth, hauteur: innerHeight };
try { try {
await fixerCadre(1800, 900); 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); await fixerCadre(1366, 700);
flushSync(); flushSync();
expect(main.getBoundingClientRect().width).toBeCloseTo(document.documentElement.clientWidth, 0); 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 { } finally {
await fixerCadre(avant.largeur, avant.hauteur); 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 // 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à // 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 // 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 // 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 // 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))); 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 charge = chargeRetenue();
const mesures = mesuresDe(charge); const mesures = mesuresDe(charge);
expect(mesures.collisionsCumulees).toBeGreaterThan(0);
const m = await monter({ const m = await monter({
fichiers: [fichier(`retenue${SUFFIXES.etat}`, etat(charge)), fichier(`club${SUFFIXES.etat}`, texteRenomme('Club des Merles'))], 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); await chargerResumes(m);
const avant = ecritures(m); const avant = ecritures(m);
const retenue = ligneDe(m, 'retenue'); 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))); expect(colonne(retenue, 'diffRedondance')).toBe(format.nombre(Math.abs(1000 - mesures.totalRedondance)));
const club = ligneDe(m, 'club'); const club = ligneDe(m, 'club');
expect(colonne(club, 'collisionsCumulees')).toBe(TIRET);
expect(colonne(club, 'diffRedondance')).toBe(TIRET); expect(colonne(club, 'diffRedondance')).toBe(TIRET);
// Chaque intitulé est un en-tête de colonne, une seule fois pour toute la // L'intitulé est un en-tête de colonne, une seule fois pour toute la
// liste. // liste ; aucune colonne ni aucun intitulé ne nomme les collisions
for (const [nom, cle] of [ // cumulées.
['collisionsCumulees', 'accueil.colonne.collisionsCumulees'], expect(tous(m, '[data-terme="diffRedondance"]').length).toBe(1);
['diffRedondance', 'accueil.colonne.diffRedondance'], expect(terme(accueil(m), 'diffRedondance').closest('thead')).not.toBeNull();
]) { expect(terme(accueil(m), 'diffRedondance').textContent).toBe(t('accueil.colonne.diffRedondance'));
expect(tous(m, `[data-terme="${nom}"]`).length, nom).toBe(1); expect(tous(m, '[data-colonne="collisionsCumulees"], [data-terme="collisionsCumulees"]')).toEqual([]);
expect(terme(accueil(m), nom).closest('thead'), nom).not.toBeNull(); expect(tous(m, '[data-ecran="accueil"] thead th').filter((th) => th.textContent.includes(t('propositions.colonne.collisionsCumulees')))).toEqual([]);
expect(terme(accueil(m), nom).textContent).toBe(t(cle));
}
expect(visible(retenue.querySelector('[data-colonne="diffRedondance"]'))).toBe(true); expect(visible(retenue.querySelector('[data-colonne="diffRedondance"]'))).toBe(true);
expect(ecritures(m)).toBe(avant); 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 () => { 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()))] }); const m = await monter({ fichiers: [fichier(`retenue${SUFFIXES.etat}`, etat(chargeRetenue()))] });
await vi.waitFor(() => expect(lignes(m).length).toBe(1)); await vi.waitFor(() => expect(lignes(m).length).toBe(1));
await eprouverTermes(accueil(m), [ await eprouverTermes(accueil(m), [['diffRedondance', 'accueil.explication.diffRedondance']]);
['collisionsCumulees', 'accueil.explication.collisionsCumulees'],
['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 () => { 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 revenirALaListe(m, 1);
await resumeRecu('retenue'); await resumeRecu('retenue');
expect(colonne(ligneDe(m, 'retenue'), 'diffRedondance')).toBe(format.nombre(Math.abs(25 - totalRedondance))); 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; return appliquerCommande('retenirProposition', charge, { id: autre.id }).charge;
} }
// Ce que la ligne, ou le bloc de la génération, écrit de la meilleure // Ce que la ligne, ou le bloc de la génération, écrit de l'identifiant de
// proposition. // la meilleure proposition.
const lu = (bloc) => ({ const lu = (bloc) => ({ id: bloc.querySelector('[data-meilleure-id]')?.textContent ?? null });
id: bloc.querySelector('[data-meilleure-id]')?.textContent ?? null,
collisions: bloc.querySelector('[data-meilleure-collisions]')?.textContent ?? null,
});
// L'en-tête du groupe de colonnes de la meilleure proposition, et celui de // L'en-tête du groupe de colonnes de la meilleure proposition, et celui de
// sa colonne d'identifiant. // sa colonne d'identifiant.
const groupeMeilleure = (m) => terme(accueil(m), 'meilleure').closest('th'); 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]'); 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 charge = chargeClassee();
const table = evaluer(charge); const table = evaluer(charge);
const [premiere] = table.propositions; const [premiere] = table.propositions;
@ -1355,35 +1343,29 @@ describe('la meilleure proposition du classement dans la liste (§ 5.4, § 5.7,
await chargerResumes(m); await chargerResumes(m);
const avant = ecritures(m); const avant = ecritures(m);
const classee = ligneDe(m, 'classee'); const classee = ligneDe(m, 'classee');
expect(lu(classee)).toEqual({ id: format.nombre(premiere.id), collisions: format.nombre(premiere.mesures.collisionsCumulees) }); expect(lu(classee)).toEqual({ id: format.nombre(premiere.id) });
expect(colonne(classee, 'collisionsCumulees')).toBe(format.nombre(table.retenu.mesures.collisionsCumulees));
// Les intitulés sont ceux du bloc de la génération, en en-têtes de // 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 // colonnes : le titre sur le groupe, l'identifiant sur sa seule colonne ;
// ses deux colonnes ; chaque valeur se tient sous son groupe. // 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(terme(accueil(m), 'meilleure').textContent).toBe(t('generation.meilleure.titre'));
expect(groupeMeilleure(m).getAttribute('scope')).toBe('colgroup'); expect(groupeMeilleure(m).getAttribute('scope')).toBe('colgroup');
expect(groupeMeilleure(m).colSpan).toBe(2); expect(groupeMeilleure(m).colSpan).toBe(1);
expect(enteteProposition(m)).toBeDefined(); expect(enteteProposition(m)).toBeDefined();
expect(terme(accueil(m), 'meilleureCollisions').textContent).toBe(t('propositions.colonne.collisionsCumulees')); const cellule = classee.querySelector('[data-meilleure-id]');
for (const valeur of ['[data-meilleure-id]', '[data-meilleure-collisions]']) { expect(visible(cellule)).toBe(true);
const cellule = classee.querySelector(valeur); expect(atteint(cellule)).toBe(true);
expect(visible(cellule), valeur).toBe(true); expect(sous(cellule, groupeMeilleure(m))).toBe(true);
expect(atteint(cellule), valeur).toBe(true); expect(sous(cellule, enteteProposition(m))).toBe(true);
expect(sous(cellule, groupeMeilleure(m)), valeur).toBe(true); expect(tous(m, '[data-ecran="accueil"] [data-meilleure-collisions], [data-terme="meilleureCollisions"]')).toEqual([]);
} expect(lu(ligneDe(m, 'club'))).toEqual({ id: TIRET });
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 });
expect(ecritures(m)).toBe(avant); 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()))] }); const m = await monter({ fichiers: [fichier(`classee${SUFFIXES.etat}`, etat(chargeClassee()))] });
await vi.waitFor(() => expect(lignes(m).length).toBe(1)); await vi.waitFor(() => expect(lignes(m).length).toBe(1));
await eprouverTermes(accueil(m), [ await eprouverTermes(accueil(m), [['meilleure', 'accueil.explication.meilleure']]);
['meilleure', 'accueil.explication.meilleure'],
['meilleureCollisions', 'propositions.explication.collisionsCumulees'],
]);
}); });
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 () => { 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); await chargerResumes(m);
const deLaListe = lu(ligneDe(m, 'classee')); const deLaListe = lu(ligneDe(m, 'classee'));
expect(deLaListe.id).toMatch(/\d/); expect(deLaListe.id).toMatch(/\d/);
const libelles = [terme(accueil(m), 'meilleure'), enteteProposition(m), terme(accueil(m), 'meilleureCollisions')].map( const libelles = [terme(accueil(m), 'meilleure'), enteteProposition(m)].map((element) => element.textContent.trim());
(element) => element.textContent.trim(),
);
await ouvrirLigne(m, 'classee'); await ouvrirLigne(m, 'classee');
await attendreEvenement(m); await attendreEvenement(m);
m.ui.allerA('generation'); m.ui.allerA('generation');
await vi.waitFor(() => expect(blocGeneration(m)?.querySelector('[data-meilleure-id]')).not.toBeNull()); await vi.waitFor(() => expect(blocGeneration(m)?.querySelector('[data-meilleure-id]')).not.toBeNull());
const bloc = blocGeneration(m); const bloc = blocGeneration(m);
expect(lu(bloc)).toEqual(deLaListe); expect(lu(bloc)).toEqual(deLaListe);
expect( expect([bloc.querySelector('h3'), bloc.querySelector('[data-meilleure-libelle-id]')].map((element) => element.textContent)).toEqual(
[bloc.querySelector('h3'), bloc.querySelector('[data-meilleure-libelle-id]'), bloc.querySelector('[data-terme]')].map( libelles,
(element) => element.textContent, );
),
).toEqual(libelles);
// Le même résumé, mais reçu de la liste relue au retour : celui d'avant // 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. // l'ouverture écrirait les mêmes valeurs sans rien prouver.
const resumeRecu = suivreResumes(m); 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))] }); const m = await monter({ fichiers: [fichier(`horsclassement${SUFFIXES.etat}`, etat(charge))] });
await vi.waitFor(() => expect(lignes(m).length).toBe(1)); await vi.waitFor(() => expect(lignes(m).length).toBe(1));
await chargerResumes(m); 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 ouvrirLigne(m, 'horsclassement');
await attendreEvenement(m); await attendreEvenement(m);
m.ui.allerA('generation'); 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; return appliquerCommande('retenirProposition', charge, { id: charge.propositions[1].id }).charge;
} }
// Les quatre valeurs de résumé d'une ligne, dans l'ordre du rendu. // Les deux 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]']; const CELLULES = ['[data-colonne="diffRedondance"]', '[data-meilleure-id]'];
const cellules = (ligne) => CELLULES.map((selecteur) => ligne.querySelector(selecteur)); const cellules = (ligne) => CELLULES.map((selecteur) => ligne.querySelector(selecteur));
// Ce que la ligne d'une charge écrit, par la table d'évaluation sollicitée // Ce que la ligne d'une charge écrit, par la table d'évaluation sollicitée
// une seconde fois (§ 19.1). // une seconde fois (§ 19.1).
function attendues(charge) { function attendues(charge) {
const table = evaluer(charge); const table = evaluer(charge);
const [premiere] = table.propositions; const [premiere] = table.propositions;
const { collisionsCumulees, totalRedondance } = table.retenu.mesures; return [Math.abs(1000 - table.retenu.mesures.totalRedondance), premiere.id].map((n) => format.nombre(n));
return [collisionsCumulees, Math.abs(1000 - totalRedondance), premiere.id, premiere.mesures.collisionsCumulees].map((n) => format.nombre(n));
} }
// Chaque valeur de la ligne attend : la cellule se dit occupée, porte un // 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), // 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)); expect(colonne(ligne, 'participants')).toBe(format.nombre(charges[ligne.dataset.evenement].participants.length));
enAttente(ligne); enAttente(ligne);
} }
expect(signes(m).length).toBe(8); expect(signes(m).length).toBe(4);
const region = regionEtat(m); const region = regionEtat(m);
expect(region.textContent).toBe(t('accueil.chargement')); expect(region.textContent).toBe(t('accueil.chargement'));
for (let image = 0; image < 3; image += 1) await uneImage(); for (let image = 0; image < 3; image += 1) await uneImage();
flushSync(); flushSync();
expect(signes(m).length).toBe(8); expect(signes(m).length).toBe(4);
m.horloge.avancer(0); m.horloge.avancer(0);
await vi.waitFor(() => resolue(ligneDe(m, 'alpha'), attendues(charges.alpha))); await vi.waitFor(() => resolue(ligneDe(m, 'alpha'), attendues(charges.alpha)));
enAttente(ligneDe(m, 'beta')); 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 () => { 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 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 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 relus = [];
const lireTexte = m.fs.lireTexte; const lireTexte = m.fs.lireTexte;
m.fs.lireTexte = (racine, chemin) => { 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([]); 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 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 m = await monter({ fichiers: Object.entries(charges).map(([base, charge]) => fichier(`${base}${SUFFIXES.etat}`, etat(charge))) });
const lireTexte = m.fs.lireTexte; 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' })) ? Promise.reject(new ErreurStockage('LECTURE', { chemin, cause: 'EIO' }))
: lireTexte.call(m.fs, racine, chemin); : lireTexte.call(m.fs, racine, chemin);
await chargerResumes(m); await chargerResumes(m);
resolue(ligneDe(m, 'alpha'), [TIRET, TIRET, TIRET, TIRET]); resolue(ligneDe(m, 'alpha'), [TIRET, TIRET]);
resolue(ligneDe(m, 'beta'), attendues(charges.beta)); 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 () => { 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 m = await monter({ fichiers: [fichier(`alpha${SUFFIXES.etat}`, etat(chargeResumee('evt-alpha')))] });
const traces = tous(m, '[data-chargement] path'); const traces = tous(m, '[data-chargement] path');
expect(traces.length).toBe(4); expect(traces.length).toBe(2);
for (const theme of ['clair', 'sombre']) { for (const theme of ['clair', 'sombre']) {
await sousTheme(theme, async () => { await sousTheme(theme, async () => {
for (const trace of traces) { 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); await chargerResumes(m);
const entetes = tous(m, '[data-ecran="accueil"] thead th[scope="colgroup"]'); 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(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[1])).toEqual(jeton('--couleur-etat-retenu-fond'));
expect(fond(entetes[2])).toEqual(jeton('--couleur-etat-propose-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. // 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'); 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 ['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); expect(dans2(ligne.querySelector('[data-colonne="diffRedondance"]'), entetes[1])).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-meilleure-id]'), entetes[2])).toBe(true);
// Le nom est l'en-tête de sa ligne ; la date et le chemin le suivent, // Le nom est l'en-tête de sa ligne ; la date et le chemin le suivent,
// dessous, en plus petit. // dessous, en plus petit.
const nom = ligne.querySelector('[data-colonne="nom"]'); 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, // Les en-têtes de colonnes sont groupés : l'événement — nom, état,
// participants, dernière modification —, le placement retenu, à la teinte // participants, dernière modification —, le placement retenu, à la teinte
// de Retenu — ses deux mesures (COLONNES_DU_RETENU) —, la meilleure // de Retenu — sa différence à la cible (COLONNES_DU_RETENU) —, la
// proposition du classement, à la teinte de Proposé, sous le titre et les // meilleure proposition du classement, à la teinte de Proposé, sous le
// libellés du bloc de la génération (MEILLEURE) — son identifiant et ses // titre et le libellé du bloc de la génération (MEILLEURE) — son
// collisions cumulées —, puis les actions. Chaque valeur est dans son // identifiant —, puis les actions. Chaque valeur est dans son propre
// propre nœud : data-colonne, data-meilleure-id, data-meilleure-collisions, // nœud : data-colonne, data-meilleure-id, « — » sans valeur. Le nom est
// « — » sans valeur. Le nom est l'en-tête de sa ligne ; la date et le // l'en-tête de sa ligne ; la date et le chemin le suivent en petite
// chemin le suivent en petite ligne, chacun précédé de son intitulé, que // ligne, chacun précédé de son intitulé, que
// lisent les technologies d'assistance et que l'œil ne voit pas, puis ses // lisent les technologies d'assistance et que l'œil ne voit pas, puis ses
// signalements traduits — les bases qu'un doublon nomme suivent son // signalements traduits — les bases qu'un doublon nomme suivent son
// texte, chacune dans son propre nœud (data-doublon). L'état est une // 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 // 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 // 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 // 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 // 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 // (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 // du dernier échec, ou null ; apresOperation() : à appeler quand une
// opération a changé le dossier. // opération a changé le dossier.
// //
// L'intitulé de chaque mesure du retenu, le titre de la meilleure // L'intitulé de la mesure du retenu et le titre de la meilleure
// proposition et l'intitulé de ses collisions s'expliquent dans // proposition s'expliquent dans l'infobulle de la liste, une seule
// l'infobulle de la liste, une seule (Infobulle, primitive) : chaque terme // (Infobulle, primitive) : chaque terme (data-terme, que pose le fragment termeExplique), un en-tête de colonne,
// (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 // 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 // quitter la referme quand elle est encore la sienne, Échap aussi. Le
// terme expliqué porte aria-describedby vers elle, et lui seul. // 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 enChargement = $derived(lignes.some((ligne) => ligneAffichee(ligne, format, resumes.get(ligne.base)).enAttente));
const groupes = $derived(groupesDeLaListe(lignes, vue.filtre, vue.groupement)); const groupes = $derived(groupesDeLaListe(lignes, vue.filtre, vue.groupement));
// Le nombre de colonnes du tableau, que l'intitulé d'un groupe couvre : // 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. // proposition, les actions.
const COLONNES = 9; const COLONNES = 7;
// Le terme expliqué : {ancre, terme, cle, details} — son élément, son nom // Le terme expliqué : {ancre, terme, cle, details} — son élément, son nom
// et la clé de son explication — ; null sans infobulle. // et la clé de son explication — ; null sans infobulle.
@ -137,18 +136,18 @@
<div class="defilement"> <div class="defilement">
<table> <table>
<colgroup span="4"></colgroup> <colgroup span="4"></colgroup>
<colgroup span="2"></colgroup> <colgroup></colgroup>
<colgroup span="2"></colgroup> <colgroup></colgroup>
<colgroup></colgroup> <colgroup></colgroup>
<thead> <thead>
<tr> <tr>
<th scope="colgroup" colspan="4" class="groupe">{t('accueil.entete.evenement')}</th> <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="entete-groupe"
><span class="signe"><Icone nom="coche" taille={14} /></span><span>{t('accueil.entete.retenu')}</span></span ><span class="signe"><Icone nom="coche" taille={14} /></span><span>{t('accueil.entete.retenu')}</span></span
></th ></th
> >
<th scope="colgroup" colspan="2" class="groupe propose" <th scope="colgroup" class="groupe propose"
><span class="entete-groupe" ><span class="entete-groupe"
><span class="signe"><Icone nom="listePoints" taille={14} /></span>{@render termeExplique( ><span class="signe"><Icone nom="listePoints" taille={14} /></span>{@render termeExplique(
'meilleure', 'meilleure',
@ -168,9 +167,6 @@
<th scope="col" class="nombre">{@render termeExplique(colonne, cle, explication)}</th> <th scope="col" class="nombre">{@render termeExplique(colonne, cle, explication)}</th>
{/each} {/each}
<th scope="col" class="nombre">{t(MEILLEURE.proposition)}</th> <th scope="col" class="nombre">{t(MEILLEURE.proposition)}</th>
<th scope="col" class="nombre"
>{@render termeExplique('meilleureCollisions', MEILLEURE.collisions, MEILLEURE.explicationCollisions)}</th
>
</tr> </tr>
</thead> </thead>
{#each groupes as groupe, rangGroupe (groupe.groupe ?? '')} {#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> <td class="nombre" data-colonne={colonne} aria-busy={occupee}>{@render resume(affichee, affichee[colonne])}</td>
{/each} {/each}
<td class="nombre" data-meilleure-id aria-busy={occupee}>{@render resume(affichee, affichee.meilleure.id)}</td> <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"> <td class="actions">
<div class="commandes-ligne"> <div class="commandes-ligne">
<button <button

View file

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

View file

@ -19,9 +19,8 @@ import { texteLivre } from '../../demo/livrees.js';
import { rechercher } from '../../moteur/recherche.js'; import { rechercher } from '../../moteur/recherche.js';
import { analyser, configurationDepuisCharge } from '../../stockage/document.js'; import { analyser, configurationDepuisCharge } from '../../stockage/document.js';
import { VERSION } from '../../version.genere.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 { TABLES_INTERFACE } from '../libelles.js';
import { COLONNES, texteCellule } from '../propositions/modele.js';
import { import {
COLONNES_DU_RETENU, COLONNES_DU_RETENU,
ETATS, ETATS,
@ -81,7 +80,6 @@ describe('ligneAffichee : les six informations (§ 8.1)', () => {
const affichee = ligneAffichee(ligne(), format); const affichee = ligneAffichee(ligne(), format);
assert.deepEqual(Object.keys(affichee).sort(), [ assert.deepEqual(Object.keys(affichee).sort(), [
'chemin', 'chemin',
'collisionsCumulees',
'date', 'date',
'diffRedondance', 'diffRedondance',
'enAttente', '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)', () => { 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', () => { 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: 1234, totalRedondance: 1500, cibleRedondance: 1000, diffRedondance: 500 }; const mesuresDuRetenu = { collisionsCumulees: 8765, totalRedondance: 1500, cibleRedondance: 1000, diffRedondance: 500 };
const affichee = ligneAffichee(ligne(), format, { ...AUCUN, mesuresDuRetenu }); const affichee = ligneAffichee(ligne(), format, { ...AUCUN, mesuresDuRetenu });
assert.equal(affichee.collisionsCumulees, format.nombre(1234));
assert.equal(affichee.diffRedondance, format.nombre(500)); 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 // La différence se lit telle que la façade la rend, jamais refaite
// depuis la cible et la somme. // depuis la cible et la somme.
const telleQuelle = ligneAffichee(ligne(), format, { ...AUCUN, mesuresDuRetenu: { ...mesuresDuRetenu, diffRedondance: 7 } }); const telleQuelle = ligneAffichee(ligne(), format, { ...AUCUN, mesuresDuRetenu: { ...mesuresDuRetenu, diffRedondance: 7 } });
assert.equal(telleQuelle.diffRedondance, format.nombre(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 } }]) { for (const champs of [{}, { nom: null, etat: null, participants: null, illisible: { raison: 'VIDE', chemin: null } }]) {
const affichee = ligneAffichee(ligne(champs), format, AUCUN); const affichee = ligneAffichee(ligne(champs), format, AUCUN);
assert.equal(affichee.collisionsCumulees, VIDE);
assert.equal(affichee.diffRedondance, 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( assert.deepEqual(
COLONNES_DU_RETENU.map(({ colonne }) => colonne), COLONNES_DU_RETENU.map(({ colonne }) => colonne),
['collisionsCumulees', 'diffRedondance'], ['diffRedondance'],
); );
assert.ok(Object.isFrozen(COLONNES_DU_RETENU)); assert.ok(Object.isFrozen(COLONNES_DU_RETENU));
for (const { colonne, cle, explication } of 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(cle).length > 0);
assert.ok(t(explication).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"); 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)', () => { 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); const attente = ligneAffichee(ligne(), format);
assert.equal(attente.enAttente, true); assert.equal(attente.enAttente, true);
assert.equal(attente.collisionsCumulees, VIDE);
assert.equal(attente.diffRedondance, 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); assert.equal(ligneAffichee(ligne(), format, AUCUN).enAttente, false);
const recu = ligneAffichee(ligne(), format, { const recu = ligneAffichee(ligne(), format, {
mesuresDuRetenu: { collisionsCumulees: 12, totalRedondance: 990, cibleRedondance: 1000, diffRedondance: 10 }, mesuresDuRetenu: { collisionsCumulees: 12, totalRedondance: 990, cibleRedondance: 1000, diffRedondance: 10 },
meilleureDuClassement: { id: 3, collisionsCumulees: 9 }, meilleureDuClassement: { id: 3, collisionsCumulees: 9 },
}); });
assert.equal(recu.enAttente, false); assert.equal(recu.enAttente, false);
assert.equal(recu.collisionsCumulees, format.nombre(12)); assert.equal(recu.diffRedondance, format.nombre(10));
assert.deepEqual(recu.meilleure, { id: format.nombre(3), collisions: format.nombre(9) }); 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', () => { 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))); 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 }); 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 } }); 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 } }]) { 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', () => { test('MEILLEURE : le titre et le libellé de l’identifiant du bloc de la génération, et l’explication du titre, qui existent', () => {
const colonne = COLONNES.find(({ nom }) => nom === COLONNE_ANNONCEE);
assert.ok(Object.isFrozen(MEILLEURE)); assert.ok(Object.isFrozen(MEILLEURE));
assert.deepEqual(MEILLEURE, { assert.deepEqual(MEILLEURE, {
titre: 'generation.meilleure.titre', titre: 'generation.meilleure.titre',
explication: 'accueil.explication.meilleure', explication: 'accueil.explication.meilleure',
proposition: 'generation.meilleure.proposition', proposition: 'generation.meilleure.proposition',
collisions: colonne.cle,
explicationCollisions: colonne.explication,
}); });
assert.equal(t(MEILLEURE.titre), 'Meilleure proposition du classement'); 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); 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 affichee = ligneAffichee(ligne(), format, { ...AUCUN, meilleureDuClassement: resumeDuMeilleur(charge) });
const bloc = meilleureProposition(charge); const bloc = meilleureProposition(charge);
if (bloc === null) { if (bloc === null) {
assert.deepEqual(affichee.meilleure, { id: VIDE, collisions: VIDE }); assert.deepEqual(affichee.meilleure, { id: VIDE });
continue; continue;
} }
classees += 1; 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); assert.equal(classees, 2);
}); });

View file

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

View file

@ -2,33 +2,34 @@
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) --> License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
<script> <script>
// Le cadre de mode (§ 8.4, § 8.5, § 14.5) : un trait autour de la surface // La barre, la tête et la surface de travail de l'événement (§ 8.4, § 8.5,
// de travail, dont la forme dit le mode — tirets en lecture, plein en // § 14.5), sans trait autour d'elles : le mode se lit dans la bande de
// écriture, double sur un plan bloqué —, et une barre qui porte, de gauche // mode (BandeMode.svelte), dans le libellé de mode du bandeau, et dans
// à droite, la place fixe, le libellé de mode, puis « Défaire » et // celui de la barre, à côté de la place fixe. data-mode porte le mode —
// « Refaire » (§ 8.3), puis les outils, outils — la suite des états du // lecture, ecriture ou bloque. La barre porte, de gauche à droite, la
// plan —, et les actions, actions — « Imprimer… », calée à droite —, quand // place fixe, le libellé de mode, puis « Défaire » et « Refaire » (§ 8.3),
// ils sont donnés. Outils et actions suivent la place fixe et ne la // puis les outils, outils — la suite des états du plan —, et les actions,
// déplacent donc jamais : un refus posé à côté de la place fixe les // actions — « Imprimer… », calée à droite —, quand ils sont donnés.
// repousse sur la ligne suivante, à côté du libellé, sans ligne de plus. // Outils et actions suivent la place fixe et ne la déplacent donc jamais :
// Une barre de moins de 80rem — la fenêtre de 1 366 px lui en donne // un refus posé à côté de la place fixe les repousse sur la ligne
// davantage — passe les outils sous les commandes et les actions, sur une // suivante, à côté du libellé, sans ligne de plus. Une barre de moins de
// ligne à eux : la barre garde deux lignes là où, dans l'ordre du // 80rem — la fenêtre de 1 366 px lui en donne davantage — passe les outils
// document, elle en prendrait trois. // 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 // 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, // l'événement et la navigation des sections —, puis les annonces,
// annonces, quand il y en a — support rompu et avertissements —, puis la // 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, // surface, children. La barre ouvre le composant, et seule la bande de
// de hauteur constante, le précède dans l'écran de l'événement : un refus // mode, de hauteur constante, le précède dans l'écran de l'événement : un
// posé dans la barre ou la tête, ou un nom qui s'étend sur plusieurs // 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 // lignes, allonge la barre ou la tête sans déplacer la place fixe
// (§ 8.4). Tête, annonces et surface défilent // (§ 8.4). Tête, annonces et surface défilent chacune dans sa zone : des
// chacune dans sa zone : des avertissements nombreux ne poussent pas la // avertissements nombreux ne poussent pas la surface hors du composant, ni
// surface hors du cadre, ni le cadre hors de la fenêtre. La surface porte // le composant hors de la fenêtre. La surface porte le plan, que
// le plan, que l'opérateur regarde : elle réclame les trois quarts du // l'opérateur regarde : elle réclame les trois quarts du composant avant
// cadre avant que les annonces ne dépassent leur première ligne, sauf // que les annonces ne dépassent leur première ligne, sauf devant un
// devant un support rompu, dont les issues restent en vue. // support rompu, dont les issues restent en vue.
// //
// Sous une tête, et hors d'un support rompu, les annonces se posent à // 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 // 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 // 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 // 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 // closes par le filet de la carte. La surface reste sur le fond de la
// page, que son trait contraste (marques.js). // page.
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import Commande from '../primitives/Commande.svelte'; import Commande from '../primitives/Commande.svelte';
import { CONFIRMATION_DEBLOCAGE } from '../primitives/commande.js'; import { CONFIRMATION_DEBLOCAGE } from '../primitives/commande.js';
import Refus from '../primitives/Refus.svelte'; 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(); let { children, tete = null, annonces = null, outils = null, actions = null } = $props();
const t = getContext('t'); const t = getContext('t');
const seance = getContext('seance'); const seance = getContext('seance');
// Sans événement — l'instant où l'écran se retire —, le cadre garde la // Le mode et la clé de son libellé, ceux de la bande. Sans événement —
// forme de la lecture, et n'a pas de libellé. // l'instant où l'écran se retire —, le mode est la lecture, sans libellé.
const cadre = $derived(cadreDeMode($seance)); const libelle = $derived(bandeDeMode($seance));
const forme = $derived(cadre?.forme ?? 'tirets'); const mode = $derived(libelle?.mode ?? 'lecture');
// 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' };
const commande = $derived(commandeFixe($seance)); const commande = $derived(commandeFixe($seance));
const verrou = $derived($seance.mode === 'lecture' ? $seance.verrouAutre : null); const verrou = $derived($seance.mode === 'lecture' ? $seance.verrouAutre : null);
const reprise = $derived(repriseOfferte($seance)); const reprise = $derived(repriseOfferte($seance));
@ -96,7 +94,7 @@
const aCote = $derived(tete !== null && annonces !== null && $seance.supportRompu === null); const aCote = $derived(tete !== null && annonces !== null && $seance.supportRompu === null);
</script> </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="barre" role="group" aria-label={t('evenement.cadre.commandes')}>
<div class="ligne"> <div class="ligne">
<div class="fixe" data-place-fixe> <div class="fixe" data-place-fixe>
@ -115,10 +113,10 @@
<Commande cle="communs.commande.lecture" nom="lecture" executer={() => seance.passerEnLecture()} /> <Commande cle="communs.commande.lecture" nom="lecture" executer={() => seance.passerEnLecture()} />
{/if} {/if}
</div> </div>
{#if cadre !== null} {#if libelle !== null}
<span class="porte-libelle"> <span class="porte-libelle">
<span class="libelle" class:plein={forme === 'plein'} class:double={forme === 'double'} data-mode-libelle="cadre" <span class="libelle" class:ecriture={mode === 'ecriture'} class:bloque={mode === 'bloque'} data-mode-libelle="cadre"
>{t(cadre.cle)}</span >{t(libelle.cle)}</span
> >
</span> </span>
{/if} {/if}
@ -187,29 +185,16 @@
</div> </div>
<style> <style>
/* Le trait du cadre : sa forme dit le mode, sa couleur le redit. */ /* Sans trait : rien autour de la surface ne change d'un mode à l'autre,
/* Une même épaisseur dans les trois formes : la place fixe ne bouge pas et la place fixe ne bouge pas. */
d'un mode à l'autre. */ .corps {
.cadre {
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
flex-direction: column; flex-direction: column;
min-height: 0; min-height: 0;
border: 6px solid var(--couleur-cadre-ecriture);
border-radius: var(--rayon-2);
} }
.cadre.tirets { /* La barre ne prend jamais toute la hauteur du composant : au-delà, ses
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
messages défilent, la place fixe en tête. */ messages défilent, la place fixe en tête. */
.barre { .barre {
display: flex; display: flex;
@ -284,12 +269,12 @@
font-weight: bold; font-weight: bold;
} }
.libelle.plein { .libelle.ecriture {
border-color: var(--couleur-mode-ecriture-filet); border-color: var(--couleur-mode-ecriture-filet);
background-color: var(--couleur-mode-ecriture-fond); background-color: var(--couleur-mode-ecriture-fond);
} }
.libelle.double { .libelle.bloque {
border-color: var(--couleur-mode-bloque-filet); border-color: var(--couleur-mode-bloque-filet);
background-color: var(--couleur-mode-bloque-fond); background-color: var(--couleur-mode-bloque-fond);
} }
@ -320,7 +305,7 @@
font-weight: bold; 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 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. */ des onglets de la navigation se pose sur son filet. */
.tete { .tete {
@ -397,7 +382,7 @@
retrait compris : 7,25rem tiennent entier, dans la place de 36 % d'une 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 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 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 { .place > .annonces {
box-sizing: border-box; box-sizing: border-box;
max-height: 7.25rem; max-height: 7.25rem;
@ -416,7 +401,7 @@
} }
/* Seules — sans tête, en plein écran, ou devant un support rompu —, les /* 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 leur hauteur à la surface : leur facteur de rétrécissement, mille fois
celui de la surface, leur fait absorber presque tout le manque de celui de la surface, leur fait absorber presque tout le manque de
place, jusqu'à leur hauteur minimale : un avertissement d'une ligne en 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 /* 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 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. */ avertissements qui le suivent défilent sous lui. */
.annonces.rompu { .annonces.rompu {
flex-shrink: 0; 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 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, 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). */ 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) --> License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
<script> <script>
// L'en-tête de l'événement ouvert, en tête de la tête du cadre : son nom, // L'en-tête de l'événement ouvert, en tête de la tête (CadreMode.svelte) :
// puis, sur la même ligne tant qu'elle a la place, son résumé — la date // 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 // quand il en a une, les participants, les tables et leurs sièges, les
// tours (resumeEvenement, modele.js) —, d'encre secondaire. Le nom se // 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 // 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 ----------------------------------------------------------------- // --- Les épreuves -----------------------------------------------------------------
describe('la bande de mode (§ 8.4)', () => { describe('la bande de mode et la pleine fenêtre (§ 8.4)', () => {
// 1 920 px dépasse --largeur-page : la colonne de la page n'y touche plus // 1 920 px dépasse --largeur-page, qui borne la liste des événements :
// les bords de la fenêtre, la bande si. // l'écran de l'événement n'y est pas borné.
for (const largeur of [1366, 1920]) { 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); await fixerCadre(largeur, 700);
const m = await neuf(); const m = await neuf();
const hauteurs = []; const hauteurs = [];
@ -163,6 +163,20 @@ describe('la bande de mode (§ 8.4)', () => {
expect([b.left, b.right], mode).toEqual([0, innerWidth]); expect([b.left, b.right], mode).toEqual([0, innerWidth]);
expect(document.documentElement.scrollWidth, mode).toBeLessThanOrEqual(innerWidth); expect(document.documentElement.scrollWidth, mode).toBeLessThanOrEqual(innerWidth);
expect(b.bottom, mode).toBeLessThanOrEqual(boite(cadre(m)).top); 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(cle));
expect(region.textContent, mode).toContain(t(explication)); expect(region.textContent, mode).toContain(t(explication));
expect(region.textContent, mode).toContain(dans(m, '[data-mode-libelle="cadre"]').textContent); 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) --> License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
<script> <script>
// La suite des états du plan (§ 9), dans la barre du cadre : sous le nom // La suite des états du plan (§ 9), dans la barre de l'événement : sous
// « État du plan », que redit un intertitre en petites capitales tant que // le nom « État du plan », que redit un intertitre en petites capitales
// la barre a la place, les // tant que la barre a la place, les quatre états dans l'ordre du § 9
// quatre états dans l'ordre du § 9 (etapesEtat, modele.js), séparés de // (etapesEtat, modele.js), séparés de chevrons cachés à l'assistance.
// chevrons cachés à l'assistance. Chaque étape est la Commande // Chaque étape est la Commande changerEtat de son état, restylée en
// changerEtat de son état, restylée en pilule, avec l'icône de sa pastille // pilule, avec l'icône de sa pastille d'état posée sur le bouton, à
// d'état posée sur le bouton, à gauche de son nom, de la marque de sa // gauche de son nom, de la marque de sa teinte. L'état courant porte en plus le fond de sa teinte, un filet
// 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 // plein et le gras ; les autres, un filet tireté, et restent cliquables
// sous le même nom. // 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, // 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 // § 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 // « 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 // 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 // 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 // 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. // 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 // 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 // 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 // du support rompu. Le texte attendu vient de la même traduction que celle
// de l'application ; les noms d'épreuve sont inventés. // de l'application ; les noms d'épreuve sont inventés.
@ -67,6 +68,7 @@ afterEach(() => {
const dans = (m, selecteur) => m.racine.querySelector(selecteur); const dans = (m, selecteur) => m.racine.querySelector(selecteur);
const tous = (m, selecteur) => [...m.racine.querySelectorAll(selecteur)]; const tous = (m, selecteur) => [...m.racine.querySelectorAll(selecteur)];
const cadre = (m) => dans(m, '[data-mode]'); 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 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 historique = (m, nom) => dans(m, `[data-mode] [data-historique] [data-commande="${nom}"]`);
const boutonEtat = (m, etat) => dans(m, `[data-etat="${etat}"] [data-commande="etat"]`); 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)', () => { describe('le mode sans cadre (§ 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 () => { 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 m = await neuf();
const releves = []; const releves = [];
const relever = () => { const relever = (mode) => {
flushSync(); flushSync();
const boite = cadre(m).getBoundingClientRect(); const boite = cadre(m).getBoundingClientRect();
const surface = dans(m, '[data-section]').getBoundingClientRect(); const surface = dans(m, '[data-section]').getBoundingClientRect();
expect(boite.left).toBeLessThanOrEqual(surface.left); expect(boite.left, mode).toBeLessThanOrEqual(surface.left);
expect(boite.top).toBeLessThanOrEqual(surface.top); expect(boite.top, mode).toBeLessThanOrEqual(surface.top);
expect(boite.right).toBeGreaterThanOrEqual(surface.right); expect(boite.right, mode).toBeGreaterThanOrEqual(surface.right);
expect(boite.bottom).toBeGreaterThanOrEqual(surface.bottom); 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)); const style = getComputedStyle(cadre(m));
expect(Number.parseFloat(style.borderTopWidth)).toBeGreaterThan(0); for (const cote of ['Top', 'Right', 'Bottom', 'Left']) {
expect(style.borderTopStyle).not.toBe('none'); expect(Number.parseFloat(style[`border${cote}Width`]), `${mode} ${cote}`).toBe(0);
}
const libelle = libelleCadre(m); const libelle = libelleCadre(m);
expect(libelle.checkVisibility()).toBe(true); expect(libelle.checkVisibility()).toBe(true);
expect(visible(libelle)).toBe(true); expect(visible(libelle)).toBe(true);
expect(libelle.contains(auCentre(libelle))).toBe(true); expect(libelle.contains(auCentre(libelle))).toBe(true);
expect(libelle.textContent).toBe(libelleBandeau(m).textContent); 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(); await m.seance.passerEnEcriture();
relever(); relever('ecriture');
await m.seance.executer('changerEtat', { etat: 'bloque' }); await m.seance.executer('changerEtat', { etat: 'bloque' });
relever(); relever('bloque');
expect(releves.map(({ texte }) => texte)).toEqual([ expect(releves).toEqual([t('coquille.mode.lecture'), t('coquille.mode.ecriture'), t('coquille.mode.bloque')]);
t('coquille.mode.lecture'),
t('coquille.mode.ecriture'),
t('coquille.mode.bloque'),
]);
expect(new Set(releves.map(({ trait }) => trait)).size).toBe(3);
// Une surface plus haute que la fenêtre : la page et la surface // 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. // défilées jusqu'en bas, le libellé garde sa boîte.
const avant = libelleCadre(m).getBoundingClientRect().toJSON(); const avant = libelleCadre(m).getBoundingClientRect().toJSON();

View file

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

View file

@ -2,8 +2,8 @@
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) // 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 // 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 // l'état observable de la séance (§ 8.4, § 8.5, § 8.8, § 8.9, § 9). La
// de mode et sa forme, la commande de la place fixe, les états du plan et // commande de la place fixe, les états du plan et
// leurs clés, la reprise de verrou que l'écran offre, et les avertissements // 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 // 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 // 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 { modelePastille } from '../primitives/pastille.js';
import { etatTable } from '../tables/modele.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 * 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 * 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 })); 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 // Le mode de la bande, son explication et son icône, pour chaque clé du
// cadre : la bande dit ce que le cadre dit. // 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([ const BANDES = new Map([
['tirets', { mode: 'lecture', explication: 'evenement.bande.lecture', icone: 'oeil' }], ['coquille.mode.lecture', { mode: 'lecture', explication: 'evenement.bande.lecture', icone: 'oeil' }],
['plein', { mode: 'ecriture', explication: 'evenement.bande.ecriture', icone: 'stylo' }], ['coquille.mode.ecriture', { mode: 'ecriture', explication: 'evenement.bande.ecriture', icone: 'stylo' }],
['double', { mode: 'bloque', explication: 'evenement.bande.bloque', icone: 'cadenas' }], ['coquille.mode.bloque', { mode: 'bloque', explication: 'evenement.bande.bloque', icone: 'cadenas' }],
]); ]);
/** /**
* La bande de mode, sous le bandeau (§ 8.4) : mode, 'lecture', 'ecriture' ou * 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é * 'bloque' — un plan bloqué l'est quel que soit le mode — ; cle, celle du
* du cadre (cadreDeMode) ; explication, la clé de la phrase qui le suit ; * 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 * 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 * de la place fixe (commandeFixe) ; devant un format plus récent, qui n'en
* offre aucune, plan bloqué compris, elle est evenement.bande.lectureSeule, * 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}} * @returns {null|{mode: 'lecture'|'ecriture'|'bloque', cle: string, explication: string, icone: string}}
*/ */
export function bandeDeMode(etatSeance) { export function bandeDeMode(etatSeance) {
const cadre = cadreDeMode(etatSeance); const cle = cleDeMode(etatSeance);
if (cadre === null) return null; if (cle === null) return null;
const { mode, explication, icone } = BANDES.get(cadre.forme); const { mode, explication, icone } = BANDES.get(cle);
const phrase = commandeFixe(etatSeance) === 'format' ? 'evenement.bande.lectureSeule' : explication; 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) // © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) // 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 // Le modèle de l'événement ouvert (§ 8.4, § 8.5, § 8.8, § 8.9, § 9) : la
// de mode et sa forme, la commande de la place fixe, les états du plan et // commande de la place fixe, les états du plan et
// leurs clés, la reprise de verrou offerte, et les avertissements ordonnés, // 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 // 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 // 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 { TABLES_APPLICATION } from '../../application/libelles.js';
import { creerTraduction } from '../../application/traduction.js'; import { creerTraduction } from '../../application/traduction.js';
import { TABLES_INTERFACE } from '../libelles.js'; import { TABLES_INTERFACE } from '../libelles.js';
import { cleDeMode } from '../mode.js';
import { import {
ETATS_PLAN, ETATS_PLAN,
avertissementsOrdonnes, avertissementsOrdonnes,
bandeDeMode, bandeDeMode,
cadreDeMode,
cleEtat, cleEtat,
commandeFixe, commandeFixe,
etapesEtat, 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)', () => { describe('commandeFixe (§ 8.4, § 8.5, § 8.8)', () => {
test('null sans événement ; modifier en lecture ; lecture en écriture ; debloquer sur un plan bloqué', () => { test('null sans événement ; modifier en lecture ; lecture en écriture ; debloquer sur un plan bloqué', () => {
assert.equal(commandeFixe(etat({ plan: null })), null); 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); 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' })), { assert.deepEqual(bandeDeMode(etat({ mode: 'lecture' })), {
mode: 'lecture', mode: 'lecture',
cle: 'coquille.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' })]; 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)); const explications = etats.map((e) => t(bandeDeMode(e).explication));
for (const texte of explications) assert.notEqual(texte.trim(), ''); for (const texte of explications) assert.notEqual(texte.trim(), '');
assert.equal(new Set(explications).size, 3); 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)); 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 = [ const attendus = [
[etat({ refus: FORMAT_REFUS }), { mode: 'lecture', cle: 'coquille.mode.lecture', icone: 'oeil' }], [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' }], [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 { estBrouillon, piedDePage, refusImpression } from '../../application/sorties.js';
import { constante } from '../../geometrie/constantes.js'; import { constante } from '../../geometrie/constantes.js';
import { DECOUPES, VUES_IMPRESSION } from '../etat_ui.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 Commande from '../primitives/Commande.svelte';
import Papier from '../sorties/Papier.svelte'; import Papier from '../sorties/Papier.svelte';
import FeuillesTables from './FeuillesTables.svelte'; import FeuillesTables from './FeuillesTables.svelte';
@ -74,7 +74,7 @@
); );
const feuille = $derived(feuilleDe(pied, estBrouillon(charge, { source: sortie.source, tour: sortie.tour }))); const feuille = $derived(feuilleDe(pied, estBrouillon(charge, { source: sortie.source, tour: sortie.tour })));
const Vue = $derived(VUES.get(options.vue)); const Vue = $derived(VUES.get(options.vue));
const cadre = $derived(cadreDeMode($seance)); const cleMode = $derived(cleDeMode($seance));
const tours = $derived(toursOfferts(charge.evenement.tours)); const tours = $derived(toursOfferts(charge.evenement.tours));
// La bande mesurée sur le gabarit, null avant la première mesure ; ce qui // 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="barre" role="group" aria-label={t('impression.barre')}>
<div class="tete"> <div class="tete">
<h2>{t('impression.titre')}</h2> <h2>{t('impression.titre')}</h2>
{#if cadre !== null} {#if cleMode !== null}
<span class="mode" data-mode-libelle="cadre">{t(cadre.cle)}</span> <span class="mode" data-mode-libelle="cadre">{t(cleMode)}</span>
{/if} {/if}
</div> </div>
<div class="groupe vues" role="group" aria-label={t('impression.vue.titre')}> <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 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, 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 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 lisent les pastilles ; la bande de mode, un fond, un filet et une
marque étant la teinte du cadre de mode ; les messages — avis, refus, teinte par mode, celle de son icône ; les messages — avis, refus,
information —, chacun son encre, un fond, un filet et une marque. 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 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-fond: #f3f0fc;
--couleur-appartenance-6-marque: #8f6dd5; --couleur-appartenance-6-marque: #8f6dd5;
/* Cadre de mode (§ 8.4), et bande de mode : fond et filet ; sa marque est /* Bande de mode (§ 8.4) : la teinte de chaque mode, que porte son icône,
la teinte du cadre. */ puis son fond et son filet. */
--couleur-cadre-lecture: #5c6878; --couleur-cadre-lecture: #5c6878;
--couleur-cadre-ecriture: #2e7d32; --couleur-cadre-ecriture: #2e7d32;
--couleur-cadre-bloque: #7b3fa0; --couleur-cadre-bloque: #7b3fa0;

View file

@ -74,18 +74,10 @@ export const TABLE = new Map([
['accueil.colonne.participants', 'Participants'], ['accueil.colonne.participants', 'Participants'],
['accueil.colonne.modification', 'Dernière modification'], ['accueil.colonne.modification', 'Dernière modification'],
['accueil.colonne.chemin', 'Fichier'], ['accueil.colonne.chemin', 'Fichier'],
// Les deux mesures du placement retenu (§ 5.4, § 8.9), et l'explication // La mesure du placement retenu (§ 5.4, § 8.9), et l'explication que
// que l'infobulle de leur intitulé donne : ce que compte le nombre, d'où // l'infobulle de son intitulé donne : ce que compte le nombre, d'où il
// il vient, et quand il vaut « — ». // vient, et quand il vaut « — ».
['accueil.colonne.collisionsCumulees', 'Collisions cumulées (paire et tour)'],
['accueil.colonne.diffRedondance', "Diff somme d'appartenance"], ['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', '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 ' + '|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, // La meilleure proposition du classement de chaque événement (§ 5.4,
// § 5.7) : son titre, l'identifiant et les collisions s'écrivent sous les // § 5.7) : son titre et son identifiant s'écrivent sous les libellés du
// libellés du bloc de la génération ; l'explication du titre est propre à // bloc de la génération ; l'explication du titre est propre à la liste,
// la liste, qui l'écrit « — » quand rien n'est classé. // qui l'écrit « — » quand rien n'est classé.
[ [
'accueil.explication.meilleure', '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 : " + "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. ' + '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 " + "« \u{2014} » quand aucune proposition n'est classée : aucune proposition, ou aucune qui décrive encore la " +
'configuration.', 'configuration.',

View file

@ -2,7 +2,7 @@
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) // 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é, // 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 // sa navigation, la reprise d'un verrou, ses avertissements, le support
// rompu (§ 8.3 à § 8.9, § 9, § 14.6). Chaque clé commence par // 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 // « 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.placer', 'Placer'],
['evenement.phase.produire', 'Produire'], ['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.cadre.commandes', 'Commandes de l\u{2019}événement'],
['evenement.commande.defaire', 'Défaire'], ['evenement.commande.defaire', 'Défaire'],
['evenement.commande.refaire', 'Refaire'], ['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 /** 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 * 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 * d'un message sur son fond ; l'icône de la bande de mode, de la teinte de
* cadre de mode, sur le fond de la bande ; le fond de l'action principale, * son mode, sur le fond de la bande ; le fond de l'action principale,
* qui délimite le bouton, sur les surfaces. */ * qui délimite le bouton, sur les surfaces. */
export const SIGNES = Object.freeze([ export const SIGNES = Object.freeze([
...TEINTES_DE_PASTILLE.map(([nom, teinte]) => marque(nom, `${teinte}-marque`, [`${teinte}-fond`, ...SURFACES])), ...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 // La sélection marque aussi une colonne du tableau des propositions, sur la
// carte, son fond sourd et la ligne du retenu. // 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]), 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]), 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 // Le mobilier sur le sol : le trait de la table, celui de la chaise, qui
// touche aussi le plateau. // touche aussi le plateau.

View file

@ -17,7 +17,7 @@ const THEMES = ['clair', 'sombre'];
const NOMS_MARQUES = [ const NOMS_MARQUES = [
'cadenasFerme', 'cadenasOuvert', 'anneau', 'hachure', 'remplissagePlein', 'cadenasFerme', 'cadenasOuvert', 'anneau', 'hachure', 'remplissagePlein',
'contourConflit', 'trameAppartenance', 'trameVoisins', 'trameTable', 'pastille', 'poignee', 'gel', 'miseEnEvidence', 'contourConflit', 'trameAppartenance', 'trameVoisins', 'trameTable', 'pastille', 'poignee', 'gel', 'miseEnEvidence',
'selection', 'cadreLecture', 'cadreEcriture', 'cadreBloque', 'focus', 'selection', 'focus',
'contourTable', 'contourChaise', 'echelle', 'fantome', 'chargement', 'filigrane', 'contourTable', 'contourChaise', 'echelle', 'fantome', 'chargement', 'filigrane',
]; ];
@ -39,7 +39,7 @@ const FONDS_REQUIS = {
const parNom = (registre, nom) => registre.find((e) => e.nom === nom); const parNom = (registre, nom) => registre.find((e) => e.nom === nom);
describe('registre des marques', () => { 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.equal(MARQUES.length, NOMS_MARQUES.length);
assert.deepEqual(MARQUES.map((m) => m.nom), NOMS_MARQUES); assert.deepEqual(MARQUES.map((m) => m.nom), NOMS_MARQUES);
assert.equal(new Set(MARQUES.map((m) => m.nom)).size, MARQUES.length); 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.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); 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']) { for (const mode of ['Ecriture', 'Lecture', 'Bloque']) {
const signe = parNom(SIGNES, `bande${mode}`); const signe = parNom(SIGNES, `bande${mode}`);
assert.deepEqual([signe.jeton, ...signe.fonds], [`--couleur-cadre-${mode.toLowerCase()}`, `--couleur-mode-${mode.toLowerCase()}-fond`]); 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 // 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 // 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. // 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. // La clé de chaque annonce de format de la séance.

View file

@ -1,9 +1,9 @@
// © 2026 TechnoLibre (http://www.technolibre.ca) // © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) // 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 // La clé du libellé de mode (§ 8.4, § 8.5), que lisent le bandeau, la bande
// cadre de mode : aucune sans événement ; le plan bloqué avant le mode, qu'il // de mode et la barre de l'événement : aucune sans événement ; le plan
// force en lecture ; sinon le mode de la séance. Et la clé de l'annonce de // 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. // format du bandeau (§ 18.6), une par annonce de la séance.
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { describe, test } from '../../test/lanceur.js'; 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 plat = (texte) => texte.replace(/\s+/g, ' ').trim();
const texteDe = (element) => plat(element.textContent); 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. // Le texte de l'état et du journal de l'événement ouvert, et sa révision.
function fichiers() { function fichiers() {
return { return {
@ -832,10 +850,10 @@ describe('le parcours par gestes, depuis un répertoire neuf', { timeout: DUREE
expect(charge().participants).toHaveLength(0); 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'); await ouvrirSection('tables');
const avant = fichiers(); const avant = fichiers();
expect(dans('[data-mode]').dataset.marque).toBe('cadreLecture'); exigerModeSansCadre('lecture');
await cliquer('[data-section="tables"] [data-commande="creerTableRonde"]'); await cliquer('[data-section="tables"] [data-commande="creerTableRonde"]');
await refusPres(() => dans('[data-commande="creerTableRonde"]').parentElement, 'MODE_LECTURE', 'modifier', 'créer une table'); await refusPres(() => dans('[data-commande="creerTableRonde"]').parentElement, 'MODE_LECTURE', 'modifier', 'créer une table');
expect(document.querySelectorAll(FENETRE)).toHaveLength(0); 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 cliquer('[data-place-fixe] [data-commande="modifier"]');
await attendre('l’écriture', () => etat().mode === 'ecriture' && dans('[data-place-fixe] [data-commande="lecture"]')); await attendre('l’écriture', () => etat().mode === 'ecriture' && dans('[data-place-fixe] [data-commande="lecture"]'));
const cadre = dans('[data-mode]'); expect(visible(dans('[data-mode]'))).toBe(true);
expect(cadre.dataset.marque).toBe('cadreEcriture'); exigerModeSansCadre('ecriture');
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));
const r = revision(); const r = revision();
await cliquer('[data-section="tables"] [data-commande="creerTableRonde"]'); 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. // 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 cliquer('[data-place-fixe] [data-commande="lecture"]');
await attendre('la lecture', () => etat().mode === 'lecture'); await attendre('la lecture', () => etat().mode === 'lecture');
expect(dans('[data-mode]').dataset.marque).toBe('cadreLecture'); exigerModeSansCadre('lecture');
expect(visible(dans('[data-mode-libelle="cadre"]'))).toBe(true);
expect(dans('[data-bandeau]').textContent).toContain(MARQUE_PROVENANCE); expect(dans('[data-bandeau]').textContent).toContain(MARQUE_PROVENANCE);
const empreinte = serialiserCharge(charge()); const empreinte = serialiserCharge(charge());
const avant = fichiers(); 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 cliquer('[data-etat="bloque"] [data-commande="etat"]');
await uneEntree(r, 'bloquer'); await uneEntree(r, 'bloquer');
expect(charge().evenement.etat).toBe('bloque'); expect(charge().evenement.etat).toBe('bloque');
expect(dans('[data-mode]').dataset.marque).toBe('cadreBloque'); exigerModeSansCadre('bloque');
const empreinte = serialiserCharge(charge()); const empreinte = serialiserCharge(charge());
const avant = fichiers(); const avant = fichiers();
const rang = m.trace.length; const rang = m.trace.length;

View file

@ -2,8 +2,8 @@
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) --> License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
<script> <script>
// Le bandeau du plan (§ 7.5), au-dessus du <svg data-plan> et dans le // Le bandeau du plan (§ 7.5), au-dessus du <svg data-plan> et dans la
// cadre de mode : vide tant qu'une table au moins est saine ; quand toutes // 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 // sont en conflit, la phrase, puis ce que le diagnostic prouve — chaque
// entrée {cle, details} du modèle (modele.js), traduite. L'appartenance // 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, // 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. // pas rognée par son défilement.
// //
// En plein écran, le titre et les panneaux s'effacent : le plan prend la // 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 // surface de travail que la bande de mode et la barre de l'événement lui
// cadre l'entoure toujours (§ 8.4). // laissent sous le bandeau (§ 8.4, § 18.6).
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { diagnostiquerCharge } from '../../application/diagnostic.js'; import { diagnostiquerCharge } from '../../application/diagnostic.js';
import { etatPlaces } from '../../application/places.js'; import { etatPlaces } from '../../application/places.js';
@ -75,8 +75,8 @@
</section> </section>
<style> <style>
/* La section prend la hauteur de la surface du cadre de mode : le plan /* La section prend la hauteur de la surface de travail de l'événement :
s'y étend, et c'est la surface du cadre qui défile, jamais la page. */ le plan s'y étend, et c'est cette surface qui défile, jamais la page. */
.section-plan { .section-plan {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -833,7 +833,7 @@ describe('le plein écran (§ 8.4, § 18.6)', () => {
expect(boite(surface(m))).toEqual(avant); 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(); const m = await petite();
await deuxImages(); await deuxImages();
const avant = boite(dans(m, '[data-mode]')); const avant = boite(dans(m, '[data-mode]'));

View file

@ -460,13 +460,17 @@ function releverEtats(m) {
return etats; return etats;
} }
// Le cadre de mode : sa signature, son libellé compris. // Le mode de l'événement : sig, la signature de la bande de mode et du
function signatureCadre(racine) { // libellé de mode de la barre ; trait, la somme des épaisseurs des bords de
const cadre = racine.querySelector('[data-mode]'); // la surface de travail, qui n'en porte aucun. null hors d'un événement.
if (cadre === null) return null; function signatureMode(racine) {
const s = getComputedStyle(cadre); const surface = racine.querySelector('[data-mode]');
const libelle = racine.querySelector('[data-mode-libelle="cadre"]')?.textContent.trim() ?? ''; const bande = racine.querySelector('[data-ecran="evenement"] > [role="status"]');
return `bords ${s.borderTopStyle} ${s.borderTopWidth} | ${libelle}`; 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 /** 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 /** 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, * 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) } = {}) { export function fautesBandeau(racine, mode, { version = VERSION.affichee, provenance = provenanceVisible(PROVENANCE, tTemoin) } = {}) {
const bandeaux = racine.querySelectorAll('[data-bandeau]'); const bandeaux = racine.querySelectorAll('[data-bandeau]');
if (bandeaux.length !== 1) return [`${bandeaux.length} bandeaux`]; 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`); if (libelle !== null) fautes.push(`libellé de mode « ${libelle.textContent.trim()} » sans événement ouvert`);
return fautes; 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})`); 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)}`); 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 (barre === null) fautes.push(`libellé de mode absent de la barre (${mode})`);
if (libelle !== null && cadre !== null && libelle.textContent.trim() !== cadre.textContent.trim()) { if (libelle !== null && barre !== null && libelle.textContent.trim() !== barre.textContent.trim()) {
fautes.push(`bandeau « ${libelle.textContent.trim()} », cadre « ${cadre.textContent.trim()} »`); fautes.push(`bandeau « ${libelle.textContent.trim()} », barre « ${barre.textContent.trim()} »`);
} }
return fautes; 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.refus = releverRefus(racine).map((r) => ({ ...r, restes: horsMarques(r.texte).filter((x) => !estDonnee(x, connues)) }));
releve.etats = releverEtats(m); releve.etats = releverEtats(m);
releve.listes = releverListes(racine); releve.listes = releverListes(racine);
releve.cadre = signatureCadre(racine); releve.signatureMode = signatureMode(racine);
releve.clair = releverTheme(racine); releve.clair = releverTheme(racine);
await cliquer(m, '[data-bandeau] [data-commande="theme"][data-theme-choix="sombre"]'); await cliquer(m, '[data-bandeau] [data-commande="theme"][data-theme-choix="sombre"]');
await attendre('le thème sombre', () => document.documentElement.getAttribute('data-theme') === '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'); 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'); const plan = atteints().find((r) => r.nom === 'evenement.plan.ecriture');
expect(plan, 'le plan en écriture relevé').toBeDefined(); expect(plan, 'le plan en écriture relevé').toBeDefined();
const e = plan.etats; 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(); expect(trois, 'le plan des trois sortes de conflit relevé').toBeDefined();
const [a, b, c] = CONFLITS.map(({ marque }) => trois.etats.trames[marque]); 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)); 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}`); 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 (const c of modes) if (c.sig === null) fautes.push(`${c.ou} : absent`);
for (let i = 0; i < cadres.length; i += 1) { 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 j = i + 1; j < cadres.length; j += 1) fautes.push(...fautesPaire(`${cadres[i].ou} / ${cadres[j].ou}`, [cadres[i]], [cadres[j]])); 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'); 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'); 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); 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'); 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-liste-travail', 'plan : une liste de travail', ['pressentis', 'titres']],
['data-marque', 'une marque porteuse d’information, par son nom au registre', null], ['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', '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-id', 'l’identifiant de la meilleure proposition', ['']],
['data-meilleure-libelle-id', 'génération : le libellé de l’identifiant de la meilleure', ['']], ['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-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-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-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-mode-libelle', 'le libellé de mode', ['bandeau', 'cadre']],
['data-motif', 'import : le motif d’une ligne refusée', ['']], ['data-motif', 'import : le motif d’une ligne refusée', ['']],
['data-multiligne', 'import : une ligne sur plusieurs lignes de texte', null], ['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-pastille', 'plan : la pastille d’une table', null],
['data-phrase', 'une phrase : diagnostic ou défaut des tables', ['']], ['data-phrase', 'une phrase : diagnostic ou défaut des tables', ['']],
['data-pied', 'aperçu : le pied d’une feuille imprimée', ['']], ['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-place-vide', 'aperçu : une place vide de la feuille par table', ['']],
['data-plan', 'plan : le <svg> du plan', ['']], ['data-plan', 'plan : le <svg> du plan', ['']],
['data-poignee', 'plan : la poignée d’une table', null], ['data-poignee', 'plan : la poignée d’une table', null],

View file

@ -424,7 +424,7 @@ export const ECRANS = Object.freeze([
// « chargement ». // « chargement ».
const m = await contexte.monter({ fichiers: [fichierEtat(chargePrincipale()), fichierEtat(chargeGelee(), 'gelee')] }); const m = await contexte.monter({ fichiers: [fichierEtat(chargePrincipale()), fichierEtat(chargeGelee(), 'gelee')] });
await attendre('le signe du chargement dans chaque valeur de résumé', () => 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; return m.racine;
}, },
@ -433,8 +433,8 @@ export const ECRANS = Object.freeze([
nom: 'accueil.mesuresRetenu', nom: 'accueil.mesuresRetenu',
mode: null, mode: null,
async atteindre(contexte) { async atteindre(contexte) {
// La principale, sa proposition retenue : la ligne montre les deux // La principale, sa proposition retenue : la ligne montre la
// mesures du retenu et la meilleure proposition du classement, et le // 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 // 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. // colonne du tableau, une fois pour la liste — ouvre son infobulle.
const principale = chargePrincipale(); const principale = chargePrincipale();
@ -442,12 +442,8 @@ export const ECRANS = Object.freeze([
const m = await contexte.monter({ fichiers: [fichierEtat(charge)] }); const m = await contexte.monter({ fichiers: [fichierEtat(charge)] });
const ligne = `[data-evenement="${BASE}"]`; const ligne = `[data-evenement="${BASE}"]`;
await chargerResumes(m); await chargerResumes(m);
await attendre('les mesures du retenu et la meilleure proposition', () => 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 ?? '')),
...['collisionsCumulees', 'diffRedondance'].map((colonne) => `[data-colonne="${colonne}"]`),
'[data-meilleure-id]',
'[data-meilleure-collisions]',
].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"]')); const { x, y } = await centre(await element(m, '[data-ecran="accueil"] thead [data-terme="diffRedondance"]'));
await pointer('bouger', x, y); await pointer('bouger', x, y);