Lot U : le lecteur refait — l'ordre en une ligne au bout de sa section, poignées sur place (Reorder), le titre et l'ordre votés à leur place ; Propositions seed en tête quand rien n'est en vigueur ; Propositions en cours repliées par section ; panneau sans onglets ; oui pour passer · non pour bloquer ; quorum de l'auteur ; votes simulés dits simulés

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 07:59:24 +01:00
co-authored by Claude Opus 5.5
parent 70f7bb7247
commit 4cf4e47ee9
17 changed files with 1198 additions and 541 deletions
+106 -9
View File
@@ -5,22 +5,26 @@
// 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); 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 ».
// offers « contre » and the withdrawal, never « pour » (Yvv, 2026-11-01). Lot U: how
// 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 ».
import { onMount } from 'svelte';
import { api } from '$lib/api';
import type { DocState } from '$lib/doc.svelte';
import { i18n, t } from '$lib/i18n/index.svelte';
import { inertiaBadge, orderNote, titleOf } from '$lib/labels';
import { needOf, orderNote, titleOf } from '$lib/labels';
import { holding } from '$lib/objections';
import { moved } from '$lib/order';
import { session } from '$lib/session.svelte';
import { toasts } from '$lib/toast.svelte';
import type { Entered, LevelInfo, Objection, Proposal, Unit } from '$lib/types';
import { announce, errorText, g1Amount, voteDirect, type Line } from '$lib/vote';
import Crans from './Crans.svelte';
import Formula from './Formula.svelte';
import InertiaPicker from './InertiaPicker.svelte';
import QuorumSetting from './QuorumSetting.svelte';
import WaterLevel from './WaterLevel.svelte';
let {
@@ -36,7 +40,7 @@
doc: DocState;
proposal: Proposal;
unit: Unit;
/** Unfolds the vote of another version (a change of inertia). */
/** Unfolds the vote of another version, where it reads (another text, a change of inertia). */
onopen: (proposalId: string) => void;
onsettings: (proposalId: string) => void;
ondetails: (proposalId: string) => void;
@@ -72,6 +76,26 @@
const shifted = $derived(order && p.status !== 'in_force' ? moved(unit.members ?? [], order.ids) : new Set<string>());
const source = $derived(view?.doc.sourceLang ?? 'fr');
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). */
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,
),
);
/** What tells another version apart, in a few words. */
function otherLabel(v: Proposal): string {
if (v.kind === 'order') {
if (v.status === 'in_force') return t('module.order.inforce');
return v.author?.name ? t('panel.by', { name: v.author.name }) : t('module.order.proposal');
}
if (v.kind === 'remove') return titleOf(v, unit);
if (v.label) return `${v.title} · ${t('deep.variant', { label: v.label })}`;
if (v.status !== 'in_force' && v.author?.name) return `${v.title} · ${t('panel.by', { name: v.author.name })}`;
return v.title;
}
$effect(() => {
const id = p.id;
@@ -119,8 +143,9 @@
}
busy = true;
try {
await voteDirect(doc, lines);
toasts.show(t(choice ? 'vote.saved' : 'vote.retracted'));
const r = await voteDirect(doc, lines);
// A simulated vote says so, never « enregistré » (ADR 0012).
toasts.show(t(!choice ? 'vote.retracted' : r.simulated ? 'vote.saved.simulated' : 'vote.saved'));
} catch (e) {
error = errorText(e);
} finally {
@@ -135,10 +160,10 @@
<div class="numbers">
<p class="tally">{t('tally.short', { for: p.tally.for, against: p.tally.against })}{#if p.tallySimulated?.for || p.tallySimulated?.against} <span class="faint small">{i18n.plural('vote.tally.simulated', p.tallySimulated.for + p.tallySimulated.against)}</span>{/if}</p>
{#if p.evaluation}
<p class="muted">{t('tally.required', { n: p.evaluation.required })}</p>
{#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>
{/if}
<p class="faint small">{inertiaBadge(p.votedAtInertia)}</p>
<p class="faint small"><Crans level={p.votedAtInertia} /></p>
</div>
</div>
{#if p.blocked}
@@ -223,7 +248,31 @@
{#if error}<p class="warn" role="alert">{error}</p>{/if}
<div class="more">
{#if others.length}
<section class="others" aria-labelledby="others-{p.id}">
<h4 id="others-{p.id}">{t('vote.others')}</h4>
<ul>
{#each others as v (v.id)}
<li>
<button type="button" class="other" onclick={() => onopen(v.id)}>
<WaterLevel tally={v.tally} evaluation={v.evaluation} blocked={v.blocked} />
<span class="about">
<span class="label">{otherLabel(v)}</span>
<span class="faint small">
{t('tally.short', { for: v.tally.for, against: v.tally.against })}
{#if v.status === 'in_force'} · {t('basket.inforce')}{/if}
{#if v.seedProposal} · {t('doc.seedProposal')}{/if}
{#if v.blocked} · {t('deep.held')}{/if}
</span>
</span>
</button>
</li>
{/each}
</ul>
</section>
{/if}
{#if p.kind !== 'inertia'}<InertiaPicker {doc} {unit} {onopen} />{/if}
{#if !unit.kind && unit.canSetQuorum}<QuorumSetting {doc} {unit} />{/if}
{#if detail && view}
<Formula
electorate={detail.electorate}
@@ -269,6 +318,54 @@
.state {
font-weight: 700;
}
.need {
font-weight: 750;
color: var(--ink-soft);
font-variant-numeric: tabular-nums;
text-decoration: underline dotted color-mix(in srgb, var(--ink-faint) 60%, transparent);
text-underline-offset: 4px;
cursor: help;
}
.others {
display: grid;
gap: 0.35rem;
min-width: 0;
}
.others h4 {
margin: 0;
font-size: 0.92rem;
font-weight: 800;
}
.others ul {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.15rem;
}
.other {
display: flex;
gap: 0.75rem;
align-items: center;
width: 100%;
min-height: 2.75rem;
padding: 0.45rem 0.6rem;
border: 0;
border-radius: var(--r-input);
background: transparent;
text-align: left;
}
.other:hover {
background: var(--surface);
}
.about {
display: grid;
min-width: 0;
}
.label {
font-weight: 750;
font-size: 0.92rem;
}
.order {
margin: 0;
padding-left: 1.6rem;