diff --git a/GUIDE-USAGE.md b/GUIDE-USAGE.md index ead44e5..d62f0c4 100644 --- a/GUIDE-USAGE.md +++ b/GUIDE-USAGE.md @@ -23,8 +23,9 @@ fichiers, lisez `GUIDE-WINDOWS.md`. ## Avant de commencer : lecture, écriture, et trois garanties **Tout événement s’ouvre en lecture.** Rien ne s’y modifie tant que vous n’avez -pas cliqué sur « Modifier », toujours à la même place, en haut de l’écran. Un -cadre autour du plan et le bandeau disent dans quel mode vous êtes. Après un +pas cliqué sur « Modifier », toujours à la même place, en haut de l’écran. La +bande de couleur sous le bandeau, le libellé à côté de « Modifier » et le +bandeau disent dans quel mode vous êtes. Après un moment d’inactivité, le logiciel revient seul en lecture, entre deux gestes et jamais au milieu de l’un d’eux. @@ -71,10 +72,10 @@ s’affiche à côté du bouton, et propose « Modifier ». ![Un ajout de table refusé en lecture, et la commande « Modifier »](doc/captures/01b-passer-en-ecriture.png) *Le refus apparaît là où vous avez cliqué, sans fenêtre. Le mot « Lecture » -figure dans le bandeau et sur le cadre de l’événement.* +figure dans le bandeau, dans la bande de mode et à côté de « Modifier ».* -Cliquez sur « Modifier » : le cadre passe en écriture, et le même geste -réussit. « Revenir en lecture » fait l’inverse. +Cliquez sur « Modifier » : la bande et le libellé passent en écriture, et le +même geste réussit. « Revenir en lecture » fait l’inverse. ## Étape 2 — Importer la liste des participants @@ -306,8 +307,8 @@ réservations imposent de ce que la recherche a choisi. proposition alors que le retenu a été ajusté à la main détruit ces ajustements : une fenêtre le dit avant. -La liste des événements résume ensuite le retenu par ses collisions cumulées et -par l’écart de sa somme de redondance d’appartenance à la cible de la soirée. +La liste des événements résume ensuite le retenu par l’écart de sa somme de +redondance d’appartenance à la cible de la soirée. Retenir n’est pas obligatoire, mais tout ce qui s’imprime sans placement retenu porte la mention BROUILLON (étape 12). @@ -329,9 +330,10 @@ Les indicateurs du tableau des propositions suivent l’ajustement. En lecture, aucun de ces gestes ne passe : -![Le plan en lecture : le cadre, son libellé, et un geste refusé](doc/captures/20-mode-lecture.png) +![Le plan en lecture : la bande de mode, le libellé, et un geste refusé](doc/captures/20-mode-lecture.png) -*Le cadre et le libellé « Lecture » entourent le plan, et le bandeau le répète. +*La bande de mode et le libellé « Lecture » le disent au-dessus du plan, et le +bandeau le répète. Glisser une personne ou une table, tirer une poignée, poser une réservation : chaque geste est refusé avec « Modifier » pour remède, et rien ne change sur le disque.* diff --git a/spec.md b/spec.md index ac43e9f..1d69cc7 100644 --- a/spec.md +++ b/spec.md @@ -178,8 +178,8 @@ propositions sur le même axe — celui du **manque** (§ 12.10) —, retient ce qu'il préfère. Le logiciel sépare ce que le moteur a décidé de ce que les réservations imposent (§ 5.4). Retenir fait passer le plan à l'état *Retenu* (§ 9) ; les propositions non retenues restent disponibles. Le retenu se résume -ensuite dans la liste des événements, par ses collisions cumulées et la -différence absolue de sa somme de redondance à la cible de l'événement (§ 8.1). +ensuite dans la liste des événements, par la différence absolue de sa somme de +redondance à la cible de l'événement (§ 8.1). **11. Ajuster à la main.** *Facultative, reprenable.* Le même geste qu'à l'étape 7 déplace maintenant une personne réelle dans le placement retenu. **Le @@ -1265,24 +1265,24 @@ pastille de l'état et son compte —, ou ne les groupe pas, au choix ; ses en-têtes de colonnes se groupent en « Événement », « Placement retenu » et « Meilleure proposition du classement », et le nom de chaque ligne en est l'en-tête, la date et le chemin en petite ligne dessous. Sous le tableau, des -cartes offrent la création, le dossier de travail et les démonstrations. Deux -colonnes résument en plus le **placement retenu** : « Collisions cumulées (paire -et tour) », les collisions cumulées du retenu (§ 5.4), et « Diff somme -d'appartenance », |cible − Σ r(p)|, où Σ r(p) est la somme de redondance du -retenu — le nombre de la colonne « Redondance d'appartenance, somme Σ r(p) » du -tableau des propositions — et la cible celle de l'événement (§ 8.9), 1000 par -défaut, réglable dans la section des propositions. Les deux se lisent dans la -même mesure que le tableau, sans rien écrire ; un événement sans retenu, ou dont -le retenu est fautif ou en dérive (§ 8.9, § 9), y porte « — ». Chaque ligne -porte aussi la **« Meilleure proposition du classement »**, sous les libellés du -bloc que la section de la génération montre en tête : l'identifiant de la -proposition au rang 1 du classement (§ 5.7) et ses collisions cumulées — la même -proposition et le même nombre que ce bloc et que la première ligne du tableau -des propositions, retenue ou non ; une proposition fautive, que l'ouverture -écarte (§ 8.9, point 3), n'y entre pas. Sans proposition classée — aucune, ou -aucune qui décrive encore la configuration —, l'un et l'autre valent « — ». -Chaque intitulé s'explique dans une infobulle, posée sur l'en-tête de sa -colonne. +cartes offrent la création, le dossier de travail et les démonstrations. Une +colonne résume en plus le **placement retenu** : « Diff somme d'appartenance », +|cible − Σ r(p)|, où Σ r(p) est la somme de redondance du retenu — le nombre de +la colonne « Redondance d'appartenance, somme Σ r(p) » du tableau des +propositions — et la cible celle de l'événement (§ 8.9), 1000 par défaut, +réglable dans la section des propositions. Elle se lit dans la même mesure que +le tableau, sans rien écrire ; un événement sans retenu, ou dont le retenu est +fautif ou en dérive (§ 8.9, § 9), y porte « — ». Chaque ligne porte aussi la +**« Meilleure proposition du classement »**, sous les libellés du bloc que la +section de la génération montre en tête : l'identifiant de la proposition au +rang 1 du classement (§ 5.7) — la même proposition que ce bloc et que la +première ligne du tableau des propositions, retenue ou non ; une proposition +fautive, que l'ouverture écarte (§ 8.9, point 3), n'y entre pas. Sans +proposition classée — aucune, ou aucune qui décrive encore la configuration —, +elle vaut « — ». La liste ne montre les collisions cumulées ni du retenu ni de +la meilleure proposition : le tableau des propositions et le bloc de la +génération les portent. Chaque intitulé s'explique dans une infobulle, posée +sur l'en-tête de sa colonne. **La liste paraît avant ses colonnes calculées** : ses lignes s'affichent dès le relevé du dossier, et les mesures du retenu et la meilleure proposition, qui évaluent chaque événement, se calculent ensuite, un événement après l'autre, la @@ -1406,30 +1406,31 @@ ou refusé, une saisie validée ou abandonnée, une fenêtre à confirmer referm et tout appui relâché sur le plan — un clic qui n'exécute rien et un déplacement de vue compris. Regarder sans appuyer n'en est pas une : zoomer à la molette, une commande de vue, changer de tour affiché ne relancent rien. Le retour -automatique se voit au cadre, au libellé, à la bande de mode et au bandeau, et -un avertissement non modal le dit jusqu'au passage en écriture suivant. +automatique se voit à la bande de mode, au libellé de mode et au bandeau, et un +avertissement non modal le dit jusqu'au passage en écriture suivant. -**Le mode est visible sur la surface de travail elle-même** — un cadre et un -libellé permanents autour du plan — et non dans un coin de barre d'outils : -l'opérateur regarde le plan. +**Le mode est visible sans quitter la surface de travail des yeux**, et non dans +un coin de barre d'outils : l'opérateur regarde le plan. Sous le bandeau, quand +un plan est ouvert, une **bande de mode** court de bord à bord de la fenêtre et +dit le mode : son icône, son libellé, et une phrase qui dit ce qu'il permet en +nommant la commande de la place fixe qui en sort — « Modifier » en lecture, +« Revenir en lecture » en écriture, « Débloquer » sur un plan bloqué, et aucune +devant un fichier d'un format plus récent (§ 8.8), qui n'en offre pas —, sur le +fond de sa couleur. D'une hauteur constante, elle le dit dès l'entrée dans +l'écran. Le **libellé de mode**, une pastille à côté de la place fixe, le redit +là où la main agit. **Aucun cadre n'entoure la surface de travail** : l'écran de +l'événement prend **toute la largeur de la fenêtre**, à son retrait près, sans +la largeur de page qui borne la liste des événements, et le plan garde chaque +pixel que la bande et la barre lui laissent. S'y ajoute un **bandeau d'application présent sur tout écran**, qui porte la version et la provenance de la construction (§ 18.6) et, **quand un plan est -ouvert**, le libellé de mode. Les deux ne font pas double emploi : le cadre -signale le mode là où la main agit ; le bandeau porte la version jusque sur la -liste des événements, qui est le premier écran d'un signalement sur deux. Le -bandeau porte aussi la marque du logiciel, le réglage de thème et « À propos » ; -il garde une hauteur d'une ligne de commande, de sorte que la surface de travail -ne perd rien. - -Sous le bandeau, quand un plan est ouvert, une **bande de mode** court de bord à -bord de la fenêtre et redit le mode : son icône, son libellé, et une phrase qui -dit ce qu'il permet en nommant la commande de la place fixe qui en sort — -« Modifier » en lecture, « Revenir en lecture » en écriture, « Débloquer » sur -un plan bloqué, et aucune devant un fichier d'un format plus récent (§ 8.8), qui -n'en offre pas —, sur le fond de sa couleur. D'une hauteur constante, elle -s'ajoute au cadre sans le remplacer : le cadre reste là où la main agit, la -bande le dit dès l'entrée dans l'écran. +ouvert**, le libellé de mode. Ils ne font pas double emploi : la bande et le +libellé de la barre signalent le mode dans l'écran de l'événement ; le bandeau +porte la version jusque sur la liste des événements, qui est le premier écran +d'un signalement sur deux. Le bandeau porte aussi la marque du logiciel, le +réglage de thème et « À propos » ; il garde une hauteur d'une ligne de commande, +de sorte que la surface de travail ne perd rien. **Le passage en écriture est une étape nommée du parcours** (§ 2.1, étape 1b), et non un sous-entendu : huit des douze étapes sont modifiantes, et toutes sont @@ -3357,7 +3358,7 @@ des fonctions pures du module de géométrie. | tenir hors ligne | se compile en JavaScript ordinaire ; Vite est une dépendance de développement qui ne survit pas à la construction | | rester rapide | compile des mises à jour fines, sans arbre virtuel ni mémoïsation à poser à la main | | se tester sans navigateur | n'impose d'écrire aucun calcul dans un composant ; le composant ne contient que le câblage | -| être joli sans y passer des semaines | un fichier de jetons — couleurs, rayons, ombres, échelle typographique, cible principale, largeur de page — décliné en deux thèmes ; ses familles de couleur : la page, le bandeau, la carte, l'action principale, les quatre états du plan, le neutre, les appartenances, la bande et le cadre de mode, les messages (avis, refus, information), le plan, le papier | +| être joli sans y passer des semaines | un fichier de jetons — couleurs, rayons, ombres, échelle typographique, cible principale, largeur de page — décliné en deux thèmes ; ses familles de couleur : la page, le bandeau, la carte, l'action principale, les quatre états du plan, le neutre, les appartenances, la bande de mode, les messages (avis, refus, information), le plan, le papier | **Les candidates écartées.** Vue 3 est le second choix défendable, écarté de peu sur le poids et sur la discipline qu'exige sa réactivité. React est écarté sur la @@ -4934,10 +4935,10 @@ complément.** Un signalement arrive le plus souvent sous forme d'image, et une capture faite à la touche d'impression d'écran, ou cadrée sur le défaut, ne contient pas toujours la barre de titre du système — elle ne la contient jamais sur la plateforme `web`, où le titre est celui d'un onglet. Un bandeau dessiné par -l'application est dans l'image quel que soit le cadrage. Le § 8.4 impose en plus un -cadre permanent **autour du plan** pour le mode : les deux coexistent, le cadre -signalant le mode là où la main agit, le bandeau portant la version jusque sur la -liste des événements. +l'application est dans l'image quel que soit le cadrage. Le § 8.4 impose en plus une +bande de mode **sous le bandeau** et un libellé de mode à côté de la place fixe : ils +coexistent, la bande et le libellé signalant le mode dans l'écran de l'événement, le +bandeau portant la version jusque sur la liste des événements. **Le bandeau annonce aussi la version de format du fichier ouvert** dès qu'elle diffère de celle que le binaire écrit : plus ancienne, « ce fichier se convertira à @@ -5088,7 +5089,7 @@ n'interviennent qu'aux étapes où une configuration **déjà tendue** est le pr | fichier | configuration | ce que la capture d'écran montre | ce que son étape affirme | |---|---|---|---| | `01-premier-ecran.png` | répertoire neuf | le répertoire vide : la commande de création et les configurations de démonstration offertes, **jamais une liste vide** (§ 8.1) | chaque configuration que le catalogue déclare est nommée et cliquable, et **leur nombre est lu dans le catalogue, jamais écrit dans le scénario** | -| `01b-passer-en-ecriture.png` | idem | la commande « Modifier » à place fixe, et le cadre de mode autour du plan (§ 8.4) | avant ce geste, une tentative de modification est refusée ; après, elle passe | +| `01b-passer-en-ecriture.png` | idem | la commande « Modifier » à place fixe, la bande de mode et le libellé de mode, sans cadre autour du plan (§ 8.4) | avant ce geste, une tentative de modification est refusée ; après, elle passe | | `02-import-apercu.png` | `participants_cas_limites.csv` | l'aperçu obligatoire (§ 10.1) : encodage et séparateur détectés et **nommés**, association par en-tête, nombre de lignes refusées | l'encodage et le séparateur affichés sont ceux que le module d'import a retenus ; l'aperçu n'est pas contournable | | `02b-import-cp1252.png` | `participants_cas_limites_cp1252.csv` | l'aperçu du même contenu en windows-1252, séparé par des virgules : l'encodage détecté et **nommé**, les cellules accentuées intactes (§ 10.1) | l'encodage affiché est celui que le module d'import a retenu ; les cellules accentuées affichées sont celles que rend le décodage du fichier, sans caractère parasite | | `03-import-rapport.png` | idem | le rapport : lignes valides, orthographes d'appartenance fondues avec leur compte, lignes refusées avec leur motif | au moins un groupe d'orthographes fondu et au moins une ligne refusée sont visibles | @@ -5111,7 +5112,7 @@ n'interviennent qu'aux étapes où une configuration **déjà tendue** est le pr | `17-comparaison-manque.png` | **variante sans exception** (§ 15.1) | les deux profils de manque qui se croisent, l'axe commun, le zéro libellé, les trois effectifs de rangs (§ 12.10.6) | la page écrit sa question en toutes lettres et les trois effectifs ; aucune échelle propre à une courbe | | `18-comparaison-escalier.png` | idem | l'escalier du manque, jumeau tabulaire de la vue précédente | les effectifs cumulés du tableau reproduisent la courbe | | `19-ajustement.png` | l'événement retenu | le déplacement d'une personne après la retenue, l'interface disant **lequel des deux gestes** est en cours (§ 5.8) | le libellé de l'état des données est visible | -| `20-mode-lecture.png` | idem | le cadre et le libellé permanents autour du plan, et le bandeau d'application (§ 8.4) | un geste de chaque famille — glisser une personne, glisser une table, tirer une poignée, poser une réservation — est tenté, et l'empreinte d'état est inchangée après les quatre | +| `20-mode-lecture.png` | idem | la bande de mode, le libellé de mode à côté de la place fixe, et le bandeau d'application (§ 8.4) | un geste de chaque famille — glisser une personne, glisser une table, tirer une poignée, poser une réservation — est tenté, et l'empreinte d'état est inchangée après les quatre | | `21-historique.png` | idem | la liste des instants, le fil courant distingué des fils abandonnés, un jalon nommé (§ 8.3) | au moins un fil abandonné est présent et marqué comme tel | | `22-plan-brouillon.png` / `23-plan-retenu.png` | idem | l'**aperçu d'impression** du plan (§ 11.7) avec son cartouche, son échelle graphique et son horodatage, **avec** puis **sans** le filigrane BROUILLON | **deux images, parce qu'une absence ne se photographie pas** : le filigrane présent sur l'un, absent sur l'autre, et la paire est le seul exposé lisible de la règle | | `24-badges-controle.png` / `25-badges-planche.png` | idem | la **feuille de contrôle** avec son échelle de 100 mm et ses comptes, puis une **planche** de quatre A6 et ses trois filets de coupe | voir § 19.6 — ces deux images ne viennent pas du navigateur. La paire est nécessaire : la feuille de contrôle est ce que l'opérateur mesure sous une règle avant d'engager la rame, et une planche seule ne le montre pas | diff --git a/src/interface/App.navigateur.test.js b/src/interface/App.navigateur.test.js index b48eaf3..7de59cc 100644 --- a/src/interface/App.navigateur.test.js +++ b/src/interface/App.navigateur.test.js @@ -1013,7 +1013,7 @@ describe('le bandeau de la maquette (§ 8.4, § 18.6)', () => { }); describe('la page de la maquette : fond, largeur bornée, styles de base', () => { - test('au-delà de la largeur de page, l’écran se centre dans --largeur-page ; en deçà, il prend la fenêtre', async () => { + test('au-delà de la largeur de page, la liste se centre dans --largeur-page ; en deçà, elle prend la fenêtre ; l’événement prend la fenêtre à toute largeur', async () => { const avant = { largeur: innerWidth, hauteur: innerHeight }; try { await fixerCadre(1800, 900); @@ -1027,6 +1027,14 @@ describe('la page de la maquette : fond, largeur bornée, styles de base', () => await fixerCadre(1366, 700); flushSync(); expect(main.getBoundingClientRect().width).toBeCloseTo(document.documentElement.clientWidth, 0); + for (const largeur of [1800, 1366]) { + await fixerCadre(largeur, 900); + const ouvert = await avecEvenementOuvert(); + const principal = ecran(ouvert).parentElement; + expect(ecran(ouvert).dataset.ecran, `${largeur}`).toBe('evenement'); + expect(principal.getBoundingClientRect().left, `${largeur}`).toBeCloseTo(0, 0); + expect(principal.getBoundingClientRect().width, `${largeur}`).toBeCloseTo(document.documentElement.clientWidth, 0); + } } finally { await fixerCadre(avant.largeur, avant.hauteur); } diff --git a/src/interface/App.svelte b/src/interface/App.svelte index 31e2ecf..d8a8250 100644 --- a/src/interface/App.svelte +++ b/src/interface/App.svelte @@ -38,7 +38,9 @@ // // La page porte le fond de la page ; l'écran — l'élément
, parent // direct de chaque [data-ecran] — se centre dans --largeur-page au-delà - // de cette largeur et prend la fenêtre en deçà. App pose les styles de + // de cette largeur et prend la fenêtre en deçà ; l'écran de l'événement + // retire cette borne et prend la fenêtre à toute largeur + // (Evenement.svelte). App pose les styles de // base d'un titre de premier niveau, des cellules d'un tableau et des // champs de saisie de l'écran, de spécificité nulle : le style propre d'un // composant l'emporte toujours, et le document imprimé (§ 11.7), sous diff --git a/src/interface/accueil/Accueil.navigateur.test.js b/src/interface/accueil/Accueil.navigateur.test.js index 27ef9de..0403173 100644 --- a/src/interface/accueil/Accueil.navigateur.test.js +++ b/src/interface/accueil/Accueil.navigateur.test.js @@ -1238,10 +1238,9 @@ describe('les mesures du retenu dans la liste (§ 5.4, § 8.1, § 8.9)', () => { return mesurer(instance, indexerPlan(instance, planDepuisFichier(charge.retenu))); } - test('un événement retenu : ses collisions cumulées et |1000 \u{2212} Σ r(p)| ; sans retenu : « — » ; rien d’écrit', async () => { + test('un événement retenu : |1000 \u{2212} Σ r(p)|, sans colonne des collisions cumulées ; sans retenu : « — » ; rien d’écrit', async () => { const charge = chargeRetenue(); const mesures = mesuresDe(charge); - expect(mesures.collisionsCumulees).toBeGreaterThan(0); const m = await monter({ fichiers: [fichier(`retenue${SUFFIXES.etat}`, etat(charge)), fichier(`club${SUFFIXES.etat}`, texteRenomme('Club des Merles'))], }); @@ -1249,21 +1248,17 @@ describe('les mesures du retenu dans la liste (§ 5.4, § 8.1, § 8.9)', () => { await chargerResumes(m); const avant = ecritures(m); const retenue = ligneDe(m, 'retenue'); - expect(colonne(retenue, 'collisionsCumulees')).toBe(format.nombre(mesures.collisionsCumulees)); expect(colonne(retenue, 'diffRedondance')).toBe(format.nombre(Math.abs(1000 - mesures.totalRedondance))); const club = ligneDe(m, 'club'); - expect(colonne(club, 'collisionsCumulees')).toBe(TIRET); expect(colonne(club, 'diffRedondance')).toBe(TIRET); - // Chaque intitulé est un en-tête de colonne, une seule fois pour toute la - // liste. - for (const [nom, cle] of [ - ['collisionsCumulees', 'accueil.colonne.collisionsCumulees'], - ['diffRedondance', 'accueil.colonne.diffRedondance'], - ]) { - expect(tous(m, `[data-terme="${nom}"]`).length, nom).toBe(1); - expect(terme(accueil(m), nom).closest('thead'), nom).not.toBeNull(); - expect(terme(accueil(m), nom).textContent).toBe(t(cle)); - } + // L'intitulé est un en-tête de colonne, une seule fois pour toute la + // liste ; aucune colonne ni aucun intitulé ne nomme les collisions + // cumulées. + expect(tous(m, '[data-terme="diffRedondance"]').length).toBe(1); + expect(terme(accueil(m), 'diffRedondance').closest('thead')).not.toBeNull(); + expect(terme(accueil(m), 'diffRedondance').textContent).toBe(t('accueil.colonne.diffRedondance')); + expect(tous(m, '[data-colonne="collisionsCumulees"], [data-terme="collisionsCumulees"]')).toEqual([]); + expect(tous(m, '[data-ecran="accueil"] thead th').filter((th) => th.textContent.includes(t('propositions.colonne.collisionsCumulees')))).toEqual([]); expect(visible(retenue.querySelector('[data-colonne="diffRedondance"]'))).toBe(true); expect(ecritures(m)).toBe(avant); }); @@ -1271,10 +1266,7 @@ describe('les mesures du retenu dans la liste (§ 5.4, § 8.1, § 8.9)', () => { test('chaque intitulé s’explique : le survol puis le focus ouvrent son infobulle, Échap et la sortie la ferment', async () => { const m = await monter({ fichiers: [fichier(`retenue${SUFFIXES.etat}`, etat(chargeRetenue()))] }); await vi.waitFor(() => expect(lignes(m).length).toBe(1)); - await eprouverTermes(accueil(m), [ - ['collisionsCumulees', 'accueil.explication.collisionsCumulees'], - ['diffRedondance', 'accueil.explication.diffRedondance'], - ]); + await eprouverTermes(accueil(m), [['diffRedondance', 'accueil.explication.diffRedondance']]); }); test('la cible changée dans les propositions, puis la liste : la différence se lit contre la nouvelle cible', async () => { @@ -1301,7 +1293,6 @@ describe('les mesures du retenu dans la liste (§ 5.4, § 8.1, § 8.9)', () => { await revenirALaListe(m, 1); await resumeRecu('retenue'); expect(colonne(ligneDe(m, 'retenue'), 'diffRedondance')).toBe(format.nombre(Math.abs(25 - totalRedondance))); - expect(colonne(ligneDe(m, 'retenue'), 'collisionsCumulees')).toBe(format.nombre(mesuresDe(charge).collisionsCumulees)); }); }); @@ -1323,12 +1314,9 @@ describe('la meilleure proposition du classement dans la liste (§ 5.4, § 5.7, return appliquerCommande('retenirProposition', charge, { id: autre.id }).charge; } - // Ce que la ligne, ou le bloc de la génération, écrit de la meilleure - // proposition. - const lu = (bloc) => ({ - id: bloc.querySelector('[data-meilleure-id]')?.textContent ?? null, - collisions: bloc.querySelector('[data-meilleure-collisions]')?.textContent ?? null, - }); + // Ce que la ligne, ou le bloc de la génération, écrit de l'identifiant de + // la meilleure proposition. + const lu = (bloc) => ({ id: bloc.querySelector('[data-meilleure-id]')?.textContent ?? null }); // L'en-tête du groupe de colonnes de la meilleure proposition, et celui de // sa colonne d'identifiant. const groupeMeilleure = (m) => terme(accueil(m), 'meilleure').closest('th'); @@ -1342,7 +1330,7 @@ describe('la meilleure proposition du classement dans la liste (§ 5.4, § 5.7, }; const blocGeneration = (m) => dans(m, '[data-section="generation"] [data-meilleure]'); - test('un événement classé : le rang 1, son identifiant et ses collisions cumulées, à part du retenu ; sans proposition, « — » ; rien d’écrit', async () => { + test('un événement classé : le rang 1 et son identifiant, à part du retenu, sans ses collisions cumulées ; sans proposition, « — » ; rien d’écrit', async () => { const charge = chargeClassee(); const table = evaluer(charge); const [premiere] = table.propositions; @@ -1355,35 +1343,29 @@ describe('la meilleure proposition du classement dans la liste (§ 5.4, § 5.7, await chargerResumes(m); const avant = ecritures(m); const classee = ligneDe(m, 'classee'); - expect(lu(classee)).toEqual({ id: format.nombre(premiere.id), collisions: format.nombre(premiere.mesures.collisionsCumulees) }); - expect(colonne(classee, 'collisionsCumulees')).toBe(format.nombre(table.retenu.mesures.collisionsCumulees)); + expect(lu(classee)).toEqual({ id: format.nombre(premiere.id) }); // Les intitulés sont ceux du bloc de la génération, en en-têtes de - // colonnes : le titre sur le groupe, l'identifiant et les collisions sur - // ses deux colonnes ; chaque valeur se tient sous son groupe. + // colonnes : le titre sur le groupe, l'identifiant sur sa seule colonne ; + // la valeur se tient sous son groupe. Aucune cellule ni aucun intitulé + // des collisions de la meilleure. expect(terme(accueil(m), 'meilleure').textContent).toBe(t('generation.meilleure.titre')); expect(groupeMeilleure(m).getAttribute('scope')).toBe('colgroup'); - expect(groupeMeilleure(m).colSpan).toBe(2); + expect(groupeMeilleure(m).colSpan).toBe(1); expect(enteteProposition(m)).toBeDefined(); - expect(terme(accueil(m), 'meilleureCollisions').textContent).toBe(t('propositions.colonne.collisionsCumulees')); - for (const valeur of ['[data-meilleure-id]', '[data-meilleure-collisions]']) { - const cellule = classee.querySelector(valeur); - expect(visible(cellule), valeur).toBe(true); - expect(atteint(cellule), valeur).toBe(true); - expect(sous(cellule, groupeMeilleure(m)), valeur).toBe(true); - } - expect(sous(classee.querySelector('[data-meilleure-id]'), enteteProposition(m))).toBe(true); - expect(sous(classee.querySelector('[data-meilleure-collisions]'), terme(accueil(m), 'meilleureCollisions').closest('th'))).toBe(true); - expect(lu(ligneDe(m, 'club'))).toEqual({ id: TIRET, collisions: TIRET }); + const cellule = classee.querySelector('[data-meilleure-id]'); + expect(visible(cellule)).toBe(true); + expect(atteint(cellule)).toBe(true); + expect(sous(cellule, groupeMeilleure(m))).toBe(true); + expect(sous(cellule, enteteProposition(m))).toBe(true); + expect(tous(m, '[data-ecran="accueil"] [data-meilleure-collisions], [data-terme="meilleureCollisions"]')).toEqual([]); + expect(lu(ligneDe(m, 'club'))).toEqual({ id: TIRET }); expect(ecritures(m)).toBe(avant); }); - test('le titre et l’intitulé des collisions s’expliquent : le survol puis le focus ouvrent l’infobulle, Échap et la sortie la ferment', async () => { + test('le titre s’explique : le survol puis le focus ouvrent l’infobulle, Échap et la sortie la ferment', async () => { const m = await monter({ fichiers: [fichier(`classee${SUFFIXES.etat}`, etat(chargeClassee()))] }); await vi.waitFor(() => expect(lignes(m).length).toBe(1)); - await eprouverTermes(accueil(m), [ - ['meilleure', 'accueil.explication.meilleure'], - ['meilleureCollisions', 'propositions.explication.collisionsCumulees'], - ]); + await eprouverTermes(accueil(m), [['meilleure', 'accueil.explication.meilleure']]); }); test('la ligne écrit ce que le bloc de la génération écrit du même événement, sous les mêmes libellés, et de nouveau au retour', async () => { @@ -1392,20 +1374,16 @@ describe('la meilleure proposition du classement dans la liste (§ 5.4, § 5.7, await chargerResumes(m); const deLaListe = lu(ligneDe(m, 'classee')); expect(deLaListe.id).toMatch(/\d/); - const libelles = [terme(accueil(m), 'meilleure'), enteteProposition(m), terme(accueil(m), 'meilleureCollisions')].map( - (element) => element.textContent.trim(), - ); + const libelles = [terme(accueil(m), 'meilleure'), enteteProposition(m)].map((element) => element.textContent.trim()); await ouvrirLigne(m, 'classee'); await attendreEvenement(m); m.ui.allerA('generation'); await vi.waitFor(() => expect(blocGeneration(m)?.querySelector('[data-meilleure-id]')).not.toBeNull()); const bloc = blocGeneration(m); expect(lu(bloc)).toEqual(deLaListe); - expect( - [bloc.querySelector('h3'), bloc.querySelector('[data-meilleure-libelle-id]'), bloc.querySelector('[data-terme]')].map( - (element) => element.textContent, - ), - ).toEqual(libelles); + expect([bloc.querySelector('h3'), bloc.querySelector('[data-meilleure-libelle-id]')].map((element) => element.textContent)).toEqual( + libelles, + ); // Le même résumé, mais reçu de la liste relue au retour : celui d'avant // l'ouverture écrirait les mêmes valeurs sans rien prouver. const resumeRecu = suivreResumes(m); @@ -1423,7 +1401,7 @@ describe('la meilleure proposition du classement dans la liste (§ 5.4, § 5.7, const m = await monter({ fichiers: [fichier(`horsclassement${SUFFIXES.etat}`, etat(charge))] }); await vi.waitFor(() => expect(lignes(m).length).toBe(1)); await chargerResumes(m); - expect(lu(ligneDe(m, 'horsclassement'))).toEqual({ id: TIRET, collisions: TIRET }); + expect(lu(ligneDe(m, 'horsclassement'))).toEqual({ id: TIRET }); await ouvrirLigne(m, 'horsclassement'); await attendreEvenement(m); m.ui.allerA('generation'); @@ -1473,16 +1451,15 @@ describe('le chargement des résumés de la liste (§ 8.1, § 14.5, § 19.2)', ( return appliquerCommande('retenirProposition', charge, { id: charge.propositions[1].id }).charge; } - // Les quatre valeurs de résumé d'une ligne, dans l'ordre du rendu. - const CELLULES = ['[data-colonne="collisionsCumulees"]', '[data-colonne="diffRedondance"]', '[data-meilleure-id]', '[data-meilleure-collisions]']; + // Les deux valeurs de résumé d'une ligne, dans l'ordre du rendu. + const CELLULES = ['[data-colonne="diffRedondance"]', '[data-meilleure-id]']; const cellules = (ligne) => CELLULES.map((selecteur) => ligne.querySelector(selecteur)); // Ce que la ligne d'une charge écrit, par la table d'évaluation sollicitée // une seconde fois (§ 19.1). function attendues(charge) { const table = evaluer(charge); const [premiere] = table.propositions; - const { collisionsCumulees, totalRedondance } = table.retenu.mesures; - return [collisionsCumulees, Math.abs(1000 - totalRedondance), premiere.id, premiere.mesures.collisionsCumulees].map((n) => format.nombre(n)); + return [Math.abs(1000 - table.retenu.mesures.totalRedondance), premiere.id].map((n) => format.nombre(n)); } // Chaque valeur de la ligne attend : la cellule se dit occupée, porte un // seul signe, muet pour l'accessibilité (aria-hidden, sans rôle ni nom), @@ -1528,12 +1505,12 @@ describe('le chargement des résumés de la liste (§ 8.1, § 14.5, § 19.2)', ( expect(colonne(ligne, 'participants')).toBe(format.nombre(charges[ligne.dataset.evenement].participants.length)); enAttente(ligne); } - expect(signes(m).length).toBe(8); + expect(signes(m).length).toBe(4); const region = regionEtat(m); expect(region.textContent).toBe(t('accueil.chargement')); for (let image = 0; image < 3; image += 1) await uneImage(); flushSync(); - expect(signes(m).length).toBe(8); + expect(signes(m).length).toBe(4); m.horloge.avancer(0); await vi.waitFor(() => resolue(ligneDe(m, 'alpha'), attendues(charges.alpha))); enAttente(ligneDe(m, 'beta')); @@ -1551,7 +1528,7 @@ describe('le chargement des résumés de la liste (§ 8.1, § 14.5, § 19.2)', ( test('démontée avant le premier pas, l’application arrête le flux des résumés : aucun état ne se relit ensuite', async () => { const charges = { alpha: chargeResumee('evt-alpha'), beta: chargeResumee('evt-beta') }; const m = await monter({ fichiers: Object.entries(charges).map(([base, charge]) => fichier(`${base}${SUFFIXES.etat}`, etat(charge))) }); - expect(signes(m).length).toBe(8); + expect(signes(m).length).toBe(4); const relus = []; const lireTexte = m.fs.lireTexte; m.fs.lireTexte = (racine, chemin) => { @@ -1568,7 +1545,7 @@ describe('le chargement des résumés de la liste (§ 8.1, § 14.5, § 19.2)', ( expect(relus).toEqual([]); }); - test('un état qui ne se relit pas pendant le chargement : « — » dans ses quatre valeurs, et l’autre ligne résolue', async () => { + test('un état qui ne se relit pas pendant le chargement : « — » dans ses deux valeurs, et l’autre ligne résolue', async () => { const charges = { alpha: chargeResumee('evt-alpha'), beta: chargeResumee('evt-beta') }; const m = await monter({ fichiers: Object.entries(charges).map(([base, charge]) => fichier(`${base}${SUFFIXES.etat}`, etat(charge))) }); const lireTexte = m.fs.lireTexte; @@ -1577,7 +1554,7 @@ describe('le chargement des résumés de la liste (§ 8.1, § 14.5, § 19.2)', ( ? Promise.reject(new ErreurStockage('LECTURE', { chemin, cause: 'EIO' })) : lireTexte.call(m.fs, racine, chemin); await chargerResumes(m); - resolue(ligneDe(m, 'alpha'), [TIRET, TIRET, TIRET, TIRET]); + resolue(ligneDe(m, 'alpha'), [TIRET, TIRET]); resolue(ligneDe(m, 'beta'), attendues(charges.beta)); }); @@ -1611,7 +1588,7 @@ describe('le chargement des résumés de la liste (§ 8.1, § 14.5, § 19.2)', ( test('le signe se voit : il tient 3:1 sur ce qui l’entoure dans les deux thèmes, sans atténuation', async () => { const m = await monter({ fichiers: [fichier(`alpha${SUFFIXES.etat}`, etat(chargeResumee('evt-alpha')))] }); const traces = tous(m, '[data-chargement] path'); - expect(traces.length).toBe(4); + expect(traces.length).toBe(2); for (const theme of ['clair', 'sombre']) { await sousTheme(theme, async () => { for (const trace of traces) { @@ -1822,7 +1799,7 @@ describe('la liste en tableau : en-tête, tuiles, groupes et cartes (§ 8.1, § await chargerResumes(m); const entetes = tous(m, '[data-ecran="accueil"] thead th[scope="colgroup"]'); expect(entetes.map(texte)).toEqual([t('accueil.entete.evenement'), t('accueil.entete.retenu'), t('generation.meilleure.titre')]); - expect(entetes.map((th) => th.colSpan)).toEqual([4, 2, 2]); + expect(entetes.map((th) => th.colSpan)).toEqual([4, 1, 1]); expect(fond(entetes[1])).toEqual(jeton('--couleur-etat-retenu-fond')); expect(fond(entetes[2])).toEqual(jeton('--couleur-etat-propose-fond')); // Le groupe porte le signe de son état, de la marque de l'état. @@ -1835,8 +1812,8 @@ describe('la liste en tableau : en-tête, tuiles, groupes et cartes (§ 8.1, § }; const ligne = ligneDe(m, 'gala'); for (const nom of ['nom', 'etat', 'participants', 'modification']) expect(dans2(ligne.querySelector(`[data-colonne="${nom}"]`), entetes[0]), nom).toBe(true); - for (const nom of ['collisionsCumulees', 'diffRedondance']) expect(dans2(ligne.querySelector(`[data-colonne="${nom}"]`), entetes[1]), nom).toBe(true); - for (const valeur of ['[data-meilleure-id]', '[data-meilleure-collisions]']) expect(dans2(ligne.querySelector(valeur), entetes[2]), valeur).toBe(true); + expect(dans2(ligne.querySelector('[data-colonne="diffRedondance"]'), entetes[1])).toBe(true); + expect(dans2(ligne.querySelector('[data-meilleure-id]'), entetes[2])).toBe(true); // Le nom est l'en-tête de sa ligne ; la date et le chemin le suivent, // dessous, en plus petit. const nom = ligne.querySelector('[data-colonne="nom"]'); diff --git a/src/interface/accueil/ListeEvenements.svelte b/src/interface/accueil/ListeEvenements.svelte index 8256f08..399ad56 100644 --- a/src/interface/accueil/ListeEvenements.svelte +++ b/src/interface/accueil/ListeEvenements.svelte @@ -14,13 +14,13 @@ // // Les en-têtes de colonnes sont groupés : l'événement — nom, état, // participants, dernière modification —, le placement retenu, à la teinte - // de Retenu — ses deux mesures (COLONNES_DU_RETENU) —, la meilleure - // proposition du classement, à la teinte de Proposé, sous le titre et les - // libellés du bloc de la génération (MEILLEURE) — son identifiant et ses - // collisions cumulées —, puis les actions. Chaque valeur est dans son - // propre nœud : data-colonne, data-meilleure-id, data-meilleure-collisions, - // « — » sans valeur. Le nom est l'en-tête de sa ligne ; la date et le - // chemin le suivent en petite ligne, chacun précédé de son intitulé, que + // de Retenu — sa différence à la cible (COLONNES_DU_RETENU) —, la + // meilleure proposition du classement, à la teinte de Proposé, sous le + // titre et le libellé du bloc de la génération (MEILLEURE) — son + // identifiant —, puis les actions. Chaque valeur est dans son propre + // nœud : data-colonne, data-meilleure-id, « — » sans valeur. Le nom est + // l'en-tête de sa ligne ; la date et le chemin le suivent en petite + // ligne, chacun précédé de son intitulé, que // lisent les technologies d'assistance et que l'œil ne voit pas, puis ses // signalements traduits — les bases qu'un doublon nomme suivent son // texte, chacune dans son propre nœud (data-doublon). L'état est une @@ -33,7 +33,7 @@ // // lignes : LigneListe[] de la façade ; resumes : les résumés reçus de // facade.resumer, par base — une ligne sans le sien attend, et chacune de - // ses quatre valeurs de résumé porte le signe du chargement (Chargement), + // ses deux valeurs de résumé porte le signe du chargement (Chargement), // décoratif, dans une cellule qui se dit occupée (aria-busy), jusqu'à ce // qu'il arrive. La région d'état qui suit le tableau // (data-chargement-liste) existe tant que la liste a des lignes, et écrit @@ -44,10 +44,9 @@ // du dernier échec, ou null ; apresOperation() : à appeler quand une // opération a changé le dossier. // - // L'intitulé de chaque mesure du retenu, le titre de la meilleure - // proposition et l'intitulé de ses collisions s'expliquent dans - // l'infobulle de la liste, une seule (Infobulle, primitive) : chaque terme - // (data-terme, que pose le fragment termeExplique), un en-tête de colonne, + // L'intitulé de la mesure du retenu et le titre de la meilleure + // proposition s'expliquent dans l'infobulle de la liste, une seule + // (Infobulle, primitive) : chaque terme (data-terme, que pose le fragment termeExplique), un en-tête de colonne, // se focalise au clavier, son survol ou son focus ouvre l'explication, le // quitter la referme quand elle est encore la sienne, Échap aussi. Le // terme expliqué porte aria-describedby vers elle, et lui seul. @@ -73,9 +72,9 @@ const enChargement = $derived(lignes.some((ligne) => ligneAffichee(ligne, format, resumes.get(ligne.base)).enAttente)); const groupes = $derived(groupesDeLaListe(lignes, vue.filtre, vue.groupement)); // Le nombre de colonnes du tableau, que l'intitulé d'un groupe couvre : - // les quatre de l'événement, les deux du retenu, les deux de la meilleure + // les quatre de l'événement, celle du retenu, celle de la meilleure // proposition, les actions. - const COLONNES = 9; + const COLONNES = 7; // Le terme expliqué : {ancre, terme, cle, details} — son élément, son nom // et la clé de son explication — ; null sans infobulle. @@ -137,18 +136,18 @@
- - + + - - {/each} - {#each groupes as groupe, rangGroupe (groupe.groupe ?? '')} @@ -231,9 +227,6 @@ {/each} -
{t('accueil.entete.evenement')}{t('accueil.entete.retenu')}{@render termeExplique( 'meilleure', @@ -168,9 +167,6 @@ {@render termeExplique(colonne, cle, explication)}{t(MEILLEURE.proposition)}{@render termeExplique('meilleureCollisions', MEILLEURE.collisions, MEILLEURE.explicationCollisions)}
{@render resume(affichee, affichee[colonne])}{@render resume(affichee, affichee.meilleure.id)}{@render resume(affichee, affichee.meilleure.collisions)}