[IMP] card and priority editor: red circled cross, number weight field
A tag's « Retirer » text button read like plain text, and the weight of a criterion took a whole column for a number from 1 to 100. The card now removes a tag with a red circled cross, shown in write mode only, named « Retirer « tag » » for screen readers, with a 24 px target; the red has its own token, exempted from the conflict-hue rule and limited to card surfaces. The weight is a narrow number field, still read and refused as before. Checked: node 4597, navigateur 965; captures 04a and 10a republished. --- FR --- [IMP] fiche et priorités : croix rouge cerclée, poids en champ nombre Le bouton texte « Retirer » d'une étiquette se lisait comme du texte, et le poids d'un critère prenait toute une colonne pour un nombre de 1 à 100. La fiche retire désormais une étiquette par une croix rouge cerclée, en mode écriture seulement, nommée « Retirer « étiquette » » pour les lecteurs d'écran, sur une cible de 24 px ; le rouge a son jeton, exempté de la règle des teintes de conflit et borné aux surfaces de la fiche. Le poids est un champ nombre étroit, lu et refusé comme avant. Vérifié : node 4597, navigateur 965 ; captures 04a et 10a republiées. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
883dcb2519
commit
426eb911c9
26 changed files with 344 additions and 96 deletions
|
|
@ -148,8 +148,8 @@ nulle part.
|
|||
|
||||

|
||||
|
||||
*Chaque étiquette est une pastille grise, suivie de sa commande « Retirer ».
|
||||
Tapez une ou plusieurs étiquettes séparées par une virgule, puis « Ajouter »
|
||||
*Chaque étiquette est une pastille grise ; en écriture, une petite croix
|
||||
rouge cerclée la suit, qui la retire. Tapez une ou plusieurs étiquettes séparées par une virgule, puis « Ajouter »
|
||||
ou Entrée. Ici, « FINANCE » s’enregistre sous « Finance », l’orthographe que
|
||||
l’événement porte déjà, et le logiciel le dit sous le champ : une même
|
||||
étiquette ne s’écrit jamais de deux façons. La légende, au-dessus de la
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ sur les captures.
|
|||
| `--couleur-pastille-texte` | texte | 4,5 | — | — | 14,51 | 19-ajustement |
|
||||
| `--couleur-poignee` | marque | 3 | 7,44 | 07-plan-tables | 11,32 | 07-plan-tables |
|
||||
| `--couleur-refus` | texte | 4,5 | 8,34 | 01b-passer-en-ecriture | 8,14 | 01b-passer-en-ecriture |
|
||||
| `--couleur-retrait` | marque | 3 | 5,62 | 04a-etiquettes | 5,96 | 04a-etiquettes |
|
||||
| `--couleur-selection` | marque | 3 | 4,57 | 08-animateurs-designes | 4,18 | 08-animateurs-designes |
|
||||
| `--couleur-table-trait` | marque | 3 | 7,44 | 07-plan-tables | 5,59 | 07-plan-tables |
|
||||
| `rgb(24, 33, 47)` | texte | 4,5 | — | — | 16,19 | 22-plan-brouillon |
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
File diff suppressed because one or more lines are too long
15
spec.md
15
spec.md
|
|
@ -2751,9 +2751,10 @@ l'appartenance s'affiche en pastille de sa teinte (§ 12.7), le titre pressenti
|
|||
et l'exclusion en pastilles à icône.
|
||||
|
||||
La fiche d'une personne porte ses étiquettes, en pastilles **neutres** — la
|
||||
teinte d'appartenance ne sert qu'à l'appartenance (§ 12.7) —, chacune avec sa
|
||||
commande « Retirer », et un champ qui en ajoute une ou plusieurs, séparées comme
|
||||
dans une cellule CSV ; la saisie passe par la même lecture que l'import (§ 4.5,
|
||||
teinte d'appartenance ne sert qu'à l'appartenance (§ 12.7) —, chacune suivie,
|
||||
en mode écriture seulement, d'une petite croix cerclée rouge qui la retire,
|
||||
nommée « Retirer « étiquette » » pour les technologies d'assistance, et un
|
||||
champ qui en ajoute une ou plusieurs, séparées comme dans une cellule CSV ; la saisie passe par la même lecture que l'import (§ 4.5,
|
||||
§ 10.1). Quand l'événement porte des étiquettes, une légende des étiquettes suit
|
||||
celle des appartenances, chacune avec le nombre de personnes qui la portent,
|
||||
exclues comprises.
|
||||
|
|
@ -3622,6 +3623,9 @@ Une page de statistiques se casse par ses bords, et chacun se produira.
|
|||
le plan de salle, et ne servent à rien d'autre ; chacune ne peint que les
|
||||
marques de sa règle. Elles se distinguent entre elles et des autres marques
|
||||
du plan **une fois les trois dichromaties simulées**.
|
||||
- **Le rouge du retrait** (`--couleur-retrait`) ne peint que la croix cerclée
|
||||
d'une commande qui retire, sur une carte, jamais sur le plan : c'est le seul
|
||||
jeton chromatique admis à moins de 30° d'une teinte de conflit.
|
||||
- **Les couleurs d'appartenance ne vont que sur les listes**, jamais sur le
|
||||
plan, dont le canal chromatique appartient aux conflits : au plus six teintes
|
||||
catégorielles, chacune à 30° au moins des teintes de conflit dans chaque
|
||||
|
|
@ -4023,7 +4027,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 — à une exception près : la table d'évaluation des propositions, gardée dans le modèle pur sous l'identité de la charge figée de la séance, qu'une écriture remplace |
|
||||
| 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 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 retrait, 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
|
||||
|
|
@ -4243,7 +4247,8 @@ l'étage intermédiaire et laisse partir les défauts de coordonnées.
|
|||
### 14.5 Accessibilité et thèmes
|
||||
|
||||
Le texte tient **4,5:1** de contraste, les marques porteuses d'information
|
||||
**3:1**. Chaque couleur est accompagnée du ratio **mesuré sur la couleur relue du
|
||||
**3:1**. Une commande sans texte, à icône seule, offre une cible de 24 × 24 px
|
||||
au moins et porte son libellé en nom accessible. Chaque couleur est accompagnée du ratio **mesuré sur la couleur relue du
|
||||
rendu**, non sur le jeton source. Les deux thèmes sont éprouvés **par rendu**,
|
||||
jamais seulement par calcul, et le tableau des ratios est **engendré pour les
|
||||
deux** à partir des styles relevés au pilotage (§ 19.5), jamais recopié. Le
|
||||
|
|
|
|||
|
|
@ -543,6 +543,10 @@ function lirePaliers(paliers) {
|
|||
});
|
||||
}
|
||||
|
||||
/** Le plus grand poids d'un critère (§ 5.7), celui du moteur : l'écran en
|
||||
* borne le champ du poids, que changerPriorites contrôle. */
|
||||
export { POIDS_MAX };
|
||||
|
||||
// Vrai pour un poids du domaine : un entier de 1 à POIDS_MAX (§ 5.7).
|
||||
const poidsAdmis = (poids) => Number.isInteger(poids) && poids >= 1 && poids <= POIDS_MAX;
|
||||
|
||||
|
|
|
|||
|
|
@ -30,11 +30,14 @@
|
|||
// contre la liste, avec son remède, et décrit le groupe de l'éditeur
|
||||
// (aria-describedby), focalisable par programme (tabindex -1) : après le
|
||||
// remède, le focus revient au groupe ; rien ne s'écrit. Le poids d'un
|
||||
// critère d'un palier partagé se saisit dans un Champ entier (poids.<nom>),
|
||||
// écrit à la sortie du champ par changerPriorites ; son refus, un poids
|
||||
// hors de son domaine compris, se pose contre le champ.
|
||||
// critère d'un palier partagé se saisit dans un Champ nombre, étroit, de
|
||||
// 1 à POIDS_MAX par pas de 1 (poids.<nom>), lu par lireEntier et écrit à
|
||||
// la sortie du champ par changerPriorites ; son refus, un poids hors de
|
||||
// son domaine compris, se pose contre le champ : les bornes de l'entrée
|
||||
// guident ses flèches et ne refusent rien.
|
||||
import { getContext, tick } from 'svelte';
|
||||
import { lireEntier } from '../../application/capacite.js';
|
||||
import { POIDS_MAX } from '../../application/commandes_mobilier.js';
|
||||
import Champ from '../primitives/Champ.svelte';
|
||||
import Commande from '../primitives/Commande.svelte';
|
||||
import Refus from '../primitives/Refus.svelte';
|
||||
|
|
@ -134,6 +137,10 @@
|
|||
valeur={poids}
|
||||
lire={lireEntier}
|
||||
executer={ecrirePoids(critere)}
|
||||
type="number"
|
||||
min={1}
|
||||
max={POIDS_MAX}
|
||||
pas={1}
|
||||
/>
|
||||
{/if}
|
||||
<div class="gestes">
|
||||
|
|
|
|||
|
|
@ -392,11 +392,22 @@ describe('l’ordre des priorités : l’éditeur (§ 5.7, A11, A12)', () => {
|
|||
expect(annonce(m)).toBe(t('generation.priorites.annonce', { critere: CUMULEES, palier: 3, paliers: 7, avec: [] }));
|
||||
});
|
||||
|
||||
test('le poids d’un critère d’un palier partagé : 0 refusé contre son champ, rien d’écrit ; 5 écrit, une entrée', async () => {
|
||||
test('le poids d’un critère d’un palier partagé : un champ nombre étroit de 1 à 100 ; 0 refusé contre son champ, rien d’écrit ; 5 écrit, une entrée', async () => {
|
||||
const m = await demonstration();
|
||||
await cliquer(m, bouton(m, REDONDANCE, 'joindrePalier'));
|
||||
const champ = () => editeur(m).querySelector(`input[data-champ="poids.${REDONDANCE}"]`);
|
||||
expect(editeur(m).querySelector(`label[for="${champ().id}"]`).textContent.trim()).toBe(t('generation.priorites.poids', { critere: REDONDANCE }));
|
||||
expect([champ().type, champ().min, champ().max, champ().step]).toEqual(['number', '1', '100', '1']);
|
||||
// Étroit : six caractères de large, le contenu de la boîte, flèches
|
||||
// comprises.
|
||||
const sonde = document.createElement('span');
|
||||
sonde.style.cssText = 'position:absolute;font:inherit;visibility:hidden';
|
||||
sonde.textContent = '0'.repeat(6);
|
||||
champ().parentElement.append(sonde);
|
||||
const sixCaracteres = sonde.getBoundingClientRect().width;
|
||||
sonde.remove();
|
||||
const style = getComputedStyle(champ());
|
||||
expect(Number.parseFloat(style.width)).toBeCloseTo(sixCaracteres, 0);
|
||||
const e0 = journal(m);
|
||||
await cliquer(m, champ());
|
||||
champ().select();
|
||||
|
|
|
|||
|
|
@ -24,11 +24,18 @@
|
|||
appartenances), --couleur-conflit-voisins (nouveaux voisins) et
|
||||
--couleur-conflit-table (nouvelle table). Chacune ne peint que la trame de
|
||||
sa règle ; tout autre jeton chromatique se tient à 30° au moins de chacune,
|
||||
et aucun ne porte la valeur de l'une. Elles se distinguent entre elles et
|
||||
des autres marques du plan sous les trois dichromaties (SEPARATIONS,
|
||||
marques.js), et tiennent 3:1 sur le fond du plateau, qui borde leurs
|
||||
traits. Le contour épaissi d'une table en conflit, commun aux trois, est
|
||||
de l'encre forte du plan, --couleur-contour-fort.
|
||||
sauf le rouge du retrait, et aucun ne porte la valeur de l'une. Elles se
|
||||
distinguent entre elles et des autres marques du plan sous les trois
|
||||
dichromaties (SEPARATIONS, marques.js), et tiennent 3:1 sur le fond du
|
||||
plateau, qui borde leurs traits. Le contour épaissi d'une table en
|
||||
conflit, commun aux trois, est de l'encre forte du plan,
|
||||
--couleur-contour-fort.
|
||||
|
||||
Le rouge du retrait, --couleur-retrait, peint la croix cerclée d'une
|
||||
commande qui retire (Commande, variante retrait), sur la carte et son
|
||||
fond sourd, jamais sur le plan : c'est le seul jeton chromatique admis à
|
||||
moins de 30° d'une teinte de conflit (JETON_RETRAIT, marques.js), le
|
||||
canal chromatique que les conflits se réservent étant celui du plan.
|
||||
|
||||
La hachure d'une place à pourvoir peint des traits --couleur-hachure sur le
|
||||
fond de la chaise, --couleur-chaise-fond : ce sont ses deux couleurs.
|
||||
|
|
@ -72,7 +79,8 @@
|
|||
le neutre et les appartenances, chacun un fond teinté et une marque, que
|
||||
lisent les pastilles ; la bande de mode, un fond, un filet et une
|
||||
teinte par mode, celle de son icône ; les messages — avis, refus,
|
||||
information —, chacun son encre, un fond, un filet et une marque.
|
||||
information —, chacun son encre, un fond, un filet et une marque ; le
|
||||
retrait, la teinte de sa croix.
|
||||
|
||||
Le texte d'une pastille ou de la bande de mode garde l'encre de la page
|
||||
(§ 12.7) : la teinte se porte par le fond et la marque. Les marques des
|
||||
|
|
@ -134,9 +142,11 @@ textarea {
|
|||
--rayon-3: 0.75rem;
|
||||
--rayon-pilule: 62.5rem;
|
||||
|
||||
/* La hauteur d'une cible principale (44 px), et la largeur au-delà de
|
||||
laquelle le contenu d'un écran se centre. */
|
||||
/* La hauteur d'une cible principale (44 px), le côté minimal d'une cible
|
||||
(24 px, § 14.5), et la largeur au-delà de laquelle le contenu d'un
|
||||
écran se centre. */
|
||||
--hauteur-cible: 2.75rem;
|
||||
--cible-minimale: 1.5rem;
|
||||
--largeur-page: 87rem;
|
||||
|
||||
/* Ombres : une couleur opaque par thème, --couleur-ombre ; celle d'une
|
||||
|
|
@ -229,6 +239,9 @@ textarea {
|
|||
--couleur-information-filet: #b7cef0;
|
||||
--couleur-information-marque: var(--couleur-information);
|
||||
|
||||
/* Retrait : la croix cerclée d'une commande qui retire. */
|
||||
--couleur-retrait: #c62828;
|
||||
|
||||
/* Désignation. Le focus, la sélection, la mise en évidence et le fantôme
|
||||
passent sur une chaise vide comme sur une chaise pleine : leur clarté se
|
||||
tient entre le fond et le plein de la chaise, à 3:1 de chacun. */
|
||||
|
|
@ -334,6 +347,8 @@ textarea {
|
|||
--couleur-information-filet: #34507a;
|
||||
--couleur-information-marque: var(--couleur-information);
|
||||
|
||||
--couleur-retrait: #ff6b6b;
|
||||
|
||||
--couleur-focus: #8764e1;
|
||||
--couleur-selection: #4070f0;
|
||||
--couleur-mise-en-evidence: #00897b;
|
||||
|
|
|
|||
|
|
@ -52,6 +52,14 @@ export const CONFLITS = Object.freeze(
|
|||
/** Les jetons des teintes de conflit, dans l'ordre de CONFLITS. */
|
||||
export const JETONS_CONFLIT = Object.freeze(CONFLITS.map(({ jeton }) => jeton));
|
||||
|
||||
/** Le jeton du rouge du retrait (§ 12.7) : la croix cerclée d'une commande
|
||||
* qui retire (Commande, variante retrait), sur la carte et son fond sourd,
|
||||
* jamais sur le plan. C'est le seul jeton chromatique admis à moins de 30°
|
||||
* d'une teinte de conflit (verifierTeintesReservees, palette.js) : le
|
||||
* canal chromatique que les conflits se réservent est celui du plan, où il
|
||||
* ne se pose pas. */
|
||||
export const JETON_RETRAIT = c('retrait');
|
||||
|
||||
/** Écarts perçus minimaux d'une teinte de conflit, en distance OKLab × 100
|
||||
* mesurée en couleurs d'origine et sous chaque dichromatie (ecartPercu,
|
||||
* palette.js) : entreConflits, d'une teinte de conflit à une autre — la
|
||||
|
|
@ -152,6 +160,9 @@ export const MARQUES = Object.freeze([
|
|||
// contenu d'une feuille imprimée, mesuré sur le papier, clair dans les
|
||||
// deux thèmes.
|
||||
marque('filigrane', 'filigrane', ['papier']),
|
||||
// La croix cerclée d'un retrait, seul contenu de son bouton : sur la
|
||||
// carte, et sur son fond sourd, celui du survol.
|
||||
marque('retrait', 'retrait', ['carte-fond', 'carte-fond-sourd']),
|
||||
]);
|
||||
|
||||
// Les fonds teintés d'une pastille : ceux de ses teintes, puis ceux des
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const NOMS_MARQUES = [
|
|||
'cadenasFerme', 'cadenasOuvert', 'anneau', 'hachure', 'remplissagePlein',
|
||||
'contourConflit', 'trameAppartenance', 'trameVoisins', 'trameTable', 'pastille', 'poignee', 'gel', 'miseEnEvidence',
|
||||
'selection', 'focus',
|
||||
'contourTable', 'contourChaise', 'echelle', 'fantome', 'chargement', 'filigrane',
|
||||
'contourTable', 'contourChaise', 'echelle', 'fantome', 'chargement', 'filigrane', 'retrait',
|
||||
];
|
||||
|
||||
// Les remplissages d'une chaise : fond, pleine, et les deux couleurs de la
|
||||
|
|
@ -39,7 +39,7 @@ const FONDS_REQUIS = {
|
|||
const parNom = (registre, nom) => registre.find((e) => e.nom === nom);
|
||||
|
||||
describe('registre des marques', () => {
|
||||
test('les vingt et une marques du contrat, noms uniques, seuil 3, au moins un fond chacune', () => {
|
||||
test('les vingt-deux marques du contrat, noms uniques, seuil 3, au moins un fond chacune', () => {
|
||||
assert.equal(MARQUES.length, NOMS_MARQUES.length);
|
||||
assert.deepEqual(MARQUES.map((m) => m.nom), NOMS_MARQUES);
|
||||
assert.equal(new Set(MARQUES.map((m) => m.nom)).size, MARQUES.length);
|
||||
|
|
|
|||
|
|
@ -273,14 +273,14 @@ export function teinte(couleur) {
|
|||
|
||||
/** § 12.7 : chaque teinte réservée — celles des conflits — est chromatique,
|
||||
* et la teinte de chaque autre jeton chromatique se tient à ECART_TEINTE
|
||||
* degrés au moins de chacune, dans chaque thème. Les teintes réservées ne
|
||||
* se mesurent pas entre elles : verifierSeparation les distingue. Rend les
|
||||
* fautes [{theme, reservee, jeton, ecart}], ecart en degrés sur le cercle,
|
||||
* dans l'ordre thème, teinte réservée, jeton ; une teinte réservée
|
||||
* achromatique est elle-même une faute, jeton égal à reservee et ecart
|
||||
* null — aucune teinte n'est alors réservée. Un jeton réservé absent d'un
|
||||
* thème lève. */
|
||||
export function verifierTeintesReservees(texteCss, reservees) {
|
||||
* degrés au moins de chacune, dans chaque thème, sauf celle d'un jeton
|
||||
* d'admis, qui ne se mesure pas. Les teintes réservées ne se mesurent pas
|
||||
* entre elles : verifierSeparation les distingue. Rend les fautes
|
||||
* [{theme, reservee, jeton, ecart}], ecart en degrés sur le cercle, dans
|
||||
* l'ordre thème, teinte réservée, jeton ; une teinte réservée achromatique
|
||||
* est elle-même une faute, jeton égal à reservee et ecart null — aucune
|
||||
* teinte n'est alors réservée. Un jeton réservé absent d'un thème lève. */
|
||||
export function verifierTeintesReservees(texteCss, reservees, admis = []) {
|
||||
const fautes = [];
|
||||
for (const theme of THEMES) {
|
||||
const jetons = lireJetons(texteCss, theme);
|
||||
|
|
@ -292,7 +292,7 @@ export function verifierTeintesReservees(texteCss, reservees) {
|
|||
continue;
|
||||
}
|
||||
for (const [nom, couleur] of jetons) {
|
||||
if (reservees.includes(nom)) continue;
|
||||
if (reservees.includes(nom) || admis.includes(nom)) continue;
|
||||
const h = teinte(couleur);
|
||||
if (h === null) continue;
|
||||
const brut = Math.abs(h - reference);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { readdirSync, readFileSync } from 'node:fs';
|
|||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, test } from '../../test/lanceur.js';
|
||||
import { JETONS_CONFLIT } from './marques.js';
|
||||
import { JETONS_CONFLIT, JETON_RETRAIT, MARQUES } from './marques.js';
|
||||
import {
|
||||
SIMULATIONS,
|
||||
contraste,
|
||||
|
|
@ -320,6 +320,9 @@ describe('teinte et verifierTeintesReservees (§ 12.7)', () => {
|
|||
assert.deepEqual(fautes.map((f) => `${f.theme}:${f.jeton}`), [
|
||||
'clair:--couleur-b', 'clair:--couleur-d', 'sombre:--couleur-b', 'sombre:--couleur-d',
|
||||
]);
|
||||
// Un jeton admis ne se mesure pas ; les autres, si.
|
||||
const admis = verifierTeintesReservees(css, ['--couleur-conflit'], ['--couleur-b']);
|
||||
assert.deepEqual(admis.map((f) => `${f.theme}:${f.jeton}`), ['clair:--couleur-d', 'sombre:--couleur-d']);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -496,6 +499,21 @@ describe('jetons.css', () => {
|
|||
// Aucun autre jeton ne porte le préfixe des teintes de conflit.
|
||||
assert.deepEqual([...jetons.keys()].filter((n) => n.startsWith('--couleur-conflit')), JETONS_CONFLIT, theme);
|
||||
}
|
||||
assert.deepEqual(verifierTeintesReservees(JETONS, JETONS_CONFLIT), []);
|
||||
assert.deepEqual(verifierTeintesReservees(JETONS, JETONS_CONFLIT, [JETON_RETRAIT]), []);
|
||||
});
|
||||
|
||||
test('le rouge du retrait est le seul jeton admis près d’une teinte de conflit : chromatique, sa valeur à lui, peint sur la carte seulement, jamais sur le plan', () => {
|
||||
assert.equal(JETON_RETRAIT, '--couleur-retrait');
|
||||
// Sans l'admission, le retrait est la seule faute.
|
||||
assert.deepEqual([...new Set(verifierTeintesReservees(JETONS, JETONS_CONFLIT).map((f) => f.jeton))], [JETON_RETRAIT]);
|
||||
for (const theme of ['clair', 'sombre']) {
|
||||
const jetons = lireJetons(JETONS, theme);
|
||||
const couleur = jetons.get(JETON_RETRAIT);
|
||||
assert.notEqual(teinte(couleur), null, theme);
|
||||
const memeValeur = [...jetons].filter(([n, c]) => n !== JETON_RETRAIT && String(c) === String(couleur));
|
||||
assert.deepEqual(memeValeur.map(([n]) => n), [], theme);
|
||||
}
|
||||
const peintes = MARQUES.filter((m) => m.jeton === JETON_RETRAIT || m.fonds.includes(JETON_RETRAIT));
|
||||
assert.deepEqual(peintes.map((m) => [m.nom, [...m.fonds]]), [['retrait', ['--couleur-carte-fond', '--couleur-carte-fond-sourd']]]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@
|
|||
//
|
||||
// Les étiquettes (etiquettes.js) : une pastille neutre à icône par
|
||||
// étiquette, dans l'ordre de la charge — la teinte d'appartenance ne sert
|
||||
// qu'à l'appartenance (§ 12.7) —, chacune suivie de sa commande
|
||||
// « Retirer », nommée par elle ; puis un formulaire d'ajout, dont le
|
||||
// champ reçoit une ou plusieurs étiquettes séparées comme dans une
|
||||
// cellule CSV. Entrée, ou « Ajouter », envoie la saisie entière à
|
||||
// qu'à l'appartenance (§ 12.7) —, chacune suivie, en écriture seulement,
|
||||
// de sa commande de retrait : une croix cerclée rouge (Commande, variante
|
||||
// retrait), nommée « Retirer « <étiquette> » » ; puis un formulaire
|
||||
// d'ajout, dont le champ reçoit une ou plusieurs étiquettes séparées
|
||||
// comme dans une cellule CSV. Entrée, ou « Ajouter », envoie la saisie entière à
|
||||
// changerEtiquettes, qui la découpe, la met en forme et la réconcilie : la
|
||||
// fiche ne lit rien elle-même. Un refus de l'ajout — étiquette mal formée,
|
||||
// trop d'étiquettes, séance en lecture, plan bloqué — se pose contre le
|
||||
|
|
@ -31,14 +32,15 @@
|
|||
// (ORTHOGRAPHE_RETENUE), tant que la révision qui l'a produit est la
|
||||
// courante et la fiche celle de la personne. Le refus d'un retrait se
|
||||
// pose à côté de sa commande (Commande). En lecture comme sur un plan
|
||||
// bloqué, pastilles et commandes restent : la séance refuse le geste, et
|
||||
// bloqué (cleDeMode, mode.js), aucune commande de retrait ne se montre ;
|
||||
// pastilles, champ et « Ajouter » restent : la séance refuse l'ajout, et
|
||||
// le refus porte son remède. Une saisie non envoyée est un geste ouvert
|
||||
// (geste.js), qui suspend le compte d'inactivité ; l'envoi n'est pas
|
||||
// ré-entrant. Passer à une autre personne vide le champ et son refus ; le
|
||||
// refus d'un envoi qui revient après ce passage n'est posé nulle part.
|
||||
// Un retrait abouti rend le focus au « Retirer » qui prend la place du
|
||||
// sien — le suivant, sinon le précédent (ligneVoisine) —, ou au champ
|
||||
// quand il n'en reste aucun.
|
||||
// Un retrait abouti rend le focus à la commande de retrait qui prend la
|
||||
// place de la sienne — la suivante, sinon la précédente (ligneVoisine) —,
|
||||
// ou au champ quand il n'en reste aucune.
|
||||
//
|
||||
// La fiche est une carte : fond et filet de la carte, rayon de 12 px,
|
||||
// ombre légère ; ses étiquettes et légendes de tableau en petites
|
||||
|
|
@ -50,6 +52,7 @@
|
|||
import { getContext, onDestroy, tick, untrack } from 'svelte';
|
||||
import { nomAffiche } from '../../application/personnes.js';
|
||||
import { etatPlaces, statuts } from '../../application/places.js';
|
||||
import { cleDeMode } from '../mode.js';
|
||||
import Commande from '../primitives/Commande.svelte';
|
||||
import Icone from '../primitives/Icone.svelte';
|
||||
import Pastille from '../primitives/Pastille.svelte';
|
||||
|
|
@ -79,7 +82,7 @@
|
|||
const etapes = $derived(itineraire(participant.id, charge.evenement.tours, (tour) => etatPlaces(charge, { tour })));
|
||||
|
||||
// La fiche et son champ d'ajout, montés : le défilement vise la fiche,
|
||||
// le focus d'après un retrait ses « Retirer » ou le champ.
|
||||
// le focus d'après un retrait ses commandes de retrait ou le champ.
|
||||
let element;
|
||||
let champ;
|
||||
$effect(() => {
|
||||
|
|
@ -97,6 +100,9 @@
|
|||
const idSignal = `${uid}-orthographe`;
|
||||
|
||||
const pastilles = $derived(etiquettesDeLaFiche(participant));
|
||||
// Les commandes de retrait ne se montrent qu'en écriture, hors d'un plan
|
||||
// bloqué.
|
||||
const retraitOffert = $derived(cleDeMode($seance) === 'coquille.mode.ecriture');
|
||||
const signaux = $derived(signauxOrthographe(dernier, { revision: $seance.evenement?.revision ?? null, participant: participant.id }));
|
||||
const decrit = $derived(refusChamp !== null ? idRefus : signaux.length > 0 ? idSignal : undefined);
|
||||
|
||||
|
|
@ -142,8 +148,8 @@
|
|||
}
|
||||
|
||||
// Retire motCle ; aboutie, quand le bouton détruit a laissé le focus au
|
||||
// document, le porte au « Retirer » voisin ou au champ. Un refus remonte
|
||||
// à Commande, qui le pose à côté du bouton resté en place.
|
||||
// document, le porte à la commande de retrait voisine ou au champ. Un
|
||||
// refus remonte à Commande, qui le pose à côté du bouton resté en place.
|
||||
async function retirer(motCle) {
|
||||
const id = participant.id;
|
||||
const rang = pastilles.indexOf(motCle);
|
||||
|
|
@ -190,13 +196,15 @@
|
|||
{#each pastilles as motCle (motCle)}
|
||||
<li>
|
||||
<Pastille sorte="statut" valeur="neutre" icone="etiquettes" libelle={motCle} />
|
||||
<Commande
|
||||
cle="participants.etiquettes.retirer"
|
||||
details={{ motCle }}
|
||||
nom="retirerEtiquette"
|
||||
variante="discrete"
|
||||
executer={() => retirer(motCle)}
|
||||
/>
|
||||
{#if retraitOffert}
|
||||
<Commande
|
||||
cle="participants.etiquettes.retirer"
|
||||
details={{ motCle }}
|
||||
nom="retirerEtiquette"
|
||||
variante="retrait"
|
||||
executer={() => retirer(motCle)}
|
||||
/>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
|
@ -399,8 +407,9 @@
|
|||
}
|
||||
|
||||
/* Les étiquettes : l'intitulé en petites capitales, les pastilles en
|
||||
rangée qui se replie, chacune suivie de son « Retirer » ; puis le champ
|
||||
d'ajout et son bouton sur une ligne, le refus ou le signal dessous. */
|
||||
rangée qui se replie, chacune suivie, en écriture, de sa croix de
|
||||
retrait ; puis le champ d'ajout et son bouton sur une ligne, le refus
|
||||
ou le signal dessous. */
|
||||
.etiquettes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -935,10 +935,11 @@ describe('les étiquettes : la fiche et la légende (§ 4.5, § 10.2, § 5.9)',
|
|||
const rubrique = (m) => fiche(m).querySelector('[data-etiquettes]');
|
||||
const champEtiquettes = (m) => fiche(m).querySelector('[data-champ="etiquettes"]');
|
||||
const ajoutEtiquettes = (m) => fiche(m).querySelector('[data-commande="ajouterEtiquettes"]');
|
||||
// La commande de retrait d'une étiquette : une croix cerclée sans texte,
|
||||
// nommée par son aria-label.
|
||||
const retraits = (m) => [...fiche(m).querySelectorAll('[data-commande="retirerEtiquette"]')];
|
||||
const retrait = (m, motCle) =>
|
||||
[...fiche(m).querySelectorAll('[data-commande="retirerEtiquette"]')].find(
|
||||
(bouton) => bouton.textContent.trim() === t('participants.etiquettes.retirer', { motCle }),
|
||||
) ?? null;
|
||||
retraits(m).find((bouton) => bouton.getAttribute('aria-label') === t('participants.etiquettes.retirer', { motCle })) ?? null;
|
||||
const legendeEtiquettes = (m) => section(m).querySelector('[data-legende-etiquettes]');
|
||||
const etiquettesDe = (m, nom) => participants(m).find((p) => p.nom === nom).etiquettes;
|
||||
|
||||
|
|
@ -964,7 +965,7 @@ describe('les étiquettes : la fiche et la légende (§ 4.5, § 10.2, § 5.9)',
|
|||
await attendreSeance(m);
|
||||
}
|
||||
|
||||
test('la fiche montre chaque étiquette en pastille neutre à icône, sa commande « Retirer » nommée par elle ; sans étiquette, elle le dit', async () => {
|
||||
test('la fiche montre chaque étiquette en pastille neutre à icône, sa croix de retrait rouge nommée par elle ; sans étiquette, elle le dit', async () => {
|
||||
const m = await evenementOuvert({ personnes: TROIS_ETIQUETEES });
|
||||
await m.seance.executer('changerEtiquettes', { id: idDe(m, 'Ombrelle'), ajouter: 'finance, Montréal' });
|
||||
await ouvrirFiche(m, 'Ombrelle');
|
||||
|
|
@ -980,10 +981,33 @@ describe('les étiquettes : la fiche et la légende (§ 4.5, § 10.2, § 5.9)',
|
|||
// La teinte d'appartenance ne sert qu'à l'appartenance (§ 12.7).
|
||||
const teintes = new Set([1, 2, 3, 4, 5, 6].map((rang) => JSON.stringify(jeton(`--couleur-appartenance-${rang}-fond`))));
|
||||
for (const li of elements) expect(teintes.has(JSON.stringify(fondDe(pastilleDans(li))))).toBe(false);
|
||||
for (const motCle of ['Montréal', 'finance']) expect(retrait(m, motCle), motCle).not.toBeNull();
|
||||
// En sombre, la pastille prend le neutre sombre.
|
||||
// La croix de retrait : une icône seule, sans texte, la croix cerclée
|
||||
// de la teinte du retrait, marquée « retrait », dans une cible de
|
||||
// 24 px de côté au moins, et un anneau de focus au clavier.
|
||||
for (const motCle of ['Montréal', 'finance']) {
|
||||
const bouton = retrait(m, motCle);
|
||||
expect(bouton, motCle).not.toBeNull();
|
||||
expect(bouton.textContent.trim(), motCle).toBe('');
|
||||
const traces = [...bouton.querySelectorAll('svg path')];
|
||||
expect(traces.length, motCle).toBeGreaterThan(0);
|
||||
for (const trace of traces) {
|
||||
expect(trace.dataset.marque).toBe('retrait');
|
||||
expect(composantes(getComputedStyle(trace).stroke)).toEqual(jeton('--couleur-retrait'));
|
||||
}
|
||||
const { width, height } = bouton.getBoundingClientRect();
|
||||
expect(Math.min(width, height), motCle).toBeGreaterThanOrEqual(24);
|
||||
}
|
||||
// Tab mène d'une croix à la suivante, qui porte l'anneau de focus.
|
||||
retrait(m, 'Montréal').focus();
|
||||
await touche('Tab');
|
||||
expect(document.activeElement).toBe(retrait(m, 'finance'));
|
||||
expect(retrait(m, 'finance').matches(':focus-visible')).toBe(true);
|
||||
expect(getComputedStyle(retrait(m, 'finance')).outlineStyle).toBe('solid');
|
||||
expect(composantes(getComputedStyle(retrait(m, 'finance')).outlineColor)).toEqual(jeton('--couleur-focus'));
|
||||
// En sombre, la pastille prend le neutre sombre, la croix le rouge sombre.
|
||||
await sousTheme('sombre', async () => {
|
||||
for (const li of elements) expect(fondDe(pastilleDans(li))).toEqual(jeton('--couleur-neutre-fond'));
|
||||
expect(composantes(getComputedStyle(retrait(m, 'finance').querySelector('path')).stroke)).toEqual(jeton('--couleur-retrait'));
|
||||
});
|
||||
|
||||
await ouvrirFiche(m, 'Pervenche');
|
||||
|
|
@ -992,7 +1016,7 @@ describe('les étiquettes : la fiche et la légende (§ 4.5, § 10.2, § 5.9)',
|
|||
expect(fiche(m).querySelectorAll('[data-commande="retirerEtiquette"]')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('au clavier : une saisie de trois étiquettes, Entrée, une entrée au journal ; le champ se vide et garde le focus ; « Retirer » ôte la sienne et rend le focus à sa voisine', async () => {
|
||||
test('au clavier : une saisie de trois étiquettes, Entrée, une entrée au journal ; le champ se vide et garde le focus ; la croix de retrait ôte la sienne et rend le focus à sa voisine', async () => {
|
||||
const m = await evenementOuvert({ personnes: TROIS_ETIQUETEES });
|
||||
await ouvrirFiche(m, 'Pervenche');
|
||||
const champ = champEtiquettes(m);
|
||||
|
|
@ -1017,8 +1041,8 @@ describe('les étiquettes : la fiche et la légende (§ 4.5, § 10.2, § 5.9)',
|
|||
expect(document.activeElement).toBe(champEtiquettes(m));
|
||||
expect([...rubrique(m).querySelectorAll('li')].map((li) => texteDe(pastilleDans(li)))).toEqual(['bénévole', 'ressources humaines', 'voile']);
|
||||
|
||||
// « Retirer » activé par Entrée : le focus passe au « Retirer » suivant,
|
||||
// sinon au précédent, sinon au champ ; jamais au document.
|
||||
// La croix de retrait activée par Entrée : le focus passe à la croix
|
||||
// suivante, sinon à la précédente, sinon au champ ; jamais au document.
|
||||
const retirerAuClavier = async (motCle) => {
|
||||
retrait(m, motCle).focus();
|
||||
await touche('Enter');
|
||||
|
|
@ -1197,29 +1221,31 @@ describe('les étiquettes : la fiche et la légende (§ 4.5, § 10.2, § 5.9)',
|
|||
expect(legendeEtiquettes(m)).toBeNull();
|
||||
});
|
||||
|
||||
test('en lecture : les pastilles restent ; « Ajouter » et « Retirer » sont refusés, chacun avec son remède, et rien ne s’écrit', async () => {
|
||||
test('en lecture : les pastilles restent, sans croix de retrait ; « Ajouter » est refusé avec son remède, rien ne s’écrit ; de retour en écriture, les croix reviennent', async () => {
|
||||
const m = await evenementOuvert({ personnes: TROIS_ETIQUETEES });
|
||||
await m.seance.executer('changerEtiquettes', { id: idDe(m, 'Ombrelle'), ajouter: 'finance' });
|
||||
await m.seance.passerEnLecture();
|
||||
await ouvrirFiche(m, 'Ombrelle');
|
||||
const avant = texteJournal(m);
|
||||
expect(texteDe(pastilleDans(rubrique(m).querySelector('li')))).toBe('finance');
|
||||
expect(retraits(m)).toHaveLength(0);
|
||||
|
||||
await ajouterParClavier(m, 'bénévole');
|
||||
const message = document.getElementById(champEtiquettes(m).getAttribute('aria-describedby'));
|
||||
expect(message.dataset.refus).toBe('MODE_LECTURE');
|
||||
expect(message.querySelector('[data-commande]')).not.toBeNull();
|
||||
|
||||
await cliquer(retrait(m, 'finance'));
|
||||
await attendreSeance(m);
|
||||
const refusRetrait = retrait(m, 'finance').parentElement.querySelector('[data-refus]');
|
||||
expect(refusRetrait.dataset.refus).toBe('MODE_LECTURE');
|
||||
expect(refusRetrait.querySelector('[data-commande]')).not.toBeNull();
|
||||
expect(etiquettesDe(m, 'Ombrelle')).toEqual(['finance']);
|
||||
expect(texteJournal(m)).toBe(avant);
|
||||
|
||||
await m.seance.passerEnEcriture();
|
||||
await rendu();
|
||||
expect(retrait(m, 'finance')).not.toBeNull();
|
||||
await m.seance.passerEnLecture();
|
||||
await rendu();
|
||||
expect(retraits(m)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('plan bloqué, en écriture : « Ajouter » et « Retirer » sont refusés avec « Débloquer » pour remède', async () => {
|
||||
test('plan bloqué : aucune croix de retrait ; « Ajouter » est refusé avec « Débloquer » pour remède', async () => {
|
||||
const m = await evenementOuvert({ sieges: 4, tours: 2, tables: 1, personnes: TROIS_ETIQUETEES });
|
||||
await m.seance.executer('changerEtiquettes', { id: idDe(m, 'Ombrelle'), ajouter: 'finance' });
|
||||
await m.app.generation().generer({ graine: 1, nombre: 1, arret: 50, historique: 1000 });
|
||||
|
|
@ -1229,13 +1255,12 @@ describe('les étiquettes : la fiche et la légende (§ 4.5, § 10.2, § 5.9)',
|
|||
await ouvrirFiche(m, 'Ombrelle');
|
||||
const avant = texteJournal(m);
|
||||
|
||||
expect(texteDe(pastilleDans(rubrique(m).querySelector('li')))).toBe('finance');
|
||||
expect(retraits(m)).toHaveLength(0);
|
||||
|
||||
await ajouterParClavier(m, 'bénévole');
|
||||
const message = document.getElementById(champEtiquettes(m).getAttribute('aria-describedby'));
|
||||
expect(message.querySelector('[data-commande="debloquer"]')).not.toBeNull();
|
||||
await cliquer(retrait(m, 'finance'));
|
||||
await attendreSeance(m);
|
||||
const refusRetrait = retrait(m, 'finance').parentElement.querySelector('[data-refus]');
|
||||
expect(refusRetrait.querySelector('[data-commande="debloquer"]')).not.toBeNull();
|
||||
expect(texteJournal(m)).toBe(avant);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,13 +36,32 @@
|
|||
// son étiquette seule ne le distingue pas d'un homonyme ; null,
|
||||
// l'étiquette le nomme. L'étiquette visible ne change pas. Le refus reçoit
|
||||
// nom pour nommer son remède.
|
||||
//
|
||||
// type est celui de l'entrée : 'text', le défaut, ou 'number', un champ
|
||||
// étroit de six caractères, qui porte min, max et pas (step) quand ils
|
||||
// sont donnés. La lecture reste celle de lire, et le domaine celui de
|
||||
// l'exécution : le navigateur rend vide un texte qu'il ne lit pas comme
|
||||
// un nombre, que lire refuse comme tout texte vide.
|
||||
import { getContext, onDestroy, untrack } from 'svelte';
|
||||
import { effetDe } from '../raccourcis.js';
|
||||
import { creerMachineChamp, estRefusSeance, refusSeanceCourant } from './champ.js';
|
||||
import { creerGeste } from './geste.js';
|
||||
import Refus from './Refus.svelte';
|
||||
|
||||
let { cle, etiquette, valeur, lire, executer, details = {}, nom = null, gestes = {} } = $props();
|
||||
let {
|
||||
cle,
|
||||
etiquette,
|
||||
valeur,
|
||||
lire,
|
||||
executer,
|
||||
details = {},
|
||||
nom = null,
|
||||
gestes = {},
|
||||
type = 'text',
|
||||
min = null,
|
||||
max = null,
|
||||
pas = null,
|
||||
} = $props();
|
||||
|
||||
const t = getContext('t');
|
||||
const seance = getContext('seance') ?? null;
|
||||
|
|
@ -192,7 +211,10 @@
|
|||
<label for={idEntree}>{t(etiquette, details)}</label>
|
||||
<input
|
||||
id={idEntree}
|
||||
type="text"
|
||||
{type}
|
||||
min={min ?? undefined}
|
||||
max={max ?? undefined}
|
||||
step={pas ?? undefined}
|
||||
data-champ={cle}
|
||||
value={saisie}
|
||||
aria-label={nom ?? undefined}
|
||||
|
|
@ -225,6 +247,11 @@
|
|||
font-size: var(--taille-texte);
|
||||
}
|
||||
|
||||
/* Un nombre : six caractères de large, flèches comprises. */
|
||||
input[type='number'] {
|
||||
width: 6ch;
|
||||
}
|
||||
|
||||
input:focus-visible {
|
||||
outline: 2px solid var(--couleur-focus);
|
||||
outline-offset: 1px;
|
||||
|
|
|
|||
|
|
@ -36,11 +36,17 @@
|
|||
// variante : 'secondaire', la défaut, à filet sur le fond de la carte ;
|
||||
// 'principale', l'action à faire sur l'écran, pleine de la teinte de
|
||||
// l'action, haute d'une cible principale ; 'discrete', sans fond ni filet
|
||||
// au repos. Elle ne change que l'apparence ; une variante hors de
|
||||
// VARIANTES_COMMANDE lève RangeError au montage.
|
||||
// au repos ; 'retrait', l'icône croixCerclee seule, de --couleur-retrait,
|
||||
// sans fond ni filet au repos, dans une cible de --cible-minimale de côté
|
||||
// au moins (§ 14.5) : son libellé, que rien ne montre, devient son nom
|
||||
// accessible, à défaut de nomAccessible, et ses tracés portent la marque
|
||||
// « retrait » du registre (marques.js). Elle ne change que l'apparence ;
|
||||
// une variante hors de VARIANTES_COMMANDE, ou le retrait d'une commande à
|
||||
// confirmer, lève RangeError au montage.
|
||||
import { getContext, onDestroy, untrack } from 'svelte';
|
||||
import { effetDe } from '../raccourcis.js';
|
||||
import { DELAI_CONFIRMATION_MS, creerMachineCommande, exigerVariante } from './commande.js';
|
||||
import Icone from './Icone.svelte';
|
||||
import Refus from './Refus.svelte';
|
||||
|
||||
let {
|
||||
|
|
@ -67,7 +73,11 @@
|
|||
|
||||
const libelle = $derived(armee ? t(confirmer) : t(cle, details));
|
||||
const designation = $derived(nom ?? cle.slice(cle.lastIndexOf('.') + 1));
|
||||
const forme = $derived(exigerVariante(variante));
|
||||
const forme = $derived(exigerVariante(variante, confirmer));
|
||||
// Le nom accessible : le libellé de confirmation, montré, quand elle est
|
||||
// armée ; sinon nomAccessible, ou le libellé d'un retrait, qui ne le
|
||||
// montre pas.
|
||||
const nomDuBouton = $derived(armee ? undefined : (nomAccessible ?? (forme === 'retrait' ? libelle : undefined)));
|
||||
|
||||
function desarmer() {
|
||||
if (minuterie === null) return;
|
||||
|
|
@ -119,12 +129,14 @@
|
|||
data-commande={designation}
|
||||
class:principale={forme === 'principale'}
|
||||
class:discrete={forme === 'discrete'}
|
||||
class:retrait={forme === 'retrait'}
|
||||
aria-busy={enCours ? 'true' : undefined}
|
||||
aria-disabled={enCours ? 'true' : undefined}
|
||||
aria-label={armee ? undefined : (nomAccessible ?? undefined)}
|
||||
aria-label={nomDuBouton}
|
||||
aria-describedby={refus === null ? undefined : idRefus}
|
||||
onkeydown={surTouche}
|
||||
onclick={cliquer}>{libelle}</button
|
||||
onclick={cliquer}
|
||||
>{#if forme === 'retrait'}<Icone nom="croixCerclee" taille={18} marque="retrait" />{:else}{libelle}{/if}</button
|
||||
>
|
||||
{#if refus !== null}
|
||||
<Refus id={idRefus} {refus} lever={() => (refus = null)} objet={nomAccessible} {gestes} />
|
||||
|
|
@ -177,6 +189,27 @@
|
|||
background-color: var(--couleur-carte-fond-sourd);
|
||||
}
|
||||
|
||||
/* Le retrait : l'icône seule, de la teinte du retrait, centrée dans une
|
||||
cible ronde de --cible-minimale de côté au moins ; ni fond ni filet au
|
||||
repos, le fond sourd de la carte au survol. */
|
||||
button.retrait {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
min-width: var(--cible-minimale);
|
||||
min-height: var(--cible-minimale);
|
||||
padding: 0;
|
||||
border-color: transparent;
|
||||
border-radius: var(--rayon-pilule);
|
||||
background-color: transparent;
|
||||
color: var(--couleur-retrait);
|
||||
}
|
||||
|
||||
button.retrait:hover {
|
||||
background-color: var(--couleur-carte-fond-sourd);
|
||||
}
|
||||
|
||||
button[aria-disabled='true'] {
|
||||
border-style: dashed;
|
||||
color: var(--couleur-page-texte-secondaire);
|
||||
|
|
|
|||
|
|
@ -6,10 +6,13 @@
|
|||
// sans remplissage, du trait de la couleur courante — le parent la règle
|
||||
// par color, d'un jeton. Elle est cachée aux technologies d'assistance et
|
||||
// ne prend jamais le focus : un libellé l'accompagne toujours. taille :
|
||||
// le côté du carré, en px. Un nom inconnu lève RangeError au montage.
|
||||
// le côté du carré, en px. marque, le nom d'une marque du registre
|
||||
// (MARQUES, marques.js), se pose en data-marque sur chacun de ses tracés,
|
||||
// que les épreuves de rendu mesurent ; null, aucun. Un nom inconnu lève
|
||||
// RangeError au montage.
|
||||
import { traceIcone } from './icones.js';
|
||||
|
||||
let { nom, taille = 16 } = $props();
|
||||
let { nom, taille = 16, marque = null } = $props();
|
||||
|
||||
const traces = $derived(traceIcone(nom));
|
||||
</script>
|
||||
|
|
@ -28,7 +31,7 @@
|
|||
focusable="false"
|
||||
>
|
||||
{#each traces as trace, rang (rang)}
|
||||
<path d={trace} />
|
||||
<path d={trace} data-marque={marque ?? undefined} />
|
||||
{/each}
|
||||
</svg>
|
||||
|
||||
|
|
|
|||
|
|
@ -39,19 +39,26 @@ import { COMMANDES } from '../../application/commandes.js';
|
|||
/** Les variantes d'une commande, la secondaire en tête — celle d'une
|
||||
* commande qui n'en donne pas : principale, l'action à faire sur l'écran,
|
||||
* pleine de la teinte de l'action ; secondaire, à filet ; discrète, sans
|
||||
* fond ni filet au repos. La variante ne change que l'apparence : le rôle,
|
||||
* les attributs et la machine restent ceux de toute commande. */
|
||||
export const VARIANTES_COMMANDE = Object.freeze(['secondaire', 'principale', 'discrete']);
|
||||
* fond ni filet au repos ; retrait, une croix cerclée de la teinte du
|
||||
* retrait, sans fond ni filet au repos, dont le libellé devient le nom
|
||||
* accessible. La variante ne change que l'apparence : le rôle, la
|
||||
* désignation et la machine restent ceux de toute commande. */
|
||||
export const VARIANTES_COMMANDE = Object.freeze(['secondaire', 'principale', 'discrete', 'retrait']);
|
||||
|
||||
/**
|
||||
* Une variante de commande, rendue telle quelle.
|
||||
* Une variante de commande, rendue telle quelle. Le retrait ne montre
|
||||
* aucun texte : son libellé armé n'aurait où s'afficher, et une commande à
|
||||
* confirmer ne le prend pas.
|
||||
*
|
||||
* @param {string} variante
|
||||
* @returns {'secondaire'|'principale'|'discrete'}
|
||||
* @throws {RangeError} hors de VARIANTES_COMMANDE, en la nommant
|
||||
* @param {string|null} [confirmer] la clé de confirmation de la commande
|
||||
* @returns {'secondaire'|'principale'|'discrete'|'retrait'}
|
||||
* @throws {RangeError} hors de VARIANTES_COMMANDE, en la nommant ; le
|
||||
* retrait d'une commande à confirmer
|
||||
*/
|
||||
export function exigerVariante(variante) {
|
||||
export function exigerVariante(variante, confirmer = null) {
|
||||
if (!VARIANTES_COMMANDE.includes(variante)) throw new RangeError(`commande : variante inconnue ${JSON.stringify(variante) ?? 'undefined'}`);
|
||||
if (variante === 'retrait' && confirmer !== null) throw new RangeError('commande : le retrait ne se confirme pas');
|
||||
return variante;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -352,8 +352,8 @@ describe('commande : l’action d’un remède', () => {
|
|||
});
|
||||
|
||||
describe('commande : la variante', () => {
|
||||
test('trois variantes, la secondaire en tête : celle d’une commande qui n’en donne pas', () => {
|
||||
assert.deepEqual(VARIANTES_COMMANDE, ['secondaire', 'principale', 'discrete']);
|
||||
test('quatre variantes, la secondaire en tête : celle d’une commande qui n’en donne pas', () => {
|
||||
assert.deepEqual(VARIANTES_COMMANDE, ['secondaire', 'principale', 'discrete', 'retrait']);
|
||||
assert.ok(Object.isFrozen(VARIANTES_COMMANDE));
|
||||
});
|
||||
|
||||
|
|
@ -363,4 +363,10 @@ describe('commande : la variante', () => {
|
|||
assert.throws(() => exigerVariante(autre), (e) => e instanceof RangeError && e.message.includes(JSON.stringify(autre) ?? 'undefined'), String(autre));
|
||||
}
|
||||
});
|
||||
|
||||
test('le retrait ne se confirme pas : avec une clé de confirmation, exigerVariante lève RangeError ; les autres variantes la prennent', () => {
|
||||
assert.throws(() => exigerVariante('retrait', 'communs.commande.confirmer'), RangeError);
|
||||
assert.equal(exigerVariante('retrait', null), 'retrait');
|
||||
for (const variante of ['secondaire', 'principale', 'discrete']) assert.equal(exigerVariante(variante, 'communs.commande.confirmer'), variante);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -415,6 +415,30 @@ describe('commande : la variante', () => {
|
|||
expect(style(secondaire).fontSize).toBe('16px');
|
||||
expect(() => commande({ variante: 'primaire' })).toThrow(RangeError);
|
||||
});
|
||||
|
||||
test('retrait : la croix cerclée seule, de la teinte du retrait, sans texte ni fond, nommée par son libellé, dans une cible de 24 px au moins ; à confirmer, elle lève au montage', async () => {
|
||||
const libelle = t('participants.etiquettes.retirer', { motCle: 'voile' });
|
||||
const retrait = commande({ variante: 'retrait', cle: 'participants.etiquettes.retirer', details: { motCle: 'voile' }, nom: 'retirerEtiquette' });
|
||||
expect(retrait.textContent.trim()).toBe('');
|
||||
expect(retrait.getAttribute('aria-label')).toBe(libelle);
|
||||
expect(retrait.getAttribute('data-commande')).toBe('retirerEtiquette');
|
||||
expect([...retrait.querySelectorAll('svg path')].map((p) => p.getAttribute('d'))).toEqual([...ICONES.croixCerclee]);
|
||||
const { width, height } = retrait.getBoundingClientRect();
|
||||
expect(Math.min(width, height)).toBeGreaterThanOrEqual(24);
|
||||
for (const theme of THEMES) {
|
||||
await sousTheme(theme, () => {
|
||||
expect(fond(retrait), theme).toEqual(TRANSPARENT);
|
||||
expect(composantes(style(retrait).borderTopColor)[3], theme).toBe(0);
|
||||
for (const trace of retrait.querySelectorAll('path')) {
|
||||
expect(trace.dataset.marque, theme).toBe('retrait');
|
||||
expect(composantes(getComputedStyle(trace).stroke), theme).toEqual(jeton('--couleur-retrait'));
|
||||
}
|
||||
});
|
||||
}
|
||||
// Un nom accessible donné passe devant le libellé.
|
||||
expect(commande({ variante: 'retrait', nomAccessible: 'Ôter' }).getAttribute('aria-label')).toBe('Ôter');
|
||||
expect(() => commande({ variante: 'retrait', confirmer: 'communs.commande.confirmerDeblocage' })).toThrow(RangeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refus, infobulle, champ et fenêtre à confirmer : habillés des jetons', () => {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ const DESSINS = [
|
|||
['demarrer', ['M8 5l11 7-11 7z']],
|
||||
// Une étiquette (§ 4.5) : l'étiquette à œillet, pointe en bas à droite.
|
||||
['etiquettes', ['M3 3h8l10 10-8 8L3 11z', cercle(7.5, 7.5, 1.5)]],
|
||||
// Un retrait (Commande, variante retrait) : une croix dans un cercle.
|
||||
['croixCerclee', [cercle(12, 12, 9), 'M9 9l6 6M15 9l-6 6']],
|
||||
];
|
||||
|
||||
/** Les noms des icônes, dans l'ordre du module. */
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ describe('icônes des primitives', () => {
|
|||
assert.deepEqual(NOMS_ICONES, [
|
||||
'crayon', 'listePoints', 'coche', 'cadenas', 'liste', 'importer', 'exporter', 'ajouter', 'chercher',
|
||||
'calendrier', 'information', 'triangle', 'interdit', 'dossier', 'oeil', 'stylo', 'defaire', 'imprimer',
|
||||
'etoile', 'croix', 'chevron', 'points', 'demarrer', 'etiquettes',
|
||||
'etoile', 'croix', 'chevron', 'points', 'demarrer', 'etiquettes', 'croixCerclee',
|
||||
]);
|
||||
assert.ok(Object.isFrozen(ICONES) && Object.isFrozen(NOMS_ICONES));
|
||||
assert.deepEqual(Object.keys(ICONES).sort(), [...NOMS_ICONES].sort());
|
||||
|
|
|
|||
|
|
@ -251,6 +251,46 @@ describe('champ : la fin du geste (§ 2.1, § 8.2)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('champ : le type de l’entrée', () => {
|
||||
test('texte par défaut, sans bornes ; nombre : min, max et pas posés, une lettre ne s’y inscrit pas, et lire refuse le vide qu’elle laisse ; des chiffres s’écrivent', async () => {
|
||||
const montage = await preparer();
|
||||
const { entree } = monterChamp(montage);
|
||||
expect(entree.type).toBe('text');
|
||||
for (const attribut of ['min', 'max', 'step']) expect(entree.hasAttribute(attribut), attribut).toBe(false);
|
||||
|
||||
const recus = [];
|
||||
const cible = monter(
|
||||
Champ,
|
||||
{
|
||||
cle: 'nombre',
|
||||
etiquette: 'communs.commande.modifier',
|
||||
valeur: 3,
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 100,
|
||||
pas: 1,
|
||||
lire: (texte) => (/^\d+$/.test(texte) ? { valeur: Number(texte) } : { refus: { code: 'NON_ENTIER', details: { saisie: texte, remede: null } } }),
|
||||
executer: (valeur) => recus.push(valeur),
|
||||
},
|
||||
montage.contexte,
|
||||
);
|
||||
const nombre = cible.querySelector('input[data-champ="nombre"]');
|
||||
expect([nombre.type, nombre.min, nombre.max, nombre.step, nombre.value]).toEqual(['number', '1', '100', '1', '3']);
|
||||
await cliquer(nombre);
|
||||
nombre.select();
|
||||
await taper('x');
|
||||
await cliquerAilleurs();
|
||||
expect(nombre.value).toBe('');
|
||||
expect(cible.querySelector('[data-refus="NON_ENTIER"]')).not.toBeNull();
|
||||
expect(recus).toEqual([]);
|
||||
await cliquer(nombre);
|
||||
await taper('42');
|
||||
await cliquerAilleurs();
|
||||
expect(recus).toEqual([42]);
|
||||
expect(cible.querySelector('[data-refus]')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('champ : le refus de l’exécution (§ 2.1, § 5.9)', () => {
|
||||
test('executer refusé avec un code : le refus se pose contre le champ, la saisie gardée, rien d’écrit ; le refus levé par son remède, le champ rend la valeur enregistrée, et une saisie nouvelle s’écrit', async () => {
|
||||
const montage = await preparer();
|
||||
|
|
|
|||
|
|
@ -659,7 +659,7 @@ export const ECRANS = Object.freeze([
|
|||
async atteindre(contexte) {
|
||||
// Deux personnes étiquetées, puis la fiche d'une troisième, où
|
||||
// « FINANCE » tapé au clavier s'enregistre sous l'orthographe que
|
||||
// l'événement porte : les pastilles neutres et leur « Retirer », le
|
||||
// l'événement porte : les pastilles neutres et leur croix de retrait, le
|
||||
// signal d'orthographe contre le champ, et la légende des étiquettes
|
||||
// sous celle des appartenances.
|
||||
let charge = chargePrincipale();
|
||||
|
|
|
|||
Loading…
Reference in a new issue