diff --git a/docs/chantiers/acte-certification/CHANTIER.md b/docs/chantiers/acte-certification/CHANTIER.md
index 3621648..7f9d863 100644
--- a/docs/chantiers/acte-certification/CHANTIER.md
+++ b/docs/chantiers/acte-certification/CHANTIER.md
@@ -35,7 +35,11 @@ les applications clientes (Cesium, Ğecko…) peuvent suivre.
ouvrent un panneau latéral (une feuille sur mobile) dont l'état vit dans l'URL.
5. **En dessous**, les propositions en cours — les 30 autres articles du texte source, à zéro
vote, inertie standard — rangées par section (introduction, fondamental, technique,
- conclusion, annexes, formule, inertie, ordonnancement), les variantes signalées.
+ conclusion, annexes, formule, inertie, ordonnancement), **une ligne par article** : un
+ article qui a plusieurs versions (variantes du seed, propositions rivales) montre leur
+ nombre et la meilleure en tête (niveau d'eau, pour / contre) ; la ligne se déplie sur ses
+ versions, chacune avec son étiquette, son niveau et ses marques, et chacune ouvre son vote.
+ Les filtres gardent un article dès qu'une de ses versions passe.
6. **Deux aperçus** : « si les bien cotées passent » et « si tout passe ».
7. **Trois langues** : fr · en · es, pour l'interface comme pour le texte. Le français fait foi ;
une traduction devenue ancienne (le français a changé depuis) est signalée.
diff --git a/docs/chantiers/acte-certification/CONTROLES.md b/docs/chantiers/acte-certification/CONTROLES.md
index f5eca6a..1690f5b 100644
--- a/docs/chantiers/acte-certification/CONTROLES.md
+++ b/docs/chantiers/acte-certification/CONTROLES.md
@@ -11,7 +11,7 @@ Où tester : les contrôles qui écrivent (C5, C6, C7) se font dans le **groupe
|---|---|---|---|
| C1 | L'accueil | ouvrir `/` sans être connectée | « Rendre possible la décision collective », la phrase d'outils, les quatre sections ; le menu à gauche avec les quatre outils, trois grisés ; « Se connecter avec sa clé Ğ1 » |
| C2 | Les groupes | se connecter, ouvrir « Groupes » | Monnaie libre, Axiom, Librodrome, Groupe local Vallée Drôme Crestois ; **pas** l'Atelier du canal |
-| C3 | Le texte en vigueur | Monnaie libre → Docs de référence → Acte d'engagement Certification | trois articles seulement (Raison d'être, Unicité du compte, Réciprocité), chacun « seed a minima · 0 vote » ; en dessous, « Propositions en cours » (32 lignes) |
+| C3 | Le texte en vigueur | Monnaie libre → Docs de référence → Acte d'engagement Certification | trois articles seulement (Raison d'être, Unicité du compte, Réciprocité), chacun « seed a minima · 0 vote » ; en dessous, « Propositions en cours » (30 lignes, une par article) |
| C4 | Gestes contextuels | survoler la Raison d'être → « Le vote » | un panneau : seuil 50 voix pour, il en manque 50 ; « La formule du seuil » se déplie ; l'URL garde le panneau ; Échap le ferme |
| C5 | Entrée en vigueur | groupe d'essai : à deux, voter « Pour » sur « Les deux garanties réciproques » | au second vote, « … est entré en vigueur » ; le texte en vigueur compte quatre articles |
| C6 | Coquille sans vote | groupe d'essai : Unicité du compte → « Le vote » → « Corriger une coquille », remplacer les apostrophes droites par des apostrophes typographiques (« Je m’engage sur l’honneur ») | « Coquille corrigée, sans toucher aux votes. » ; un changement de sens est refusé : « Ce changement touche au sens : propose-le comme modification. » |
@@ -22,6 +22,7 @@ Où tester : les contrôles qui écrivent (C5, C6, C7) se font dans le **groupe
| C11 | Une personne, une voix | avec un lien d'invitation, ouvrir le vote d'un article de Monnaie libre | « Ici, seuls les membres de la toile de confiance Ğ1 votent… » à la place des boutons ; dans le groupe d'essai, les boutons sont là |
| 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 |
| C13 | La Raison d'être | Monnaie libre → Acte d'engagement Certification, puis EN, puis ES | en tête du texte en vigueur, « Raison d'être » : la première phrase en italique, puis deux paragraphes ; « Purpose », « Razón de ser » |
+| C14 | Les versions d'un article | Monnaie libre → « Propositions en cours » → « 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 ouvre son vote ; un nouveau clic sur la ligne la replie |
| M1 | Mobile | téléphone (ou fenêtre étroite) | menu en tiroir, les trois gestes visibles sans survol, le panneau s'ouvre en feuille |
## Au-delà de la liste
diff --git a/web/e2e/reader.spec.ts b/web/e2e/reader.spec.ts
index 37b072a..b01ae6e 100644
--- a/web/e2e/reader.spec.ts
+++ b/web/e2e/reader.spec.ts
@@ -33,7 +33,8 @@ test('C3 — le texte en vigueur : raison d’être et deux engagements, seed a
await expect(units.nth(2).locator('h2')).toHaveText('Réciprocité');
await expect(page.getByText('seed a minima · 0 vote')).toHaveCount(3);
await expect(page.getByRole('heading', { name: 'Propositions en cours' })).toBeVisible();
- await expect(page.locator('ul.deep button.row')).toHaveCount(32);
+ // One line per article: E3 and E7 hold their two variants inside.
+ await expect(page.locator('ul.deep button.row')).toHaveCount(30);
});
test('C4 — les actions sont contextuelles, le vote s’ouvre en panneau', async ({ page }) => {
@@ -147,13 +148,13 @@ test('C11 — une personne, une voix : dans Monnaie libre, seuls les membres de
await invited.goto(`/invitation#${link.split('#')[1]}`);
await expect(invited).toHaveURL(/\/groupes$/);
await invited.goto(DOC);
- await invited.locator('ul.deep button.row', { hasText: 'Connaissance suffisante' }).first().click();
+ await invited.locator('ul.deep button.row', { hasText: 'Vérification personnelle de la clé' }).click();
const panel = invited.locator('aside.panel');
await expect(panel.getByText('Ici, seuls les membres de la toile de confiance Ğ1 votent', { exact: false })).toBeVisible();
await expect(panel.getByRole('button', { name: 'Pour', exact: true })).toHaveCount(0);
// In the test group, the same person votes.
await invited.goto(TEST_DOC);
- await invited.locator('ul.deep button.row', { hasText: 'Connaissance suffisante' }).first().click();
+ await invited.locator('ul.deep button.row', { hasText: 'Vérification personnelle de la clé' }).click();
await expect(invited.locator('aside.panel').getByRole('button', { name: 'Pour', exact: true })).toBeVisible();
});
@@ -205,3 +206,27 @@ test('C13 — la Raison d’être ouvre le texte en vigueur, en trois langues',
await page.getByRole('radiogroup', { name: 'Text language' }).getByRole('radio', { name: 'ES' }).click();
await expect(first.locator('h2')).toHaveText('Razón de ser');
});
+
+test('C14 — un article, une ligne : ses versions se déplient', async ({ page }) => {
+ await signIn(page, 'Chiara');
+ await page.goto(DOC);
+ const e3 = page.locator('ul.deep button.row', { hasText: 'Connaissance suffisante' });
+ await expect(e3).toHaveCount(1);
+ await expect(e3).toContainText('2 versions');
+ await expect(e3).toHaveAttribute('aria-expanded', 'false');
+ // Unfolded with the keyboard: its two versions, each with its label.
+ await e3.focus();
+ await page.keyboard.press('Enter');
+ await expect(e3).toHaveAttribute('aria-expanded', 'true');
+ const versions = page.locator(`#${await e3.getAttribute('aria-controls')}`).getByRole('button');
+ await expect(versions).toHaveCount(2);
+ await expect(versions.nth(0)).toContainText('variante 1.0.0');
+ await expect(versions.nth(1)).toContainText('variante 0.4.0-draft');
+ // A version opens its vote, as any proposal.
+ await versions.nth(1).click();
+ await expect(page.locator('aside.panel').getByText('Seuil : 50 voix pour')).toBeVisible();
+ await expect(page).toHaveURL(/version=/);
+ await e3.click();
+ await expect(e3).toHaveAttribute('aria-expanded', 'false');
+ await expect(versions).toHaveCount(0);
+});
diff --git a/web/src/lib/components/DeepList.svelte b/web/src/lib/components/DeepList.svelte
index c16b3d3..e09907a 100644
--- a/web/src/lib/components/DeepList.svelte
+++ b/web/src/lib/components/DeepList.svelte
@@ -1,45 +1,126 @@
+{#snippet marks(p: Proposal)}
+ {#if p.evaluation?.wellRated}{t('deep.wellrated')}{/if}
+ {#if p.blocked}{t('deep.blocked')}{/if}
+ {#if p.myVoteStale}{t('deep.stale')}{/if}
+ {#if p.mine}{t('deep.mine')}{/if}
+{/snippet}
+
- {#each items as { unit, proposal: p }, i (p.id)}
- {#if sectionLabel(unit.section) && unit.section !== items[i - 1]?.unit.section}
+ {#each articles as a, i (a.unit.id)}
+ {@const unit = a.unit}
+ {@const best = a.versions[0]}
+ {#if sectionLabel(unit.section) && unit.section !== articles[i - 1]?.unit.section}
{sectionLabel(unit.section)}
{/if}
-
-
-
+
+
+ {:else if best}
+ {@const open = isOpen(a)}
+ {@const title = headline(a)}
+ {@const told = toldApart(a)}
+