forked from yvv/decision
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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user