Lot U2 : le lecteur — cibles de 44 px sur téléphone dans tout le vote déplié et « Tout déplier » ; les trois lectures passent à la ligne au lieu de cacher « Si tout passe » ; « Proposer cet ordre » éteint dit pourquoi, à côté ; l'inertie dite une fois (ses crans dans « Inertie »), la ligne d'une Proposition seed s'efface sous son vote déplié, les versions déjà listées ne sont pas redites dans « Autres versions » ; l'en-tête compte ce que montre la liste (les Propositions seed à part) ; un changement d'inertie d'un ordre ou du titre a sa pastille sur la ligne du module (fr, en, es)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 11:32:35 +01:00
co-authored by Claude Opus 5.5
parent b0e849b9cb
commit 1fadc1ecb6
15 changed files with 161 additions and 48 deletions
+3 -2
View File
@@ -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();
+7 -4
View File
@@ -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>
+6 -4
View File
@@ -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;
} }
} }
+10 -3
View File
@@ -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>
+18 -4
View File
@@ -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;
+7 -1
View File
@@ -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>
+27 -6
View File
@@ -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 {
+7 -2
View File
@@ -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>
+21 -10
View File
@@ -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;
+22 -5
View File
@@ -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;
+4
View File
@@ -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,6 +209,8 @@ 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 favour needed.', 'tally.missing.one': '{n} more vote in favour needed.',
+4
View File
@@ -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.',
+4
View File
@@ -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.',
@@ -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;