forked from yvv/decision
- types.ts : evaluation.quorum / yesNeeded / noNeeded, levels[].quorum, l'unité quorum / quorumDefault / canSetQuorum, doc.quorums (optionnels) ; publication « suspended » (ADR 0011) ; - Réglages de la formule : la colonne « Quorum » des cinq crans et sa note, la participation de référence « deux fois 50, le quorum de référence » ; plus de ligne C ; sur téléphone, les six colonnes tiennent dans la feuille ; - La formule du seuil : T' = max(T, Q), R = B^W + ratio(T')·T', la ligne Q (le quorum de la version) au lieu de C ; - textes : deep.note, formula.reading, settings.* parlent du quorum, plus de C (test vu rouge puis vert, tests/unit/inertia.test.ts) ; - e2e : Seuil 9 (cran 2) et 39 (cran 3) dans Monnaie libre, les quorums 12 · 12 · 50 · 50 · 50, voix pour à 100 votants 60 · 67 · 75 · 83 · 90, à 0 votant 8 · 9 · 39 · 43 · 46. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
212 lines
6.4 KiB
Svelte
212 lines
6.4 KiB
Svelte
<script lang="ts">
|
|
// The settings of the formula, read only: W and its source, B, the usual
|
|
// turnout of the group (median or reference) and the five levels on its
|
|
// scale, each with its quorum (ADR 0013). From the head of the document, the
|
|
// *for* votes asked are those of the usual turnout; from a version, those of
|
|
// its own turnout, on its article's quorum.
|
|
import { api } from '$lib/api';
|
|
import type { DocState } from '$lib/doc.svelte';
|
|
import { i18n, t } from '$lib/i18n/index.svelte';
|
|
import { usualShown } from '$lib/participation';
|
|
import type { Inertia, LevelInfo } from '$lib/types';
|
|
|
|
let { doc, proposalId = null }: { doc: DocState; proposalId?: string | null } = $props();
|
|
|
|
interface Version {
|
|
levels: LevelInfo[];
|
|
proposal: { votedAtInertia: Inertia };
|
|
}
|
|
|
|
let version = $state<Version | null>(null);
|
|
$effect(() => {
|
|
const id = proposalId;
|
|
void doc.view;
|
|
version = null;
|
|
if (!id) return;
|
|
api
|
|
.get<Version>(`/proposals/${id}`)
|
|
.then((d) => {
|
|
if (proposalId === id) version = d;
|
|
})
|
|
.catch(() => {
|
|
version = null;
|
|
});
|
|
});
|
|
|
|
const view = $derived(doc.view);
|
|
const p = $derived(view?.doc.participation);
|
|
const shown = $derived(p && view ? usualShown(p, view.doc.electorate, view.doc.voteFloor) : null);
|
|
const levels = $derived(version?.levels ?? view?.levels ?? []);
|
|
const cast = $derived(levels[0]?.cast ?? 0);
|
|
const current = $derived(version?.proposal.votedAtInertia ?? null);
|
|
const pct = (ratio: number) => t('common.pct', { n: i18n.number(Math.round(ratio * 10000) / 100) });
|
|
const whole = (n: number) => t('common.pct', { n: i18n.number(n) });
|
|
const gradient = (g: number) => i18n.number(Math.round(g * 1000) / 1000);
|
|
</script>
|
|
|
|
{#if view && p}
|
|
<section class="settings" aria-labelledby="settings-title">
|
|
<h3 id="settings-title">{t('settings.title')}</h3>
|
|
<p class="muted">{t('settings.intro')}</p>
|
|
|
|
<ul class="values">
|
|
<li>{t('formula.W', { n: i18n.number(view.doc.electorate), source: t(view.doc.electorateSource === 'wot' ? 'formula.W.wot' : 'formula.W.group') })}</li>
|
|
<li>{t('formula.B', { n: i18n.number(view.doc.formula.base) })}</li>
|
|
</ul>
|
|
|
|
<div class="usual">
|
|
<h4>{t('settings.usual')}</h4>
|
|
<p>
|
|
{#if p.source === 'median'}
|
|
{i18n.plural('settings.usual.median', shown?.voters ?? p.voters, { electorate: i18n.number(view.doc.electorate), pct: pct(shown?.ratio ?? p.ratio), sample: p.sample })}
|
|
{:else}
|
|
{i18n.plural('settings.usual.reference', shown?.voters ?? p.voters, { electorate: i18n.number(view.doc.electorate), pct: pct(shown?.ratio ?? p.ratio), q: i18n.number(view.doc.voteFloor), min: p.minSample, sample: p.sample })}
|
|
{/if}
|
|
</p>
|
|
{#if p.bounded}<p class="faint small">{i18n.plural('settings.usual.bounded', p.voters, { lo: pct(p.bounds[0]), hi: pct(p.bounds[1]), pct: pct(p.ratio) })}</p>{/if}
|
|
<p class="faint small">{p.measuredAt ? t('settings.usual.when', { date: i18n.date(p.measuredAt) }) : t('settings.usual.never')}</p>
|
|
</div>
|
|
|
|
<h4>{t('settings.levels')}</h4>
|
|
<div class="scroll">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">{t('settings.col.level')}</th>
|
|
<th scope="col">M</th>
|
|
<th scope="col">G</th>
|
|
<th scope="col">{t('settings.col.share')}</th>
|
|
<th scope="col">{t('settings.col.quorum')}</th>
|
|
<th scope="col">{t('settings.col.required')}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each levels as l (l.level)}
|
|
<tr class:current={l.level === current}>
|
|
<th scope="row">
|
|
<span class="lvl">
|
|
<span class="k">{l.level}</span>
|
|
<span class="name">{t(`inertia.name.${l.level}`)}</span>
|
|
{#if l.level === current}<span class="tag water">{t('settings.current')}</span>{/if}
|
|
</span>
|
|
</th>
|
|
<td>{whole(l.majorityPct)}</td>
|
|
<td>{gradient(l.gradient)}</td>
|
|
<td>{whole(l.sharePct)}</td>
|
|
<td>{l.quorum === undefined ? '—' : i18n.number(l.quorum)}</td>
|
|
<td class="required">{l.required === null ? '—' : i18n.number(l.required)}</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<p class="faint small">{i18n.plural('settings.share.note', p.voters)}</p>
|
|
<p class="faint small">{t('settings.quorum.note')}</p>
|
|
<p class="faint small">{i18n.plural(version ? 'settings.required.version' : 'settings.required.usual', cast)}</p>
|
|
<p class="faint small">{t('settings.reading')}</p>
|
|
</section>
|
|
{/if}
|
|
|
|
<style>
|
|
.settings {
|
|
display: grid;
|
|
gap: 0.8rem;
|
|
min-width: 0;
|
|
}
|
|
h3 {
|
|
font-family: var(--font-text);
|
|
font-weight: 500;
|
|
font-size: 1.35rem;
|
|
}
|
|
h4 {
|
|
margin: 0;
|
|
font-size: 0.92rem;
|
|
font-weight: 800;
|
|
}
|
|
.small {
|
|
font-size: 0.84rem;
|
|
}
|
|
.values {
|
|
margin: 0;
|
|
padding-left: 1.1rem;
|
|
font-size: 0.92rem;
|
|
display: grid;
|
|
gap: 0.15rem;
|
|
}
|
|
.usual {
|
|
display: grid;
|
|
gap: 0.3rem;
|
|
padding: 0.8rem 1rem;
|
|
border-radius: var(--r-card);
|
|
background: var(--water-wash);
|
|
font-size: 0.92rem;
|
|
}
|
|
.scroll {
|
|
overflow-x: auto;
|
|
border-radius: var(--r-input);
|
|
background: var(--surface-2);
|
|
}
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.88rem;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
th,
|
|
td {
|
|
padding: 0.45rem 0.55rem;
|
|
text-align: right;
|
|
white-space: nowrap;
|
|
}
|
|
thead th {
|
|
font-size: 0.74rem;
|
|
font-weight: 800;
|
|
letter-spacing: 0.04em;
|
|
color: var(--ink-soft);
|
|
}
|
|
th[scope='row'],
|
|
thead th:first-child {
|
|
text-align: left;
|
|
}
|
|
th[scope='row'] {
|
|
font-weight: 600;
|
|
}
|
|
.lvl {
|
|
display: inline-flex;
|
|
flex-wrap: wrap;
|
|
align-items: baseline;
|
|
gap: 0.35rem;
|
|
}
|
|
tbody tr:nth-child(odd) {
|
|
background: color-mix(in srgb, var(--surface) 55%, transparent);
|
|
}
|
|
tr.current {
|
|
background: var(--water-wash) !important;
|
|
}
|
|
.k {
|
|
font-weight: 800;
|
|
font-size: 1rem;
|
|
}
|
|
.name {
|
|
color: var(--ink-soft);
|
|
font-size: 0.8rem;
|
|
}
|
|
.required {
|
|
font-weight: 800;
|
|
}
|
|
.tag {
|
|
font-size: 0.68rem;
|
|
}
|
|
/* On a phone, six columns hold in the sheet: the headers and the level's name wrap. */
|
|
@media (max-width: 480px) {
|
|
th,
|
|
td {
|
|
padding: 0.4rem 0.3rem;
|
|
}
|
|
thead th,
|
|
th[scope='row'] {
|
|
white-space: normal;
|
|
}
|
|
}
|
|
</style>
|