From 1fadc1ecb6a2730b8e1024a2569ed02f6332d51a Mon Sep 17 00:00:00 2001 From: Yvv Date: Tue, 3 Nov 2026 11:32:35 +0100 Subject: [PATCH] =?UTF-8?q?Lot=20U2=20:=20le=20lecteur=20=E2=80=94=20cible?= =?UTF-8?q?s=20de=2044=20px=20sur=20t=C3=A9l=C3=A9phone=20dans=20tout=20le?= =?UTF-8?q?=20vote=20d=C3=A9pli=C3=A9=20et=20=C2=AB=20Tout=20d=C3=A9plier?= =?UTF-8?q?=20=C2=BB=20;=20les=20trois=20lectures=20passent=20=C3=A0=20la?= =?UTF-8?q?=20ligne=20au=20lieu=20de=20cacher=20=C2=AB=20Si=20tout=20passe?= =?UTF-8?q?=20=C2=BB=20;=20=C2=AB=20Proposer=20cet=20ordre=20=C2=BB=20?= =?UTF-8?q?=C3=A9teint=20dit=20pourquoi,=20=C3=A0=20c=C3=B4t=C3=A9=20;=20l?= =?UTF-8?q?'inertie=20dite=20une=20fois=20(ses=20crans=20dans=20=C2=AB=20I?= =?UTF-8?q?nertie=20=C2=BB),=20la=20ligne=20d'une=20Proposition=20seed=20s?= =?UTF-8?q?'efface=20sous=20son=20vote=20d=C3=A9pli=C3=A9,=20les=20version?= =?UTF-8?q?s=20d=C3=A9j=C3=A0=20list=C3=A9es=20ne=20sont=20pas=20redites?= =?UTF-8?q?=20dans=20=C2=AB=20Autres=20versions=20=C2=BB=20;=20l'en-t?= =?UTF-8?q?=C3=AAte=20compte=20ce=20que=20montre=20la=20liste=20(les=20Pro?= =?UTF-8?q?positions=20seed=20=C3=A0=20part)=20;=20un=20changement=20d'ine?= =?UTF-8?q?rtie=20d'un=20ordre=20ou=20du=20titre=20a=20sa=20pastille=20sur?= =?UTF-8?q?=20la=20ligne=20du=20module=20(fr,=20en,=20es)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- web/e2e/modules.spec.ts | 5 +-- web/src/lib/components/Crans.svelte | 11 ++++--- web/src/lib/components/DeepList.svelte | 10 +++--- web/src/lib/components/Formula.svelte | 13 ++++++-- web/src/lib/components/InertiaPicker.svelte | 22 ++++++++++--- web/src/lib/components/OrderLine.svelte | 8 ++++- web/src/lib/components/QuorumSetting.svelte | 6 ++++ web/src/lib/components/Reorder.svelte | 33 +++++++++++++++---- web/src/lib/components/TitleBar.svelte | 9 +++-- web/src/lib/components/UnitBlock.svelte | 31 +++++++++++------ web/src/lib/components/VoteFold.svelte | 27 ++++++++++++--- web/src/lib/i18n/en.ts | 4 +++ web/src/lib/i18n/es.ts | 4 +++ web/src/lib/i18n/fr.ts | 4 +++ .../routes/g/[group]/docs/[doc]/+page.svelte | 22 +++++++++---- 15 files changed, 161 insertions(+), 48 deletions(-) diff --git a/web/e2e/modules.spec.ts b/web/e2e/modules.spec.ts index 4c10d14..f21f6a5 100644 --- a/web/e2e/modules.spec.ts +++ b/web/e2e/modules.spec.ts @@ -60,7 +60,8 @@ test('C25 — l’ordre des articles : une ligne en fin de section, réordonnée await fond.hover(); await fond.getByRole('button', { name: 'Le vote' }).click(); 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 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. const vote = bar.locator('section.fold'); 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 shot(page, 'c26-2-titre-vote'); await bar.getByRole('button', { name: 'Le vote' }).click(); diff --git a/web/src/lib/components/Crans.svelte b/web/src/lib/components/Crans.svelte index cc0d1c5..8cce24a 100644 --- a/web/src/lib/components/Crans.svelte +++ b/web/src/lib/components/Crans.svelte @@ -3,12 +3,13 @@ // (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 // 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 { inertiaBadge } from '$lib/labels'; + import { inertiaBadge, inertiaLevel } from '$lib/labels'; 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)); @@ -16,7 +17,9 @@ - {#if short}{full}{:else}{full}{/if} + {#if short}{full} + {:else if bare}{inertiaLevel(level)} + {:else}{full}{/if} diff --git a/web/src/lib/components/InertiaPicker.svelte b/web/src/lib/components/InertiaPicker.svelte index 0f09226..e2c6b34 100644 --- a/web/src/lib/components/InertiaPicker.svelte +++ b/web/src/lib/components/InertiaPicker.svelte @@ -1,12 +1,13 @@
-

{t('inertia.title')} · {inertiaLevel(unit.inertia)}

+

{t('inertia.title')} ·

{t('inertia.explain')}

{#each unit.inertiaChanges as c (c.at + c.to)}

{t(`inertia.history.${c.cause}`, { from: level(c.from), to: level(c.to), date: i18n.date(c.at) })}

@@ -195,6 +196,19 @@ font-weight: 700; 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) { .levels button { transition: none; diff --git a/web/src/lib/components/OrderLine.svelte b/web/src/lib/components/OrderLine.svelte index fcdabfd..d134b0f 100644 --- a/web/src/lib/components/OrderLine.svelte +++ b/web/src/lib/components/OrderLine.svelte @@ -2,7 +2,8 @@ // 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 // 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 // the section, in place. import type { Snippet } from 'svelte'; @@ -37,6 +38,7 @@ const p = $derived(unit.inForce); 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. 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)); @@ -53,6 +55,10 @@ {@const first = proposed[0]} {/if} + {#if inertia.length && !reordering} + {@const first = inertia[0]} + + {/if} {#if !reordering} {#if p}{/if} diff --git a/web/src/lib/components/QuorumSetting.svelte b/web/src/lib/components/QuorumSetting.svelte index 080f78b..b4da047 100644 --- a/web/src/lib/components/QuorumSetting.svelte +++ b/web/src/lib/components/QuorumSetting.svelte @@ -110,4 +110,10 @@ font-weight: 700; 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; + } + } diff --git a/web/src/lib/components/Reorder.svelte b/web/src/lib/components/Reorder.svelte index f202b97..784bb53 100644 --- a/web/src/lib/components/Reorder.svelte +++ b/web/src/lib/components/Reorder.svelte @@ -6,7 +6,8 @@ // 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 // 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 { flip } from 'svelte/animate'; import { api } from '$lib/api'; @@ -141,10 +142,7 @@ } async function submit() { - if (unchanged) { - error = t('order.edit.unchanged'); - return; - } + if (unchanged) return; if (existing) { ondone(existing.id); return; @@ -192,8 +190,15 @@ {#if existing && !unchanged}

{t('order.edit.exists')}

{/if} {#if error}{/if}
+ {#if unchanged}

{t('order.edit.unchanged')}

{/if} - +
@@ -338,15 +343,31 @@ .actions { display: flex; flex-wrap: wrap; + align-items: center; gap: 0.5rem; 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. */ @media (max-width: 899px) { .arrow { width: 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 { from { diff --git a/web/src/lib/components/TitleBar.svelte b/web/src/lib/components/TitleBar.svelte index b7fde60..d04c7eb 100644 --- a/web/src/lib/components/TitleBar.svelte +++ b/web/src/lib/components/TitleBar.svelte @@ -1,8 +1,8 @@ @@ -49,6 +50,10 @@ {@const first = proposed[0]} {/if} + {#if inertia.length} + {@const first = inertia[0]} + + {/if} {#if p}{/if} diff --git a/web/src/lib/components/UnitBlock.svelte b/web/src/lib/components/UnitBlock.svelte index 8e903fb..22da5aa 100644 --- a/web/src/lib/components/UnitBlock.svelte +++ b/web/src/lib/components/UnitBlock.svelte @@ -67,15 +67,19 @@ {#if actions}
- - {#if seed} - {@const need = needOf(seed)} - {t('tally.short', { for: seed.tally.for, against: seed.tally.against })}{#if need}{' · '}{need.short}{/if} - - {:else if seedMinimal}{i18n.plural('doc.seed.votes', votes)} - {:else if enteredAt}{t('doc.adopted', { date: i18n.date(enteredAt) })}{/if} - {#if rivals > 0} · {i18n.plural('doc.others', rivals)}{/if} - + + {#if !(seed && voteOpen)} + + {#if seed} + {@const need = needOf(seed)} + {t('tally.short', { for: seed.tally.for, against: seed.tally.against })}{#if need}{' · '}{need.short}{/if} + + {:else if seedMinimal}{i18n.plural('doc.seed.votes', votes)} + {:else if enteredAt}{t('doc.adopted', { date: i18n.date(enteredAt) })}{/if} + {#if rivals > 0 && !voteOpen}{' · '}{i18n.plural('doc.others', rivals)}{/if} + + {/if} @@ -129,11 +133,13 @@ .crans { 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 { display: flex; flex-wrap: wrap; align-items: center; - justify-content: space-between; + justify-content: flex-start; gap: 0.4rem 1rem; margin-top: 0.7rem; min-height: 2.25rem; @@ -170,6 +176,11 @@ opacity: 1; } } + @media (min-width: 900px) { + .links { + margin-left: auto; + } + } @media (max-width: 899px) { .links button { min-height: 2.75rem; diff --git a/web/src/lib/components/VoteFold.svelte b/web/src/lib/components/VoteFold.svelte index 1deecb0..d8fab1a 100644 --- a/web/src/lib/components/VoteFold.svelte +++ b/web/src/lib/components/VoteFold.svelte @@ -9,7 +9,8 @@ // 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 // 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 { api } from '$lib/api'; import type { DocState } from '$lib/doc.svelte'; @@ -31,6 +32,7 @@ doc, proposal: p, unit, + besides = [], onopen, onsettings, ondetails, @@ -40,6 +42,8 @@ doc: DocState; proposal: Proposal; 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). */ onopen: (proposalId: string) => void; onsettings: (proposalId: string) => void; @@ -80,12 +84,17 @@ 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. */ 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( [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. */ function otherLabel(v: Proposal): string { @@ -165,7 +174,7 @@ {#if need}

{need.long}

{/if}

{p.evaluation.reached ? t('tally.reached') : i18n.plural('tally.missing', p.evaluation.missing)}

{/if} -

+ {#if ownInertia}

{/if} {#if p.blocked} @@ -277,7 +286,7 @@
{/if} - {#if p.kind !== 'inertia'}{/if} + {#if !ownInertia}{/if} {#if !unit.kind && unit.canSetQuorum}{/if} {#if detail && view} u.section))]); const deepSecs = $derived(deepSections(deep, deepOrders, sectionKeys)); 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. */ 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 @@ } -{#snippet foldOf(unit: Unit, p: Proposal)} +{#snippet foldOf(unit: Unit, p: Proposal, besides: string[] = [])} go(panel, id)} onsettings={(id) => openPanel({ unitId: unit.id, proposalId: id, tab: 'settings' })} ondetails={(id) => openPanel({ unitId: unit.id, proposalId: id, tab: 'details' })} @@ -223,7 +229,7 @@ {/if}

{summary}

- {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} ·

@@ -517,13 +523,15 @@ .new { 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) { .seg.views { - flex-wrap: nowrap; - max-width: 100%; - overflow-x: auto; - scrollbar-width: none; + display: flex; + gap: 2px; + } + .seg.views button { + flex: 1 1 auto; } .seg button { min-height: 2.75rem;