v3 : interface d'une version retenue — « Contre » et « Retirer mon vote », jamais « Pour »

Sous l'article, une version retenue montre « Contre » et le retrait, avec la
phrase qui dit pourquoi. Dans le panier, elle se choisit en « contre » ou en
retrait ; « Tout pour » la saute et le dit, « Tout contre » la prend ; un « pour »
gardé d'avant l'objection quitte le panier (basket.kept). Les phrases qui disaient
« ne se vote plus » disent la règle d'Yvv, en fr, en et es. C28 ajusté.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-02 00:41:50 +01:00
co-authored by Claude Opus 5.5
parent 238c7db93f
commit 5d1f8652fc
11 changed files with 223 additions and 79 deletions
+45 -9
View File
@@ -4,6 +4,10 @@
// whole document — but never an article whose versions compete: there, one
// chooses one's version. Then one gesture sends it all: one write on the
// server, or one transaction on the Ğ1 chain.
//
// A version held back by an objection (ADR 0010) takes against or a withdrawal,
// never a « for » (Yvv, 2026-11-01: « Retirer ou "contre" »): « tout pour »
// skips it, « tout contre » takes it.
import type { Choice, Modules, Proposal, Unit } from './types';
@@ -17,7 +21,10 @@ export interface Slot {
versions: Proposal[];
/** Open changes of its inertia: another question, never filled by a grouped gesture. */
inertia: Proposal[];
/** Versions held back by an objection: shown, not votable until it is lifted (ADR 0010). */
/**
* Versions held back by an objection (ADR 0010): apart from those in competition,
* against or a withdrawal only until it is lifted — never a « for ».
*/
held: Proposal[];
}
@@ -68,24 +75,55 @@ export function sections(units: Unit[], modules: Modules = { title: null, orders
return out;
}
/** « Tout pour » / « tout contre » over some articles: only those with a single version. */
/**
* « Tout pour » / « tout contre » over some articles: only those with a single version.
* A version held back by an objection: « tout contre » takes it, « tout pour » skips it.
*/
export function fill(slots: Slot[], choice: 'for' | 'against', picks: Picks): Picks {
const next = { ...picks };
for (const s of slots) {
const [only] = s.versions;
if (only && s.versions.length === 1) next[only.id] = choice;
if (choice === 'against') for (const p of s.held) next[p.id] = choice;
}
return next;
}
/** The versions held back by an objection among these articles: those « tout pour » skips. */
export function heldIn(slots: Slot[]): Proposal[] {
return slots.flatMap((s) => s.held);
}
/** Articles of several versions where no version is chosen yet: the interface shows them. */
export function waiting(slots: Slot[], picks: Picks): Slot[] {
return slots.filter((s) => s.versions.length > 1 && !s.versions.some((p) => p.id in picks));
}
/**
* The versions one may vote now, and how: `free`, any choice; `held`, held back by an
* objection — against or a withdrawal only.
*/
export type Votable = Map<string, 'free' | 'held'>;
/** The versions one may vote now: those a choice of the basket is sent for. */
export function votableIds(all: Section[]): Set<string> {
return new Set(all.flatMap((s) => s.slots.flatMap((x) => [...x.versions, ...x.inertia].map((p) => p.id))));
export function votableIds(all: Section[]): Votable {
const out: Votable = new Map();
for (const x of all.flatMap((s) => s.slots)) {
for (const p of [...x.versions, ...x.inertia]) out.set(p.id, 'free');
for (const p of x.held) out.set(p.id, 'held');
}
return out;
}
/** May this choice be sent for this version now? Never a « for » on a held one. */
export function admits(known: Votable, id: string, choice: Choice): boolean {
const way = known.get(id);
return way === 'free' || (way === 'held' && choice !== 'for');
}
/** The choices of the basket that may be sent now: a version gone, or a « for » on a held one, is left out. */
export function kept(picks: Picks, known: Votable): Picks {
return Object.fromEntries(Object.entries(picks).filter(([id, c]) => admits(known, id, c)));
}
/** Empties the basket of these articles only. */
@@ -94,11 +132,9 @@ export function clearScope(slots: Slot[], picks: Picks): Picks {
return Object.fromEntries(Object.entries(picks).filter(([id]) => !ids.has(id)));
}
/** What is sent: one line per version still in the document; withdrawing is a null choice. */
export function lines(picks: Picks, known: Set<string>): { proposalId: string; choice: 'for' | 'against' | null }[] {
return Object.entries(picks)
.filter(([id]) => known.has(id))
.map(([proposalId, c]) => ({ proposalId, choice: c === 'none' ? null : c }));
/** What is sent: one line per choice that may be sent now (`kept`); withdrawing is a null choice. */
export function lines(picks: Picks, known: Votable): { proposalId: string; choice: 'for' | 'against' | null }[] {
return Object.entries(kept(picks, known)).map(([proposalId, c]) => ({ proposalId, choice: c === 'none' ? null : c }));
}
export function counts(picks: Picks): { for: number; against: number; none: number; total: number } {
+41 -8
View File
@@ -3,9 +3,11 @@
// section entière, ou un article avec son titre, ou séparément »): choices
// article by article, « tout pour » / « tout contre » for a section or the whole
// document — never for an article whose versions compete —, then one gesture.
// A version held back by an objection is shown, with nothing to choose.
// A version held back by an objection takes against or a withdrawal, never a
// « for » (Yvv, 2026-11-01: « Retirer ou "contre" »): « tout pour » skips it
// and says so, « tout contre » takes it.
import { onMount } from 'svelte';
import { clearScope, counts, fill, lines, sections, votableIds, waiting, type Section, type Slot } from '$lib/basket';
import { clearScope, counts, fill, heldIn, kept, lines, sections, votableIds, waiting, type Section, type Slot } from '$lib/basket';
import type { DocState } from '$lib/doc.svelte';
import { i18n, t } from '$lib/i18n/index.svelte';
import { moduleName, relationOf, sectionLabel, titleOf } from '$lib/labels';
@@ -38,12 +40,14 @@
const slots = $derived(all.flatMap((s) => s.slots));
const known = $derived(votableIds(all));
const toSend = $derived(lines(basket.picks, known));
const tally = $derived(counts(Object.fromEntries(Object.entries(basket.picks).filter(([id]) => known.has(id)))));
const tally = $derived(counts(kept(basket.picks, known)));
const left = $derived(waiting(slots, basket.picks));
const open = $derived(!!access?.open);
let busy = $state(false);
let error = $state('');
let list: HTMLElement | undefined = $state();
/** After « tout pour »: where, and how many versions held back by an objection it left aside. */
let skipped = $state<{ key: string | null; n: number } | null>(null);
onMount(() => {
if (section) list?.querySelector(`[data-section="${CSS.escape(section)}"]`)?.scrollIntoView({ block: 'start' });
@@ -69,6 +73,14 @@
const scope = (key: string | null): Slot[] => (key === '*' ? slots : (all.find((s: Section) => s.key === key)?.slots ?? []));
/** « Tout pour » / « tout contre » / « vider » over a scope; « tout pour » says what it skipped. */
function gesture(key: string | null, what: 'for' | 'against' | 'clear') {
const within = scope(key);
basket.set(what === 'clear' ? clearScope(within, basket.picks) : fill(within, what, basket.picks));
const n = what === 'for' ? heldIn(within).length : 0;
skipped = n ? { key, n } : null;
}
async function send() {
error = '';
if (!toSend.length) return;
@@ -91,12 +103,18 @@
{#snippet bulk(key: string | null, label: string)}
<div class="bulk" role="group" aria-label={label}>
<button class="btn small" type="button" disabled={!open} onclick={() => basket.set(fill(scope(key), 'for', basket.picks))}>{t('basket.for')}</button>
<button class="btn small" type="button" disabled={!open} onclick={() => basket.set(fill(scope(key), 'against', basket.picks))}>{t('basket.against')}</button>
<button class="btn quiet small" type="button" onclick={() => basket.set(clearScope(scope(key), basket.picks))}>{t('basket.clear')}</button>
<button class="btn small" type="button" disabled={!open} onclick={() => gesture(key, 'for')}>{t('basket.for')}</button>
<button class="btn small" type="button" disabled={!open} onclick={() => gesture(key, 'against')}>{t('basket.against')}</button>
<button class="btn quiet small" type="button" onclick={() => gesture(key, 'clear')}>{t('basket.clear')}</button>
</div>
{/snippet}
{#snippet skippedNote(key: string | null)}
{#if skipped && skipped.key === key}
<p class="note" role="status">{i18n.plural('basket.held.skipped', skipped.n)}</p>
{/if}
{/snippet}
<section class="basket" aria-labelledby="basket-title">
<h3 id="basket-title">{t('basket.title')}</h3>
<p class="muted">{t('basket.intro')}</p>
@@ -108,6 +126,7 @@
<span class="eyebrow">{t('basket.doc')}</span>
{@render bulk('*', t('basket.doc'))}
</div>
{@render skippedNote('*')}
{#if left.length}
<p class="note" role="status">{i18n.plural('basket.waiting', left.length)}</p>
{/if}
@@ -121,6 +140,7 @@
<h4 class="eyebrow">{label}</h4>
{@render bulk(s.key, label)}
</header>
{@render skippedNote(s.key)}
{/if}
<ul>
{#each s.slots as slot (slot.unit.id)}
@@ -152,9 +172,20 @@
<div class="version held">
<span class="about">
<span class="label">{labelOf(v, slot)}</span>
<span class="faint small">{t('tally.short', { for: v.tally.for, against: v.tally.against })}</span>
<span class="faint small">
{t('tally.short', { for: v.tally.for, against: v.tally.against })}
{#if v.myVote && !v.myVoteStale} · {t('basket.mine', { choice: t(`vote.choice.${v.myVote}`) })}{/if}
</span>
<span class="held-note">{t('basket.held')}</span>
</span>
<span class="held-note">{t('basket.held')}</span>
<ChoicePicker
held
value={basket.picks[v.id]}
voted={!!v.myVote}
label={t('basket.pick', { title: slot.versions.length || slot.held.length > 1 ? `${title} — ${labelOf(v, slot)}` : title })}
disabled={!open}
onpick={(c) => basket.pick(v.id, c)}
/>
</div>
{/each}
</li>
@@ -297,6 +328,8 @@
color: var(--ink-soft);
}
.held-note {
justify-self: start;
margin-top: 0.2rem;
padding: 0.25rem 0.7rem;
border-radius: var(--r-pill);
background: var(--ochre-wash);
+15 -3
View File
@@ -1,6 +1,7 @@
<script lang="ts">
// One choice of the basket: for, against, or withdraw one's vote (when there is
// one). Pressing the chosen one again takes the version out of the basket.
// one). Pressing the chosen one again takes the version out of the basket. A
// version held back by an objection offers no « for » (Yvv, 2026-11-01).
import { t } from '$lib/i18n/index.svelte';
import type { Choice } from '$lib/types';
@@ -8,13 +9,24 @@
value,
voted,
label,
held = false,
disabled = false,
onpick,
}: { value: Choice | undefined; voted: boolean; label: string; disabled?: boolean; onpick: (c: Choice) => void } = $props();
}: {
value: Choice | undefined;
voted: boolean;
label: string;
/** Held back by an objection: against or a withdrawal only. */
held?: boolean;
disabled?: boolean;
onpick: (c: Choice) => void;
} = $props();
</script>
<span class="picker" role="group" aria-label={label}>
<button type="button" class="for" aria-pressed={value === 'for'} {disabled} onclick={() => onpick('for')}>{t('vote.for')}</button>
{#if !held}
<button type="button" class="for" aria-pressed={value === 'for'} {disabled} onclick={() => onpick('for')}>{t('vote.for')}</button>
{/if}
<button type="button" class="against" aria-pressed={value === 'against'} {disabled} onclick={() => onpick('against')}>{t('vote.against')}</button>
{#if voted || value === 'none'}
<button type="button" class="none" aria-pressed={value === 'none'} {disabled} onclick={() => onpick('none')}>{t('basket.pick.retract')}</button>
+6 -4
View File
@@ -4,7 +4,8 @@
// toggle, pas une fenêtre qui s'ouvre »): the level of the water, the totals —
// never who voted what —, how far the threshold is, your vote and where it
// stands, then the inertia and the formula. A version held back by an
// objection says so, and tells each one what is theirs to do (ADR 0010). The
// objection says so, and tells each one what is theirs to do (ADR 0010); it
// offers « contre » and the withdrawal, never « pour » (Yvv, 2026-11-01). The
// rest of a version (typos, translations, objections, history) lives in the
// panel, tab « Détails ».
import { onMount } from 'svelte';
@@ -200,13 +201,14 @@
{#if !live}
<p class="muted">{t('vote.closed')}</p>
{:else if p.blocked}
<!-- Held back: no new vote, nor a vote taken back, until the objection is lifted. -->
{:else if access && !access.open}
<p class="closed">{t(access.reason === 'chain_off' ? 'vote.chain.off' : 'vote.members')}</p>
{:else if access}
<div class="choice" role="group" aria-label={t('vote.yours')}>
<button class="btn for" class:on={p.myVote === 'for' && voted} aria-pressed={p.myVote === 'for' && voted} disabled={busy} onclick={() => cast('for')}>{t('vote.for')}</button>
<!-- Held back by an objection: against or a withdrawal, never a « for » (Yvv, 2026-11-01). -->
{#if !p.blocked}
<button class="btn for" class:on={p.myVote === 'for' && voted} aria-pressed={p.myVote === 'for' && voted} disabled={busy} onclick={() => cast('for')}>{t('vote.for')}</button>
{/if}
<button class="btn against" class:on={p.myVote === 'against' && voted} aria-pressed={p.myVote === 'against' && voted} disabled={busy} onclick={() => cast('against')}>{t('vote.against')}</button>
{#if p.myVote}<button class="btn quiet small" disabled={busy} onclick={() => cast(null)}>{t('vote.retract')}</button>{/if}
</div>
+12 -10
View File
@@ -142,7 +142,7 @@ export const en: Messages = {
'deep.addition': 'new article',
'deep.removal': 'removal of “{title}”',
'deep.inertia': 'inertia to {level}',
'deep.held': 'not open to vote: objection',
'deep.held': 'held back by an objection',
'deep.mine': 'your proposal',
'deep.stale': 'needs reconfirming',
'deep.wellrated': 'well-rated',
@@ -231,7 +231,7 @@ export const en: Messages = {
'vote.stale': 'The text has changed in substance since you voted: your vote no longer counts until you reconfirm it.',
'vote.reconfirm': 'Reconfirm: {choice}',
'vote.private': 'Nobody sees your vote: only the totals are visible.',
'vote.held': 'An objection is holding this version back: it cannot be voted on until the objection is lifted. Votes already cast stay.',
'vote.held': 'An objection is holding this version back: until it is lifted, nobody can vote for it any more. You can vote against it or take your vote back; once lifted, it enters into force only with the votes still there.',
'vote.held.redit': 'It repeats “{other}”.',
'vote.held.contredit': 'It contradicts “{other}”.',
'vote.held.author.redit': 'For it to be voted on again, change the article it repeats. The person who raised the objection then lifts it or reiterates it; with no answer from them within 3 days, it is lifted.',
@@ -289,7 +289,9 @@ export const en: Messages = {
'basket.sent.other': '{n} votes saved.',
'basket.section': 'Vote on this section',
'basket.mine': 'your vote: {choice}',
'basket.held': 'not open to vote: an objection holds it back',
'basket.held': 'an objection holds it back: against or withdraw, no “for”',
'basket.held.skipped.one': 'You put everything for, except {n} version an objection holds back: on it, you can vote against or take your vote back.',
'basket.held.skipped.other': 'You put everything for, except {n} versions an objection holds back: on them, you can vote against or take your vote back.',
'sign.title': 'Vote with your Ğ1 wallet',
'sign.summary.one': '{n} vote, in one transaction: {amount} Ğ1 to the vote account, with your sealed ballot.',
'sign.summary.other': '{n} votes, in one transaction: {amount} Ğ1 to the vote account, with your sealed ballot.',
@@ -443,12 +445,12 @@ export const en: Messages = {
'objection.reason.redit': 'How these two texts say the same thing',
'objection.reason.contredit': 'How these two texts contradict each other',
'objection.send': 'Send the objection',
'objection.sent': 'Objection saved: the later of the two versions cannot be voted on until it is lifted.',
'objection.note': 'An objection makes the later of the two versions not open to vote until it is lifted — never a text in force. Its author answers: for a repetition, they change the article it repeats; for a contradiction, they reword. You then lift the objection or reiterate it; with no answer from you within 3 days, it is lifted.',
'objection.sent': 'Objection saved: until it is lifted, nobody can vote for the later of the two versions any more.',
'objection.note': 'An objection holds back the later of the two versions until it is lifted — never a text in force: nobody can vote for it any more, anyone can vote against it or take their vote back. Its author answers: for a repetition, they change the article it repeats; for a contradiction, they reword. You then lift the objection or reiterate it; with no answer from you within 3 days, it is lifted.',
'objection.says.redit': '“{held}” repeats “{other}”',
'objection.says.contredit': '“{held}” contradicts “{other}”',
'objection.by': 'objection by {name}',
'objection.held.here': 'this version cannot be voted on',
'objection.held.here': 'nobody can vote for this version any more',
'objection.phase.author': "waiting for the author's answer",
'objection.phase.objector': 'answer received: lifted or reiterated by {date}, otherwise lifted by itself',
'objection.answer.revision': 'answer: the held text was reworded',
@@ -477,9 +479,9 @@ export const en: Messages = {
'messages.open': 'See it in the text',
'messages.topic.objection': 'Objection',
'messages.topic.typo': 'Typo',
'messages.objection_raised.redit': '{name} raises an objection: your version “{held}” repeats “{other}”. It cannot be voted on until the objection is lifted. To answer, change the article it repeats.',
'messages.objection_raised.contredit': '{name} raises an objection: your version “{held}” contradicts “{other}”. It cannot be voted on until the objection is lifted. To answer, reword it.',
'messages.objection_reiterated': '{name} reiterates their objection: “{held}” still cannot be voted on. Answer again.',
'messages.objection_raised.redit': '{name} raises an objection: your version “{held}” repeats “{other}”. Until it is lifted, nobody can vote for it any more. To answer, change the article it repeats.',
'messages.objection_raised.contredit': '{name} raises an objection: your version “{held}” contradicts “{other}”. Until it is lifted, nobody can vote for it any more. To answer, reword it.',
'messages.objection_reiterated': '{name} reiterates their objection: nobody can vote for “{held}” yet. Answer again.',
'messages.objection_answered': 'Your objection on “{held}” has been answered. Lift it if the answer suits you, or reiterate it. With no answer from you, it is lifted.',
'messages.typo_pending.author': '{name} suggests a typo fix in your version “{title}”.',
'messages.typo_pending.admin': '{name} suggests a typo fix in “{title}”, a seed text: an administrator validates it.',
@@ -638,7 +640,7 @@ export const en: Messages = {
'error.not_a_title': 'This module is not the title of the document.',
'error.same_title': 'This is already the title in force.',
'error.section': 'This section does not exist in this document.',
'error.objection': 'An objection is holding this version back: it cannot be voted on until the objection is lifted.',
'error.objection': 'An objection is holding this version back: until it is lifted, you can vote against it or take your vote back, not vote for it.',
'error.kind': 'Say whether this text repeats the other or contradicts it.',
'error.other': 'Choose another article, in force or proposed.',
'error.both_in_force': 'Both texts are in force: an objection always targets a version that is not in force yet.',
+12 -10
View File
@@ -142,7 +142,7 @@ export const es: Messages = {
'deep.addition': 'artículo nuevo',
'deep.removal': 'retirada de «{title}»',
'deep.inertia': 'inercia a {level}',
'deep.held': 'no votable: objeción',
'deep.held': 'retenida por una objeción',
'deep.mine': 'tu propuesta',
'deep.stale': 'por reconfirmar',
'deep.wellrated': 'bien valorada',
@@ -231,7 +231,7 @@ export const es: Messages = {
'vote.stale': 'El texto ha cambiado de fondo desde tu voto: tu voto deja de contar hasta que lo reconfirmes.',
'vote.reconfirm': 'Reconfirmar: {choice}',
'vote.private': 'Tu voto no se muestra a nadie: solo se ven los totales.',
'vote.held': 'Una objeción retiene esta versión: no se vota hasta que se levante. Los votos ya emitidos se mantienen.',
'vote.held': 'Una objeción retiene esta versión: hasta que se levante, nadie puede votar a favor. Puedes votar en contra o retirar tu voto; al levantarse, solo entra en vigor con los votos que sigan ahí.',
'vote.held.redit': 'Repite «{other}».',
'vote.held.contredit': 'Contradice «{other}».',
'vote.held.author.redit': 'Para que vuelva a votarse, modifica el artículo que repite. La persona que hizo la objeción la levanta entonces o la reitera; sin respuesta suya en 3 días, se levanta.',
@@ -289,7 +289,9 @@ export const es: Messages = {
'basket.sent.other': '{n} votos registrados.',
'basket.section': 'Votar esta sección',
'basket.mine': 'tu voto: {choice}',
'basket.held': 'no votable: una objeción la retiene',
'basket.held': 'una objeción la retiene: en contra o retirar, sin «a favor»',
'basket.held.skipped.one': 'Lo pusiste todo a favor, salvo {n} versión que una objeción retiene: en ella, puedes votar en contra o retirar tu voto.',
'basket.held.skipped.other': 'Lo pusiste todo a favor, salvo {n} versiones que una objeción retiene: en ellas, puedes votar en contra o retirar tu voto.',
'sign.title': 'Votar con tu monedero Ğ1',
'sign.summary.one': '{n} voto, en una transacción: {amount} Ğ1 a la cuenta de los votos, con tu papeleta sellada.',
'sign.summary.other': '{n} votos, en una transacción: {amount} Ğ1 a la cuenta de los votos, con tu papeleta sellada.',
@@ -443,12 +445,12 @@ export const es: Messages = {
'objection.reason.redit': 'En qué dicen lo mismo estos dos textos',
'objection.reason.contredit': 'En qué se contradicen estos dos textos',
'objection.send': 'Enviar la objeción',
'objection.sent': 'Objeción registrada: la más reciente de las dos versiones no se vota hasta que se levante.',
'objection.note': 'Una objeción hace no votable la más reciente de las dos versiones hasta que se levante — nunca un texto en vigor. Su autor responde: para una repetición, modifica el artículo que repite; para una contradicción, reformula. Tú levantas entonces la objeción o la reiteras; sin respuesta tuya en 3 días, se levanta.',
'objection.sent': 'Objeción registrada: hasta que se levante, nadie puede votar a favor de la más reciente de las dos versiones.',
'objection.note': 'Una objeción retiene la más reciente de las dos versiones hasta que se levante — nunca un texto en vigor: nadie puede votar a favor, cada cual puede votar en contra o retirar su voto. Su autor responde: para una repetición, modifica el artículo que repite; para una contradicción, reformula. Tú levantas entonces la objeción o la reiteras; sin respuesta tuya en 3 días, se levanta.',
'objection.says.redit': '«{held}» repite «{other}»',
'objection.says.contredit': '«{held}» contradice «{other}»',
'objection.by': 'objeción de {name}',
'objection.held.here': 'esta versión no se vota',
'objection.held.here': 'nadie puede votar a favor de esta versión',
'objection.phase.author': 'en espera de la respuesta del autor',
'objection.phase.objector': 'respuesta recibida: levantada o reiterada antes del {date}, si no se levanta sola',
'objection.answer.revision': 'respuesta: el texto retenido se reformuló',
@@ -477,9 +479,9 @@ export const es: Messages = {
'messages.open': 'Verlo en el texto',
'messages.topic.objection': 'Objeción',
'messages.topic.typo': 'Errata',
'messages.objection_raised.redit': '{name} hace una objeción: tu versión «{held}» repite «{other}». No se vota hasta que se levante. Para responder, modifica el artículo que repite.',
'messages.objection_raised.contredit': '{name} hace una objeción: tu versión «{held}» contradice «{other}». No se vota hasta que se levante. Para responder, reformúlala.',
'messages.objection_reiterated': '{name} reitera su objeción: «{held}» sigue sin votarse. Responde de nuevo.',
'messages.objection_raised.redit': '{name} hace una objeción: tu versión «{held}» repite «{other}». Hasta que se levante, nadie puede votar a favor. Para responder, modifica el artículo que repite.',
'messages.objection_raised.contredit': '{name} hace una objeción: tu versión «{held}» contradice «{other}». Hasta que se levante, nadie puede votar a favor. Para responder, reformúlala.',
'messages.objection_reiterated': '{name} reitera su objeción: nadie puede votar aún a favor de «{held}». Responde de nuevo.',
'messages.objection_answered': 'Tu objeción sobre «{held}» recibió una respuesta. Levántala si te convence, o reitérala. Sin respuesta tuya, se levanta.',
'messages.typo_pending.author': '{name} propone una corrección de errata en tu versión «{title}».',
'messages.typo_pending.admin': '{name} propone una corrección de errata en «{title}», un texto del seed: un administrador la valida.',
@@ -638,7 +640,7 @@ export const es: Messages = {
'error.not_a_title': 'Este módulo no es el título del documento.',
'error.same_title': 'Ya es el título en vigor.',
'error.section': 'Esta sección no existe en este documento.',
'error.objection': 'Una objeción retiene esta versión: no se vota hasta que se levante.',
'error.objection': 'Una objeción retiene esta versión: hasta que se levante, puedes votar en contra o retirar tu voto, no votar a favor.',
'error.kind': 'Di si este texto repite el otro o lo contradice.',
'error.other': 'Elige otro artículo, en vigor o propuesto.',
'error.both_in_force': 'Los dos textos están en vigor: una objeción siempre apunta a una versión que aún no lo está.',
+12 -10
View File
@@ -145,7 +145,7 @@ export const fr = {
'deep.addition': 'nouvel article',
'deep.removal': 'retrait de « {title} »',
'deep.inertia': 'inertie vers {level}',
'deep.held': 'non votable : objection',
'deep.held': 'retenue par une objection',
'deep.mine': 'ta proposition',
'deep.stale': 'à reconfirmer',
'deep.wellrated': 'bien cotée',
@@ -234,7 +234,7 @@ export const fr = {
'vote.stale': "Le texte a changé de fond depuis ton vote : ton vote ne compte plus tant que tu ne l'as pas reconfirmé.",
'vote.reconfirm': 'Reconfirmer : {choice}',
'vote.private': 'Ton vote ne se montre à personne : seuls les totaux sont visibles.',
'vote.held': "Une objection retient cette version : elle ne se vote plus jusqu'à sa levée. Les votes déjà faits restent.",
'vote.held': "Une objection retient cette version : jusqu'à sa levée, personne ne peut plus voter pour. Tu peux voter contre ou retirer ton vote ; à la levée, elle n'entre en vigueur qu'avec les voix encore là.",
'vote.held.redit': 'Elle redit « {other} ».',
'vote.held.contredit': 'Elle contredit « {other} ».',
'vote.held.author.redit': "Pour qu'elle se vote à nouveau, modifie l'article visé. La personne qui a fait l'objection la lève alors, ou la réitère ; sans réponse de sa part sous 3 jours, elle est levée.",
@@ -292,7 +292,9 @@ export const fr = {
'basket.sent.other': '{n} votes enregistrés.',
'basket.section': 'Voter cette section',
'basket.mine': 'ton vote : {choice}',
'basket.held': 'non votable : une objection la retient',
'basket.held': 'une objection la retient : contre ou retrait, pas de « pour »',
'basket.held.skipped.one': "Tu as tout mis pour, sauf {n} version qu'une objection retient : sur elle, tu peux voter contre ou retirer ton vote.",
'basket.held.skipped.other': "Tu as tout mis pour, sauf {n} versions qu'une objection retient : sur elles, tu peux voter contre ou retirer ton vote.",
'sign.title': 'Voter avec ton portefeuille Ğ1',
'sign.summary.one': '{n} vote, en une transaction : {amount} Ğ1 vers le compte des votes, avec ton bulletin scellé.',
'sign.summary.other': '{n} votes, en une transaction : {amount} Ğ1 vers le compte des votes, avec ton bulletin scellé.',
@@ -446,12 +448,12 @@ export const fr = {
'objection.reason.redit': 'En quoi ces deux textes disent la même chose',
'objection.reason.contredit': 'En quoi ces deux textes se contredisent',
'objection.send': "Envoyer l'objection",
'objection.sent': "Objection enregistrée : la dernière arrivée des deux versions ne se vote plus jusqu'à sa levée.",
'objection.note': "Une objection rend la dernière arrivée des deux versions non votable jusqu'à sa levée — jamais un texte en vigueur. Son auteur répond : pour une redite, il modifie l'article visé ; pour une contradiction, il reformule. Tu lèves alors l'objection ou tu la réitères ; sans réponse de ta part sous 3 jours, elle est levée.",
'objection.sent': "Objection enregistrée : jusqu'à sa levée, personne ne peut plus voter pour la dernière arrivée des deux versions.",
'objection.note': "Une objection retient la dernière arrivée des deux versions jusqu'à sa levée — jamais un texte en vigueur : personne ne peut plus voter pour elle, chacun peut voter contre ou retirer son vote. Son auteur répond : pour une redite, il modifie l'article visé ; pour une contradiction, il reformule. Tu lèves alors l'objection ou tu la réitères ; sans réponse de ta part sous 3 jours, elle est levée.",
'objection.says.redit': '« {held} » redit « {other} »',
'objection.says.contredit': '« {held} » contredit « {other} »',
'objection.by': 'objection de {name}',
'objection.held.here': 'cette version ne se vote plus',
'objection.held.here': 'personne ne peut plus voter pour cette version',
'objection.phase.author': "en attente d'une réponse de l'auteur",
'objection.phase.objector': "réponse arrivée : levée ou réitérée d'ici le {date}, sinon levée d'elle-même",
'objection.answer.revision': 'réponse : le texte retenu a été reformulé',
@@ -480,9 +482,9 @@ export const fr = {
'messages.open': 'Voir dans le texte',
'messages.topic.objection': 'Objection',
'messages.topic.typo': 'Coquille',
'messages.objection_raised.redit': "{name} fait une objection : ta version « {held} » redit « {other} ». Elle ne se vote plus jusqu'à sa levée. Pour répondre, modifie l'article visé.",
'messages.objection_raised.contredit': "{name} fait une objection : ta version « {held} » contredit « {other} ». Elle ne se vote plus jusqu'à sa levée. Pour répondre, reformule-la.",
'messages.objection_reiterated': '{name} réitère son objection : « {held} » ne se vote toujours pas. Réponds à nouveau.',
'messages.objection_raised.redit': "{name} fait une objection : ta version « {held} » redit « {other} ». Jusqu'à sa levée, personne ne peut plus voter pour elle. Pour répondre, modifie l'article visé.",
'messages.objection_raised.contredit': "{name} fait une objection : ta version « {held} » contredit « {other} ». Jusqu'à sa levée, personne ne peut plus voter pour elle. Pour répondre, reformule-la.",
'messages.objection_reiterated': '{name} réitère son objection : personne ne peut encore voter pour « {held} ». Réponds à nouveau.',
'messages.objection_answered': 'Ton objection sur « {held} » a reçu une réponse. Lève-la si elle te convient, ou réitère-la. Sans réponse de ta part, elle est levée.',
'messages.typo_pending.author': '{name} propose une correction de coquille dans ta version « {title} ».',
'messages.typo_pending.admin': '{name} propose une correction de coquille dans « {title} », un texte du seed : un administrateur la valide.',
@@ -641,7 +643,7 @@ export const fr = {
'error.not_a_title': "Ce module n'est pas le titre du document.",
'error.same_title': "C'est déjà le titre en vigueur.",
'error.section': "Cette section n'existe pas dans ce document.",
'error.objection': "Une objection retient cette version : elle ne se vote plus jusqu'à sa levée.",
'error.objection': "Une objection retient cette version : jusqu'à sa levée, tu peux voter contre ou retirer ton vote, pas voter pour.",
'error.kind': "Dis si ce texte redit l'autre ou le contredit.",
'error.other': 'Choisis un autre article, en vigueur ou proposé.',
'error.both_in_force': 'Ces deux textes sont en vigueur : une objection vise toujours une version qui ne l’est pas encore.',
+9 -6
View File
@@ -1,15 +1,18 @@
// The basket of the reader: the choices not sent yet, kept for the tab (a reload
// or a look at another article does not lose them), one basket per document.
import type { Picks } from './basket';
import { kept, type Picks, type Votable } from './basket';
import type { Choice } from './types';
export class BasketState {
picks = $state<Picks>({});
#key = '';
/** Takes back the basket of this document, keeping only the versions still open to vote. */
load(docId: string, known: Set<string>) {
/**
* Takes back the basket of this document, keeping only the choices still open: a
* version gone leaves it, and so does a « for » on a version an objection holds back.
*/
load(docId: string, known: Votable) {
const key = `ld:basket:${docId}`;
if (key === this.#key) {
this.#keep(known);
@@ -27,9 +30,9 @@ export class BasketState {
this.#keep(known);
}
#keep(known: Set<string>) {
const kept = Object.fromEntries(Object.entries(this.picks).filter(([id]) => known.has(id)));
if (Object.keys(kept).length !== Object.keys(this.picks).length) this.set(kept);
#keep(known: Votable) {
const still = kept(this.picks, known);
if (Object.keys(still).length !== Object.keys(this.picks).length) this.set(still);
}
set(picks: Picks) {
@@ -29,7 +29,7 @@
void doc.load(group, slug);
});
// The basket of this document, kept for the tab; versions no longer open to vote
// (or held back by an objection) leave it.
// leave it, and so does a « for » on a version held back by an objection.
$effect(() => {
const v = doc.view;
if (!v) return;