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} - {#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;