forked from yvv/decision
Lot U : contrôles repris sans les affaiblir (C3, C4, C6, C19 à C21, C23 à C26, C34, M1) ; CONTROLES.md (C35 à C37) ; retouches vues aux captures (ligne d'ordre discrète, en-tête plus court sur téléphone, liste à poignées)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,10 @@ import { ALICE, chain, DOC, EVE, person, shot, signIn, VOTE_ACCOUNT, wallet } fr
|
||||
type Entry = { id: string; status: string; comment: string };
|
||||
const mine = async (page: Page): Promise<Entry[]> => (await (await page.request.get('/api/ballots')).json()).ballots;
|
||||
|
||||
/** Unfolds the vote of an open proposal, from « Propositions en cours ». */
|
||||
/** Unfolds the vote of an open proposal, from « Propositions en cours » (its sections folded: lot U). */
|
||||
async function unfold(page: Page, title: string) {
|
||||
const all = page.getByRole('button', { name: 'Tout déplier' });
|
||||
if (await all.isVisible()) await all.click();
|
||||
// By its title only: the excerpt of another proposal may name it.
|
||||
await page.locator('ul.deep button.row').filter({ has: page.locator('span.title').getByText(title, { exact: true }) }).click();
|
||||
return page.getByRole('region', { name: `Le vote de « ${title} »` });
|
||||
|
||||
@@ -80,14 +80,18 @@ test('M1 — mobile : logo dans la barre, l’accueil sous la voûte, menu en ti
|
||||
await drawer.getByRole('button', { name: 'Fermer' }).first().click();
|
||||
|
||||
// The vote unfolds under the article, on a phone too (a seed proposal, ADR 0011: nothing is
|
||||
// in force at the start, its vote unfolds in the list of proposals).
|
||||
await page.locator('ul.deep button.row').first().click();
|
||||
const fold = page.locator('ul.deep section.fold');
|
||||
await expect(fold.getByText('Seuil : 9 voix pour')).toBeVisible();
|
||||
// in force at the start, the seed proposals open the text — lot U).
|
||||
const first = page.locator('section.text article.unit').first();
|
||||
const toggle = first.getByRole('button', { name: 'Le vote' });
|
||||
// A finger's target, and nothing to hover with: the gestures are in sight.
|
||||
expect((await toggle.boundingBox())?.height ?? 0).toBeGreaterThanOrEqual(44);
|
||||
await toggle.click();
|
||||
const fold = page.locator('section.text section.fold');
|
||||
await expect(fold.getByText('9 oui sur 12 votants pour passer', { exact: true })).toBeVisible();
|
||||
// The settings of the formula read on a phone: the table of the five levels holds in the sheet.
|
||||
await fold.getByRole('button', { name: 'Réglages de la formule' }).click();
|
||||
const panel = page.locator('aside.panel');
|
||||
await expect(panel.getByRole('tab', { name: 'Réglages' })).toBeInViewport();
|
||||
await expect(panel.getByRole('heading', { name: 'Réglages de la formule' })).toBeInViewport();
|
||||
await expect(panel.locator('table tbody tr')).toHaveCount(5);
|
||||
const fits = await panel.locator('.scroll').evaluate((el) => el.scrollWidth <= el.clientWidth + 1);
|
||||
expect(fits).toBe(true);
|
||||
|
||||
+54
-62
@@ -1,22 +1,18 @@
|
||||
import { devices, expect, test, type Browser, type Page } from '@playwright/test';
|
||||
import { ALICE, DOC, person, shot, signIn, TEST_DOC, wallet } from './helpers';
|
||||
import { ALICE, DOC, person, shot, signIn, TEST_DOC, unfold, wallet } from './helpers';
|
||||
|
||||
// Lot D — the modules (Yvv, 2026-10-30; ADR 0009): at the end of each section, the
|
||||
// order of its articles (inertia 2), proposed with a list one clicks; at the head of
|
||||
// the document, its title (inertia 4). Both voted like an article: unfolded, in the
|
||||
// basket, on the Ğ1 chain in Monnaie libre (simulated here: e2e/chain-mock.mjs).
|
||||
// The server is shared with the specs that follow: the test group keeps its articles
|
||||
// and their votes; its conclusion is reordered and its title changes.
|
||||
// order of its articles (inertia 2); at the head of the document, its title (inertia 4).
|
||||
// Both voted like an article: unfolded, in the basket, on the Ğ1 chain in Monnaie libre
|
||||
// (simulated here: e2e/chain-mock.mjs). Lot U (Yvv, 2026-11-02): the order is one line at
|
||||
// the end of its section, never the list of its articles again; « Réordonner » puts handles
|
||||
// on them in place (C35); every version of a module is voted at its place, never among the
|
||||
// proposals. The server is shared with the specs that follow: the test group keeps its
|
||||
// articles and their votes; its conclusion is reordered and its title changes.
|
||||
|
||||
const K1 = 'Règles abrégées de la toile de confiance';
|
||||
const K2 = 'Monnaie G1';
|
||||
|
||||
/** A vote unfolded in the list of proposals: « Pour », then the news. */
|
||||
async function voteFor(page: Page, row: string) {
|
||||
await page.locator('ul.deep button.row', { hasText: row }).click();
|
||||
await page.locator('ul.deep section.fold').getByRole('button', { name: 'Pour', exact: true }).click();
|
||||
}
|
||||
|
||||
/** Whether a version of a module of the test group is in force now. */
|
||||
async function inForce(page: Page, proposalId: string): Promise<boolean> {
|
||||
const g = await (await page.request.get('/api/groups/essai')).json();
|
||||
@@ -27,37 +23,39 @@ async function inForce(page: Page, proposalId: string): Promise<boolean> {
|
||||
/**
|
||||
* The voices that bring a module version of the test group into force: hers, then a
|
||||
* second one if hers was not enough. Every dev sign-in joins the test group: from 16
|
||||
* members B^W vanishes and, C being 1, one voice may suffice (ADR 0007). Returns the
|
||||
* number of voices for it.
|
||||
* members B^W vanishes and, the quorum being 1, one voice may suffice (ADR 0013). Returns
|
||||
* the number of voices for it. The second one votes at the module, by its address.
|
||||
*/
|
||||
async function secondVoice(page: Page, browser: Browser, proposalId: string, row: string, news: string): Promise<number> {
|
||||
async function secondVoice(page: Page, browser: Browser, proposalId: string, fold: string, news: string): Promise<number> {
|
||||
if (await inForce(page, proposalId)) {
|
||||
await expect(page.getByText(news)).toBeVisible();
|
||||
return 1;
|
||||
}
|
||||
const yvv = await person(browser, 'Yvv');
|
||||
await yvv.goto(TEST_DOC);
|
||||
await voteFor(yvv, row);
|
||||
await yvv.goto(`${TEST_DOC}?vote=${proposalId}`);
|
||||
await yvv.getByRole('region', { name: fold }).getByRole('button', { name: 'Pour', exact: true }).click();
|
||||
await expect(yvv.getByText(news)).toBeVisible();
|
||||
return 2;
|
||||
}
|
||||
|
||||
test('C25 — l’ordre des articles : en fin de section, proposé en cliquant, au clavier, voté, en vigueur', async ({ page, browser }) => {
|
||||
test('C25 — l’ordre des articles : une ligne en fin de section, réordonnée sur place, votée là, en vigueur', async ({ page, browser }) => {
|
||||
await signIn(page, 'Chiara');
|
||||
await page.goto(TEST_DOC);
|
||||
// Each section ends with its order; a section of one article has nothing to decide.
|
||||
const modules = page.locator('section.text [role="group"]');
|
||||
await unfold(page);
|
||||
// Each section ends with its order, in one line; a section of one article has nothing to decide.
|
||||
const modules = page.getByRole('group', { name: /^Ordre des articles · / });
|
||||
await expect(modules).toHaveCount(7);
|
||||
await expect(page.getByRole('group', { name: 'Ordre des articles · Ordonnancement' })).toHaveCount(0);
|
||||
const order = page.getByRole('group', { name: 'Ordre des articles · Conclusion' });
|
||||
await expect(order.locator(':scope > ol li')).toHaveText([`1 ${K1}`, `2 ${K2}`]);
|
||||
await expect(order).toContainText('2 articles · inertie 2 / 5 · basse');
|
||||
await expect(order).toContainText('Ordre de la section');
|
||||
await expect(order).toContainText('inertie 2 / 5 · basse');
|
||||
await expect(order.getByText('seed a minima · 0 vote')).toBeVisible();
|
||||
// Never the list of its articles again: it is read in its vote, or while reordering.
|
||||
await expect(order.getByText(K1)).toHaveCount(0);
|
||||
await order.scrollIntoViewIfNeeded();
|
||||
await shot(page, 'c25-1-ordre-lecture');
|
||||
|
||||
// Its vote unfolds under it, like an article's, the list whole (the mark's real number of
|
||||
// votes is seen in Monnaie libre, below: here one voice may confirm a seed).
|
||||
// Its vote unfolds under its line, like an article's, the list whole.
|
||||
const fond = page.getByRole('group', { name: 'Ordre des articles · Engagements fondamentaux' });
|
||||
await fond.hover();
|
||||
await fond.getByRole('button', { name: 'Le vote' }).click();
|
||||
@@ -66,22 +64,21 @@ test('C25 — l’ordre des articles : en fin de section, proposé en cliquant,
|
||||
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();
|
||||
|
||||
// Another order: the article, then its new place.
|
||||
// Another order: « Réordonner », the article, then its new place — in place, no panel.
|
||||
await order.hover();
|
||||
await order.getByRole('button', { name: 'Proposer un autre ordre' }).click();
|
||||
await expect(page).toHaveURL(/onglet=proposer/);
|
||||
const panel = page.locator('aside.panel');
|
||||
const list = panel.getByRole('list', { name: 'Nouvel ordre des articles' });
|
||||
await order.getByRole('button', { name: 'Réordonner' }).click();
|
||||
await expect(page.locator('aside.panel')).toHaveCount(0);
|
||||
const list = order.getByRole('list', { name: 'Nouvel ordre des articles' });
|
||||
const items = list.locator('button.item');
|
||||
const names = list.locator('button.item .name');
|
||||
await expect(names).toHaveText([K1, K2]);
|
||||
const submit = panel.getByRole('button', { name: 'Proposer cet ordre' });
|
||||
const submit = order.getByRole('button', { name: 'Proposer cet ordre' });
|
||||
await expect(submit).toBeDisabled();
|
||||
await items.nth(1).click();
|
||||
await expect(items.nth(1)).toHaveAttribute('aria-pressed', 'true');
|
||||
await items.nth(0).click();
|
||||
await expect(names).toHaveText([K2, K1]);
|
||||
await expect(panel.getByText(`« ${K2} » en place 1 sur 2.`)).toBeAttached();
|
||||
await expect(order.getByText(`« ${K2} » en place 1 sur 2.`)).toBeAttached();
|
||||
await expect(submit).toBeEnabled();
|
||||
// With the keyboard: the moved article keeps the focus, the arrows move it.
|
||||
await expect(items.nth(0)).toBeFocused();
|
||||
@@ -91,38 +88,42 @@ test('C25 — l’ordre des articles : en fin de section, proposé en cliquant,
|
||||
await page.keyboard.press('ArrowUp');
|
||||
await expect(names).toHaveText([K2, K1]);
|
||||
// Or its arrows.
|
||||
await panel.getByRole('button', { name: `Descendre « ${K2} »` }).click();
|
||||
await panel.getByRole('button', { name: `Monter « ${K2} »` }).click();
|
||||
await order.getByRole('button', { name: `Descendre « ${K2} »` }).click();
|
||||
await order.getByRole('button', { name: `Monter « ${K2} »` }).click();
|
||||
await expect(names).toHaveText([K2, K1]);
|
||||
await shot(page, 'c25-2-ordre-propose');
|
||||
await submit.click();
|
||||
await expect(page.getByText('Ordre proposé au vote.')).toBeVisible();
|
||||
|
||||
// Its vote unfolds in the list of proposals, the list it proposes in sight.
|
||||
const fold = page.locator('ul.deep section.fold');
|
||||
// Its vote unfolds at the line, the list it proposes in sight; never a line among the proposals.
|
||||
const fold = order.locator('section.fold');
|
||||
await expect(fold).toHaveAttribute('aria-label', 'Le vote de « Ordre des articles · Conclusion »');
|
||||
await expect(fold.locator('ol.order li')).toHaveText([K2, K1]);
|
||||
await expect(page.locator('ul.deep li.section', { hasText: 'Conclusion' })).toBeVisible();
|
||||
await expect(page.locator('ul.deep button.row', { hasText: 'Ordre des articles · Conclusion' })).toContainText(`${K2} · ${K1}`);
|
||||
await expect(fold.getByRole('list', { name: 'Ordre proposé' }).locator('li')).toHaveText([K2, K1]);
|
||||
await expect(order).toContainText('1 ordre proposé');
|
||||
await expect(page.locator('ul.deep button.row', { hasText: 'Ordre des articles' })).toHaveCount(0);
|
||||
// The order in force is read from it, among its other versions.
|
||||
await expect(fold.getByRole('region', { name: 'Autres versions' })).toContainText('Ordre en vigueur');
|
||||
const pid = ((await fold.getAttribute('id')) ?? '').replace(/^vote-/, '');
|
||||
await fold.getByRole('button', { name: 'Pour', exact: true }).click();
|
||||
await expect(page.getByText('Vote enregistré.')).toBeVisible();
|
||||
await shot(page, 'c25-3-ordre-vote');
|
||||
// Into force (her voice, or a second one): the section reads in its new order.
|
||||
const voices = await secondVoice(page, browser, pid, 'Ordre des articles · Conclusion', 'Le nouvel ordre de « Conclusion » est entré en vigueur.');
|
||||
const voices = await secondVoice(page, browser, pid, 'Le vote de « Ordre des articles · Conclusion »', 'Le nouvel ordre de « Conclusion » est entré en vigueur.');
|
||||
|
||||
// In the basket too: at the end of its section.
|
||||
await page.getByRole('button', { name: /^Voter en lot/ }).click();
|
||||
const panel = page.locator('aside.panel');
|
||||
const basket = panel.getByRole('region', { name: 'Conclusion' });
|
||||
await expect(basket.locator('li.slot').last()).toContainText('Ordre des articles · Conclusion');
|
||||
await panel.getByRole('button', { name: 'Fermer le panneau' }).click();
|
||||
await page.reload();
|
||||
await expect(order.locator(':scope > ol li')).toHaveText([`1 ${K2}`, `2 ${K1}`]);
|
||||
// In force now: its vote stays unfolded, under the module, the list whole.
|
||||
// In force now: its vote stays unfolded under its line, the list whole, in the new order.
|
||||
await expect(order.locator('section.fold')).toContainText(`${voices} pour · 0 contre`);
|
||||
await expect(order.getByRole('list', { name: 'Ordre en vigueur' }).locator('li')).toHaveText([K2, K1]);
|
||||
await order.scrollIntoViewIfNeeded();
|
||||
await shot(page, 'c25-3b-ordre-en-vigueur');
|
||||
await expect(order).toContainText(/adopté le/);
|
||||
await unfold(page);
|
||||
const rows = await page.locator('ul.deep button.row .title').allTextContents();
|
||||
expect(rows.indexOf(K2)).toBeLessThan(rows.indexOf(K1));
|
||||
});
|
||||
@@ -131,12 +132,14 @@ test('C26 — le titre du document : proposé et voté en tête, ses traductions
|
||||
await signIn(page, 'Chiara');
|
||||
await page.goto(TEST_DOC);
|
||||
const head = page.locator('header.head');
|
||||
const bar = head.locator('.title-unit');
|
||||
await expect(bar).toContainText('Titre du document · inertie 4 / 5 · haute · seed a minima · 0 vote');
|
||||
const bar = head.getByRole('group', { name: 'Titre du document' });
|
||||
await expect(bar).toContainText('Titre du document · seed a minima · 0 vote');
|
||||
await expect(bar).toContainText('inertie 4 / 5 · haute');
|
||||
await head.hover();
|
||||
await bar.getByRole('button', { name: 'Proposer un autre titre' }).click();
|
||||
await expect(page).toHaveURL(/onglet=proposer/);
|
||||
const panel = page.locator('aside.panel');
|
||||
await expect(panel.getByRole('heading', { name: 'Proposer un autre titre' })).toBeVisible();
|
||||
const input = panel.getByRole('textbox', { name: 'Nouveau titre' });
|
||||
await expect(input).toHaveValue("Acte d'engagement Certification");
|
||||
const submit = panel.getByRole('button', { name: 'Proposer un autre titre' });
|
||||
@@ -147,13 +150,15 @@ test('C26 — le titre du document : proposé et voté en tête, ses traductions
|
||||
await shot(page, 'c26-1-titre-propose');
|
||||
await submit.click();
|
||||
await expect(page.getByText('Titre proposé au vote.')).toBeVisible();
|
||||
const fold = page.locator('ul.deep section.fold');
|
||||
// Its vote unfolds under the title, where it is read; never a line among the proposals.
|
||||
const fold = bar.locator('section.fold');
|
||||
await expect(fold).toHaveAttribute('aria-label', `Le vote de « ${next} »`);
|
||||
await expect(page.locator('ul.deep button.row', { hasText: next })).toContainText('Titre du document');
|
||||
await expect(bar).toContainText('1 titre proposé');
|
||||
await expect(page.locator('ul.deep').getByText(next)).toHaveCount(0);
|
||||
const pid = ((await fold.getAttribute('id')) ?? '').replace(/^vote-/, '');
|
||||
await fold.getByRole('button', { name: 'Pour', exact: true }).click();
|
||||
await expect(page.getByText('Vote enregistré.')).toBeVisible();
|
||||
const voices = await secondVoice(page, browser, pid, next, `Le titre « ${next} » est entré en vigueur.`);
|
||||
const voices = await secondVoice(page, browser, pid, `Le vote de « ${next} »`, `Le titre « ${next} » est entré en vigueur.`);
|
||||
|
||||
// The document changes title; its translations stay editorial, to review.
|
||||
await page.reload();
|
||||
@@ -194,7 +199,7 @@ test('C27 — la participation ramenée dans ses bornes : la mesure, puis ce que
|
||||
await shot(page, 'c27-participation-bornee');
|
||||
});
|
||||
|
||||
test('C25 — sur la chaîne Ğ1 et sur un téléphone : l’ordre se vote dans le panier, la liste tient', async ({ browser }) => {
|
||||
test('C25 — sur la chaîne Ğ1 et sur un téléphone : l’ordre se vote dans le panier, puis se lit à sa ligne', async ({ browser }) => {
|
||||
// The chain is waited for: the server reads the indexer every 10 seconds.
|
||||
test.setTimeout(90_000);
|
||||
const ctx = await browser.newContext({ ...devices['Pixel 7'], locale: 'fr-FR' });
|
||||
@@ -219,23 +224,10 @@ test('C25 — sur la chaîne Ğ1 et sur un téléphone : l’ordre se vote dans
|
||||
);
|
||||
await fond.scrollIntoViewIfNeeded();
|
||||
await shot(page, 'c25-4-ordre-mobile');
|
||||
// The title on a phone: its gestures in sight, without a pointer to hover with.
|
||||
// The title on a phone: its gestures in sight, without a pointer to hover with. Reordering on a
|
||||
// phone: C35.
|
||||
await page.goto(TEST_DOC);
|
||||
await expect(page.locator('.title-unit .links')).toHaveCSS('opacity', '1');
|
||||
await shot(page, 'c26-4-titre-mobile');
|
||||
// The list one clicks, on a phone: nothing overflows, the buttons are big enough to touch.
|
||||
const tech = page.getByRole('group', { name: 'Ordre des articles · Engagements techniques' });
|
||||
await tech.getByRole('button', { name: 'Proposer un autre ordre' }).click();
|
||||
const items = panel.getByRole('list', { name: 'Nouvel ordre des articles' }).locator('button.item');
|
||||
await expect(items).toHaveCount(11);
|
||||
await items.nth(3).click();
|
||||
await items.nth(0).click();
|
||||
await expect(panel.getByRole('button', { name: 'Proposer cet ordre' })).toBeEnabled();
|
||||
const holds = await panel.locator('.body').evaluate((el) => el.scrollWidth <= el.clientWidth + 1);
|
||||
expect(holds).toBe(true);
|
||||
const box = await items.nth(0).boundingBox();
|
||||
expect(box?.height ?? 0).toBeGreaterThanOrEqual(44);
|
||||
await shot(page, 'c25-5-ordre-propose-mobile');
|
||||
await page.getByRole('heading', { level: 1 }).scrollIntoViewIfNeeded();
|
||||
await ctx.close();
|
||||
});
|
||||
|
||||
@@ -28,7 +28,8 @@ async function openBox(page: Page, unread: number) {
|
||||
async function details(page: Page, id: string) {
|
||||
await page.goto(`${TEST_DOC}?onglet=details&version=${id}`);
|
||||
const panel = page.locator('aside.panel');
|
||||
await expect(panel.getByRole('tab', { name: 'Détails' })).toHaveAttribute('aria-selected', 'true');
|
||||
// One thing at a time in the panel (lot U), named at its head.
|
||||
await expect(panel.getByRole('heading', { name: 'Détails', exact: true })).toBeVisible();
|
||||
return panel;
|
||||
}
|
||||
|
||||
|
||||
+32
-18
@@ -1,5 +1,5 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { BOB, deepRow, DOC, expectWelcome, person, seedsInForce, signIn, TEST_DOC, testDoc, wallet } from './helpers';
|
||||
import { BOB, DOC, expectWelcome, person, seedsInForce, signIn, TEST_DOC, testDoc, unfold, wallet } from './helpers';
|
||||
|
||||
// The seed proposals (ADR 0011) are not in force at the start: the controls that read a
|
||||
// text in force find I1, E1 and E2 brought in by the vote in the test group.
|
||||
@@ -37,22 +37,29 @@ test('C2 — l’accueil avec session : le choix du groupe, ceux de la même cl
|
||||
await expect(page).toHaveURL((u) => u.pathname === '/');
|
||||
});
|
||||
|
||||
test('C3 — au départ, aucun texte en vigueur : la raison d’être et les deux engagements sont des Propositions seed', async ({ page }) => {
|
||||
test('C3 — au départ, aucun texte en vigueur : la raison d’être et les deux engagements, Propositions seed, ouvrent le texte', async ({ page }) => {
|
||||
await signIn(page, 'Chiara');
|
||||
await page.goto(DOC);
|
||||
await expect(page.getByRole('heading', { level: 1 })).toHaveText("Acte d'engagement Certification");
|
||||
// Nothing in force (Yvv, 2026-11-02): said once, above the orders of the sections.
|
||||
await expect(page.locator('section.text article.unit')).toHaveCount(0);
|
||||
await expect(page.getByText("Aucun article n'est en vigueur. Les propositions ci-dessous remontent dès qu'elles atteignent leur seuil.")).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Propositions en cours' })).toBeVisible();
|
||||
// One line per article: the 33, E3 and E7 holding their two variants inside.
|
||||
await expect(page.locator('ul.deep button.row')).toHaveCount(33);
|
||||
// The three seed proposals, marked, at the low level they start at.
|
||||
const seeds = page.locator('ul.deep button.row', { has: page.locator('.tag', { hasText: 'Proposition seed' }) });
|
||||
await expect(seeds.locator('.title')).toHaveText(["Raison d'être", 'Unicité du compte', 'Réciprocité']);
|
||||
// Nothing in force (Yvv, 2026-11-02): the page says where to begin, once — the seed proposals,
|
||||
// read whole at the head of the text (lot U), each marked: never the bare page of a text in force.
|
||||
await expect(page.getByText("Aucun article n'est encore en vigueur. Commence par les Propositions seed : elles y entrent par le vote, comme toute proposition.")).toBeVisible();
|
||||
const seeds = page.locator('section.text article.unit');
|
||||
await expect(seeds.locator('h2')).toHaveText(["Raison d'être", 'Unicité du compte', 'Réciprocité']);
|
||||
await expect(seeds.locator('.tag', { hasText: 'Proposition seed' })).toHaveCount(3);
|
||||
await expect(seeds.getByTitle("Texte de départ mis en avant pour lancer le vote. Il n'est pas en vigueur : il y entre par le vote, comme toute proposition.")).toHaveCount(3);
|
||||
for (const row of await seeds.all()) await expect(row).toContainText('0 pour · 0 contre');
|
||||
for (const row of await seeds.all()) await expect(row).toContainText('inertie 2 / 5 · basse');
|
||||
// At the low level they start at, how far each is from passing.
|
||||
for (const seed of await seeds.all()) await expect(seed).toContainText('0 pour · 0 contre · 9 oui sur 12');
|
||||
for (const seed of await seeds.all()) await expect(seed).toContainText('inertie 2 / 5 · basse');
|
||||
// Below, the open proposals, folded by section; the seed proposals are not said twice.
|
||||
await expect(page.getByRole('heading', { name: 'Propositions en cours' })).toBeVisible();
|
||||
await expect(page.locator('ul.deep li.section')).toHaveCount(8);
|
||||
await expect(page.locator('ul.deep button.row')).toHaveCount(0);
|
||||
await expect(page.locator('ul.deep li.section', { hasText: 'Engagements techniques' })).toContainText('11 articles');
|
||||
await unfold(page);
|
||||
// One line per article: the 33 less the 3 at the head, E3 and E7 holding their two variants inside.
|
||||
await expect(page.locator('ul.deep button.row')).toHaveCount(30);
|
||||
await expect(page.locator('ul.deep .tag', { hasText: 'Proposition seed' })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('C4 — les actions sont contextuelles, le vote se déplie sous l’article', async ({ page }) => {
|
||||
@@ -66,7 +73,8 @@ test('C4 — les actions sont contextuelles, le vote se déplie sous l’article
|
||||
await toggle.click();
|
||||
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
||||
const fold = first.locator('section.fold');
|
||||
await expect(fold.getByText(/^Seuil : \d+ voix pour$/)).toBeVisible();
|
||||
// How many « oui » pass it, at its quorum (ADR 0013): in force at no vote, the quorum of the test group.
|
||||
await expect(fold.getByText(/^\d+ oui sur \d+ votants? pour passer$/)).toBeVisible();
|
||||
await expect(fold.getByText(/^Il manque \d+ voix pour\.$/)).toBeVisible();
|
||||
await fold.getByText('La formule du seuil').click();
|
||||
await expect(fold.getByText(/W — électorat/)).toBeVisible();
|
||||
@@ -93,6 +101,7 @@ test('C5 — une proposition remonte en vigueur quand elle atteint son seuil (gr
|
||||
for (const name of ['Yvv', 'Chiara']) {
|
||||
last = await person(browser, name);
|
||||
await last.goto(TEST_DOC);
|
||||
await unfold(last);
|
||||
await last.locator('ul.deep button.row', { hasText: title }).click();
|
||||
await last.locator('section.fold').getByRole('button', { name: 'Pour', exact: true }).click();
|
||||
await expect(last.getByText('Vote enregistré.')).toBeVisible();
|
||||
@@ -118,7 +127,7 @@ test('C6 — une coquille se corrige sans vote', async ({ page }) => {
|
||||
// What is not the vote lives in the panel, tab « Détails ».
|
||||
await unicity.getByRole('button', { name: 'Coquilles, traductions, objections, historique' }).click();
|
||||
const panel = page.locator('aside.panel');
|
||||
await expect(panel.getByRole('tab', { name: 'Détails' })).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(panel.getByRole('heading', { name: 'Détails', exact: true })).toBeVisible();
|
||||
await panel.getByText('Corriger une coquille').click();
|
||||
const body = panel.getByLabel('Texte', { exact: true });
|
||||
// Typographic apostrophes: the letters and the signs stay, only their style changes.
|
||||
@@ -195,6 +204,7 @@ test('C11 — une personne, une voix : dans Monnaie libre, seuls les membres de
|
||||
await invited.goto(`/invitation#${link.split('#')[1]}`);
|
||||
await expect(invited).toHaveURL((u) => u.pathname === '/');
|
||||
await invited.goto(DOC);
|
||||
await unfold(invited);
|
||||
await invited.locator('ul.deep button.row', { hasText: 'Vérification personnelle de la clé' }).click();
|
||||
const fold = invited.locator('section.fold');
|
||||
// An invitation gives no voice: the voice is that of the Ğ1 identity of the wallet that signs.
|
||||
@@ -209,6 +219,7 @@ test('C11 — une personne, une voix : dans Monnaie libre, seuls les membres de
|
||||
await expect(fold.getByText('0 pour · 0 contre')).toBeVisible();
|
||||
// In the test group, the same person votes, directly.
|
||||
await invited.goto(TEST_DOC);
|
||||
await unfold(invited);
|
||||
await invited.locator('ul.deep button.row', { hasText: 'Vérification personnelle de la clé' }).click();
|
||||
await expect(invited.locator('section.fold').getByRole('button', { name: 'Pour', exact: true })).toBeVisible();
|
||||
await expect(invited.locator('section.fold').getByText('Ta voix compte', { exact: false })).toHaveCount(0);
|
||||
@@ -268,6 +279,7 @@ test('C18 — la Raison d’être ouvre le texte, en trois langues', async ({ pa
|
||||
test('C19 — un article, une ligne : ses versions se déplient', async ({ page }) => {
|
||||
await signIn(page, 'Chiara');
|
||||
await page.goto(DOC);
|
||||
await unfold(page);
|
||||
const e3 = page.locator('ul.deep button.row', { hasText: 'Connaissance suffisante' });
|
||||
await expect(e3).toHaveCount(1);
|
||||
await expect(e3).toContainText('2 versions');
|
||||
@@ -284,7 +296,7 @@ test('C19 — un article, une ligne : ses versions se déplient', async ({ page
|
||||
await versions.nth(1).click();
|
||||
await expect(versions.nth(1)).toHaveAttribute('aria-expanded', 'true');
|
||||
// Level 3, the quorum 50 (ADR 0013): 39 « pour » at no vote, the voters missing counted as « non ».
|
||||
await expect(page.locator('section.fold').getByText('Seuil : 39 voix pour')).toBeVisible();
|
||||
await expect(page.locator('section.fold').getByText('39 oui sur 50 votants pour passer', { exact: true })).toBeVisible();
|
||||
await expect(page).toHaveURL(/vote=/);
|
||||
await e3.click();
|
||||
await expect(e3).toHaveAttribute('aria-expanded', 'false');
|
||||
@@ -318,8 +330,10 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => {
|
||||
await expect(panel.getByText(/^Quorum : en dessous, les votants qui manquent comptent comme « non »\./)).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// From the vote of an article: Unicité du compte, a seed proposal, is at 2 / 5, its row is marked.
|
||||
await deepRow(page, 'Unicité du compte').click();
|
||||
// From the vote of an article: Unicité du compte, a seed proposal at the head, is at 2 / 5, its row is marked.
|
||||
const unicity = page.locator('section.text article.unit', { hasText: 'Unicité du compte' });
|
||||
await unicity.hover();
|
||||
await unicity.getByRole('button', { name: 'Le vote' }).click();
|
||||
const e1 = page.getByRole('region', { name: 'Le vote de « Unicité du compte »' });
|
||||
await expect(e1.getByRole('heading', { name: 'Inertie · 2 / 5 · basse' })).toBeVisible();
|
||||
await e1.getByRole('button', { name: 'Réglages de la formule' }).click();
|
||||
|
||||
+28
-14
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { ALICE, chain, CHARLIE, deepRow, DOC, person, seedsInForce, shot, signed, signIn, TEST_DOC, VOTE_ACCOUNT, wallet } from './helpers';
|
||||
import { ALICE, chain, CHARLIE, deepRow, DOC, person, seedsInForce, shot, signed, signIn, TEST_DOC, unfold, VOTE_ACCOUNT, wallet } from './helpers';
|
||||
|
||||
// Lot B — the vote (Yvv, 2026-10-30): unfolded under the article, grouped in a
|
||||
// basket, and on the Ğ1 chain where the electorate is the web of trust. The chain
|
||||
@@ -21,7 +21,7 @@ test('C21 — le vote déplié sous l’article : ton vote, les totaux, le seuil
|
||||
await expect(toggle).toHaveAttribute('aria-controls', (await fold.getAttribute('id')) ?? '');
|
||||
// How far the threshold is: the water, the totals, the votes asked and those missing.
|
||||
await expect(fold.getByRole('img', { name: /Niveau : \d+ % du seuil/ })).toBeVisible();
|
||||
await expect(fold.getByText(/^Seuil : \d+ voix pour$/)).toBeVisible();
|
||||
await expect(fold.getByText(/^\d+ oui sur \d+ votants? pour passer$/)).toBeVisible();
|
||||
await expect(fold.getByText(/^Il manque \d+ voix pour\.$/)).toBeVisible();
|
||||
await expect(fold.getByText("Tu n'as pas voté sur cette version.")).toBeVisible();
|
||||
await expect(fold.getByText('0 pour · 0 contre')).toBeVisible();
|
||||
@@ -119,8 +119,9 @@ test('C23 — le vote par la chaîne Ğ1 : un geste, confirmé par l’indexeur
|
||||
await expect(panel.getByText("Rien dans le panier pour l'instant.")).toBeVisible();
|
||||
await panel.getByRole('button', { name: 'Fermer le panneau' }).click();
|
||||
|
||||
// Under the seed proposal: her vote, from the chain, and the totals.
|
||||
const e1 = deepRow(page, 'Unicité du compte');
|
||||
// Under the seed proposal, at the head of the text: her vote, from the chain, and the totals.
|
||||
const seed = (title: string) => page.locator('section.text article.unit', { hasText: title }).getByRole('button', { name: 'Le vote' });
|
||||
const e1 = seed('Unicité du compte');
|
||||
await e1.click();
|
||||
const fold1 = page.getByRole('region', { name: 'Le vote de « Unicité du compte »' });
|
||||
await expect(fold1.locator('.voted')).toHaveText('Ton vote : pour · inscrit sur la chaîne Ğ1');
|
||||
@@ -128,7 +129,7 @@ test('C23 — le vote par la chaîne Ğ1 : un geste, confirmé par l’indexeur
|
||||
|
||||
// The last one stands, in the order of the chain: a « contre » included first, but seen late
|
||||
// by the indexer, never overrides the withdrawal included after it.
|
||||
const e2 = deepRow(page, 'Réciprocité');
|
||||
const e2 = seed('Réciprocité');
|
||||
await e2.click();
|
||||
const fold2 = page.getByRole('region', { name: 'Le vote de « Réciprocité »' });
|
||||
await control('hide');
|
||||
@@ -155,8 +156,8 @@ test('C23 — le vote par la chaîne Ğ1 : un geste, confirmé par l’indexeur
|
||||
// Without a wallet in the browser: Ğecko, a link and a QR code; the page waits for the chain.
|
||||
const other = await person(browser, 'Maëlle');
|
||||
await other.goto(DOC);
|
||||
await deepRow(other, "Raison d'être").click();
|
||||
await other.locator('ul.deep section.fold').getByRole('button', { name: 'Pour', exact: true }).click();
|
||||
await other.locator('section.text article.unit', { hasText: "Raison d'être" }).getByRole('button', { name: 'Le vote' }).click();
|
||||
await other.locator('section.text section.fold').getByRole('button', { name: 'Pour', exact: true }).click();
|
||||
const sheet = other.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' });
|
||||
await expect(sheet.getByText("Aucun portefeuille n'est branché à ce navigateur", { exact: false })).toBeVisible();
|
||||
await sheet.getByRole('button', { name: 'Passer par Ğecko' }).click();
|
||||
@@ -191,13 +192,12 @@ test('C24 — sans compte de vote, le vote par la chaîne est fermé et le dit ;
|
||||
await page.goto(DOC);
|
||||
await expect(page.getByRole('button', { name: /^Voter en lot/ })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: 'Voter cette section' })).toHaveCount(0);
|
||||
await page.locator('ul.deep button.row').first().click();
|
||||
const fold = page.locator('ul.deep section.fold');
|
||||
await page.locator('section.text article.unit').first().getByRole('button', { name: 'Le vote' }).click();
|
||||
const fold = page.locator('section.text section.fold');
|
||||
await expect(fold.getByText("Le vote par la chaîne Ğ1 n'est pas encore ouvert : le compte qui reçoit les votes reste à créer.", { exact: false })).toBeVisible();
|
||||
await expect(fold.getByRole('button', { name: 'Pour', exact: true })).toHaveCount(0);
|
||||
// The totals and the threshold stay in sight.
|
||||
// Raison d'être, level 2, the quorum 12 (ADR 0013).
|
||||
await expect(fold.getByText('Seuil : 9 voix pour')).toBeVisible();
|
||||
// The totals and how far it is stay in sight: Raison d'être, level 2, the quorum 12 (ADR 0013).
|
||||
await expect(fold.getByText('9 oui sur 12 votants pour passer', { exact: true })).toBeVisible();
|
||||
await fold.getByText('Ton vote ne se montre à personne', { exact: false }).scrollIntoViewIfNeeded();
|
||||
await shot(page, '6-vote-ferme');
|
||||
// The basket, opened by its address, says the vote is closed — never « Tes votes comptent dès l'envoi ».
|
||||
@@ -239,11 +239,16 @@ test('C34 — la chaîne fermée, Monnaie libre vote quand même : le vote simul
|
||||
const before = await version(page, title);
|
||||
expect(before.vote).toMatchObject({ way: 'server', open: true, simulated: true });
|
||||
await page.goto(DOC);
|
||||
// The basket is there, without a wallet: the vote leaves by the server.
|
||||
// The basket is there, without a wallet: the vote leaves by the server, marked « vote simulé ».
|
||||
await expect(page.getByRole('button', { name: /^Voter en lot/ })).toBeVisible();
|
||||
await expect(page.locator('header.head .tag', { hasText: 'vote simulé' })).toBeVisible();
|
||||
await unfold(page);
|
||||
await deepRow(page, title).click();
|
||||
const fold = page.getByRole('region', { name: `Le vote de « ${title} »` });
|
||||
await fold.getByRole('button', { name: 'Contre', exact: true }).click();
|
||||
// Said simulated at once, never « enregistré » (review of M6).
|
||||
await expect(page.getByText('Vote simulé.', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('Vote enregistré.')).toHaveCount(0);
|
||||
await expect(page.getByRole('dialog', { name: 'Voter avec ton portefeuille Ğ1' })).toHaveCount(0);
|
||||
await expect(fold.locator('.voted')).toHaveText(/^Ton vote : contre/);
|
||||
await expect(fold.getByText(`0 pour · ${before.p.tally.against + 1} contre`)).toBeVisible();
|
||||
@@ -257,10 +262,19 @@ test('C34 — la chaîne fermée, Monnaie libre vote quand même : le vote simul
|
||||
const after = await version(page, title);
|
||||
expect(after.p.myVoteState).toBe('simulated');
|
||||
expect(after.p.tallySimulated.against).toBe(before.p.tallySimulated.against + 1);
|
||||
// The basket too says « simulé », never « enregistrés » (review of M6): « contre » again, one line.
|
||||
await page.getByRole('button', { name: /^Voter en lot/ }).click();
|
||||
const basket = page.locator('aside.panel');
|
||||
await basket.getByRole('group', { name: `Ton choix pour « ${title} »` }).getByRole('button', { name: 'Contre' }).click();
|
||||
await basket.getByRole('button', { name: 'Voter (1)' }).click();
|
||||
await expect(page.getByText('1 vote simulé.', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('enregistré', { exact: false })).toHaveCount(0);
|
||||
await basket.getByRole('button', { name: 'Fermer le panneau' }).click();
|
||||
// In her ballots: simulated, counted for her session, never a block of the chain.
|
||||
await page.goto('/bulletins');
|
||||
const ballot = page.locator('li.ballot').first();
|
||||
await expect(ballot.locator('.state')).toHaveText("Simulé : compté pour « Sacha » jusqu'à l'ouverture de la chaîne Ğ1");
|
||||
// Until a human act erases the simulated votes — never « until the chain opens » (review of M6).
|
||||
await expect(ballot.locator('.state')).toHaveText('Simulé : compté pour « Sacha » tant que les votes simulés ne sont pas effacés');
|
||||
await expect(ballot.getByText('bloc', { exact: false })).toHaveCount(0);
|
||||
} finally {
|
||||
// The next controls need the chain.
|
||||
|
||||
@@ -250,7 +250,7 @@
|
||||
.toggle {
|
||||
flex: 1 1 16rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 0.25rem 0.7rem;
|
||||
min-height: 3rem;
|
||||
@@ -271,14 +271,18 @@
|
||||
color: var(--ink);
|
||||
}
|
||||
.count {
|
||||
flex: none;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 750;
|
||||
color: var(--water);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.waters {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
.head .chevron {
|
||||
display: inline-flex;
|
||||
@@ -296,7 +300,7 @@
|
||||
font-size: 0.84rem;
|
||||
padding: 0.3rem 0.7rem;
|
||||
min-height: 2.25rem;
|
||||
margin-right: 0.6rem;
|
||||
margin: 0 0.6rem 0 auto;
|
||||
border-radius: var(--r-pill);
|
||||
}
|
||||
.vote-section:hover {
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<div class="bar">
|
||||
<span class="mark" aria-hidden="true"><Icon name="grip" size={16} /></span>
|
||||
<span class="name">{t('order.line')}</span>
|
||||
<Crans level={unit.inertia} />
|
||||
<Crans level={unit.inertia} short />
|
||||
{#if p?.seedMinimal}<span class="faint" title={t('doc.seed.long')}>{i18n.plural('doc.seed.votes', votes)}</span>
|
||||
{:else if p?.enteredAt}<span class="faint">{t('doc.adopted', { date: i18n.date(p.enteredAt) })}</span>{/if}
|
||||
{#if proposed.length && !reordering}
|
||||
@@ -112,11 +112,24 @@
|
||||
min-height: 2rem;
|
||||
border-radius: var(--r-pill);
|
||||
}
|
||||
/* Discreet as under an article: the gestures show when the line is pointed at. */
|
||||
.links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.2rem;
|
||||
margin-left: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
.order-line:hover .links,
|
||||
.order-line:focus-within .links,
|
||||
.order-line.unfolded .links {
|
||||
opacity: 1;
|
||||
}
|
||||
@media (hover: none) {
|
||||
.links {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.links button {
|
||||
border: 0;
|
||||
@@ -141,7 +154,8 @@
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.order-line {
|
||||
.order-line,
|
||||
.links {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,8 +175,10 @@
|
||||
</span>
|
||||
<button type="button" class="item" data-id={id} aria-pressed={chosen === id} disabled={busy} onclick={() => pick(id)} onkeydown={(e) => key(e, id)}>
|
||||
<span class="n" aria-hidden="true">{i + 1}</span>
|
||||
<span class="name">{name}</span>
|
||||
{#if proposed(id)}<span class="tag">{t('order.edit.proposed')}</span>{/if}
|
||||
<span class="what">
|
||||
<span class="name">{name}</span>
|
||||
{#if proposed(id)}<span class="tag">{t('order.edit.proposed')}</span>{/if}
|
||||
</span>
|
||||
</button>
|
||||
<span class="arrows">
|
||||
<button type="button" class="arrow up" aria-label={t('order.edit.up', { title: name })} disabled={i === 0 || busy} onclick={() => shift(id, -1)}><Icon name="chevron" size={16} /></button>
|
||||
@@ -252,9 +254,8 @@
|
||||
.item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.3rem 0.6rem;
|
||||
gap: 0.6rem;
|
||||
min-width: 0;
|
||||
min-height: 2.75rem;
|
||||
padding: 0.45rem 0.3rem;
|
||||
@@ -276,6 +277,13 @@
|
||||
.dragging .n {
|
||||
color: inherit;
|
||||
}
|
||||
.what {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.25rem 0.6rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.name {
|
||||
font-family: var(--font-text);
|
||||
font-weight: 600;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { i18n, t } from '$lib/i18n/index.svelte';
|
||||
import { needOf } from '$lib/labels';
|
||||
import type { Proposal } from '$lib/types';
|
||||
import Crans from './Crans.svelte';
|
||||
import Prose from './Prose.svelte';
|
||||
import WaterLevel from './WaterLevel.svelte';
|
||||
|
||||
@@ -69,7 +70,8 @@
|
||||
<span class="status faint">
|
||||
{#if seed}
|
||||
{@const need = needOf(seed)}
|
||||
{t('tally.short', { for: seed.tally.for, against: seed.tally.against })}{#if need} · <span title={need.title}>{need.short}</span>{/if}
|
||||
{t('tally.short', { for: seed.tally.for, against: seed.tally.against })}{#if need}{' · '}<span title={need.title}>{need.short}</span>{/if}
|
||||
<span class="crans"><Crans level={seed.votedAtInertia} short /></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} · {i18n.plural('doc.others', rivals)}{/if}
|
||||
@@ -124,6 +126,9 @@
|
||||
.replaced {
|
||||
background: var(--moss-wash);
|
||||
}
|
||||
.crans {
|
||||
margin-left: 0.6rem;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -162,8 +162,8 @@ export const en: Messages = {
|
||||
'deep.version': 'version {n}',
|
||||
'deep.lead': 'leading: {label}',
|
||||
'deep.section.none': 'No section',
|
||||
'deep.section.count.one': '{n} article proposed',
|
||||
'deep.section.count.other': '{n} articles proposed',
|
||||
'deep.section.count.one': '{n} article',
|
||||
'deep.section.count.other': '{n} articles',
|
||||
'deep.unfold': 'Unfold all',
|
||||
'deep.fold': 'Fold all',
|
||||
'section.introduction': 'Introduction',
|
||||
|
||||
@@ -162,8 +162,8 @@ export const es: Messages = {
|
||||
'deep.version': 'versión {n}',
|
||||
'deep.lead': 'en cabeza: {label}',
|
||||
'deep.section.none': 'Sin sección',
|
||||
'deep.section.count.one': '{n} artículo propuesto',
|
||||
'deep.section.count.other': '{n} artículos propuestos',
|
||||
'deep.section.count.one': '{n} artículo',
|
||||
'deep.section.count.other': '{n} artículos',
|
||||
'deep.unfold': 'Desplegar todo',
|
||||
'deep.fold': 'Plegar todo',
|
||||
'section.introduction': 'Introducción',
|
||||
|
||||
@@ -166,8 +166,8 @@ export const fr = {
|
||||
'deep.version': 'version {n}',
|
||||
'deep.lead': 'en tête : {label}',
|
||||
'deep.section.none': 'Sans section',
|
||||
'deep.section.count.one': '{n} article proposé',
|
||||
'deep.section.count.other': '{n} articles proposés',
|
||||
'deep.section.count.one': '{n} article',
|
||||
'deep.section.count.other': '{n} articles',
|
||||
'deep.unfold': 'Tout déplier',
|
||||
'deep.fold': 'Tout replier',
|
||||
'section.introduction': 'Introduction',
|
||||
|
||||
@@ -228,26 +228,25 @@
|
||||
· <button class="linkish" type="button" onclick={() => openPanel({ unitId: null, proposalId: null, tab: 'settings' })}>{t('settings.title')}</button>
|
||||
</p>
|
||||
|
||||
{#if canVote}
|
||||
<div class="basket-row">
|
||||
<!-- One line of tools — the basket, the language of the text —, then the readings. -->
|
||||
<div class="tools">
|
||||
{#if canVote}
|
||||
<button class="btn small basket-btn" type="button" onclick={() => openPanel({ unitId: null, proposalId: null, tab: 'basket' })}>
|
||||
{basket.size ? t('doc.basket.count', { n: basket.size }) : t('doc.basket')}
|
||||
</button>
|
||||
{#if view.vote.simulated}<span class="tag" title={t('vote.simulated.note')}>{t('vote.simulated.mark')}</span>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="controls">
|
||||
<div class="seg views" role="radiogroup" aria-label={t('doc.view.label')}>
|
||||
{#each VIEWS as v (v)}
|
||||
<button type="button" role="radio" aria-checked={doc.mode === v} class:on={doc.mode === v} onclick={() => doc.setMode(v)}>{t(`doc.view.${v}`)}</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="seg small" role="radiogroup" aria-label={t('doc.lang.label')}>
|
||||
{/if}
|
||||
<div class="seg small langs" role="radiogroup" aria-label={t('doc.lang.label')}>
|
||||
{#each LANGS as l (l)}
|
||||
<button type="button" role="radio" aria-checked={lang === l} class:on={lang === l} onclick={() => { doc.original = l === source && i18n.lang !== source; if (l !== source) i18n.set(l); }}>{l.toUpperCase()}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="seg views" role="radiogroup" aria-label={t('doc.view.label')}>
|
||||
{#each VIEWS as v (v)}
|
||||
<button type="button" role="radio" aria-checked={doc.mode === v} class:on={doc.mode === v} onclick={() => doc.setMode(v)}>{t(`doc.view.${v}`)}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if lang !== source}
|
||||
<p class="note faint">{t('doc.lang.translation')} <button class="linkish" type="button" onclick={() => (doc.original = true)}>{t('doc.lang.original')}</button></p>
|
||||
{/if}
|
||||
@@ -402,22 +401,18 @@
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6rem;
|
||||
justify-content: space-between;
|
||||
margin-top: 1.4rem;
|
||||
}
|
||||
.basket-row {
|
||||
.tools {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
.basket-row + .controls {
|
||||
margin-top: 0.8rem;
|
||||
.langs {
|
||||
margin-left: auto;
|
||||
}
|
||||
.views {
|
||||
margin-top: 0.7rem;
|
||||
}
|
||||
.basket-btn {
|
||||
background: var(--water);
|
||||
@@ -471,7 +466,8 @@
|
||||
margin-top: 2.2rem;
|
||||
}
|
||||
.lead {
|
||||
margin-bottom: 1.4rem;
|
||||
margin-bottom: 1.2rem;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 650;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user