forked from yvv/decision
Fusion du lot U2 : les mineurs d'interface des relectures de M7 et M6c (cibles de 44 px sur téléphone, les trois lectures entières, « Proposer cet ordre » dit pourquoi, une information à un seul endroit dans le vote déplié, l'en-tête compte ce que montre la liste, le changement d'inertie d'un module à sa ligne, settings.reading, l'anglais, le commentaire du moteur) ; C38, M2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -96,7 +96,7 @@ web/src/
|
|||||||
# · g/[group]/decisions · g/[group]/mandats ·
|
# · g/[group]/decisions · g/[group]/mandats ·
|
||||||
# g/[group]/protocoles (à venir) · acces (trustinWallet) · messages (la boîte) ;
|
# g/[group]/protocoles (à venir) · acces (trustinWallet) · messages (la boîte) ;
|
||||||
# /groupes, /decisions… redirigent
|
# /groupes, /decisions… redirigent
|
||||||
web/e2e/ # Playwright C1–C37 + M1 ; serve.sh = serveur jetable /tmp/ld-e2e sur :8092 et
|
web/e2e/ # Playwright C1–C38 + M1, M2 ; serve.sh = serveur jetable /tmp/ld-e2e sur :8092 et
|
||||||
# chain-mock.mjs (nœud et indexeur Ğ1 simulés) ; faux portefeuille dans helpers.ts
|
# chain-mock.mjs (nœud et indexeur Ğ1 simulés) ; faux portefeuille dans helpers.ts
|
||||||
docker/ # app.Dockerfile · app-compose.yml (sonic)
|
docker/ # app.Dockerfile · app-compose.yml (sonic)
|
||||||
.woodpecker/ # app.yml · maquette.yml
|
.woodpecker/ # app.yml · maquette.yml
|
||||||
@@ -118,7 +118,7 @@ maquette/ # v2 intacte (frontend Nuxt, backend FastAPI, docker, docs-v
|
|||||||
```bash
|
```bash
|
||||||
export PATH=$HOME/.cargo/bin:$PATH
|
export PATH=$HOME/.cargo/bin:$PATH
|
||||||
cargo fmt --all --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace
|
cargo fmt --all --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace
|
||||||
cd web && npm run check && npm test && npm run build && npm run e2e # e2e : 38 contrôles (40 tests, C25 et C35 en deux)
|
cd web && npm run check && npm test && npm run build && npm run e2e # e2e : 40 contrôles (42 tests, C25 et C35 en deux)
|
||||||
|
|
||||||
LD_DEV_LOGIN=1 cargo run -p ld-server # :8002, sert web/build
|
LD_DEV_LOGIN=1 cargo run -p ld-server # :8002, sert web/build
|
||||||
cd web && npm run dev # :3002 à chaud
|
cd web && npm run dev # :3002 à chaud
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! R is the minimum number of *for* votes; a version passes when `for ≥ ⌈R⌉`.
|
//! R is the minimum number of *for* votes; a version passes when `for ≥ ⌈R⌉`.
|
||||||
//! W is the size of the electorate, T the votes cast (for + against). At low
|
//! W is the size of the electorate, T the votes cast (for + against). The share of
|
||||||
//! participation the formula asks for near-unanimity, at full participation the
|
//! *for* asked is judged on T' voters, never fewer than the quorum: it is at its
|
||||||
//! simple majority M suffices. Q is the **quorum** (ADR 0013, Yvv, 2026-11-02:
|
//! highest at the quorum (the share of the level at T' = Q), then falls as turnout
|
||||||
|
//! rises above it, down to the simple majority M when the whole electorate votes.
|
||||||
|
//! Below the quorum it does not rise any further: the voters missing count as
|
||||||
|
//! against. Q is the **quorum** (ADR 0013, Yvv, 2026-11-02:
|
||||||
//! « ce n'est pas un nombre de vote pour a priori, c'est plutôt un quorum, à partir
|
//! « ce n'est pas un nombre de vote pour a priori, c'est plutôt un quorum, à partir
|
||||||
//! de ce nombre, alors il faut la bonne proportion attendue par l'indice ») — and
|
//! de ce nombre, alors il faut la bonne proportion attendue par l'indice ») — and
|
||||||
//! below it the voters missing count as against (`judged_voters`). It replaces the
|
//! below it the voters missing count as against (`judged_voters`). It replaces the
|
||||||
@@ -234,6 +237,28 @@ mod tests {
|
|||||||
assert_eq!(e.missing, 0);
|
assert_eq!(e.missing, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_share_is_highest_at_the_quorum_then_falls_to_the_majority() {
|
||||||
|
// What the head of this module says (lot U2, review of M6c): judged on T' voters, the
|
||||||
|
// share of *for* is at its highest at the quorum, falls as turnout rises, and is M when
|
||||||
|
// the whole electorate votes; below the quorum it stays that of the quorum.
|
||||||
|
let (w, q) = (5745, 12);
|
||||||
|
let p = standard(q);
|
||||||
|
let share = |cast: u32| required_ratio(w, judged_voters(cast, q), &p);
|
||||||
|
for cast in 0..q {
|
||||||
|
assert!((share(cast) - share(q)).abs() < 1e-12, "below the quorum, at {cast}");
|
||||||
|
}
|
||||||
|
let mut last = share(q);
|
||||||
|
for cast in (q + 1..w).step_by(7).chain([w]) {
|
||||||
|
let now = share(cast);
|
||||||
|
assert!(now < last, "falls at {cast}: {now} after {last}");
|
||||||
|
last = now;
|
||||||
|
}
|
||||||
|
assert!((share(w) - 0.5).abs() < 1e-12, "M at full turnout");
|
||||||
|
// Never « near-unanimity »: at this level, 85 % of the quorum.
|
||||||
|
assert!(share(q) > 0.8 && share(q) < 0.9, "at the quorum: {}", share(q));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parity_with_the_typescript_reference() {
|
fn parity_with_the_typescript_reference() {
|
||||||
// Values computed by maquette/frontend/app/engine/threshold.ts (C = 0).
|
// Values computed by maquette/frontend/app/engine/threshold.ts (C = 0).
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Chaque contrôle existe en deux versions : **automatique** (Playwright, `web/e2e
|
|||||||
`npm run e2e`) et **humaine** (Chiara, sur la production ou en local). Une remarque de Chiara
|
`npm run e2e`) et **humaine** (Chiara, sur la production ou en local). Une remarque de Chiara
|
||||||
sur un contrôle vaut échec du contrôle tant qu'elle n'est pas traitée.
|
sur un contrôle vaut échec du contrôle tant qu'elle n'est pas traitée.
|
||||||
|
|
||||||
Où tester : les contrôles qui écrivent (C5, C6, C7, C21, C22, C25, C26, C28–C31, C35, C37) se font dans le **groupe
|
Où tester : les contrôles qui écrivent (C5, C6, C7, C21, C22, C25, C26, C28–C31, C35, C37, C38, M2) se font dans le **groupe
|
||||||
d'essai** (caché, quorum 1 à chaque cran — une voix seule y passe, ADR 0013 —, jamais publié) — jamais sur le texte de la communauté. Au départ, rien
|
d'essai** (caché, quorum 1 à chaque cran — une voix seule y passe, ADR 0013 —, jamais publié) — jamais sur le texte de la communauté. Au départ, rien
|
||||||
n'y est en vigueur (ADR 0011) : les contrôles qui lisent un texte en vigueur (C4 à C6, C20, C21, C24,
|
n'y est en vigueur (ADR 0011) : les contrôles qui lisent un texte en vigueur (C4 à C6, C20, C21, C24,
|
||||||
C28 à C31) y font d'abord entrer Raison d'être, Unicité du compte et Réciprocité par le vote (une voix
|
C28 à C31) y font d'abord entrer Raison d'être, Unicité du compte et Réciprocité par le vote (une voix
|
||||||
@@ -28,20 +28,21 @@ pour une personne, ils coûtent 0,1 Ğ1 réels : c'est un acte « paye » d'Yvv.
|
|||||||
| C11 | Une personne, une voix | avec un lien d'invitation et un portefeuille dont le compte n'est pas membre de la toile (Bob, chaîne simulée), voter « Pour » sur un article de Monnaie libre | le vote dit que la voix est celle de l'identité Ğ1 du portefeuille qui signe ; après signature : « Ton vote n'a pas été compté : le portefeuille qui a signé n'est pas celui d'un membre de la toile de confiance Ğ1. », et sous l'article « Ton dernier vote n'a pas été compté… », 0 pour ; dans le groupe d'essai, la même personne vote directement, sans portefeuille |
|
| C11 | Une personne, une voix | avec un lien d'invitation et un portefeuille dont le compte n'est pas membre de la toile (Bob, chaîne simulée), voter « Pour » sur un article de Monnaie libre | le vote dit que la voix est celle de l'identité Ğ1 du portefeuille qui signe ; après signature : « Ton vote n'a pas été compté : le portefeuille qui a signé n'est pas celui d'un membre de la toile de confiance Ğ1. », et sous l'article « Ton dernier vote n'a pas été compté… », 0 pour ; dans le groupe d'essai, la même personne vote directement, sans portefeuille |
|
||||||
| C12 | Lien d'équipe | administrateur : `/acces` → « Liens d'équipe », cocher le groupe d'essai, créer ; l'ouvrir dans deux navigateurs, sous deux noms | chacun arrive sur ses groupes sous son nom ; dans `/acces`, « 2 / 30 personnes » et deux identités « venue par » le lien ; un nom déjà pris est refusé, même autrement écrit (« luna » pour « Luna ») ; sur un navigateur où quelqu'un est connecté, « Ce n'est pas moi » le déconnecte ; « Révoquer » ferme le lien |
|
| C12 | Lien d'équipe | administrateur : `/acces` → « Liens d'équipe », cocher le groupe d'essai, créer ; l'ouvrir dans deux navigateurs, sous deux noms | chacun arrive sur ses groupes sous son nom ; dans `/acces`, « 2 / 30 personnes » et deux identités « venue par » le lien ; un nom déjà pris est refusé, même autrement écrit (« luna » pour « Luna ») ; sur un navigateur où quelqu'un est connecté, « Ce n'est pas moi » le déconnecte ; « Révoquer » ferme le lien |
|
||||||
| M1 | Mobile | téléphone (ou fenêtre étroite) | le logo dans la barre du haut ; l'accueil sous la voûte tient à 412 et à 360 de large (les trois points dans l'ouverture, rien ne déborde) et montre les groupes ; menu en tiroir (le nombre à côté de « Docs de référence », les quatre pastilles d'ambiance) ; **au clavier, les pastilles** : un seul arrêt de Tab, les flèches gauche / droite passent d'une ambiance à l'autre et la choisissent ; l'anneau de focus, en pointillé, se lit sur la page (contraste 3:1 au moins, dans les quatre ambiances) et ne se confond pas avec l'anneau plein de l'ambiance choisie ; les gestes visibles sans survol, « Le vote » d'au moins 44 px ; le vote de la Raison d'être (en tête) se déplie sous elle : « 9 oui sur 12 votants pour passer » ; « Réglages de la formule » depuis le vote : le panneau en feuille, nommé « Réglages de la formule » en tête, le tableau des cinq crans tient dans la feuille ; « Voter en lot » : le panier tient dans la feuille, son bouton « Signer et envoyer » reste en vue |
|
| M1 | Mobile | téléphone (ou fenêtre étroite) | le logo dans la barre du haut ; l'accueil sous la voûte tient à 412 et à 360 de large (les trois points dans l'ouverture, rien ne déborde) et montre les groupes ; menu en tiroir (le nombre à côté de « Docs de référence », les quatre pastilles d'ambiance) ; **au clavier, les pastilles** : un seul arrêt de Tab, les flèches gauche / droite passent d'une ambiance à l'autre et la choisissent ; l'anneau de focus, en pointillé, se lit sur la page (contraste 3:1 au moins, dans les quatre ambiances) et ne se confond pas avec l'anneau plein de l'ambiance choisie ; les gestes visibles sans survol, « Le vote » d'au moins 44 px ; le vote de la Raison d'être (en tête) se déplie sous elle : « 9 oui sur 12 votants pour passer » ; « Réglages de la formule » depuis le vote : le panneau en feuille, nommé « Réglages de la formule » en tête, le tableau des cinq crans tient dans la feuille ; « Voter en lot » : le panier tient dans la feuille, son bouton « Signer et envoyer » reste en vue |
|
||||||
|
| M2 | Téléphone : les gestes du lecteur, les trois lectures (lot U2) | téléphone (Pixel 7), groupe d'essai : une version concurrente de la Raison d'être et un article à soi (l'automate les propose) ; lire la rangée « Lire » ; « Tout déplier » ; la Raison d'être → « Le vote » → « Autres versions (1) », « La formule du seuil » ; puis le vote de son article → « Proposer un changement d'inertie » | « En vigueur », « Si les bien cotées passent », « Si tout passe » se lisent en entier : la rangée passe à la ligne, rien ne défile ni ne se coupe ; « Tout déplier », « Autres versions (1) », « Fixer ce quorum » et **tous** les gestes en vue du vote déplié (Pour, Contre, les dépliants, « Réglages de la formule », « Coquilles… », « Soumettre au vote ») font au moins 44 px de haut |
|
||||||
| C13 | Logo et ambiances | ouvrir `/` (appareil en clair, puis en sombre) ; choisir chaque pastille d'ambiance ; recharger | le logo « Clé de voûte » (signe, *libre*, **Decision**) droit, sans tampon ; le sceau 井 en bas à droite ; première visite : pierre (clair) ou nuit (sombre) ; les quatre pastilles (Pierre, Calcaire, Nuit, Ardoise) changent l'ambiance, qui reste après rechargement ; favicon, icône Apple et manifeste servis |
|
| C13 | Logo et ambiances | ouvrir `/` (appareil en clair, puis en sombre) ; choisir chaque pastille d'ambiance ; recharger | le logo « Clé de voûte » (signe, *libre*, **Decision**) droit, sans tampon ; le sceau 井 en bas à droite ; première visite : pierre (clair) ou nuit (sombre) ; les quatre pastilles (Pierre, Calcaire, Nuit, Ardoise) changent l'ambiance, qui reste après rechargement ; favicon, icône Apple et manifeste servis |
|
||||||
| C14 | La page d'un groupe | Monnaie libre | le nom ; le pavé des quatre piliers (Docs de référence « ouvert », les trois autres « à venir ») ; « En cours » : l'Acte d'engagement Certification, qui ouvre son lecteur |
|
| C14 | La page d'un groupe | Monnaie libre | le nom ; le pavé des quatre piliers (Docs de référence « ouvert », les trois autres « à venir ») ; « En cours » : l'Acte d'engagement Certification, qui ouvre son lecteur |
|
||||||
| C15 | Le menu | survoler les outils ; ouvrir Docs, puis Mandats ; revenir à l'accueil | « 1 » à côté de « Docs de référence » ; aucun outil surligné sur la page du groupe, seul celui où l'on est ensuite ; au survol, seul Docs réagit ; Mandats s'ouvre quand même (ses « À venir ») ; hors d'un groupe, les outils mènent au dernier groupe ouvert (ou à l'accueil s'il n'y en a pas) |
|
| C15 | Le menu | survoler les outils ; ouvrir Docs, puis Mandats ; revenir à l'accueil | « 1 » à côté de « Docs de référence » ; aucun outil surligné sur la page du groupe, seul celui où l'on est ensuite ; au survol, seul Docs réagit ; Mandats s'ouvre quand même (ses « À venir ») ; hors d'un groupe, les outils mènent au dernier groupe ouvert (ou à l'accueil s'il n'y en a pas) |
|
||||||
| C16 | En cours, à venir | Monnaie libre → Docs, Mandats, Protocoles ; les anciennes adresses `/decisions`, `/mandats`, `/protocoles` | Docs : « En cours » (Acte d'engagement Certification) et « À venir » (Acte d'engagement forgerons) ; Mandats : Administration forum Duniter, Administration forum Monnaie libre ; Protocoles : Runtime upgrade — décision on-chain ; les « À venir » sont éteints, sans lien ; ils suivent la langue ; les anciennes adresses mènent aux outils du groupe courant |
|
| C16 | En cours, à venir | Monnaie libre → Docs, Mandats, Protocoles ; les anciennes adresses `/decisions`, `/mandats`, `/protocoles` | Docs : « En cours » (Acte d'engagement Certification) et « À venir » (Acte d'engagement forgerons) ; Mandats : Administration forum Duniter, Administration forum Monnaie libre ; Protocoles : Runtime upgrade — décision on-chain ; les « À venir » sont éteints, sans lien ; ils suivent la langue ; les anciennes adresses mènent aux outils du groupe courant |
|
||||||
| C17 | Décisions | Monnaie libre → Décisions ; voter (par la chaîne : bulletin, transaction, confirmation) sur une version d'un article, recharger | une ligne par doc : « Acte d'engagement Certification · 41 modules à voter · dont 41 sans ton vote » (33 articles, le titre, l'ordre de 7 sections), puis « dont 40 » ; la ligne ouvre le lecteur |
|
| C17 | Décisions | Monnaie libre → Décisions ; voter (par la chaîne : bulletin, transaction, confirmation) sur une version d'un article, recharger | une ligne par doc : « Acte d'engagement Certification · 41 modules à voter · dont 41 sans ton vote » (33 articles, le titre, l'ordre de 7 sections), puis « dont 40 » ; la ligne ouvre le lecteur |
|
||||||
| C18 | La Raison d'être | Monnaie libre → Acte d'engagement Certification → « Si tout passe », puis EN, puis ES | en tête du texte, « Raison d'être » : la première phrase en italique, puis deux paragraphes ; « Purpose », « Razón de ser » |
|
| C18 | La Raison d'être | Monnaie libre → Acte d'engagement Certification → « Si tout passe », puis EN, puis ES | en tête du texte, « Raison d'être » : la première phrase en italique, puis deux paragraphes ; « Purpose », « Razón de ser » |
|
||||||
| C19 | Les versions d'un article | Monnaie libre → « Propositions en cours » → « Tout déplier » → « Connaissance suffisante » (au clavier : Tab jusqu'à la ligne, Entrée) | une seule ligne, « 2 versions » ; dépliée : « variante 1.0.0 » et « variante 0.4.0-draft », chacune avec son niveau et ce qui la distingue ; un clic sur une version déplie son vote sous elle (cran 3, quorum 50 : « 39 oui sur 50 votants pour passer ») ; un nouveau clic sur la ligne la replie |
|
| C19 | Les versions d'un article | Monnaie libre → « Propositions en cours » → « Tout déplier » → « Connaissance suffisante » (au clavier : Tab jusqu'à la ligne, Entrée) | une seule ligne, « 2 versions » ; dépliée : « variante 1.0.0 » et « variante 0.4.0-draft », chacune avec son niveau et ce qui la distingue ; un clic sur une version déplie son vote sous elle (cran 3, quorum 50 : « 39 oui sur 50 votants pour passer »), sans « Autres versions » : l'autre version est juste là, dans la liste (lot U2) ; un nouveau clic sur la ligne la replie |
|
||||||
| C20 | Réglages de la formule et cinq crans | Monnaie libre → Acte d'engagement : « Réglages de la formule » (en-tête) ; puis, en tête du texte, Unicité du compte (Proposition seed) → « Le vote » → « Réglages de la formule » ; dans le groupe d'essai, Réciprocité (entrée en vigueur par le vote) → « Le vote » → « Proposer un changement d'inertie » → 5 → « Soumettre au vote » | W 5 745 (toile de confiance Ğ1), B 0,1 ; « 100 votants sur 5 745 (1,74 %), deux fois 50, le quorum de référence : la participation de référence… (0 pour l'instant) » ; le tableau des cinq crans : très basse, basse, moyenne, haute, très haute ; M 50, 50, 55, 60, 66 % ; part exigée 60, 67, 75, 83, 90 % ; quorum 12, 12, 50, 50, 50 (ADR 0013) ; voix pour à 100 votants 60, 67, 75, 83, 90, « Voix pour : à 100 votants, comme d'habitude. », « Part exigée : la part de « pour » demandée, à 100 votants comme d'habitude. », « Quorum : en dessous, les votants qui manquent comptent comme « non ». … » ; depuis le vote déplié : « Inertie · 2 / 5 · basse », la ligne du cran 2 marquée « cet article », à 0 votant 8, 9, 39, 43, 46 voix, « Voix pour : à 0 votant, la participation de cette version. » ; le sélecteur montre les cinq crans, 2 « actuelle », atteignable au clavier mais jamais choisi ; un seul arrêt de tabulation ; les flèches passent d'un cran à l'autre et le choisissent, en boucle ; le vote de la proposition se déplie : « inertie vers 5 / 5 · très haute », votée à « inertie 2 / 5 · basse » |
|
| C20 | Réglages de la formule et cinq crans | Monnaie libre → Acte d'engagement : « Réglages de la formule » (en-tête) ; puis, en tête du texte, Unicité du compte (Proposition seed) → « Le vote » → « Réglages de la formule » ; dans le groupe d'essai, Réciprocité (entrée en vigueur par le vote) → « Le vote » → « Proposer un changement d'inertie » → 5 → « Soumettre au vote » | W 5 745 (toile de confiance Ğ1), B 0,1 ; « 100 votants sur 5 745 (1,74 %), deux fois 50, le quorum de référence : la participation de référence… (0 pour l'instant) » ; le tableau des cinq crans : très basse, basse, moyenne, haute, très haute ; M 50, 50, 55, 60, 66 % ; part exigée 60, 67, 75, 83, 90 % ; quorum 12, 12, 50, 50, 50 (ADR 0013) ; voix pour à 100 votants 60, 67, 75, 83, 90, « Voix pour : à 100 votants, comme d'habitude. », « Part exigée : la part de « pour » demandée, à 100 votants comme d'habitude. », « Quorum : en dessous, les votants qui manquent comptent comme « non ». … » ; depuis le vote déplié : « Inertie · 2 / 5 · basse », la ligne du cran 2 marquée « cet article », à 0 votant 8, 9, 39, 43, 46 voix, « Voix pour : à 0 votant, la participation de cette version. » ; le sélecteur montre les cinq crans, 2 « actuelle », atteignable au clavier mais jamais choisi ; un seul arrêt de tabulation ; les flèches passent d'un cran à l'autre et le choisissent, en boucle ; le vote de la proposition se déplie : « inertie vers 5 / 5 · très haute », votée à « inertie 2 / 5 · basse » |
|
||||||
| C21 | Le vote déplié | groupe d'essai : Unicité du compte → « Le vote », puis « Pour » ; une autre personne ouvre le même vote | sous l'article : le niveau d'eau, « 0 pour · 0 contre », « N oui sur Q votants pour passer » (le quorum en infobulle), « Il manque … », les crans de l'article, « Tu n'as pas voté sur cette version. » ; après « Pour » : « Ton vote : pour · enregistré », « 1 pour · 0 contre », le bouton enfoncé, « Ton vote ne se montre à personne : seuls les totaux sont visibles. » ; l'autre personne voit 1 pour, jamais qui ; « Le vote » replie ; « Retirer mon vote » remet 0 |
|
| C21 | Le vote déplié | groupe d'essai : Unicité du compte → « Le vote », puis « Pour » ; une autre personne ouvre le même vote | sous l'article : le niveau d'eau, « 0 pour · 0 contre », « N oui sur Q votants pour passer » (le quorum en infobulle), « Il manque … », les crans de l'article, « Tu n'as pas voté sur cette version. » ; après « Pour » : « Ton vote : pour · enregistré », « 1 pour · 0 contre », le bouton enfoncé, « Ton vote ne se montre à personne : seuls les totaux sont visibles. » ; l'autre personne voit 1 pour, jamais qui ; « Le vote » replie ; « Retirer mon vote » remet 0 |
|
||||||
| C22 | Le vote groupé | groupe d'essai : « Propositions en cours » → « Engagements techniques » → « Voter cette section » ; « Tout pour » ; dans « Connaissance suffisante », « Pour » sur la variante 1.0.0 ; recharger ; « Voter (n) » | le panier s'ouvre sur la section ; « Tout pour » remplit les articles à une version, pas « Connaissance suffisante » ni « Rencontre physique… » (« Plusieurs versions : choisis la tienne. », et le compte des articles laissés) ; le panier reste après rechargement (« Voter en lot · n ») ; un seul envoi : « n votes enregistrés. », le panier vide, la variante choisie à 1 pour |
|
| C22 | Le vote groupé | groupe d'essai : « Propositions en cours » → « Engagements techniques » → « Voter cette section » ; « Tout pour » ; dans « Connaissance suffisante », « Pour » sur la variante 1.0.0 ; recharger ; « Voter (n) » | le panier s'ouvre sur la section ; « Tout pour » remplit les articles à une version, pas « Connaissance suffisante » ni « Rencontre physique… » (« Plusieurs versions : choisis la tienne. », et le compte des articles laissés) ; le panier reste après rechargement (« Voter en lot · n ») ; un seul envoi : « n votes enregistrés. », le panier vide, la variante choisie à 1 pour |
|
||||||
| C23 | Le vote par la chaîne Ğ1 | Monnaie libre, un portefeuille dans le navigateur : « Voter en lot », « Pour » sur Unicité du compte et Réciprocité (Propositions seed), « Signer et envoyer (2) » ; puis, en tête du texte, sous Réciprocité, « Contre » (l'indexeur en retard) puis « Retirer mon vote » ; puis sans portefeuille : la Raison d'être → « Pour » → « Passer par Ğecko » | une seule signature pour les deux : 0,1 Ğ1 vers le compte des votes et une remarque `ld1:` + 64 chiffres, rien du vote en clair ; « Ton vote compte : il est inscrit sur la chaîne Ğ1. », « Compté pour l'identité Ğ1 « Alice ». » ; sous la proposition « Ton vote : pour · inscrit sur la chaîne Ğ1 », 1 pour ; « En attente de la chaîne Ğ1 : contre. » ; le retrait, plus récent sur la chaîne, fait foi même quand le « contre » est confirmé après lui (0 pour · 0 contre) ; Ğecko : un QR code et le lien `g1://<compte>?amount=0.1&comment=ld1:…`, puis la confirmation dès que la chaîne la montre |
|
| C23 | Le vote par la chaîne Ğ1 | Monnaie libre, un portefeuille dans le navigateur : « Voter en lot », « Pour » sur Unicité du compte et Réciprocité (Propositions seed), « Signer et envoyer (2) » ; puis, en tête du texte, sous Réciprocité, « Contre » (l'indexeur en retard) puis « Retirer mon vote » ; puis sans portefeuille : la Raison d'être → « Pour » → « Passer par Ğecko » | une seule signature pour les deux : 0,1 Ğ1 vers le compte des votes et une remarque `ld1:` + 64 chiffres, rien du vote en clair ; « Ton vote compte : il est inscrit sur la chaîne Ğ1. », « Compté pour l'identité Ğ1 « Alice ». » ; sous la proposition « Ton vote : pour · inscrit sur la chaîne Ğ1 », 1 pour ; « En attente de la chaîne Ğ1 : contre. » ; le retrait, plus récent sur la chaîne, fait foi même quand le « contre » est confirmé après lui (0 pour · 0 contre) ; Ğecko : un QR code et le lien `g1://<compte>?amount=0.1&comment=ld1:…`, puis la confirmation dès que la chaîne la montre |
|
||||||
| C24 | Sans compte des votes | Monnaie libre sur un serveur sans `LD_VOTE_ACCOUNT`, puis le groupe d'essai | pas de « Voter en lot » ni « Voter cette section » ; sous la première Proposition seed : « Le vote par la chaîne Ğ1 n'est pas encore ouvert : le compte qui reçoit les votes reste à créer. Tu peux lire, proposer et faire une objection. », pas de bouton, les totaux et « 9 oui sur 12 votants pour passer » restent ; le panier ouvert par son adresse (`?onglet=panier`) dit la même chose, son bouton éteint, et **jamais** « Tes votes comptent dès l'envoi » ; dans le groupe d'essai, le panier et les boutons sont là |
|
| C24 | Sans compte des votes | Monnaie libre sur un serveur sans `LD_VOTE_ACCOUNT`, puis le groupe d'essai | pas de « Voter en lot » ni « Voter cette section » ; sous la première Proposition seed : « Le vote par la chaîne Ğ1 n'est pas encore ouvert : le compte qui reçoit les votes reste à créer. Tu peux lire, proposer et faire une objection. », pas de bouton, les totaux et « 9 oui sur 12 votants pour passer » restent ; le panier ouvert par son adresse (`?onglet=panier`) dit la même chose, son bouton éteint, et **jamais** « Tes votes comptent dès l'envoi » ; dans le groupe d'essai, le panier et les boutons sont là |
|
||||||
| C25 | L'ordre des articles | groupe d'essai : « Tout déplier » ; en fin de « Conclusion », la ligne « Ordre de la section » → « Réordonner » ; cliquer « Monnaie G1 » puis la place 1 ; au clavier, flèche bas puis haut ; « Proposer cet ordre » ; « Pour » ; une deuxième personne vote « Pour » si cette voix ne suffit pas (quorum 1 dans le groupe d'essai : ADR 0013) ; puis, dans Monnaie libre, sur un téléphone, avec un portefeuille : « Voter en lot », « Pour » sur « Ordre des articles · Engagements fondamentaux », « Signer et envoyer (1) » | chaque section finit par **une ligne** « Ordre de la section » (7, aucune pour l'Ordonnancement, un seul article) : la poignée, les crans « basse », « seed a minima · 0 vote », **jamais la liste de ses articles** ; à la fin du texte quand la section s'y lit, sinon à la fin de sa section dans « Propositions en cours » ; le vote d'un ordre en vigueur déplié sous sa ligne montre la liste entière ; « Réordonner » : les articles de la section, sur place, sans panneau ; l'article choisi marqué, puis à sa place, annoncé (« « Monnaie G1 » en place 1 sur 2. »), « Proposer cet ordre » éteint tant que c'est l'ordre en vigueur ; le vote de l'ordre proposé se déplie sous la ligne, l'ordre proposé en clair, « Ordre en vigueur » parmi ses « Autres versions », « 1 ordre proposé » sur la ligne ; aucune ligne d'ordre parmi les propositions ; il est dans le panier, à la fin de sa section ; à la voix qui suffit : « Le nouvel ordre de « Conclusion » est entré en vigueur. », la section se lit dans le nouvel ordre ; par la chaîne : « Ton vote compte : il est inscrit sur la chaîne Ğ1. », « seed a minima · 1 vote » ; sur un téléphone, les gestes du titre sans survol (réordonner sur un téléphone : C35) |
|
| C25 | L'ordre des articles | groupe d'essai : « Tout déplier » ; en fin de « Conclusion », la ligne « Ordre de la section » → « Réordonner » ; cliquer « Monnaie G1 » puis la place 1 ; au clavier, flèche bas puis haut ; « Proposer cet ordre » ; « Pour » ; une deuxième personne vote « Pour » si cette voix ne suffit pas (quorum 1 dans le groupe d'essai : ADR 0013) ; puis, dans Monnaie libre, sur un téléphone, avec un portefeuille : « Voter en lot », « Pour » sur « Ordre des articles · Engagements fondamentaux », « Signer et envoyer (1) » | chaque section finit par **une ligne** « Ordre de la section » (7, aucune pour l'Ordonnancement, un seul article) : la poignée, les crans « basse », « seed a minima · 0 vote », **jamais la liste de ses articles** ; à la fin du texte quand la section s'y lit, sinon à la fin de sa section dans « Propositions en cours » ; le vote d'un ordre en vigueur déplié sous sa ligne montre la liste entière, et son inertie une fois, « Inertie · 2 / 5 · basse » (lot U2) ; « Réordonner » : les articles de la section, sur place, sans panneau ; l'article choisi marqué, puis à sa place, annoncé (« « Monnaie G1 » en place 1 sur 2. »), « Proposer cet ordre » éteint tant que c'est l'ordre en vigueur, et il le dit à côté (« C'est l'ordre en vigueur : déplace au moins un article. ») ; le vote de l'ordre proposé se déplie sous la ligne, l'ordre proposé en clair, « Ordre en vigueur » parmi ses « Autres versions », « 1 ordre proposé » sur la ligne ; aucune ligne d'ordre parmi les propositions ; il est dans le panier, à la fin de sa section ; à la voix qui suffit : « Le nouvel ordre de « Conclusion » est entré en vigueur. », la section se lit dans le nouvel ordre ; par la chaîne : « Ton vote compte : il est inscrit sur la chaîne Ğ1. », « seed a minima · 1 vote » ; sur un téléphone, les gestes du titre sans survol (réordonner sur un téléphone : C35) |
|
||||||
| C26 | Le titre du document | groupe d'essai : sous le titre, « Proposer un autre titre » ; « Acte d'engagement de la certification » ; « Pour » ; une deuxième personne vote « Pour » si cette voix ne suffit pas ; lire en anglais | sous le titre, une ligne : « Titre du document · seed a minima · 0 vote », les crans « inertie 4 / 5 · haute », deux gestes ; le panneau nommé « Proposer un autre titre » ; le titre proposé se vote **sous le titre** (« 1 titre proposé »), jamais parmi les propositions ; à la voix qui suffit : « Le titre « … » est entré en vigueur. », le document change de titre, son vote reste déplié sous le titre ; en anglais, l'ancien titre traduit, marqué « translation needs review » |
|
| C26 | Le titre du document | groupe d'essai : sous le titre, « Proposer un autre titre » ; « Acte d'engagement de la certification » ; « Pour » ; une deuxième personne vote « Pour » si cette voix ne suffit pas ; lire en anglais | sous le titre, une ligne : « Titre du document · seed a minima · 0 vote », les crans « inertie 4 / 5 · haute », deux gestes ; le panneau nommé « Proposer un autre titre » ; le titre proposé se vote **sous le titre** (« 1 titre proposé »), jamais parmi les propositions ; à la voix qui suffit : « Le titre « … » est entré en vigueur. », le document change de titre, son vote reste déplié sous le titre, son inertie dite une fois (« Inertie · 4 / 5 · haute ») ; en anglais, l'ancien titre traduit, marqué « translation needs review » |
|
||||||
| C27 | La participation ramenée dans ses bornes | un groupe de 4 dont la médiane est 2,5 votants, ramenée à 50 % (l'automate fait répondre ces chiffres au serveur) : « Réglages de la formule » | « 3 votants sur 4 (62,5 %) : la médiane des 12 propositions votées depuis un an. » — la part de la mesure elle-même, jamais 3 sur 4 arrondi d'abord ; puis « Ramenée entre 5 % et 50 % de l'électorat : le groupe demande ses voix pour comme si 2 personnes votaient (50 %). » ; « Part exigée : la part de « pour » demandée, à 2 votants comme d'habitude. » |
|
| C27 | La participation ramenée dans ses bornes | un groupe de 4 dont la médiane est 2,5 votants, ramenée à 50 % (l'automate fait répondre ces chiffres au serveur) : « Réglages de la formule » | « 3 votants sur 4 (62,5 %) : la médiane des 12 propositions votées depuis un an. » — la part de la mesure elle-même, jamais 3 sur 4 arrondi d'abord ; puis « Ramenée entre 5 % et 50 % de l'électorat : le groupe demande ses voix pour comme si 2 personnes votaient (50 %). » ; « Part exigée : la part de « pour » demandée, à 2 votants comme d'habitude. » |
|
||||||
| C28 | Objection « redit » ; sur la version retenue, « contre » et retrait, jamais « pour » | groupe d'essai, à trois (Aude, Oscar, Vera) : Aude propose un nouvel article qui répète « Unicité du compte » ; Oscar, dans « Détails » de cette proposition → « Faire une objection » → « Redit », « En vigueur — Unicité du compte », un motif → « Envoyer l'objection » ; Vera déplie son vote → « Contre », puis « Retirer mon vote » ; Vera ouvre « Voter en lot » → « Tout le document » : « Tout pour », puis « Tout contre », puis « Vider » ; Aude ouvre « Messages » → « Modifier l'article visé », complète le texte et soumet ; Oscar ouvre « Messages » → « Lever l'objection » ; Vera déplie de nouveau son vote | « Objection enregistrée : jusqu'à sa levée, personne ne peut plus voter pour la dernière arrivée des deux versions. » ; chez Oscar, « « … » redit « Unicité du compte » », « personne ne peut plus voter pour cette version », « Lever l'objection » ; chez Vera, « Une objection retient cette version : jusqu'à sa levée, personne ne peut plus voter pour. Tu peux voter contre ou retirer ton vote ; à la levée, elle n'entre en vigueur qu'avec les voix encore là. », « Elle redit « Unicité du compte ». », pas de « Pour », mais « Contre », la ligne marquée « retenue par une objection » ; après « Contre » : « Vote enregistré. », « Ton vote : contre », « 0 pour · 1 contre », et « Retirer mon vote » ; après le retrait : « Vote retiré. », « 0 pour · 0 contre », toujours pas de « Pour » ; dans le panier, la version retenue montre « Contre » sans « Pour », avec « une objection la retient : contre ou retrait, pas de « pour » » ; « Tout pour » la laisse de côté et le dit (« Tu as tout mis pour, sauf 1 version qu'une objection retient : sur elle, tu peux voter contre ou retirer ton vote. », le nombre suivant les versions retenues du document) ; « Tout contre » la prend (« Contre » enfoncé) ; chez Aude, « Messages, 1 non lu », le message « Oscar fait une objection : ta version « … » redit « Unicité du compte »… Pour répondre, modifie l'article visé. », puis le panneau « Proposer » sur Unicité du compte, partant de son texte, avec « Cette proposition répond à une objection… » ; chez Oscar, « Ton objection sur « … » a reçu une réponse. », une échéance, « Lever l'objection » et « Réitérer l'objection » ; levée : « Objection levée. », le message dit « objection levée » sans plus aucun geste ; Vera peut de nouveau voter, « Pour » compris |
|
| C28 | Objection « redit » ; sur la version retenue, « contre » et retrait, jamais « pour » | groupe d'essai, à trois (Aude, Oscar, Vera) : Aude propose un nouvel article qui répète « Unicité du compte » ; Oscar, dans « Détails » de cette proposition → « Faire une objection » → « Redit », « En vigueur — Unicité du compte », un motif → « Envoyer l'objection » ; Vera déplie son vote → « Contre », puis « Retirer mon vote » ; Vera ouvre « Voter en lot » → « Tout le document » : « Tout pour », puis « Tout contre », puis « Vider » ; Aude ouvre « Messages » → « Modifier l'article visé », complète le texte et soumet ; Oscar ouvre « Messages » → « Lever l'objection » ; Vera déplie de nouveau son vote | « Objection enregistrée : jusqu'à sa levée, personne ne peut plus voter pour la dernière arrivée des deux versions. » ; chez Oscar, « « … » redit « Unicité du compte » », « personne ne peut plus voter pour cette version », « Lever l'objection » ; chez Vera, « Une objection retient cette version : jusqu'à sa levée, personne ne peut plus voter pour. Tu peux voter contre ou retirer ton vote ; à la levée, elle n'entre en vigueur qu'avec les voix encore là. », « Elle redit « Unicité du compte ». », pas de « Pour », mais « Contre », la ligne marquée « retenue par une objection » ; après « Contre » : « Vote enregistré. », « Ton vote : contre », « 0 pour · 1 contre », et « Retirer mon vote » ; après le retrait : « Vote retiré. », « 0 pour · 0 contre », toujours pas de « Pour » ; dans le panier, la version retenue montre « Contre » sans « Pour », avec « une objection la retient : contre ou retrait, pas de « pour » » ; « Tout pour » la laisse de côté et le dit (« Tu as tout mis pour, sauf 1 version qu'une objection retient : sur elle, tu peux voter contre ou retirer ton vote. », le nombre suivant les versions retenues du document) ; « Tout contre » la prend (« Contre » enfoncé) ; chez Aude, « Messages, 1 non lu », le message « Oscar fait une objection : ta version « … » redit « Unicité du compte »… Pour répondre, modifie l'article visé. », puis le panneau « Proposer » sur Unicité du compte, partant de son texte, avec « Cette proposition répond à une objection… » ; chez Oscar, « Ton objection sur « … » a reçu une réponse. », une échéance, « Lever l'objection » et « Réitérer l'objection » ; levée : « Objection levée. », le message dit « objection levée » sans plus aucun geste ; Vera peut de nouveau voter, « Pour » compris |
|
||||||
| C29 | Objection « contredit », levée à l'échéance | groupe d'essai : Aude propose un article qui contredit « Réciprocité » ; Oscar fait une objection « Contredit » ; Aude déplie le vote de sa proposition → « Reformuler », change le fond, « Enregistrer la correction » ; Oscar ne fait rien ; trois jours passent (l'automate avance l'horloge du serveur : `POST /api/dev/clock`, connexion de développement seulement) | chez Aude : « Elle contredit « Réciprocité ». », « Pour qu'elle se vote à nouveau, reformule-la… », « Reformuler » ouvre « Détails » avec le formulaire de modification ; « Nouvelle version enregistrée : les votants devront reconfirmer. », puis « Une réponse est arrivée : Oscar lève ou réitère l'objection d'ici le … » ; trois jours après : l'objection dit « levée : 3 jours sans suite », la proposition se vote de nouveau, le message d'Oscar dit « objection levée : 3 jours sans suite », sans geste |
|
| C29 | Objection « contredit », levée à l'échéance | groupe d'essai : Aude propose un article qui contredit « Réciprocité » ; Oscar fait une objection « Contredit » ; Aude déplie le vote de sa proposition → « Reformuler », change le fond, « Enregistrer la correction » ; Oscar ne fait rien ; trois jours passent (l'automate avance l'horloge du serveur : `POST /api/dev/clock`, connexion de développement seulement) | chez Aude : « Elle contredit « Réciprocité ». », « Pour qu'elle se vote à nouveau, reformule-la… », « Reformuler » ouvre « Détails » avec le formulaire de modification ; « Nouvelle version enregistrée : les votants devront reconfirmer. », puis « Une réponse est arrivée : Oscar lève ou réitère l'objection d'ici le … » ; trois jours après : l'objection dit « levée : 3 jours sans suite », la proposition se vote de nouveau, le message d'Oscar dit « objection levée : 3 jours sans suite », sans geste |
|
||||||
@@ -50,9 +51,10 @@ pour une personne, ils coûtent 0,1 Ğ1 réels : c'est un acte « paye » d'Yvv.
|
|||||||
| C32 | Lâcher ou garder un bulletin | Monnaie libre, un portefeuille dans le navigateur (Eve, chaîne simulée), « Propositions en cours » → Document de révocation : « Contre » le nœud éteint ; puis « Contre », fermer la fenêtre pendant que le portefeuille demande, refuser dans le portefeuille ; puis « Contre », fermer, signer dans le portefeuille ; une autre personne, sans portefeuille : « Contre » → « Passer par Ğecko », fermer, payer depuis un téléphone | nœud éteint : « La chaîne Ğ1 ne répond pas. Réessaie dans un moment. », le bulletin lâché aussitôt, rien « en attente » sous l'article ; fenêtre fermée puis refus : lâché ; fenêtre fermée puis signature : gardé, et la chaîne le confirme sans la page (« Ton vote : contre · inscrit sur la chaîne Ğ1 ») ; Ğecko, fenêtre fermée : « En attente de la chaîne Ğ1 : contre. », puis confirmé sans la page |
|
| C32 | Lâcher ou garder un bulletin | Monnaie libre, un portefeuille dans le navigateur (Eve, chaîne simulée), « Propositions en cours » → Document de révocation : « Contre » le nœud éteint ; puis « Contre », fermer la fenêtre pendant que le portefeuille demande, refuser dans le portefeuille ; puis « Contre », fermer, signer dans le portefeuille ; une autre personne, sans portefeuille : « Contre » → « Passer par Ğecko », fermer, payer depuis un téléphone | nœud éteint : « La chaîne Ğ1 ne répond pas. Réessaie dans un moment. », le bulletin lâché aussitôt, rien « en attente » sous l'article ; fenêtre fermée puis refus : lâché ; fenêtre fermée puis signature : gardé, et la chaîne le confirme sans la page (« Ton vote : contre · inscrit sur la chaîne Ğ1 ») ; Ğecko, fenêtre fermée : « En attente de la chaîne Ğ1 : contre. », puis confirmé sans la page |
|
||||||
| C33 | Le reçu de ton vote | Monnaie libre, un portefeuille : « Contre » sur Ne jamais certifier seul (« Propositions en cours »), « Télécharger ton reçu », « Tes bulletins » ; sur « Tes bulletins », « Fichier du reçu » : le reçu ; puis coller le reçu avec un choix changé, « Vérifier » ; une autre personne ouvre « Tes bulletins » | le reçu `recu-vote-<date>-<id>.json` : les votes, le sel, la remarque `ld1:…`, « confirmed », l'identité « Alice » ; « Tes bulletins de vote » : « Compté pour l'identité Ğ1 « Alice » » et la remarque ; « Le reçu tient : ses votes et son sel donnent bien ce commentaire. », « Ne jamais certifier seul : contre » ; changé : « Le reçu ne tient pas » ; l'autre personne : « Tu n'as encore envoyé aucun bulletin par la chaîne Ğ1. », et le reçu lui est refusé |
|
| C33 | Le reçu de ton vote | Monnaie libre, un portefeuille : « Contre » sur Ne jamais certifier seul (« Propositions en cours »), « Télécharger ton reçu », « Tes bulletins » ; sur « Tes bulletins », « Fichier du reçu » : le reçu ; puis coller le reçu avec un choix changé, « Vérifier » ; une autre personne ouvre « Tes bulletins » | le reçu `recu-vote-<date>-<id>.json` : les votes, le sel, la remarque `ld1:…`, « confirmed », l'identité « Alice » ; « Tes bulletins de vote » : « Compté pour l'identité Ğ1 « Alice » » et la remarque ; « Le reçu tient : ses votes et son sel donnent bien ce commentaire. », « Ne jamais certifier seul : contre » ; changé : « Le reçu ne tient pas » ; l'autre personne : « Tu n'as encore envoyé aucun bulletin par la chaîne Ğ1. », et le reçu lui est refusé |
|
||||||
| C34 | Le vote simulé, la chaîne fermée (ADR 0012) | Monnaie libre sur un serveur sans compte des votes (l'automate ferme la chaîne du serveur jetable, puis la rouvre) : « Propositions en cours » → Logiciel auditable → « Contre », sans portefeuille ; puis « Voter en lot », « Contre » encore, « Voter (1) » ; puis « Tes bulletins » | « Voter en lot » est là, marqué « vote simulé » ; aucune fenêtre de portefeuille ; « Vote simulé. », **jamais** « Vote enregistré. » ; « Ton vote : contre · simulé » aussitôt, et un « contre » de plus dans les totaux ; dans l'API, `myVoteState` « simulated » et `tallySimulated` ; le panier : « 1 vote simulé. », jamais « enregistré » ; dans « Tes bulletins » : « Simulé : compté pour « Sacha » tant que les votes simulés ne sont pas effacés », sans bloc (l'ouverture de la chaîne n'efface rien : c'est un acte humain, sans date promise) |
|
| C34 | Le vote simulé, la chaîne fermée (ADR 0012) | Monnaie libre sur un serveur sans compte des votes (l'automate ferme la chaîne du serveur jetable, puis la rouvre) : « Propositions en cours » → Logiciel auditable → « Contre », sans portefeuille ; puis « Voter en lot », « Contre » encore, « Voter (1) » ; puis « Tes bulletins » | « Voter en lot » est là, marqué « vote simulé » ; aucune fenêtre de portefeuille ; « Vote simulé. », **jamais** « Vote enregistré. » ; « Ton vote : contre · simulé » aussitôt, et un « contre » de plus dans les totaux ; dans l'API, `myVoteState` « simulated » et `tallySimulated` ; le panier : « 1 vote simulé. », jamais « enregistré » ; dans « Tes bulletins » : « Simulé : compté pour « Sacha » tant que les votes simulés ne sont pas effacés », sans bloc (l'ouverture de la chaîne n'efface rien : c'est un acte humain, sans date promise) |
|
||||||
| C35 | Réordonner une section, par poignées, puis voter (lot U) | groupe d'essai : à la fin des « Engagements fondamentaux », la ligne « Ordre de la section » → « Réordonner » ; glisser la poignée de Réciprocité sur Unicité du compte ; au clavier, flèche haut puis bas ; « Descendre « Réciprocité » » ; cliquer Réciprocité puis la place 2 ; « Annuler », puis « Réordonner » et « Monter « Réciprocité » » ; « Proposer cet ordre » ; « Pour » (une deuxième voix si la sienne ne suffit pas) ; puis, sur un téléphone, dans Monnaie libre : « Tout déplier », « Ordre des articles · Engagements techniques » → « Réordonner » | la ligne ne répète aucun article ; « Réordonner » : les articles de la section, en vigueur ou seulement proposés (« Engagements fondamentaux » marqué « proposé »), sur place (pas de panneau), une poignée devant chacun, les articles en vigueur de la section repliés dans la liste (pas deux fois) ; chaque geste annonce la place (« « Réciprocité » en place 2 sur 3. ») ; « Proposer cet ordre » éteint sur l'ordre en vigueur ; « Annuler » rend la section telle qu'elle se lisait ; « Ordre proposé au vote. », son vote déplié aussitôt sous la ligne, l'ordre proposé en clair, « 1 ordre proposé » ; « Vote enregistré. » ; en vigueur, la section se lit dans le nouvel ordre, « adopté le … » ; sur un téléphone : 11 articles, rien ne déborde de la page, l'article, sa poignée et ses flèches font au moins 44 px |
|
| C35 | Réordonner une section, par poignées, puis voter (lot U) | groupe d'essai : à la fin des « Engagements fondamentaux », la ligne « Ordre de la section » → « Réordonner » ; glisser la poignée de Réciprocité sur Unicité du compte ; au clavier, flèche haut puis bas ; « Descendre « Réciprocité » » ; cliquer Réciprocité puis la place 2 ; « Annuler », puis « Réordonner » et « Monter « Réciprocité » » ; « Proposer cet ordre » ; « Pour » (une deuxième voix si la sienne ne suffit pas) ; puis, sur un téléphone, dans Monnaie libre : « Tout déplier », « Ordre des articles · Engagements techniques » → « Réordonner » | la ligne ne répète aucun article ; « Réordonner » : les articles de la section, en vigueur ou seulement proposés (« Engagements fondamentaux » marqué « proposé »), sur place (pas de panneau), une poignée devant chacun, les articles en vigueur de la section repliés dans la liste (pas deux fois) ; chaque geste annonce la place (« « Réciprocité » en place 2 sur 3. ») ; « Proposer cet ordre » éteint sur l'ordre en vigueur, et il le dit à côté, lu aussi comme sa description (« C'est l'ordre en vigueur : déplace au moins un article. ») : la phrase s'en va dès qu'un article bouge et revient quand l'ordre redevient celui en vigueur (lot U2) ; « Annuler » rend la section telle qu'elle se lisait ; « Ordre proposé au vote. », son vote déplié aussitôt sous la ligne, l'ordre proposé en clair, « 1 ordre proposé » ; « Vote enregistré. » ; en vigueur, la section se lit dans le nouvel ordre, « adopté le … » ; sur un téléphone : 11 articles, rien ne déborde de la page, l'article, sa poignée et ses flèches font au moins 44 px |
|
||||||
| C36 | Combien de oui pour passer, combien de non pour bloquer (ADR 0013 ; Yvv, 02/11) | Monnaie libre : la Raison d'être (en tête) → « Le vote » ; « Tout déplier » ; puis, l'automate donnant à une proposition du groupe d'essai les totaux 9 pour · 4 contre (cran 2, quorum 12), puis 3 pour · 0 contre | avant tout vote, sous l'article : « 9 oui sur 12 votants pour passer », au survol « Quorum : 12 votants. En dessous, les votants qui manquent comptent comme « non ». » ; dans la liste, en bref : « 39 oui sur 50 » (cran 3) ; à 9 pour · 4 contre : « 11 oui pour passer · 4 non pour bloquer » sous l'article, « passe à 11 oui · bloquée à 4 non » dans la liste, « Il manque 2 voix pour. » ; à 3 pour · 0 contre : « 9 oui pour passer · le quorum la bloque », « passe à 9 oui » |
|
| C36 | Combien de oui pour passer, combien de non pour bloquer (ADR 0013 ; Yvv, 02/11) | Monnaie libre : la Raison d'être (en tête) → « Le vote » ; « Tout déplier » ; puis, l'automate donnant à une proposition du groupe d'essai les totaux 9 pour · 4 contre (cran 2, quorum 12), puis 3 pour · 0 contre | avant tout vote, sous l'article : « 9 oui sur 12 votants pour passer », au survol « Quorum : 12 votants. En dessous, les votants qui manquent comptent comme « non ». » ; la ligne de la Proposition seed, juste au-dessus, ne redit plus les totaux ni les oui (lot U2) ; dans la liste, en bref : « 39 oui sur 50 » (cran 3) ; à 9 pour · 4 contre : « 11 oui pour passer · 4 non pour bloquer » sous l'article, « passe à 11 oui · bloquée à 4 non » dans la liste, « Il manque 2 voix pour. » ; à 3 pour · 0 contre : « 9 oui pour passer · le quorum la bloque », « passe à 9 oui » |
|
||||||
| C37 | Le quorum d'un article, par son autrice, avant le premier vote (ADR 0013) | groupe d'essai : proposer un article, puis son vote → « Quorum » : 3 → « Fixer ce quorum » ; « Revenir au défaut (1) » ; 100000 → « Fixer ce quorum » ; une autre personne ouvre le même vote, puis vote « contre » ; l'autrice tente 2 | le réglage n'apparaît qu'à l'autrice (`canSetQuorum`) : « Quorum · 1 votant », « Par défaut, celui de son cran : 1 votant. », le champ « Quorum de l'article » ; « Quorum fixé : 3 votants. », la ligne dit « … oui sur 3 votants pour passer » ; « Quorum revenu au défaut : 1 votant. » ; hors bornes (400) : « Ce quorum sort des bornes : au moins 2 votants (1 dans le groupe d'essai), au plus … » ; l'autre personne ne voit pas le réglage ; après son vote (409) : « Quelqu'un a déjà voté sur cet article : son quorum ne change plus. », puis le réglage disparaît |
|
| C37 | Le quorum d'un article, par son autrice, avant le premier vote (ADR 0013) | groupe d'essai : proposer un article, puis son vote → « Quorum » : 3 → « Fixer ce quorum » ; « Revenir au défaut (1) » ; 100000 → « Fixer ce quorum » ; une autre personne ouvre le même vote, puis vote « contre » ; l'autrice tente 2 | le réglage n'apparaît qu'à l'autrice (`canSetQuorum`) : « Quorum · 1 votant », « Par défaut, celui de son cran : 1 votant. », le champ « Quorum de l'article » ; « Quorum fixé : 3 votants. », la ligne dit « … oui sur 3 votants pour passer » ; « Quorum revenu au défaut : 1 votant. » ; hors bornes (400) : « Ce quorum sort des bornes : au moins 2 votants (1 dans le groupe d'essai), au plus … » ; l'autre personne ne voit pas le réglage ; après son vote (409) : « Quelqu'un a déjà voté sur cet article : son quorum ne change pas. » (lot T2), puis le réglage disparaît |
|
||||||
|
| C38 | Ce que compte l'en-tête ; un changement d'inertie d'un ordre ou du titre (lot U2) | Monnaie libre : lire la ligne de faits sous le résumé ; « Tout déplier », compter les versions de la liste ; puis, dans le groupe d'essai, proposer un changement d'inertie de l'ordre de « Conclusion » et du titre (l'automate passe par `POST /api/units/{id}/inertia`, puis les retire) | « 0 article en vigueur · 3 Propositions seed · N propositions en cours », N le nombre de versions que montre la liste : les Propositions seed lues en tête sont dites à part, les ordres et titres proposés ne sont pas comptés (ils se votent à la ligne de leur module) ; sous le titre, la pastille « 1 changement d'inertie proposé », qui déplie son vote sous le titre ; à la fin de « Conclusion », la même pastille sur la ligne d'ordre, qui déplie son vote là ; aucun « inertie vers … » dans « Propositions en cours », et l'en-tête compte toujours ce que la liste montre |
|
||||||
|
|
||||||
## Au-delà de la liste
|
## Au-delà de la liste
|
||||||
|
|
||||||
@@ -93,3 +95,4 @@ page, la langue et la taille de l'écran. Les retours se traitent dans trustinWa
|
|||||||
| 2026-11-03 | automate | local, lot S (Propositions seed, ADR 0011 ; le quorum par cran, ADR 0013 : C3, C4, C5, C9, C18 à C20, C23, C24, C27, M1 repris) | C1–C33, M1 | 34 / 34 verts (35 tests) |
|
| 2026-11-03 | automate | local, lot S (Propositions seed, ADR 0011 ; le quorum par cran, ADR 0013 : C3, C4, C5, C9, C18 à C20, C23, C24, C27, M1 repris) | C1–C33, M1 | 34 / 34 verts (35 tests) |
|
||||||
| 2026-11-03 | automate | local, lot U sur `6fb8238` (le lecteur refait : l'ordre en une ligne au bout de sa section, poignées sur place et vote ; oui pour passer · non pour bloquer ; quorum de l'auteur ; panneau sans onglets ; C3, C4, C6, C19 à C21, C23 à C26, C34, M1 repris) | C1–C37, M1 | 38 / 38 verts (40 tests) |
|
| 2026-11-03 | automate | local, lot U sur `6fb8238` (le lecteur refait : l'ordre en une ligne au bout de sa section, poignées sur place et vote ; oui pour passer · non pour bloquer ; quorum de l'auteur ; panneau sans onglets ; C3, C4, C6, C19 à C21, C23 à C26, C34, M1 repris) | C1–C37, M1 | 38 / 38 verts (40 tests) |
|
||||||
| 2026-11-03 | automate | local, lot T2 sur `f27ee08` (les trois phrases de F1.1, F1.2 et N1.1 ; l'espace perdu du vote déplié ; C34 lit la ligne entière, C37 « ne change pas ») | C1–C37, M1 | 38 / 38 verts (40 tests), du premier coup |
|
| 2026-11-03 | automate | local, lot T2 sur `f27ee08` (les trois phrases de F1.1, F1.2 et N1.1 ; l'espace perdu du vote déplié ; C34 lit la ligne entière, C37 « ne change pas ») | C1–C37, M1 | 38 / 38 verts (40 tests), du premier coup |
|
||||||
|
| 2026-11-03 | automate | local, lot U2 sur `bb4e7b7` (les mineurs de la relecture de M7 : cibles de 44 px et trois lectures entières sur téléphone, M2 ; « Proposer cet ordre » dit pourquoi, C35 ; une information à un seul endroit, C19, C36 ; l'en-tête et les changements d'inertie d'un module, C38 ; C25 et C26 lisent l'inertie dans « Inertie ») | C1–C38, M1, M2 | 40 / 40 verts (42 tests), du premier coup |
|
||||||
|
|||||||
@@ -169,3 +169,27 @@ refus du serveur sont dits dans les mots de la personne : hors bornes (400), un
|
|||||||
- La ligne d'ordre, ses gestes discrets comme ceux d'un article (au survol sur bureau, toujours
|
- La ligne d'ordre, ses gestes discrets comme ceux d'un article (au survol sur bureau, toujours
|
||||||
visibles sur téléphone) : elle ne crie plus, mais « Réordonner » se trouve au survol.
|
visibles sur téléphone) : elle ne crie plus, mais « Réordonner » se trouve au survol.
|
||||||
- Le mot court dans la liste : « passe à 11 oui · bloquée à 4 non ».
|
- Le mot court dans la liste : « passe à 11 oui · bloquée à 4 non ».
|
||||||
|
|
||||||
|
## Après la relecture de M7 (lot U2, 03/11)
|
||||||
|
|
||||||
|
Les mineurs de la relecture, tranchés ainsi (captures `/tmp/ld-u2-shots/`, avant et après) :
|
||||||
|
|
||||||
|
- **Une information à un seul endroit** dans le vote déplié : l'inertie se dit une fois, dans
|
||||||
|
« Inertie », ses crans avec elle (« Inertie · ▮▮▯▯▯ 2 / 5 · basse ») ; seul le vote d'un
|
||||||
|
changement d'inertie dit sous ses totaux le cran où il se vote. La ligne d'état d'une
|
||||||
|
Proposition seed (totaux, oui, crans, autres versions) s'efface tant que son vote est déplié
|
||||||
|
juste dessous ; les gestes gardent leur place (à droite sur bureau, à gauche sur téléphone).
|
||||||
|
« Autres versions » ne nomme pas les versions déjà listées à côté (un article à deux versions,
|
||||||
|
dans la liste) ; il nomme encore le texte en vigueur, lu plus haut.
|
||||||
|
- **La ligne de faits compte ce que la page montre** : « N articles en vigueur · N Propositions
|
||||||
|
seed · N propositions en cours ». Les Propositions seed n'y sont dites que lorsqu'elles sont lues
|
||||||
|
en tête (rien en vigueur) ; « propositions en cours » compte les versions que montre la liste
|
||||||
|
du bas, jamais les ordres, titres ni changements d'inertie d'un module, qui se votent à la ligne
|
||||||
|
de leur module. La carte du document, sur la page du groupe, garde le total du serveur.
|
||||||
|
- **Un changement d'inertie d'un ordre ou du titre** a sa pastille sur la ligne de son module,
|
||||||
|
à côté de « N ordres proposés » ou « N titres proposés » : « 1 changement d'inertie proposé »,
|
||||||
|
qui déplie son vote là.
|
||||||
|
- **« Proposer cet ordre » éteint le dit**, à sa gauche (sur téléphone, au-dessus) : « C'est
|
||||||
|
l'ordre en vigueur : déplace au moins un article. », lu aussi comme description du bouton.
|
||||||
|
- **Sur téléphone** : les trois lectures passent à la ligne plutôt que de cacher « Si tout
|
||||||
|
passe » ; tous les gestes du vote déplié, « Tout déplier » compris, font au moins 44 px.
|
||||||
|
|||||||
+66
-1
@@ -1,5 +1,5 @@
|
|||||||
import { expect, test, type Locator } from '@playwright/test';
|
import { expect, test, type Locator } from '@playwright/test';
|
||||||
import { DOC, expectWelcome, shot, signIn } from './helpers';
|
import { DOC, expectWelcome, propose, seedsInForce, shot, signIn, TEST_DOC, testDoc } from './helpers';
|
||||||
|
|
||||||
/** WCAG contrast ratio of two CSS colours `rgb(r, g, b)`. */
|
/** WCAG contrast ratio of two CSS colours `rgb(r, g, b)`. */
|
||||||
function contrast(a: string, b: string): number {
|
function contrast(a: string, b: string): number {
|
||||||
@@ -112,3 +112,68 @@ test('M1 — mobile : logo dans la barre, l’accueil sous la voûte, menu en ti
|
|||||||
await panel.getByRole('group', { name: 'Tout le document' }).getByRole('button', { name: 'Vider' }).click();
|
await panel.getByRole('group', { name: 'Tout le document' }).getByRole('button', { name: 'Vider' }).click();
|
||||||
await expect(panel.getByText("Rien dans le panier pour l'instant.")).toBeVisible();
|
await expect(panel.getByText("Rien dans le panier pour l'instant.")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** The height of a target, in CSS pixels. */
|
||||||
|
async function height(target: Locator): Promise<number> {
|
||||||
|
return (await target.boundingBox())?.height ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Every gesture in sight under `root` — buttons, links, summaries — under 44 px high. */
|
||||||
|
async function small(root: Locator): Promise<string[]> {
|
||||||
|
return root.evaluate((el) =>
|
||||||
|
[...el.querySelectorAll('button, a[href], summary, [role="radio"]')]
|
||||||
|
.filter((b) => b.checkVisibility({ visibilityProperty: true }) && b.getClientRects().length > 0)
|
||||||
|
.map((b) => ({ what: (b.textContent || b.getAttribute('aria-label') || '').replace(/\s+/g, ' ').trim(), h: b.getBoundingClientRect().height }))
|
||||||
|
.filter((x) => x.h < 44)
|
||||||
|
.map((x) => `${x.what} (${Math.round(x.h)} px)`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('M2 — téléphone : les gestes du lecteur font au moins 44 px, les trois lectures se lisent en entier', async ({ page, browser }) => {
|
||||||
|
await seedsInForce(browser);
|
||||||
|
await signIn(page, 'Chiara');
|
||||||
|
// In the test group: a rival of the Raison d'être (in force), so that its vote names its
|
||||||
|
// « Autres versions », and an article of hers, whose quorum she sets before any vote.
|
||||||
|
const doc = await testDoc(page);
|
||||||
|
const i1 = doc.units.find((u) => u.code === 'I1');
|
||||||
|
const r = await page.request.post(`/api/docs/${doc.id}/proposals`, {
|
||||||
|
data: { kind: 'text', unitId: i1?.id, title: "Raison d'être", body: 'Une clé, une personne vivante : la certification ne garantit rien d’autre.', submit: true },
|
||||||
|
headers: { 'x-requested-with': 'libredecision' },
|
||||||
|
});
|
||||||
|
expect(r.ok()).toBe(true);
|
||||||
|
const mine = await propose(page, 'Cibles au doigt', 'Je propose cet article pour régler son quorum au doigt.');
|
||||||
|
await page.goto(TEST_DOC);
|
||||||
|
|
||||||
|
// The three readings, each whole on the screen: « Si tout passe » is never cut (lot U2).
|
||||||
|
const views = page.getByRole('radiogroup', { name: 'Lire' });
|
||||||
|
const frame = await views.boundingBox();
|
||||||
|
expect.soft(await views.evaluate((el) => el.scrollWidth <= el.clientWidth + 1)).toBe(true);
|
||||||
|
for (const reading of await views.getByRole('radio').all()) {
|
||||||
|
const b = await reading.boundingBox();
|
||||||
|
expect.soft(b && frame && b.x >= frame.x - 1 && b.x + b.width <= frame.x + frame.width + 1, await reading.textContent()).toBe(true);
|
||||||
|
}
|
||||||
|
await expect.soft(views.getByRole('radio', { name: 'Si tout passe' })).toBeInViewport({ ratio: 1 });
|
||||||
|
await shot(page, 'm2-1-lectures');
|
||||||
|
|
||||||
|
// « Tout déplier », then the vote of the Raison d'être: every gesture a finger's target.
|
||||||
|
expect.soft(await height(page.getByRole('button', { name: 'Tout déplier' }))).toBeGreaterThanOrEqual(44);
|
||||||
|
const first = page.locator('section.text article.unit').first();
|
||||||
|
await first.getByRole('button', { name: 'Le vote' }).click();
|
||||||
|
const fold = first.locator('section.fold');
|
||||||
|
const others = fold.getByRole('button', { name: 'Autres versions (1)' });
|
||||||
|
expect.soft(await height(others)).toBeGreaterThanOrEqual(44);
|
||||||
|
await others.click();
|
||||||
|
await fold.getByText('La formule du seuil').click();
|
||||||
|
expect.soft(await small(fold)).toEqual([]);
|
||||||
|
await shot(page, 'm2-2-vote-deplie');
|
||||||
|
|
||||||
|
// Her own article: « Fixer ce quorum » and the rest of its vote.
|
||||||
|
await page.goto(`${TEST_DOC}?vote=${mine}`);
|
||||||
|
const hers = page.getByRole('region', { name: 'Le vote de « Cibles au doigt »' });
|
||||||
|
const set = hers.getByRole('button', { name: 'Fixer ce quorum' });
|
||||||
|
expect.soft(await height(set)).toBeGreaterThanOrEqual(44);
|
||||||
|
await hers.getByText("Proposer un changement d'inertie").click();
|
||||||
|
expect.soft(await small(hers)).toEqual([]);
|
||||||
|
await set.scrollIntoViewIfNeeded();
|
||||||
|
await shot(page, 'm2-3-quorum');
|
||||||
|
});
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ test('C25 — l’ordre des articles : une ligne en fin de section, réordonnée
|
|||||||
await fond.hover();
|
await fond.hover();
|
||||||
await fond.getByRole('button', { name: 'Le vote' }).click();
|
await fond.getByRole('button', { name: 'Le vote' }).click();
|
||||||
const fondVote = page.getByRole('region', { name: 'Le vote de « Ordre des articles · Engagements fondamentaux »' });
|
const fondVote = page.getByRole('region', { name: 'Le vote de « Ordre des articles · Engagements fondamentaux »' });
|
||||||
await expect(fondVote.getByText('inertie 2 / 5 · basse', { exact: true })).toBeVisible();
|
// Its inertia, said once, in its own place (lot U2).
|
||||||
|
await expect(fondVote.getByRole('heading', { name: 'Inertie · 2 / 5 · basse' })).toBeVisible();
|
||||||
await expect(fondVote.getByRole('list', { name: 'Ordre en vigueur' }).locator('li')).toHaveText(['Engagements fondamentaux', 'Unicité du compte', 'Réciprocité']);
|
await expect(fondVote.getByRole('list', { name: 'Ordre en vigueur' }).locator('li')).toHaveText(['Engagements fondamentaux', 'Unicité du compte', 'Réciprocité']);
|
||||||
await fond.getByRole('button', { name: 'Le vote' }).click();
|
await fond.getByRole('button', { name: 'Le vote' }).click();
|
||||||
|
|
||||||
@@ -171,7 +172,7 @@ test('C26 — le titre du document : proposé et voté en tête, ses traductions
|
|||||||
// In force now: its vote stays unfolded, under the title.
|
// In force now: its vote stays unfolded, under the title.
|
||||||
const vote = bar.locator('section.fold');
|
const vote = bar.locator('section.fold');
|
||||||
await expect(bar.getByRole('button', { name: 'Le vote' })).toHaveAttribute('aria-expanded', 'true');
|
await expect(bar.getByRole('button', { name: 'Le vote' })).toHaveAttribute('aria-expanded', 'true');
|
||||||
await expect(vote.getByText('inertie 4 / 5 · haute', { exact: true })).toBeVisible();
|
await expect(vote.getByRole('heading', { name: 'Inertie · 4 / 5 · haute' })).toBeVisible();
|
||||||
await expect(vote.getByText(`${voices} pour · 0 contre`)).toBeVisible();
|
await expect(vote.getByText(`${voices} pour · 0 contre`)).toBeVisible();
|
||||||
await shot(page, 'c26-2-titre-vote');
|
await shot(page, 'c26-2-titre-vote');
|
||||||
await bar.getByRole('button', { name: 'Le vote' }).click();
|
await bar.getByRole('button', { name: 'Le vote' }).click();
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ test('C35 — réordonner une section : les poignées sur ses articles, au gliss
|
|||||||
await expect(list.locator('.grip')).toHaveCount(3);
|
await expect(list.locator('.grip')).toHaveCount(3);
|
||||||
const submit = page.getByRole('button', { name: 'Proposer cet ordre' });
|
const submit = page.getByRole('button', { name: 'Proposer cet ordre' });
|
||||||
await expect(submit).toBeDisabled();
|
await expect(submit).toBeDisabled();
|
||||||
|
// Off, and it says why (UX-lecteur.md: « le bouton reste éteint et le dit » — lot U2).
|
||||||
|
const why = "C'est l'ordre en vigueur : déplace au moins un article.";
|
||||||
|
await expect(page.getByText(why)).toBeVisible();
|
||||||
|
await expect(submit).toHaveAccessibleDescription(why);
|
||||||
await shot(page, 'c35-2-poignees');
|
await shot(page, 'c35-2-poignees');
|
||||||
|
|
||||||
// By dragging its handle: Réciprocité onto the place of Unicité du compte.
|
// By dragging its handle: Réciprocité onto the place of Unicité du compte.
|
||||||
@@ -55,6 +59,7 @@ test('C35 — réordonner une section : les poignées sur ses articles, au gliss
|
|||||||
await expect(names).toHaveText([EF, E2, E1]);
|
await expect(names).toHaveText([EF, E2, E1]);
|
||||||
await expect(page.getByText(`« ${E2} » en place 2 sur 3.`)).toBeAttached();
|
await expect(page.getByText(`« ${E2} » en place 2 sur 3.`)).toBeAttached();
|
||||||
await expect(submit).toBeEnabled();
|
await expect(submit).toBeEnabled();
|
||||||
|
await expect(page.getByText(why)).toHaveCount(0);
|
||||||
// With the keyboard: the article keeps the focus, the arrows move it.
|
// With the keyboard: the article keeps the focus, the arrows move it.
|
||||||
const items = list.locator('button.item');
|
const items = list.locator('button.item');
|
||||||
await items.nth(1).focus();
|
await items.nth(1).focus();
|
||||||
@@ -67,6 +72,7 @@ test('C35 — réordonner une section : les poignées sur ses articles, au gliss
|
|||||||
await list.getByRole('button', { name: `Descendre « ${E2} »` }).click();
|
await list.getByRole('button', { name: `Descendre « ${E2} »` }).click();
|
||||||
await expect(names).toHaveText([EF, E1, E2]);
|
await expect(names).toHaveText([EF, E1, E2]);
|
||||||
await expect(submit).toBeDisabled();
|
await expect(submit).toBeDisabled();
|
||||||
|
await expect(page.getByText(why)).toBeVisible();
|
||||||
await items.nth(2).click();
|
await items.nth(2).click();
|
||||||
await expect(items.nth(2)).toHaveAttribute('aria-pressed', 'true');
|
await expect(items.nth(2)).toHaveAttribute('aria-pressed', 'true');
|
||||||
await items.nth(1).click();
|
await items.nth(1).click();
|
||||||
@@ -145,6 +151,9 @@ test('C36 — combien de oui pour passer, combien de non pour bloquer : sous l
|
|||||||
await expect(need).toBeVisible();
|
await expect(need).toBeVisible();
|
||||||
// The quorum in a tooltip, never as a title.
|
// The quorum in a tooltip, never as a title.
|
||||||
await expect(need).toHaveAttribute('title', 'Quorum : 12 votants. En dessous, les votants qui manquent comptent comme « non ».');
|
await expect(need).toHaveAttribute('title', 'Quorum : 12 votants. En dessous, les votants qui manquent comptent comme « non ».');
|
||||||
|
// Said once: the line of the seed proposal, right above its vote, does not say it again (lot U2).
|
||||||
|
await expect(i1.locator('footer')).not.toContainText(/oui (sur|pour)/);
|
||||||
|
await expect(i1.locator('footer')).not.toContainText(/\d+ pour · \d+ contre/);
|
||||||
// In the list, short: at level 3, quorum 50.
|
// In the list, short: at level 3, quorum 50.
|
||||||
await unfold(page);
|
await unfold(page);
|
||||||
await expect(deepRow(page, 'Vérification personnelle de la clé')).toContainText('39 oui sur 50');
|
await expect(deepRow(page, 'Vérification personnelle de la clé')).toContainText('39 oui sur 50');
|
||||||
@@ -237,3 +246,48 @@ test('C37 — le quorum d’un article : son auteur le fixe avant le premier vot
|
|||||||
await expect(fold.getByRole('region', { name: /^Quorum/ })).toHaveCount(0);
|
await expect(fold.getByRole('region', { name: /^Quorum/ })).toHaveCount(0);
|
||||||
await yvv.context().close();
|
await yvv.context().close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('C38 — l’en-tête compte ce que montre la liste ; un changement d’inertie d’un ordre ou du titre se trouve à sa ligne', async ({ page }) => {
|
||||||
|
await signIn(page, 'Chiara');
|
||||||
|
/** The versions « Propositions en cours » shows: one per line, or those inside a line of several. */
|
||||||
|
const listed = () =>
|
||||||
|
page.locator('ul.deep li.article').evaluateAll((lis) => lis.reduce((n, li) => n + Math.max(1, li.querySelectorAll('ul.versions > li').length), 0));
|
||||||
|
// Monnaie libre, nothing in force: the seed proposals at the head, said apart; the count of the
|
||||||
|
// proposals in progress is what the list below shows (lot U2: the orders are not counted there).
|
||||||
|
await page.goto(DOC);
|
||||||
|
await unfold(page);
|
||||||
|
const facts = page.locator('.facts');
|
||||||
|
await expect.soft(facts).toContainText('0 article en vigueur · 3 Propositions seed · ');
|
||||||
|
await expect.soft(facts).toContainText(new RegExp(`· ${await listed()} propositions en cours`));
|
||||||
|
|
||||||
|
// A change of inertia on the order of a section and on the title (test group): each is found at
|
||||||
|
// the line of its module, where its vote unfolds — never a line of « Propositions en cours ».
|
||||||
|
const g = await (await page.request.get('/api/groups/essai')).json();
|
||||||
|
const d = await (await page.request.get(`/api/docs/${g.docs[0].id}`)).json();
|
||||||
|
const modules: { id: string; inertia: number }[] = [d.modules.orders.find((o: { section: string }) => o.section === 'conclusion'), d.modules.title];
|
||||||
|
const made: string[] = [];
|
||||||
|
for (const unit of modules) {
|
||||||
|
const r = await page.request.post(`/api/units/${unit.id}/inertia`, { data: { target: unit.inertia === 5 ? 4 : unit.inertia + 1 }, headers: CSRF });
|
||||||
|
expect(r.ok()).toBe(true);
|
||||||
|
made.push((await r.json()).id);
|
||||||
|
}
|
||||||
|
await page.goto(TEST_DOC);
|
||||||
|
const title = page.getByRole('group', { name: 'Titre du document' });
|
||||||
|
const titleChange = title.getByRole('button', { name: "1 changement d'inertie proposé" });
|
||||||
|
await expect(titleChange).toBeVisible();
|
||||||
|
await titleChange.click();
|
||||||
|
await expect(title.getByRole('region', { name: /^Le vote de « inertie vers \d \/ 5 · / })).toBeVisible();
|
||||||
|
await shot(page, 'c38-1-titre-inertie');
|
||||||
|
await unfold(page);
|
||||||
|
await expect.soft(facts).toContainText(new RegExp(`· ${await listed()} propositions en cours`));
|
||||||
|
await expect(page.locator('ul.deep').getByText(/inertie vers/)).toHaveCount(0);
|
||||||
|
const order = page.getByRole('group', { name: 'Ordre des articles · Conclusion' });
|
||||||
|
const orderChange = order.getByRole('button', { name: "1 changement d'inertie proposé" });
|
||||||
|
await expect(orderChange).toBeVisible();
|
||||||
|
await orderChange.click();
|
||||||
|
await expect(order.getByRole('region', { name: /^Le vote de « inertie vers \d \/ 5 · / })).toBeVisible();
|
||||||
|
await order.scrollIntoViewIfNeeded();
|
||||||
|
await shot(page, 'c38-2-ordre-inertie');
|
||||||
|
// Withdrawn: the group is left as found.
|
||||||
|
for (const id of made) expect((await page.request.post(`/api/proposals/${id}/withdraw`, { headers: CSRF })).ok()).toBe(true);
|
||||||
|
});
|
||||||
|
|||||||
@@ -298,6 +298,8 @@ test('C19 — un article, une ligne : ses versions se déplient', async ({ page
|
|||||||
// Level 3, the quorum 50 (ADR 0013): 39 « oui » pass it, the voters missing counted as « non »
|
// Level 3, the quorum 50 (ADR 0013): 39 « oui » pass it, the voters missing counted as « non »
|
||||||
// (« sur 50 votants » before any vote; a « contre » of another control may be there already).
|
// (« sur 50 votants » before any vote; a « contre » of another control may be there already).
|
||||||
await expect(page.locator('section.fold .need')).toHaveText(/^39 oui (sur 50 votants pour passer|pour passer · le quorum la bloque)$/);
|
await expect(page.locator('section.fold .need')).toHaveText(/^39 oui (sur 50 votants pour passer|pour passer · le quorum la bloque)$/);
|
||||||
|
// Its other version is right there in the list: the vote does not list it again (lot U2).
|
||||||
|
await expect(page.locator('section.fold').getByRole('button', { name: /^Autres versions/ })).toHaveCount(0);
|
||||||
await expect(page).toHaveURL(/vote=/);
|
await expect(page).toHaveURL(/vote=/);
|
||||||
await e3.click();
|
await e3.click();
|
||||||
await expect(e3).toHaveAttribute('aria-expanded', 'false');
|
await expect(e3).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
// (ADR 0007, « cinq crans »). The water level of a vote stands upright; the
|
// (ADR 0007, « cinq crans »). The water level of a vote stands upright; the
|
||||||
// weight of a text lies flat — two shapes one does not confuse. The words stay
|
// weight of a text lies flat — two shapes one does not confuse. The words stay
|
||||||
// beside it: « inertie 3 / 5 · moyenne », or only « moyenne » when `short`
|
// beside it: « inertie 3 / 5 · moyenne », or only « moyenne » when `short`
|
||||||
// (a screen reader still hears the whole).
|
// (a screen reader still hears the whole), or « 3 / 5 · moyenne » when `bare`,
|
||||||
|
// after a heading that already says « Inertie ».
|
||||||
import { t } from '$lib/i18n/index.svelte';
|
import { t } from '$lib/i18n/index.svelte';
|
||||||
import { inertiaBadge } from '$lib/labels';
|
import { inertiaBadge, inertiaLevel } from '$lib/labels';
|
||||||
import type { Inertia } from '$lib/types';
|
import type { Inertia } from '$lib/types';
|
||||||
|
|
||||||
let { level, short = false }: { level: Inertia; short?: boolean } = $props();
|
let { level, short = false, bare = false }: { level: Inertia; short?: boolean; bare?: boolean } = $props();
|
||||||
const full = $derived(inertiaBadge(level));
|
const full = $derived(inertiaBadge(level));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -16,7 +17,9 @@
|
|||||||
<span class="notches" aria-hidden="true">
|
<span class="notches" aria-hidden="true">
|
||||||
{#each [1, 2, 3, 4, 5] as k (k)}<span class="notch" class:on={k <= level}></span>{/each}
|
{#each [1, 2, 3, 4, 5] as k (k)}<span class="notch" class:on={k <= level}></span>{/each}
|
||||||
</span>
|
</span>
|
||||||
{#if short}<span class="visually-hidden">{full}</span><span aria-hidden="true">{t(`inertia.name.${level}`)}</span>{:else}<span>{full}</span>{/if}
|
{#if short}<span class="visually-hidden">{full}</span><span aria-hidden="true">{t(`inertia.name.${level}`)}</span>
|
||||||
|
{:else if bare}<span>{inertiaLevel(level)}</span>
|
||||||
|
{:else}<span>{full}</span>{/if}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -36,7 +36,8 @@
|
|||||||
/** Every section open (a filter is on). */
|
/** Every section open (a filter is on). */
|
||||||
all?: boolean;
|
all?: boolean;
|
||||||
ontoggle: (unit: Unit, p: Proposal) => void;
|
ontoggle: (unit: Unit, p: Proposal) => void;
|
||||||
fold: Snippet<[Unit, Proposal]>;
|
/** The vote of a version, and the versions already listed beside it (not named again inside). */
|
||||||
|
fold: Snippet<[Unit, Proposal, string[]]>;
|
||||||
/** The order line of a section, at its end. */
|
/** The order line of a section, at its end. */
|
||||||
order: Snippet<[Unit]>;
|
order: Snippet<[Unit]>;
|
||||||
/** Opens the basket on a section; absent when this session does not vote here. */
|
/** Opens the basket on a section; absent when this session does not vote here. */
|
||||||
@@ -153,7 +154,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{#if vote === best.id}<div class="fold">{@render fold(unit, best)}</div>{/if}
|
{#if vote === best.id}<div class="fold">{@render fold(unit, best, [])}</div>{/if}
|
||||||
</li>
|
</li>
|
||||||
{:else if best}
|
{:else if best}
|
||||||
{@const vopen = isVersionsOpen(a)}
|
{@const vopen = isVersionsOpen(a)}
|
||||||
@@ -194,7 +195,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{#if vote === p.id}{@render fold(unit, p)}{/if}
|
{#if vote === p.id}{@render fold(unit, p, a.versions.map((v) => v.id))}{/if}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -431,7 +432,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 899px) {
|
@media (max-width: 899px) {
|
||||||
.vote-section {
|
.vote-section,
|
||||||
|
.linkish {
|
||||||
min-height: 2.75rem;
|
min-height: 2.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,15 +70,17 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
border-radius: var(--r-input);
|
border-radius: var(--r-input);
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
padding: 0.7rem 0.9rem;
|
padding: 0 0.9rem;
|
||||||
}
|
}
|
||||||
|
/* The whole height of the closed box is the summary: a target a finger finds (lot U2). */
|
||||||
summary {
|
summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 750;
|
font-weight: 750;
|
||||||
color: var(--ink-soft);
|
color: var(--ink-soft);
|
||||||
|
padding-block: 0.7rem;
|
||||||
}
|
}
|
||||||
.formula[open] summary {
|
.formula[open] {
|
||||||
margin-bottom: 0.7rem;
|
padding-bottom: 0.7rem;
|
||||||
}
|
}
|
||||||
.formula > :not(summary) + :not(summary) {
|
.formula > :not(summary) + :not(summary) {
|
||||||
margin-top: 0.6rem;
|
margin-top: 0.6rem;
|
||||||
@@ -112,4 +114,9 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-underline-offset: 3px;
|
text-underline-offset: 3px;
|
||||||
}
|
}
|
||||||
|
@media (max-width: 899px) {
|
||||||
|
.settings-link {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// The inertia of an article: its level out of five, its past, the changes
|
// The inertia of an article: its level out of five — drawn, said once in the vote (lot
|
||||||
// under vote, and the choice of a new level. A change of inertia is voted at
|
// U2) —, its past, the changes under vote, and the choice of a new level. A change of
|
||||||
// the level of the article itself (ADR 0007).
|
// inertia is voted at the level of the article itself (ADR 0007).
|
||||||
import { api, ApiError } from '$lib/api';
|
import { api, ApiError } from '$lib/api';
|
||||||
import type { DocState } from '$lib/doc.svelte';
|
import type { DocState } from '$lib/doc.svelte';
|
||||||
import { i18n, t, type Key } from '$lib/i18n/index.svelte';
|
import { i18n, t, type Key } from '$lib/i18n/index.svelte';
|
||||||
import { inertiaLevel } from '$lib/labels';
|
import { inertiaLevel } from '$lib/labels';
|
||||||
import { INERTIA_LEVELS, type Inertia, type Unit } from '$lib/types';
|
import { INERTIA_LEVELS, type Inertia, type Unit } from '$lib/types';
|
||||||
|
import Crans from './Crans.svelte';
|
||||||
|
|
||||||
let { doc, unit, onopen }: { doc: DocState; unit: Unit; onopen: (proposalId: string) => void } = $props();
|
let { doc, unit, onopen }: { doc: DocState; unit: Unit; onopen: (proposalId: string) => void } = $props();
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="inertia" aria-labelledby="inertia-{unit.id}">
|
<section class="inertia" aria-labelledby="inertia-{unit.id}">
|
||||||
<h4 id="inertia-{unit.id}">{t('inertia.title')} · <span class="muted">{inertiaLevel(unit.inertia)}</span></h4>
|
<h4 id="inertia-{unit.id}">{t('inertia.title')} · <span class="muted"><Crans level={unit.inertia} bare /></span></h4>
|
||||||
<p class="faint small">{t('inertia.explain')}</p>
|
<p class="faint small">{t('inertia.explain')}</p>
|
||||||
{#each unit.inertiaChanges as c (c.at + c.to)}
|
{#each unit.inertiaChanges as c (c.at + c.to)}
|
||||||
<p class="faint small">{t(`inertia.history.${c.cause}`, { from: level(c.from), to: level(c.to), date: i18n.date(c.at) })}</p>
|
<p class="faint small">{t(`inertia.history.${c.cause}`, { from: level(c.from), to: level(c.to), date: i18n.date(c.at) })}</p>
|
||||||
@@ -195,6 +196,19 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
/* On a phone: every gesture big enough for a finger (lot U2). */
|
||||||
|
@media (max-width: 899px) {
|
||||||
|
details > summary {
|
||||||
|
padding-block: 0.75rem;
|
||||||
|
}
|
||||||
|
details[open] > summary {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.btn,
|
||||||
|
.link {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.levels button {
|
.levels button {
|
||||||
transition: none;
|
transition: none;
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
// The order of a section, at its end (ADR 0009; Yvv, 2026-11-02: « la répétition de
|
// The order of a section, at its end (ADR 0009; Yvv, 2026-11-02: « la répétition de
|
||||||
// "ordonnancement" est nulle. Il faudrait que ce soit en fin de section »): one line, never
|
// "ordonnancement" est nulle. Il faudrait que ce soit en fin de section »): one line, never
|
||||||
// the list of its articles again — the handle that marks an order, « Ordre de la section »,
|
// the list of its articles again — the handle that marks an order, « Ordre de la section »,
|
||||||
// its weight in notches, its state, the orders proposed. « Le vote » unfolds here the vote
|
// its weight in notches, its state, the orders proposed and the changes of its inertia
|
||||||
|
// proposed (lot U2: found here, where their vote unfolds). « Le vote » unfolds here the vote
|
||||||
// of any of its versions (the list whole); « Réordonner » puts handles on the articles of
|
// of any of its versions (the list whole); « Réordonner » puts handles on the articles of
|
||||||
// the section, in place.
|
// the section, in place.
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
|
|
||||||
const p = $derived(unit.inForce);
|
const p = $derived(unit.inForce);
|
||||||
const proposed = $derived(unit.proposals.filter((x) => x.status === 'open' && x.kind === 'order'));
|
const proposed = $derived(unit.proposals.filter((x) => x.status === 'open' && x.kind === 'order'));
|
||||||
|
const inertia = $derived(unit.proposals.filter((x) => x.status === 'open' && x.kind === 'inertia'));
|
||||||
// Any version of the module unfolds here: its order in force, an order proposed, a change of its inertia.
|
// Any version of the module unfolds here: its order in force, an order proposed, a change of its inertia.
|
||||||
const shown = $derived([p, ...unit.proposals.filter((x) => x.status === 'open')].find((x) => x?.id === vote) ?? null);
|
const shown = $derived([p, ...unit.proposals.filter((x) => x.status === 'open')].find((x) => x?.id === vote) ?? null);
|
||||||
const votes = $derived((p?.tally.for ?? 0) + (p?.tally.against ?? 0));
|
const votes = $derived((p?.tally.for ?? 0) + (p?.tally.against ?? 0));
|
||||||
@@ -53,6 +55,10 @@
|
|||||||
{@const first = proposed[0]}
|
{@const first = proposed[0]}
|
||||||
<button type="button" class="proposed" onclick={() => first && onvote(first.id)}>{i18n.plural('order.proposed', proposed.length)}</button>
|
<button type="button" class="proposed" onclick={() => first && onvote(first.id)}>{i18n.plural('order.proposed', proposed.length)}</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if inertia.length && !reordering}
|
||||||
|
{@const first = inertia[0]}
|
||||||
|
<button type="button" class="proposed" onclick={() => first && onvote(first.id)}>{i18n.plural('module.inertia.proposed', inertia.length)}</button>
|
||||||
|
{/if}
|
||||||
{#if !reordering}
|
{#if !reordering}
|
||||||
<span class="links">
|
<span class="links">
|
||||||
{#if p}<button type="button" class:on={shown?.id === p.id} aria-expanded={shown?.id === p.id} aria-controls="vote-{p.id}" onclick={() => onvote(p.id)}>{t('doc.action.vote')}</button>{/if}
|
{#if p}<button type="button" class:on={shown?.id === p.id} aria-expanded={shown?.id === p.id} aria-controls="vote-{p.id}" onclick={() => onvote(p.id)}>{t('doc.action.vote')}</button>{/if}
|
||||||
|
|||||||
@@ -110,4 +110,10 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
/* On a phone: « Fixer ce quorum » and « Revenir au défaut » as tall as the field (lot U2). */
|
||||||
|
@media (max-width: 899px) {
|
||||||
|
.btn {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
// or uses its arrows; with the keyboard, the up and down arrows move the article that has
|
// or uses its arrows; with the keyboard, the up and down arrows move the article that has
|
||||||
// the focus, and the new place is said. Then « Proposer cet ordre »: the order goes to the
|
// the focus, and the new place is said. Then « Proposer cet ordre »: the order goes to the
|
||||||
// vote (ADR 0009, `POST /units/{id}/order`) and its vote unfolds at once, here. An order
|
// vote (ADR 0009, `POST /units/{id}/order`) and its vote unfolds at once, here. An order
|
||||||
// already proposed is voted, never proposed twice.
|
// already proposed is voted, never proposed twice. While the order is the one in force,
|
||||||
|
// the button stays off and says why, beside it (lot U2, UX-lecteur.md).
|
||||||
import { tick, untrack } from 'svelte';
|
import { tick, untrack } from 'svelte';
|
||||||
import { flip } from 'svelte/animate';
|
import { flip } from 'svelte/animate';
|
||||||
import { api } from '$lib/api';
|
import { api } from '$lib/api';
|
||||||
@@ -141,10 +142,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
if (unchanged) {
|
if (unchanged) return;
|
||||||
error = t('order.edit.unchanged');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
ondone(existing.id);
|
ondone(existing.id);
|
||||||
return;
|
return;
|
||||||
@@ -192,8 +190,15 @@
|
|||||||
{#if existing && !unchanged}<p class="note" role="status">{t('order.edit.exists')}</p>{/if}
|
{#if existing && !unchanged}<p class="note" role="status">{t('order.edit.exists')}</p>{/if}
|
||||||
{#if error}<p class="warn" role="alert">{error}</p>{/if}
|
{#if error}<p class="warn" role="alert">{error}</p>{/if}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
{#if unchanged}<p class="why" id="order-why-{unit.id}">{t('order.edit.unchanged')}</p>{/if}
|
||||||
<button class="btn quiet small" type="button" disabled={busy} onclick={oncancel}>{t('common.cancel')}</button>
|
<button class="btn quiet small" type="button" disabled={busy} onclick={oncancel}>{t('common.cancel')}</button>
|
||||||
<button class="btn primary small" type="button" disabled={busy || unchanged} onclick={submit}>{t(existing ? 'order.edit.vote' : 'order.edit.submit')}</button>
|
<button
|
||||||
|
class="btn primary small"
|
||||||
|
type="button"
|
||||||
|
disabled={busy || unchanged}
|
||||||
|
aria-describedby={unchanged ? `order-why-${unit.id}` : undefined}
|
||||||
|
onclick={submit}>{t(existing ? 'order.edit.vote' : 'order.edit.submit')}</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -338,15 +343,31 @@
|
|||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
/* Why the button is off, on its left: read before one reaches for it. */
|
||||||
|
.why {
|
||||||
|
flex: 1 1 14rem;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
font-weight: 650;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
/* On a phone: every target is big enough for a finger. */
|
/* On a phone: every target is big enough for a finger. */
|
||||||
@media (max-width: 899px) {
|
@media (max-width: 899px) {
|
||||||
.arrow {
|
.arrow {
|
||||||
width: 2.75rem;
|
width: 2.75rem;
|
||||||
height: 2.75rem;
|
height: 2.75rem;
|
||||||
}
|
}
|
||||||
|
.actions .btn {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
}
|
||||||
|
/* Its own line, above the two buttons kept together. */
|
||||||
|
.why {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@keyframes unfold {
|
@keyframes unfold {
|
||||||
from {
|
from {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// The title of the document is a module (ADR 0009, inertia 4): one line under the title,
|
// The title of the document is a module (ADR 0009, inertia 4): one line under the title,
|
||||||
// of the same voice as the order of a section — its weight in notches, its state, the
|
// of the same voice as the order of a section — its weight in notches, its state, the
|
||||||
// titles proposed. « Le vote » unfolds here the vote of any of its versions; another title
|
// titles proposed and the changes of its inertia proposed (lot U2). « Le vote » unfolds here
|
||||||
// is written in the panel.
|
// the vote of any of its versions; another title is written in the panel.
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { i18n, t } from '$lib/i18n/index.svelte';
|
import { i18n, t } from '$lib/i18n/index.svelte';
|
||||||
import { moduleName } from '$lib/labels';
|
import { moduleName } from '$lib/labels';
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
const p = $derived(unit.inForce);
|
const p = $derived(unit.inForce);
|
||||||
const votes = $derived((p?.tally.for ?? 0) + (p?.tally.against ?? 0));
|
const votes = $derived((p?.tally.for ?? 0) + (p?.tally.against ?? 0));
|
||||||
const proposed = $derived(unit.proposals.filter((x) => x.status === 'open' && x.kind === 'title'));
|
const proposed = $derived(unit.proposals.filter((x) => x.status === 'open' && x.kind === 'title'));
|
||||||
|
const inertia = $derived(unit.proposals.filter((x) => x.status === 'open' && x.kind === 'inertia'));
|
||||||
const shown = $derived([p, ...unit.proposals.filter((x) => x.status === 'open')].find((x) => x?.id === vote) ?? null);
|
const shown = $derived([p, ...unit.proposals.filter((x) => x.status === 'open')].find((x) => x?.id === vote) ?? null);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -49,6 +50,10 @@
|
|||||||
{@const first = proposed[0]}
|
{@const first = proposed[0]}
|
||||||
<button type="button" class="proposed" onclick={() => first && onvote(first.id)}>{i18n.plural('title.proposed', proposed.length)}</button>
|
<button type="button" class="proposed" onclick={() => first && onvote(first.id)}>{i18n.plural('title.proposed', proposed.length)}</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if inertia.length}
|
||||||
|
{@const first = inertia[0]}
|
||||||
|
<button type="button" class="proposed" onclick={() => first && onvote(first.id)}>{i18n.plural('module.inertia.proposed', inertia.length)}</button>
|
||||||
|
{/if}
|
||||||
<span class="links">
|
<span class="links">
|
||||||
{#if p}<button type="button" class:on={shown?.id === p.id} aria-expanded={shown?.id === p.id} aria-controls="vote-{p.id}" onclick={() => onvote(p.id)}>{t('doc.action.vote')}</button>{/if}
|
{#if p}<button type="button" class:on={shown?.id === p.id} aria-expanded={shown?.id === p.id} aria-controls="vote-{p.id}" onclick={() => onvote(p.id)}>{t('doc.action.vote')}</button>{/if}
|
||||||
<button type="button" onclick={onpropose}>{t('title.propose')}</button>
|
<button type="button" onclick={onpropose}>{t('title.propose')}</button>
|
||||||
|
|||||||
@@ -67,15 +67,19 @@
|
|||||||
<Prose text={text.body} />
|
<Prose text={text.body} />
|
||||||
{#if actions}
|
{#if actions}
|
||||||
<footer class="actions">
|
<footer class="actions">
|
||||||
<span class="status faint">
|
<!-- Its vote unfolded says the totals, the « oui » to pass, the inertia and the other
|
||||||
{#if seed}
|
versions: the line does not say them again (lot U2). -->
|
||||||
{@const need = needOf(seed)}
|
{#if !(seed && voteOpen)}
|
||||||
{t('tally.short', { for: seed.tally.for, against: seed.tally.against })}{#if need}{' · '}<span title={need.title}>{need.short}</span>{/if}
|
<span class="status faint">
|
||||||
<span class="crans"><Crans level={seed.votedAtInertia} short /></span>
|
{#if seed}
|
||||||
{:else if seedMinimal}<span title={t('doc.seed.long')}>{i18n.plural('doc.seed.votes', votes)}</span>
|
{@const need = needOf(seed)}
|
||||||
{:else if enteredAt}{t('doc.adopted', { date: i18n.date(enteredAt) })}{/if}
|
{t('tally.short', { for: seed.tally.for, against: seed.tally.against })}{#if need}{' · '}<span title={need.title}>{need.short}</span>{/if}
|
||||||
{#if rivals > 0} · {i18n.plural('doc.others', rivals)}{/if}
|
<span class="crans"><Crans level={seed.votedAtInertia} short /></span>
|
||||||
</span>
|
{:else if seedMinimal}<span title={t('doc.seed.long')}>{i18n.plural('doc.seed.votes', votes)}</span>
|
||||||
|
{:else if enteredAt}{t('doc.adopted', { date: i18n.date(enteredAt) })}{/if}
|
||||||
|
{#if rivals > 0 && !voteOpen}{' · '}{i18n.plural('doc.others', rivals)}{/if}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
<span class="links">
|
<span class="links">
|
||||||
<button type="button" class:on={voteOpen} aria-expanded={voteOpen} aria-controls={voteFor ? `vote-${voteFor}` : undefined} onclick={() => onaction('vote')}>{t('doc.action.vote')}</button>
|
<button type="button" class:on={voteOpen} aria-expanded={voteOpen} aria-controls={voteFor ? `vote-${voteFor}` : undefined} onclick={() => onaction('vote')}>{t('doc.action.vote')}</button>
|
||||||
<button type="button" onclick={() => onaction('propose')}>{proposeLabel ?? t('doc.action.propose')}</button>
|
<button type="button" onclick={() => onaction('propose')}>{proposeLabel ?? t('doc.action.propose')}</button>
|
||||||
@@ -129,11 +133,13 @@
|
|||||||
.crans {
|
.crans {
|
||||||
margin-left: 0.6rem;
|
margin-left: 0.6rem;
|
||||||
}
|
}
|
||||||
|
/* The gestures keep their place whether the line of state is there or not (it steps aside
|
||||||
|
while the vote is unfolded, lot U2): on the right on a desktop, on the left on a phone. */
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
gap: 0.4rem 1rem;
|
gap: 0.4rem 1rem;
|
||||||
margin-top: 0.7rem;
|
margin-top: 0.7rem;
|
||||||
min-height: 2.25rem;
|
min-height: 2.25rem;
|
||||||
@@ -170,6 +176,11 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
.links {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@media (max-width: 899px) {
|
@media (max-width: 899px) {
|
||||||
.links button {
|
.links button {
|
||||||
min-height: 2.75rem;
|
min-height: 2.75rem;
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
// many « oui » pass it, how many « non » block it (ADR 0013), its weight in notches,
|
// many « oui » pass it, how many « non » block it (ADR 0013), its weight in notches,
|
||||||
// its other versions — each opens its own vote where it reads —, and the quorum its
|
// its other versions — each opens its own vote where it reads —, and the quorum its
|
||||||
// author may set before the first vote. The rest of a version (typos, translations,
|
// author may set before the first vote. The rest of a version (typos, translations,
|
||||||
// objections, history) lives in the panel, « Détails ».
|
// objections, history) lives in the panel, « Détails ». Lot U2: each thing said once —
|
||||||
|
// the inertia in its own place, the other versions only those not already in sight.
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { api } from '$lib/api';
|
import { api } from '$lib/api';
|
||||||
import type { DocState } from '$lib/doc.svelte';
|
import type { DocState } from '$lib/doc.svelte';
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
doc,
|
doc,
|
||||||
proposal: p,
|
proposal: p,
|
||||||
unit,
|
unit,
|
||||||
|
besides = [],
|
||||||
onopen,
|
onopen,
|
||||||
onsettings,
|
onsettings,
|
||||||
ondetails,
|
ondetails,
|
||||||
@@ -40,6 +42,8 @@
|
|||||||
doc: DocState;
|
doc: DocState;
|
||||||
proposal: Proposal;
|
proposal: Proposal;
|
||||||
unit: Unit;
|
unit: Unit;
|
||||||
|
/** The versions already in sight beside this vote (the versions of its article, listed): not named again. */
|
||||||
|
besides?: string[];
|
||||||
/** Unfolds the vote of another version, where it reads (another text, a change of inertia). */
|
/** Unfolds the vote of another version, where it reads (another text, a change of inertia). */
|
||||||
onopen: (proposalId: string) => void;
|
onopen: (proposalId: string) => void;
|
||||||
onsettings: (proposalId: string) => void;
|
onsettings: (proposalId: string) => void;
|
||||||
@@ -80,12 +84,17 @@
|
|||||||
const nameOf = (id: string) => doc.articleTitle(id, doc.original ? source : i18n.lang, source);
|
const nameOf = (id: string) => doc.articleTitle(id, doc.original ? source : i18n.lang, source);
|
||||||
/** « 9 oui pour passer · 4 non pour bloquer » — the quorum in its tooltip. */
|
/** « 9 oui pour passer · 4 non pour bloquer » — the quorum in its tooltip. */
|
||||||
const need = $derived(needOf(p));
|
const need = $derived(needOf(p));
|
||||||
/** The other live versions of the module (its changes of inertia live with its inertia). */
|
/**
|
||||||
|
* The other live versions of the module (its changes of inertia live with its inertia),
|
||||||
|
* less those already in sight beside the vote.
|
||||||
|
*/
|
||||||
const others = $derived(
|
const others = $derived(
|
||||||
[unit.inForce, ...unit.proposals.filter((x) => x.status === 'open' && x.kind !== 'inertia')].filter(
|
[unit.inForce, ...unit.proposals.filter((x) => x.status === 'open' && x.kind !== 'inertia')].filter(
|
||||||
(x): x is Proposal => !!x && x.id !== p.id,
|
(x): x is Proposal => !!x && x.id !== p.id && !besides.includes(x.id),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
/** The inertia is said by « Inertie », below; a change of inertia has none: its level is said here. */
|
||||||
|
const ownInertia = $derived(p.kind === 'inertia');
|
||||||
|
|
||||||
/** What tells another version apart, in a few words. */
|
/** What tells another version apart, in a few words. */
|
||||||
function otherLabel(v: Proposal): string {
|
function otherLabel(v: Proposal): string {
|
||||||
@@ -165,7 +174,7 @@
|
|||||||
{#if need}<p class="need" title={need.title}>{need.long}</p>{/if}
|
{#if need}<p class="need" title={need.title}>{need.long}</p>{/if}
|
||||||
<p class="state">{p.evaluation.reached ? t('tally.reached') : i18n.plural('tally.missing', p.evaluation.missing)}</p>
|
<p class="state">{p.evaluation.reached ? t('tally.reached') : i18n.plural('tally.missing', p.evaluation.missing)}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<p class="faint small"><Crans level={p.votedAtInertia} /></p>
|
{#if ownInertia}<p class="faint small"><Crans level={p.votedAtInertia} /></p>{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if p.blocked}
|
{#if p.blocked}
|
||||||
@@ -277,7 +286,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
{#if p.kind !== 'inertia'}<InertiaPicker {doc} {unit} {onopen} />{/if}
|
{#if !ownInertia}<InertiaPicker {doc} {unit} {onopen} />{/if}
|
||||||
{#if !unit.kind && unit.canSetQuorum}<QuorumSetting {doc} {unit} />{/if}
|
{#if !unit.kind && unit.canSetQuorum}<QuorumSetting {doc} {unit} />{/if}
|
||||||
{#if detail && view}
|
{#if detail && view}
|
||||||
<Formula
|
<Formula
|
||||||
@@ -504,6 +513,14 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-underline-offset: 3px;
|
text-underline-offset: 3px;
|
||||||
}
|
}
|
||||||
|
/* On a phone: every gesture of the vote big enough for a finger (lot U2). */
|
||||||
|
@media (max-width: 899px) {
|
||||||
|
.choice .btn,
|
||||||
|
.disclose,
|
||||||
|
.linkish {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@keyframes unfold {
|
@keyframes unfold {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
+20
-16
@@ -103,6 +103,8 @@ export const en: Messages = {
|
|||||||
'docs.inForce.other': '{n} articles in force',
|
'docs.inForce.other': '{n} articles in force',
|
||||||
'docs.open.one': '{n} open proposal',
|
'docs.open.one': '{n} open proposal',
|
||||||
'docs.open.other': '{n} open proposals',
|
'docs.open.other': '{n} open proposals',
|
||||||
|
'docs.seeds.one': '{n} seed proposal',
|
||||||
|
'docs.seeds.other': '{n} seed proposals',
|
||||||
|
|
||||||
'doc.view.label': 'Read',
|
'doc.view.label': 'Read',
|
||||||
'doc.view.inforce': 'In force',
|
'doc.view.inforce': 'In force',
|
||||||
@@ -207,10 +209,12 @@ export const en: Messages = {
|
|||||||
'title.submitted': 'Title submitted to vote.',
|
'title.submitted': 'Title submitted to vote.',
|
||||||
'title.proposed.one': '{n} title proposed',
|
'title.proposed.one': '{n} title proposed',
|
||||||
'title.proposed.other': '{n} titles proposed',
|
'title.proposed.other': '{n} titles proposed',
|
||||||
|
'module.inertia.proposed.one': '{n} inertia change proposed',
|
||||||
|
'module.inertia.proposed.other': '{n} inertia changes proposed',
|
||||||
|
|
||||||
'tally.short': '{for} for · {against} against',
|
'tally.short': '{for} for · {against} against',
|
||||||
'tally.missing.one': '{n} more vote in favor needed.',
|
'tally.missing.one': '{n} more vote in favour needed.',
|
||||||
'tally.missing.other': '{n} more votes in favor needed.',
|
'tally.missing.other': '{n} more votes in favour needed.',
|
||||||
'tally.reached': 'Threshold reached.',
|
'tally.reached': 'Threshold reached.',
|
||||||
'tally.level': 'Level: {pct}% of the threshold',
|
'tally.level': 'Level: {pct}% of the threshold',
|
||||||
'inertia.name.1': 'very low',
|
'inertia.name.1': 'very low',
|
||||||
@@ -242,15 +246,15 @@ export const en: Messages = {
|
|||||||
'vote.simulated.mark': 'simulated vote',
|
'vote.simulated.mark': 'simulated vote',
|
||||||
'vote.others': 'Other versions',
|
'vote.others': 'Other versions',
|
||||||
'vote.others.count': 'Other versions ({n})',
|
'vote.others.count': 'Other versions ({n})',
|
||||||
'vote.need': '{yes} yes to pass · {no} no to block',
|
'vote.need': '{yes} for to pass · {no} against to block',
|
||||||
'vote.need.quorum': '{yes} yes to pass · the quorum blocks it',
|
'vote.need.quorum': '{yes} for to pass · the quorum blocks it',
|
||||||
'vote.need.first.one': '{yes} yes out of {quorum} voter to pass',
|
'vote.need.first.one': '{yes} for out of {quorum} voter to pass',
|
||||||
'vote.need.first.other': '{yes} yes out of {quorum} voters to pass',
|
'vote.need.first.other': '{yes} for out of {quorum} voters to pass',
|
||||||
'vote.need.short': 'passes at {yes} yes · blocked at {no} no',
|
'vote.need.short': 'passes at {yes} for · blocked at {no} against',
|
||||||
'vote.need.short.quorum': 'passes at {yes} yes',
|
'vote.need.short.quorum': 'passes at {yes} for',
|
||||||
'vote.need.short.first': '{yes} yes out of {quorum}',
|
'vote.need.short.first': '{yes} for out of {quorum}',
|
||||||
'vote.need.title.one': 'Quorum: {n} voter. Below it, the missing voters count as “no”.',
|
'vote.need.title.one': 'Quorum: {n} voter. Below it, the missing voters count as against.',
|
||||||
'vote.need.title.other': 'Quorum: {n} voters. Below it, the missing voters count as “no”.',
|
'vote.need.title.other': 'Quorum: {n} voters. Below it, the missing voters count as against.',
|
||||||
'vote.retracted': 'Vote withdrawn.',
|
'vote.retracted': 'Vote withdrawn.',
|
||||||
'vote.stale': 'The text has changed in substance since you voted: your vote no longer counts until you reconfirm it.',
|
'vote.stale': 'The text has changed in substance since you voted: your vote no longer counts until you reconfirm it.',
|
||||||
'vote.reconfirm': 'Reconfirm: {choice}',
|
'vote.reconfirm': 'Reconfirm: {choice}',
|
||||||
@@ -295,7 +299,7 @@ export const en: Messages = {
|
|||||||
'quorum.heading.other': 'Quorum · {n} voters',
|
'quorum.heading.other': 'Quorum · {n} voters',
|
||||||
'quorum.default.one': 'By default, that of its level: {n} voter.',
|
'quorum.default.one': 'By default, that of its level: {n} voter.',
|
||||||
'quorum.default.other': 'By default, that of its level: {n} voters.',
|
'quorum.default.other': 'By default, that of its level: {n} voters.',
|
||||||
'quorum.explain': 'You set the quorum of this article as long as nobody has voted. Below it, the missing voters count as “no”.',
|
'quorum.explain': 'You set the quorum of this article as long as nobody has voted. Below it, the missing voters count as against.',
|
||||||
'quorum.field': 'Quorum of the article',
|
'quorum.field': 'Quorum of the article',
|
||||||
'quorum.set': 'Set this quorum',
|
'quorum.set': 'Set this quorum',
|
||||||
'quorum.reset': 'Back to the default ({n})',
|
'quorum.reset': 'Back to the default ({n})',
|
||||||
@@ -400,7 +404,7 @@ export const en: Messages = {
|
|||||||
'ballots.verify.unreadable': 'This is not a readable vote receipt.',
|
'ballots.verify.unreadable': 'This is not a readable vote receipt.',
|
||||||
|
|
||||||
'inertia.title': 'Inertia',
|
'inertia.title': 'Inertia',
|
||||||
'inertia.explain': 'The higher the inertia, the more votes it takes to change this text. Below the quorum, the missing voters count as “no”; beyond it, the share of “yes” asked for falls as turnout rises.',
|
'inertia.explain': 'The higher the inertia, the more votes it takes to change this text. Below the quorum, the missing voters count as against; beyond it, the share of votes in favour asked for falls as turnout rises.',
|
||||||
'inertia.change': 'Propose an inertia change',
|
'inertia.change': 'Propose an inertia change',
|
||||||
'inertia.change.to': 'New inertia',
|
'inertia.change.to': 'New inertia',
|
||||||
'inertia.change.note': "A change of inertia is itself voted at the article's inertia: {level}.",
|
'inertia.change.note': "A change of inertia is itself voted at the article's inertia: {level}.",
|
||||||
@@ -411,7 +415,7 @@ export const en: Messages = {
|
|||||||
'inertia.history.amendment': 'Went from {from} to {to} on {date}: starting setting of the text.',
|
'inertia.history.amendment': 'Went from {from} to {to} on {date}: starting setting of the text.',
|
||||||
|
|
||||||
'formula.title': 'The threshold formula',
|
'formula.title': 'The threshold formula',
|
||||||
'formula.intro': 'The number of votes in favor required, R, depends on turnout:',
|
'formula.intro': 'The number of votes in favour required, R, depends on turnout:',
|
||||||
'formula.W': 'W — electorate: {n} ({source})',
|
'formula.W': 'W — electorate: {n} ({source})',
|
||||||
'formula.W.wot': 'members of the Ğ1 web of trust',
|
'formula.W.wot': 'members of the Ğ1 web of trust',
|
||||||
'formula.W.group': 'group members',
|
'formula.W.group': 'group members',
|
||||||
@@ -420,7 +424,7 @@ export const en: Messages = {
|
|||||||
'formula.G': 'G — gradient: {n}',
|
'formula.G': 'G — gradient: {n}',
|
||||||
'formula.B': 'B — base: {n}',
|
'formula.B': 'B — base: {n}',
|
||||||
'formula.Q': 'Q — quorum: {n}',
|
'formula.Q': 'Q — quorum: {n}',
|
||||||
'formula.R': 'R — current threshold: {n} votes in favor',
|
'formula.R': 'R — current threshold: {n} votes in favour',
|
||||||
'formula.reading': 'Below the quorum Q, the missing voters count as against. Beyond it, the share of votes in favour asked for, highest at the quorum, falls as turnout rises; when everyone votes, majority M is enough.',
|
'formula.reading': 'Below the quorum Q, the missing voters count as against. Beyond it, the share of votes in favour asked for, highest at the quorum, falls as turnout rises; when everyone votes, majority M is enough.',
|
||||||
'common.pct': '{n}%',
|
'common.pct': '{n}%',
|
||||||
'settings.title': 'Formula settings',
|
'settings.title': 'Formula settings',
|
||||||
@@ -447,7 +451,7 @@ export const en: Messages = {
|
|||||||
'settings.required.version.other': "Votes for: at {n} voters, this version's turnout.",
|
'settings.required.version.other': "Votes for: at {n} voters, this version's turnout.",
|
||||||
'settings.required.usual.one': 'Votes for: at {n} voter, as usual.',
|
'settings.required.usual.one': 'Votes for: at {n} voter, as usual.',
|
||||||
'settings.required.usual.other': 'Votes for: at {n} voters, as usual.',
|
'settings.required.usual.other': 'Votes for: at {n} voters, as usual.',
|
||||||
'settings.reading': 'At each level, the group asks for its share of votes in favour when it votes as usual; the more voters, the closer it comes to M. Below the quorum of the level, the missing voters count as against.',
|
'settings.reading': 'At each level, the group asks for its share of votes in favour when it votes as usual; beyond the quorum, the more voters, the closer it comes to M. Below the quorum of the level, the missing voters count as against.',
|
||||||
|
|
||||||
'history.title': 'Corrections',
|
'history.title': 'Corrections',
|
||||||
'history.initial': 'initial text',
|
'history.initial': 'initial text',
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ export const es: Messages = {
|
|||||||
'docs.inForce.other': '{n} artículos en vigor',
|
'docs.inForce.other': '{n} artículos en vigor',
|
||||||
'docs.open.one': '{n} propuesta en curso',
|
'docs.open.one': '{n} propuesta en curso',
|
||||||
'docs.open.other': '{n} propuestas en curso',
|
'docs.open.other': '{n} propuestas en curso',
|
||||||
|
'docs.seeds.one': '{n} propuesta semilla',
|
||||||
|
'docs.seeds.other': '{n} propuestas semilla',
|
||||||
|
|
||||||
'doc.view.label': 'Leer',
|
'doc.view.label': 'Leer',
|
||||||
'doc.view.inforce': 'En vigor',
|
'doc.view.inforce': 'En vigor',
|
||||||
@@ -207,6 +209,8 @@ export const es: Messages = {
|
|||||||
'title.submitted': 'Título sometido a votación.',
|
'title.submitted': 'Título sometido a votación.',
|
||||||
'title.proposed.one': '{n} título propuesto',
|
'title.proposed.one': '{n} título propuesto',
|
||||||
'title.proposed.other': '{n} títulos propuestos',
|
'title.proposed.other': '{n} títulos propuestos',
|
||||||
|
'module.inertia.proposed.one': '{n} cambio de inercia propuesto',
|
||||||
|
'module.inertia.proposed.other': '{n} cambios de inercia propuestos',
|
||||||
|
|
||||||
'tally.short': '{for} a favor · {against} en contra',
|
'tally.short': '{for} a favor · {against} en contra',
|
||||||
'tally.missing.one': 'Falta {n} voto a favor.',
|
'tally.missing.one': 'Falta {n} voto a favor.',
|
||||||
@@ -447,7 +451,7 @@ export const es: Messages = {
|
|||||||
'settings.required.version.other': 'Votos a favor: con {n} votantes, la participación de esta versión.',
|
'settings.required.version.other': 'Votos a favor: con {n} votantes, la participación de esta versión.',
|
||||||
'settings.required.usual.one': 'Votos a favor: con {n} votante, como de costumbre.',
|
'settings.required.usual.one': 'Votos a favor: con {n} votante, como de costumbre.',
|
||||||
'settings.required.usual.other': 'Votos a favor: con {n} votantes, como de costumbre.',
|
'settings.required.usual.other': 'Votos a favor: con {n} votantes, como de costumbre.',
|
||||||
'settings.reading': 'En cada nivel, el grupo pide su parte de votos a favor cuando vota como de costumbre; cuantos más votantes, más se acerca a M. Por debajo del quórum del nivel, los votantes que faltan cuentan como «no».',
|
'settings.reading': 'En cada nivel, el grupo pide su parte de votos a favor cuando vota como de costumbre; más allá del quórum, cuantos más votantes, más se acerca a M. Por debajo del quórum del nivel, los votantes que faltan cuentan como «no».',
|
||||||
|
|
||||||
'history.title': 'Correcciones',
|
'history.title': 'Correcciones',
|
||||||
'history.initial': 'texto inicial',
|
'history.initial': 'texto inicial',
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ export const fr = {
|
|||||||
'docs.inForce.other': '{n} articles en vigueur',
|
'docs.inForce.other': '{n} articles en vigueur',
|
||||||
'docs.open.one': '{n} proposition en cours',
|
'docs.open.one': '{n} proposition en cours',
|
||||||
'docs.open.other': '{n} propositions en cours',
|
'docs.open.other': '{n} propositions en cours',
|
||||||
|
'docs.seeds.one': '{n} Proposition seed',
|
||||||
|
'docs.seeds.other': '{n} Propositions seed',
|
||||||
|
|
||||||
'doc.view.label': 'Lire',
|
'doc.view.label': 'Lire',
|
||||||
'doc.view.inforce': 'En vigueur',
|
'doc.view.inforce': 'En vigueur',
|
||||||
@@ -211,6 +213,8 @@ export const fr = {
|
|||||||
'title.submitted': 'Titre proposé au vote.',
|
'title.submitted': 'Titre proposé au vote.',
|
||||||
'title.proposed.one': '{n} titre proposé',
|
'title.proposed.one': '{n} titre proposé',
|
||||||
'title.proposed.other': '{n} titres proposés',
|
'title.proposed.other': '{n} titres proposés',
|
||||||
|
'module.inertia.proposed.one': "{n} changement d'inertie proposé",
|
||||||
|
'module.inertia.proposed.other': "{n} changements d'inertie proposés",
|
||||||
|
|
||||||
'tally.short': '{for} pour · {against} contre',
|
'tally.short': '{for} pour · {against} contre',
|
||||||
'tally.missing.one': 'Il manque {n} voix pour.',
|
'tally.missing.one': 'Il manque {n} voix pour.',
|
||||||
@@ -451,7 +455,7 @@ export const fr = {
|
|||||||
'settings.required.version.other': 'Voix pour : à {n} votants, la participation de cette version.',
|
'settings.required.version.other': 'Voix pour : à {n} votants, la participation de cette version.',
|
||||||
'settings.required.usual.one': "Voix pour : à {n} votant, comme d'habitude.",
|
'settings.required.usual.one': "Voix pour : à {n} votant, comme d'habitude.",
|
||||||
'settings.required.usual.other': "Voix pour : à {n} votants, comme d'habitude.",
|
'settings.required.usual.other': "Voix pour : à {n} votants, comme d'habitude.",
|
||||||
'settings.reading': "À chaque cran, le groupe demande sa part de « pour » quand il vote comme d'habitude ; plus il y a de votants, plus elle se rapproche de M. Sous le quorum du cran, les votants qui manquent comptent comme « non ».",
|
'settings.reading': "À chaque cran, le groupe demande sa part de « pour » quand il vote comme d'habitude ; au-delà du quorum, plus il y a de votants, plus elle se rapproche de M. Sous le quorum du cran, les votants qui manquent comptent comme « non ».",
|
||||||
|
|
||||||
'history.title': 'Corrections',
|
'history.title': 'Corrections',
|
||||||
'history.initial': 'texte initial',
|
'history.initial': 'texte initial',
|
||||||
|
|||||||
@@ -153,6 +153,11 @@
|
|||||||
const sectionKeys = $derived([...new Set((view?.units ?? []).map((u) => u.section))]);
|
const sectionKeys = $derived([...new Set((view?.units ?? []).map((u) => u.section))]);
|
||||||
const deepSecs = $derived(deepSections(deep, deepOrders, sectionKeys));
|
const deepSecs = $derived(deepSections(deep, deepOrders, sectionKeys));
|
||||||
const canVote = $derived(!!view?.vote.open);
|
const canVote = $derived(!!view?.vote.open);
|
||||||
|
// The facts of the head say what the page shows (lot U2): the seed proposals read at the head
|
||||||
|
// while nothing is in force, apart; then the proposals of « Propositions en cours » — the
|
||||||
|
// articles' versions, never the orders and titles proposed, voted at their module's line.
|
||||||
|
const headSeeds = $derived(doc.seedHead.length);
|
||||||
|
const openArticles = $derived(doc.open.filter((x) => !x.unit.kind).length - headSeeds);
|
||||||
|
|
||||||
/** The other versions of an article open to the vote, besides the one read. */
|
/** The other versions of an article open to the vote, besides the one read. */
|
||||||
const othersOf = (e: Entry) => e.unit.proposals.filter((p) => p.status === 'open' && p.kind !== 'inertia' && p.id !== e.proposal.id).length;
|
const othersOf = (e: Entry) => e.unit.proposals.filter((p) => p.status === 'open' && p.kind !== 'inertia' && p.id !== e.proposal.id).length;
|
||||||
@@ -164,11 +169,12 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet foldOf(unit: Unit, p: Proposal)}
|
{#snippet foldOf(unit: Unit, p: Proposal, besides: string[] = [])}
|
||||||
<VoteFold
|
<VoteFold
|
||||||
{doc}
|
{doc}
|
||||||
proposal={p}
|
proposal={p}
|
||||||
{unit}
|
{unit}
|
||||||
|
{besides}
|
||||||
onopen={(id) => go(panel, id)}
|
onopen={(id) => go(panel, id)}
|
||||||
onsettings={(id) => openPanel({ unitId: unit.id, proposalId: id, tab: 'settings' })}
|
onsettings={(id) => openPanel({ unitId: unit.id, proposalId: id, tab: 'settings' })}
|
||||||
ondetails={(id) => openPanel({ unitId: unit.id, proposalId: id, tab: 'details' })}
|
ondetails={(id) => openPanel({ unitId: unit.id, proposalId: id, tab: 'details' })}
|
||||||
@@ -223,7 +229,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<p class="summary">{summary}</p>
|
<p class="summary">{summary}</p>
|
||||||
<p class="facts faint">
|
<p class="facts faint">
|
||||||
{i18n.plural('docs.inForce', doc.inForce.length)} · {i18n.plural('docs.open', doc.open.length)}
|
{i18n.plural('docs.inForce', doc.inForce.length)} · {#if headSeeds}{i18n.plural('docs.seeds', headSeeds)}{' · '}{/if}{i18n.plural('docs.open', openArticles)}
|
||||||
{#if view.doc.publication.state === 'done'} · {t('doc.published')}{:else if view.doc.publication.state === 'pending'} · {t('doc.publish.pending')}{/if}
|
{#if view.doc.publication.state === 'done'} · {t('doc.published')}{:else if view.doc.publication.state === 'pending'} · {t('doc.publish.pending')}{/if}
|
||||||
· <button class="linkish" type="button" onclick={() => openPanel({ unitId: null, proposalId: null, tab: 'settings' })}>{t('settings.title')}</button>
|
· <button class="linkish" type="button" onclick={() => openPanel({ unitId: null, proposalId: null, tab: 'settings' })}>{t('settings.title')}</button>
|
||||||
</p>
|
</p>
|
||||||
@@ -517,13 +523,15 @@
|
|||||||
.new {
|
.new {
|
||||||
margin-top: 1.4rem;
|
margin-top: 1.4rem;
|
||||||
}
|
}
|
||||||
/* On a phone: the three readings on one line that scrolls, the targets big enough for a finger. */
|
/* On a phone: the three readings go to the line rather than hide « Si tout passe » off the
|
||||||
|
edge (lot U2); each row fills the width, the targets big enough for a finger. */
|
||||||
@media (max-width: 899px) {
|
@media (max-width: 899px) {
|
||||||
.seg.views {
|
.seg.views {
|
||||||
flex-wrap: nowrap;
|
display: flex;
|
||||||
max-width: 100%;
|
gap: 2px;
|
||||||
overflow-x: auto;
|
}
|
||||||
scrollbar-width: none;
|
.seg.views button {
|
||||||
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
.seg button {
|
.seg button {
|
||||||
min-height: 2.75rem;
|
min-height: 2.75rem;
|
||||||
|
|||||||
@@ -55,3 +55,27 @@ describe('anti-lexicon', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Lot U2 (reviews of M6c and M7): the same words for the same things.
|
||||||
|
describe('the reading of the formula', () => {
|
||||||
|
it('the settings say, like N1.1 and the formula, that the share falls beyond the quorum', () => {
|
||||||
|
expect(fr['formula.reading']).toContain('Au-delà');
|
||||||
|
expect(fr['settings.reading']).toContain('au-delà du quorum');
|
||||||
|
expect(en['settings.reading']).toContain('beyond the quorum');
|
||||||
|
expect(es['settings.reading']).toContain('más allá del quórum');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('English: one vocabulary', () => {
|
||||||
|
it('« Pour » and « Contre » are for and against, never yes and no', () => {
|
||||||
|
for (const [k, v] of Object.entries(en)) {
|
||||||
|
// What is read, the placeholders ({yes}, {no}: the numbers) left out.
|
||||||
|
const shown = v.replace(/\{\w+\}/g, '#');
|
||||||
|
expect(shown, k).not.toMatch(/\byes\b/i);
|
||||||
|
expect(shown, k).not.toMatch(/“no”|# no\b/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('one spelling, the one that dominates: favour', () => {
|
||||||
|
for (const [k, v] of Object.entries(en)) expect(v, k).not.toMatch(/\bfavor\b/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
// Lot U2 (review of M7): in the vote unfolded, one piece of information in one place. Rendered as
|
||||||
|
// the server would (svelte/server), and read as a person reads it: the tags left out, and what a
|
||||||
|
// closed <details> holds left out too (only its summary shows).
|
||||||
|
import { render } from 'svelte/server';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import UnitBlock from '../../src/lib/components/UnitBlock.svelte';
|
||||||
|
import VoteFold from '../../src/lib/components/VoteFold.svelte';
|
||||||
|
import type { DocState } from '../../src/lib/doc.svelte';
|
||||||
|
import type { Inertia, Proposal, Unit } from '../../src/lib/types';
|
||||||
|
|
||||||
|
function version(id: string, over: Partial<Proposal> = {}): Proposal {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
unitId: 'u1',
|
||||||
|
kind: 'text',
|
||||||
|
inertiaTarget: null,
|
||||||
|
title: 'Connaissance suffisante',
|
||||||
|
body: 'Je connais suffisamment la personne.',
|
||||||
|
status: 'open',
|
||||||
|
origin: 'seed',
|
||||||
|
label: null,
|
||||||
|
author: null,
|
||||||
|
mine: false,
|
||||||
|
revision: 1,
|
||||||
|
createdAt: '2026-11-03T00:00:00Z',
|
||||||
|
updatedAt: '2026-11-03T00:00:00Z',
|
||||||
|
submittedAt: null,
|
||||||
|
enteredAt: null,
|
||||||
|
confirmedAt: null,
|
||||||
|
seedMinimal: false,
|
||||||
|
tally: { for: 0, against: 0 },
|
||||||
|
tallyPending: { for: 0, against: 0 },
|
||||||
|
tallySimulated: { for: 0, against: 0 },
|
||||||
|
evaluation: { cast: 0, required: 9, reached: false, missing: 9, requiredRatio: 0.75, wellRated: false, quorum: 12, yesNeeded: 9, noNeeded: 0 },
|
||||||
|
votedAtInertia: 2,
|
||||||
|
blocked: false,
|
||||||
|
votable: true,
|
||||||
|
objections: 0,
|
||||||
|
myVote: null,
|
||||||
|
myVoteState: null,
|
||||||
|
myVoteStale: false,
|
||||||
|
myVoteChain: false,
|
||||||
|
myBallot: null,
|
||||||
|
translations: {},
|
||||||
|
...over,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function unitOf(proposals: Proposal[], inertia: Inertia = 2): Unit {
|
||||||
|
return { id: 'u1', code: 'E3', section: 'technique', position: 1, inertia, inertiaChanges: [], inForce: null, proposals };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The text a person reads: no tags, no server markers, nothing inside a closed <details> but its summary. */
|
||||||
|
function read(html: string): string {
|
||||||
|
return html
|
||||||
|
.replace(/<!--[\s\S]*?-->/g, '')
|
||||||
|
.replace(/(<details(?![^>]*\sopen)[^>]*>[\s\S]*?<\/summary>)[\s\S]*?<\/details>/g, '$1')
|
||||||
|
.replace(/<[^>]+>/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
const noop = () => {};
|
||||||
|
const doc = { view: null, original: false, articleTitle: () => '', unit: () => undefined } as unknown as DocState;
|
||||||
|
|
||||||
|
function fold(p: Proposal, unit: Unit, besides?: string[]): string {
|
||||||
|
const { body } = render(VoteFold, {
|
||||||
|
props: { doc, proposal: p, unit, besides, onopen: noop, onsettings: noop, ondetails: noop, onanswer: noop, onsign: noop },
|
||||||
|
});
|
||||||
|
return read(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = (text: string, part: string) => text.split(part).length - 1;
|
||||||
|
|
||||||
|
describe('the vote unfolded says each thing once', () => {
|
||||||
|
it('the inertia of the article, once (its notches with it)', () => {
|
||||||
|
const p = version('p1');
|
||||||
|
const text = fold(p, unitOf([p]));
|
||||||
|
expect(count(text, '2 / 5 · basse'), text).toBe(1);
|
||||||
|
});
|
||||||
|
it('the inertia a change of inertia is voted at, once', () => {
|
||||||
|
const p = version('p2', { kind: 'inertia', inertiaTarget: 4, title: 'inertia:4' });
|
||||||
|
const text = fold(p, unitOf([p]));
|
||||||
|
expect(count(text, '2 / 5 · basse'), text).toBe(1);
|
||||||
|
});
|
||||||
|
it('the other versions already listed beside the vote are not listed again', () => {
|
||||||
|
const a = version('p1', { label: '1.0.0' });
|
||||||
|
const b = version('p2', { label: '0.4.0-draft' });
|
||||||
|
const unit = unitOf([a, b]);
|
||||||
|
// Alone (in the text, say), it names its other version …
|
||||||
|
expect(fold(a, unit)).toContain('Autres versions (1)');
|
||||||
|
// … but in the list of the versions of its article, the other one is right there.
|
||||||
|
expect(fold(a, unit, ['p1', 'p2'])).not.toContain('Autres versions');
|
||||||
|
});
|
||||||
|
it('an other version not in sight is still named (the text in force, above)', () => {
|
||||||
|
const inForce = version('p0', { status: 'in_force', title: 'Connaissance suffisante' });
|
||||||
|
const a = version('p1', { label: '1.0.0' });
|
||||||
|
const b = version('p2', { label: '0.4.0-draft' });
|
||||||
|
const unit = { ...unitOf([a, b]), inForce };
|
||||||
|
expect(fold(a, unit, ['p1', 'p2'])).toContain('Autres versions (1)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function block(seed: Proposal, voteOpen: boolean, rivals = 0): string {
|
||||||
|
const { body } = render(UnitBlock, {
|
||||||
|
props: {
|
||||||
|
text: { title: seed.title, body: seed.body, translated: false, stale: false, missing: false },
|
||||||
|
seed,
|
||||||
|
rivals,
|
||||||
|
voteFor: seed.id,
|
||||||
|
voteOpen,
|
||||||
|
onaction: noop,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return read(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('a seed proposal at the head, its vote unfolded', () => {
|
||||||
|
const seed = version('p1', { seedProposal: true, title: "Raison d'être" });
|
||||||
|
it('folded: its line says how far it is from passing', () => {
|
||||||
|
const text = block(seed, false, 1);
|
||||||
|
expect(text).toContain('0 pour · 0 contre · 9 oui sur 12');
|
||||||
|
expect(text).toContain('1 autre version');
|
||||||
|
});
|
||||||
|
it('unfolded: the vote below says it, the line does not say it again', () => {
|
||||||
|
const text = block(seed, true, 1);
|
||||||
|
expect(text).not.toContain('0 pour · 0 contre');
|
||||||
|
expect(text).not.toContain('9 oui sur 12');
|
||||||
|
expect(text).not.toContain('basse');
|
||||||
|
expect(text).not.toContain('autre version');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user