forked from yvv/decision
v3 : inertie à cinq crans — interface (L10)
L'inertie se lit en crans, « 3 / 5 · moyenne » (très basse, basse, moyenne, haute, très haute ; fr, en, es). Le sélecteur offre les cinq crans, l'actuel marqué ; le changement se vote au cran de l'article. L'article dit le passé de son inertie (vote ou réglage de départ). « Réglages de la formule », en lecture seule, depuis l'en-tête du doc et depuis le vote d'un article (onglet « Réglages » du panneau, dans l'URL) : W et sa source, C, B, la participation habituelle (médiane ou référence, sur combien de propositions, quand), et les cinq crans : M, G, part exigée, voix pour exigées à la participation de la version regardée (ou habituelle depuis l'en-tête). Composants à part (InertiaPicker, FormulaSettings), branchés par quelques lignes dans VoteView. La formule dépliée ne fait plus déborder le panneau. e2e : C4 (cran de l'article dans la formule), C20 (réglages et cinq crans), M1 (réglages lisibles sur mobile). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
// unfolds the others, each with its label, its water level and its marks — and
|
||||
// each opens its own vote.
|
||||
import { plain } from '$lib/markdown';
|
||||
import { relationOf, titleOf } from '$lib/labels';
|
||||
import { inertiaBadge, relationOf, titleOf } from '$lib/labels';
|
||||
import { i18n, t, type Key } from '$lib/i18n/index.svelte';
|
||||
import type { Proposal, Unit } from '$lib/types';
|
||||
import { excerpts, type Article } from '$lib/versions';
|
||||
@@ -73,7 +73,7 @@
|
||||
<span class="meta">
|
||||
<span>{t('tally.short', { for: best.tally.for, against: best.tally.against })}</span>
|
||||
{#if relationOf(best, unit)}<span class="faint">{relationOf(best, unit)}</span>{/if}
|
||||
{#if unit.inertia !== 'standard'}<span class="faint">{t(`inertia.${unit.inertia}`)}</span>{/if}
|
||||
{#if unit.inertia !== 3}<span class="faint">{inertiaBadge(unit.inertia)}</span>{/if}
|
||||
{@render marks(best)}
|
||||
</span>
|
||||
</span>
|
||||
@@ -94,7 +94,7 @@
|
||||
{#if a.leads}<span>{t('deep.lead', { label: labelOf(best, a) })}</span>{/if}
|
||||
<span>{t('tally.short', { for: best.tally.for, against: best.tally.against })}</span>
|
||||
{#if relationOf(best, unit)}<span class="faint">{relationOf(best, unit)}</span>{/if}
|
||||
{#if unit.inertia !== 'standard'}<span class="faint">{t(`inertia.${unit.inertia}`)}</span>{/if}
|
||||
{#if unit.inertia !== 3}<span class="faint">{inertiaBadge(unit.inertia)}</span>{/if}
|
||||
{#if best.evaluation?.wellRated}<span class="tag water">{t('deep.wellrated')}</span>{/if}
|
||||
{#if a.versions.some((p) => p.myVoteStale)}<span class="tag ochre">{t('deep.stale')}</span>{/if}
|
||||
{#if a.versions.some((p) => p.mine)}<span class="tag">{t('deep.mine')}</span>{/if}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { t } from '$lib/i18n/index.svelte';
|
||||
import { relationOf, titleOf } from '$lib/labels';
|
||||
import type { Lang } from '$lib/types';
|
||||
import FormulaSettings from './FormulaSettings.svelte';
|
||||
import Icon from './Icon.svelte';
|
||||
import ProposeView from './ProposeView.svelte';
|
||||
import VoteView from './VoteView.svelte';
|
||||
@@ -27,21 +28,30 @@
|
||||
['vote', t('panel.vote'), !!voteId],
|
||||
['proposals', `${t('panel.proposals')}${versions.length > 1 ? ` · ${versions.length}` : ''}`, !!unit],
|
||||
['propose', t('panel.propose'), true],
|
||||
// Reached from the head of the document or from a vote; shown while it is open.
|
||||
['settings', t('panel.settings'), panel.tab === 'settings'],
|
||||
] as const
|
||||
).filter(([, , show]) => show),
|
||||
);
|
||||
let body: HTMLElement | undefined = $state();
|
||||
let tablist: HTMLElement | undefined = $state();
|
||||
|
||||
$effect(() => {
|
||||
void panel.tab;
|
||||
void panel.proposalId;
|
||||
body?.scrollTo({ top: 0 });
|
||||
});
|
||||
|
||||
// On a narrow sheet the tabs scroll: the open one stays in sight.
|
||||
$effect(() => {
|
||||
void panel.tab;
|
||||
tablist?.querySelector<HTMLElement>('[aria-selected="true"]')?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
});
|
||||
</script>
|
||||
|
||||
<aside class="panel card" aria-label={t('panel.vote')}>
|
||||
<div class="bar">
|
||||
<div class="tabs" role="tablist">
|
||||
<div class="tabs" role="tablist" bind:this={tablist}>
|
||||
{#each tabs as [key, label] (key)}
|
||||
<button role="tab" type="button" aria-selected={panel.tab === key} class:on={panel.tab === key} onclick={() => onchange({ ...panel, tab: key, prefill: null })}>{label}</button>
|
||||
{/each}
|
||||
@@ -57,6 +67,7 @@
|
||||
{lang}
|
||||
onopen={(id) => onchange({ ...panel, proposalId: id, tab: 'vote' })}
|
||||
onpropose={(prefill) => onchange({ ...panel, tab: 'propose', prefill, draftId: null })}
|
||||
onsettings={() => onchange({ ...panel, proposalId: voteId, tab: 'settings' })}
|
||||
/>
|
||||
{:else if panel.tab === 'proposals' && unit}
|
||||
<ul class="versions">
|
||||
@@ -82,6 +93,8 @@
|
||||
prefill={panel.prefill ?? null}
|
||||
ondone={(r) => onchange(r.proposalId ? { unitId: doc.proposal(r.proposalId)?.unitId ?? panel.unitId, proposalId: r.proposalId, tab: 'vote' } : { ...panel, tab: 'proposals' })}
|
||||
/>
|
||||
{:else if panel.tab === 'settings'}
|
||||
<FormulaSettings {doc} proposalId={voteId} />
|
||||
{/if}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
// The threshold formula, typeset with native MathML — no KaTeX needed.
|
||||
import { i18n, t } from '$lib/i18n/index.svelte';
|
||||
import type { Evaluation, Inertia, Tally } from '$lib/types';
|
||||
import { inertiaBadge } from '$lib/labels';
|
||||
import type { Evaluation, Inertia, LevelInfo, Tally } from '$lib/types';
|
||||
|
||||
let {
|
||||
electorate,
|
||||
@@ -9,21 +10,24 @@
|
||||
tally,
|
||||
evaluation,
|
||||
inertia,
|
||||
presets,
|
||||
levels,
|
||||
floor,
|
||||
base,
|
||||
onsettings,
|
||||
}: {
|
||||
electorate: number;
|
||||
source: 'wot' | 'group';
|
||||
tally: Tally;
|
||||
evaluation: Evaluation | null;
|
||||
inertia: Inertia;
|
||||
presets: { key: Inertia; majorityPct: number; gradient: number }[];
|
||||
levels: LevelInfo[];
|
||||
floor: number;
|
||||
base: number;
|
||||
/** Opens the settings of the formula (the five levels, the usual turnout). */
|
||||
onsettings?: () => void;
|
||||
} = $props();
|
||||
|
||||
const p = $derived(presets.find((x) => x.key === inertia));
|
||||
const p = $derived(levels.find((x) => x.level === inertia));
|
||||
</script>
|
||||
|
||||
<details class="formula">
|
||||
@@ -41,18 +45,23 @@
|
||||
<li>{t('formula.W', { n: i18n.number(electorate), source: t(source === 'wot' ? 'formula.W.wot' : 'formula.W.group') })}</li>
|
||||
<li>{t('formula.T', { n: tally.for + tally.against })}</li>
|
||||
{#if p}
|
||||
<li>{t('formula.M', { n: p.majorityPct })} — {t(`inertia.${inertia}`)}</li>
|
||||
<li>{t('formula.G', { n: p.gradient })}</li>
|
||||
<li>{t('formula.M', { n: i18n.number(p.majorityPct) })} — {inertiaBadge(inertia)}</li>
|
||||
<li>{t('formula.G', { n: i18n.number(Math.round(p.gradient * 1000) / 1000) })}</li>
|
||||
{/if}
|
||||
<li>{t('formula.B', { n: base })}</li>
|
||||
<li>{t('formula.C', { n: floor })}</li>
|
||||
<li>{t('formula.B', { n: i18n.number(base) })}</li>
|
||||
<li>{t('formula.C', { n: i18n.number(floor) })}</li>
|
||||
{#if evaluation}<li class="r">{t('formula.R', { n: evaluation.required })}</li>{/if}
|
||||
</ul>
|
||||
<p class="faint">{t('formula.reading')}</p>
|
||||
</details>
|
||||
{#if onsettings}
|
||||
<button class="settings-link" type="button" onclick={onsettings}>{t('settings.title')}</button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.formula {
|
||||
/* A grid item may shrink below its widest line: the formula scrolls, the panel does not. */
|
||||
min-width: 0;
|
||||
border-radius: var(--r-input);
|
||||
background: var(--surface-2);
|
||||
padding: 0.7rem 0.9rem;
|
||||
@@ -86,4 +95,15 @@
|
||||
.faint {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.settings-link {
|
||||
justify-self: start;
|
||||
border: 0;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: var(--water);
|
||||
font-weight: 750;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
<script lang="ts">
|
||||
// The settings of the formula, read only: W and its source, C, B, the usual
|
||||
// turnout of the group (median or reference) and the five levels on its
|
||||
// scale. From the head of the document, the *for* votes asked are those of
|
||||
// the usual turnout; from a version, those of its own turnout.
|
||||
import { api } from '$lib/api';
|
||||
import type { DocState } from '$lib/doc.svelte';
|
||||
import { i18n, t } from '$lib/i18n/index.svelte';
|
||||
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 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.C', { n: i18n.number(view.doc.voteFloor) })}</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', p.voters, { electorate: i18n.number(view.doc.electorate), pct: pct(p.ratio), sample: p.sample })}
|
||||
{:else}
|
||||
{i18n.plural('settings.usual.reference', p.voters, { electorate: i18n.number(view.doc.electorate), pct: pct(p.ratio), min: p.minSample, sample: p.sample })}
|
||||
{/if}
|
||||
</p>
|
||||
{#if p.bounded}<p class="faint small">{t('settings.usual.bounded', { lo: pct(p.bounds[0]), hi: pct(p.bounds[1]) })}</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.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 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">{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;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,171 @@
|
||||
<script lang="ts">
|
||||
// The inertia of an article: its level out of five, its past, the changes
|
||||
// under vote, and the choice of a new level. A change of inertia is voted at
|
||||
// the level of the article itself (ADR 0007).
|
||||
import { api, ApiError } from '$lib/api';
|
||||
import type { DocState } from '$lib/doc.svelte';
|
||||
import { i18n, t, type Key } from '$lib/i18n/index.svelte';
|
||||
import { inertiaLevel } from '$lib/labels';
|
||||
import { INERTIA_LEVELS, type Inertia, type Unit } from '$lib/types';
|
||||
|
||||
let { doc, unit, onopen }: { doc: DocState; unit: Unit; onopen: (proposalId: string) => void } = $props();
|
||||
|
||||
let target = $state<Inertia | null>(null);
|
||||
let busy = $state(false);
|
||||
let error = $state('');
|
||||
const open = $derived(unit.proposals.filter((x) => x.kind === 'inertia' && x.status === 'open'));
|
||||
const level = (k: Inertia | null) => (k ? inertiaLevel(k) : '?');
|
||||
|
||||
async function submit() {
|
||||
if (!target) return;
|
||||
busy = true;
|
||||
try {
|
||||
const r = await api.post<{ id: string }>(`/units/${unit.id}/inertia`, { target });
|
||||
target = null;
|
||||
error = '';
|
||||
await doc.reload();
|
||||
onopen(r.id);
|
||||
} catch (e) {
|
||||
error = t(`error.${e instanceof ApiError ? e.code : 'internal'}` as Key);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="inertia" aria-labelledby="inertia-{unit.id}">
|
||||
<h4 id="inertia-{unit.id}">{t('inertia.title')} · <span class="muted">{inertiaLevel(unit.inertia)}</span></h4>
|
||||
<p class="faint small">{t('inertia.explain')}</p>
|
||||
{#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>
|
||||
{/each}
|
||||
{#each open as ip (ip.id)}
|
||||
<button class="link" type="button" onclick={() => onopen(ip.id)}>
|
||||
{t('inertia.change.open', { level: level(ip.inertiaTarget) })} — {t('tally.short', { for: ip.tally.for, against: ip.tally.against })}
|
||||
</button>
|
||||
{/each}
|
||||
<details>
|
||||
<summary>{t('inertia.change')}</summary>
|
||||
<div class="levels" role="radiogroup" aria-label={t('inertia.change.to')}>
|
||||
{#each INERTIA_LEVELS as k (k)}
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={target === k}
|
||||
class:on={target === k}
|
||||
class:current={k === unit.inertia}
|
||||
disabled={k === unit.inertia || busy}
|
||||
onclick={() => (target = k)}
|
||||
>
|
||||
<span class="k">{k}</span>
|
||||
<span class="name">{t(`inertia.name.${k}`)}</span>
|
||||
{#if k === unit.inertia}<span class="cur">{t('inertia.change.current')}</span>{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<button class="btn small" type="button" disabled={!target || busy} onclick={submit}>{t('inertia.change.submit')}</button>
|
||||
<p class="faint small">{t('inertia.change.note', { level: inertiaLevel(unit.inertia) })}</p>
|
||||
</details>
|
||||
{#if error}<p class="warn" role="alert">{error}</p>{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.inertia {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.small {
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
details {
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
details > summary {
|
||||
cursor: pointer;
|
||||
font-weight: 750;
|
||||
color: var(--water);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
details[open] > summary {
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
details > :not(summary) + :not(summary) {
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
.levels {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.levels button {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
align-content: start;
|
||||
gap: 0.1rem;
|
||||
min-height: 4rem;
|
||||
padding: 0.5rem 0.25rem;
|
||||
border: 0;
|
||||
border-radius: var(--r-input);
|
||||
background: var(--surface-2);
|
||||
color: var(--ink-soft);
|
||||
text-align: center;
|
||||
transition: background-color 0.15s var(--ease), color 0.15s var(--ease);
|
||||
}
|
||||
.levels button:hover:not(:disabled) {
|
||||
background: var(--water-wash);
|
||||
color: var(--ink);
|
||||
}
|
||||
.levels button.current {
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
.levels button.on,
|
||||
.levels button.on:hover {
|
||||
background: var(--water);
|
||||
color: var(--on-water);
|
||||
}
|
||||
.k {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.name {
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
}
|
||||
.cur {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 800;
|
||||
color: var(--water);
|
||||
}
|
||||
.btn {
|
||||
justify-self: start;
|
||||
}
|
||||
.link {
|
||||
border: 0;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: var(--water);
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.warn {
|
||||
color: var(--ochre);
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.levels button {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,11 +2,12 @@
|
||||
import { api, ApiError } from '$lib/api';
|
||||
import { shown, type DocState } from '$lib/doc.svelte';
|
||||
import { i18n, t, type Key } from '$lib/i18n/index.svelte';
|
||||
import { titleOf } from '$lib/labels';
|
||||
import { inertiaBadge, titleOf } from '$lib/labels';
|
||||
import { toasts } from '$lib/toast.svelte';
|
||||
import type { DiffPart, Entered, Inertia, Lang, Proposal } from '$lib/types';
|
||||
import type { DiffPart, Entered, Inertia, Lang, LevelInfo, Proposal } from '$lib/types';
|
||||
import DiffText from './DiffText.svelte';
|
||||
import Formula from './Formula.svelte';
|
||||
import InertiaPicker from './InertiaPicker.svelte';
|
||||
import Prose from './Prose.svelte';
|
||||
import WaterLevel from './WaterLevel.svelte';
|
||||
|
||||
@@ -16,12 +17,14 @@
|
||||
lang,
|
||||
onopen,
|
||||
onpropose,
|
||||
onsettings,
|
||||
}: {
|
||||
doc: DocState;
|
||||
proposalId: string;
|
||||
lang: Lang;
|
||||
onopen: (proposalId: string) => void;
|
||||
onpropose: (prefill: { title: string; body: string }) => void;
|
||||
onsettings?: () => void;
|
||||
} = $props();
|
||||
|
||||
interface Detail {
|
||||
@@ -36,13 +39,13 @@
|
||||
}[];
|
||||
electorate: number;
|
||||
voteFloor: number;
|
||||
levels: LevelInfo[];
|
||||
}
|
||||
|
||||
let detail = $state<Detail | null>(null);
|
||||
let busy = $state(false);
|
||||
let showDiff = $state(false);
|
||||
let error = $state('');
|
||||
let inertiaTarget = $state<Inertia | ''>('');
|
||||
let typoOpen = $state(false);
|
||||
let typoTitle = $state('');
|
||||
let typoBody = $state('');
|
||||
@@ -58,7 +61,6 @@
|
||||
const source = $derived(view?.doc.sourceLang ?? 'fr');
|
||||
const text = $derived(p ? shown(p, lang, source) : null);
|
||||
const votable = $derived(p?.status === 'open' || p?.status === 'in_force');
|
||||
const inertiaOpen = $derived((unit?.proposals ?? []).filter((x) => x.kind === 'inertia' && x.status === 'open'));
|
||||
const signalTargets = $derived(
|
||||
(view?.units ?? []).flatMap((u) =>
|
||||
[u.inForce, ...u.proposals]
|
||||
@@ -122,21 +124,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function proposeInertia() {
|
||||
if (!unit || !inertiaTarget) return;
|
||||
busy = true;
|
||||
try {
|
||||
const r = await api.post<{ id: string }>(`/units/${unit.id}/inertia`, { target: inertiaTarget });
|
||||
inertiaTarget = '';
|
||||
await doc.reload();
|
||||
onopen(r.id);
|
||||
} catch (e) {
|
||||
error = message(e);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTypo(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
busy = true;
|
||||
@@ -250,7 +237,7 @@
|
||||
<p class="muted">{t('tally.required', { n: p.evaluation.required })}</p>
|
||||
<p class="state">{p.evaluation.reached ? t('tally.reached') : i18n.plural('tally.missing', p.evaluation.missing)}</p>
|
||||
{/if}
|
||||
<p class="faint small">{t(`inertia.${p.votedAtInertia}`)}</p>
|
||||
<p class="faint small">{inertiaBadge(p.votedAtInertia)}</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if p.blocked}<p class="warn">{t('vote.blocked')}</p>{/if}
|
||||
@@ -271,30 +258,9 @@
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if p.kind !== 'inertia' && unit}
|
||||
<section class="sub">
|
||||
<h4>{t('inertia.title')} · <span class="muted">{t(`inertia.level.${unit.inertia}`)}</span></h4>
|
||||
<p class="faint small">{t('inertia.explain')}</p>
|
||||
{#each inertiaOpen as ip (ip.id)}
|
||||
<button class="link" type="button" onclick={() => onopen(ip.id)}>{t('inertia.change.open', { level: t(`inertia.level.${ip.inertiaTarget ?? 'standard'}`) })} — {t('tally.short', { for: ip.tally.for, against: ip.tally.against })}</button>
|
||||
{/each}
|
||||
<details>
|
||||
<summary>{t('inertia.change')}</summary>
|
||||
<div class="row">
|
||||
<select class="select" bind:value={inertiaTarget} aria-label={t('inertia.change.to')}>
|
||||
<option value="" disabled>{t('inertia.change.to')}</option>
|
||||
{#each ['low', 'standard', 'high', 'max'] as const as level (level)}
|
||||
{#if level !== unit.inertia}<option value={level}>{t(`inertia.level.${level}`)}</option>{/if}
|
||||
{/each}
|
||||
</select>
|
||||
<button class="btn small" type="button" disabled={!inertiaTarget || busy} onclick={proposeInertia}>{t('inertia.change.submit')}</button>
|
||||
</div>
|
||||
<p class="faint small">{t('inertia.change.note', { level: t(`inertia.level.${({ low: 'standard', standard: 'high', high: 'max', max: 'max' } as const)[unit.inertia]}`) })}</p>
|
||||
</details>
|
||||
</section>
|
||||
{/if}
|
||||
{#if p.kind !== 'inertia' && unit}<InertiaPicker {doc} {unit} {onopen} />{/if}
|
||||
|
||||
<Formula electorate={detail.electorate} source={view.doc.electorateSource} tally={p.tally} evaluation={p.evaluation} inertia={p.votedAtInertia} presets={view.inertia} floor={detail.voteFloor} base={view.doc.formula.base} />
|
||||
<Formula electorate={detail.electorate} source={view.doc.electorateSource} tally={p.tally} evaluation={p.evaluation} inertia={p.votedAtInertia} levels={detail.levels} floor={detail.voteFloor} base={view.doc.formula.base} {onsettings} />
|
||||
|
||||
{#if p.kind === 'text' && (p.status === 'in_force' || p.origin === 'seed' || p.mine) && votable}
|
||||
<section class="sub">
|
||||
@@ -474,11 +440,6 @@
|
||||
color: var(--water);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.form {
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
@@ -490,15 +451,6 @@
|
||||
.textarea.short {
|
||||
min-height: 5rem;
|
||||
}
|
||||
.link {
|
||||
border: 0;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: var(--water);
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.history {
|
||||
margin: 0;
|
||||
padding-left: 1rem;
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { GroupView } from './group';
|
||||
import type { DocView, Lang, PreviewUnit, Proposal, Translation, Unit } from './types';
|
||||
|
||||
export type View = 'inforce' | 'wellrated' | 'all';
|
||||
export type Tab = 'vote' | 'proposals' | 'propose';
|
||||
export type Tab = 'vote' | 'proposals' | 'propose' | 'settings';
|
||||
|
||||
export interface PanelState {
|
||||
unitId: string | null;
|
||||
|
||||
+36
-10
@@ -134,7 +134,7 @@ export const en: Messages = {
|
||||
'deep.rival': 'would replace “{title}”',
|
||||
'deep.addition': 'new article',
|
||||
'deep.removal': 'removal of “{title}”',
|
||||
'deep.inertia': 'inertia: {level}',
|
||||
'deep.inertia': 'inertia to {level}',
|
||||
'deep.blocked': 'blocked',
|
||||
'deep.mine': 'your proposal',
|
||||
'deep.stale': 'needs reconfirming',
|
||||
@@ -163,19 +163,19 @@ export const en: Messages = {
|
||||
'tally.missing.other': '{n} more votes in favor needed.',
|
||||
'tally.reached': 'Threshold reached.',
|
||||
'tally.level': 'Level: {pct}% of the threshold',
|
||||
'inertia.name.1': 'very low',
|
||||
'inertia.name.2': 'low',
|
||||
'inertia.name.3': 'medium',
|
||||
'inertia.name.4': 'high',
|
||||
'inertia.name.5': 'very high',
|
||||
'inertia.level': '{k} / 5 · {name}',
|
||||
'inertia.badge': 'inertia {level}',
|
||||
|
||||
'inertia.low': 'low inertia',
|
||||
'inertia.standard': 'standard inertia',
|
||||
'inertia.high': 'high inertia',
|
||||
'inertia.max': 'maximum inertia',
|
||||
'inertia.level.low': 'low',
|
||||
'inertia.level.standard': 'standard',
|
||||
'inertia.level.high': 'high',
|
||||
'inertia.level.max': 'maximum',
|
||||
|
||||
'panel.vote': 'Voting',
|
||||
'panel.proposals': 'Proposals',
|
||||
'panel.propose': 'Propose',
|
||||
'panel.settings': 'Settings',
|
||||
'panel.close': 'Close panel',
|
||||
'panel.inforce': 'In force',
|
||||
'panel.proposal': 'Proposal',
|
||||
@@ -205,9 +205,12 @@ export const en: Messages = {
|
||||
'inertia.explain': 'The higher the inertia, the more votes it takes to change this text when few people vote.',
|
||||
'inertia.change': 'Propose an inertia change',
|
||||
'inertia.change.to': 'New inertia',
|
||||
'inertia.change.note': 'An inertia change is itself voted on with the next inertia up: {level}.',
|
||||
'inertia.change.note': "A change of inertia is itself voted at the article's inertia: {level}.",
|
||||
'inertia.change.submit': 'Submit to vote',
|
||||
'inertia.change.open': 'Inertia change in progress: to {level}',
|
||||
'inertia.change.current': 'current',
|
||||
'inertia.history.vote': 'Went from {from} to {to} on {date}, by a vote.',
|
||||
'inertia.history.amendment': 'Went from {from} to {to} on {date}: starting setting of the text.',
|
||||
|
||||
'formula.title': 'The threshold formula',
|
||||
'formula.intro': 'The number of votes in favor required, R, depends on turnout:',
|
||||
@@ -221,6 +224,29 @@ export const en: Messages = {
|
||||
'formula.C': 'C — minimum votes in favor: {n}',
|
||||
'formula.R': 'R — current threshold: {n} votes in favor',
|
||||
'formula.reading': 'At low turnout, near-unanimity is needed; when everyone votes, majority M is enough. Nothing passes with fewer than C votes in favor.',
|
||||
'common.pct': '{n}%',
|
||||
'settings.title': 'Formula settings',
|
||||
'settings.intro': 'To change a text, the group asks for a number of votes in favour that depends on its electorate, its usual turnout and the inertia of the article.',
|
||||
'settings.usual': 'Usual turnout',
|
||||
'settings.usual.median.one': '{n} voter out of {electorate} ({pct}): the median of the {sample} proposals voted on over the past year.',
|
||||
'settings.usual.median.other': '{n} voters out of {electorate} ({pct}): the median of the {sample} proposals voted on over the past year.',
|
||||
'settings.usual.reference.one': '{n} voter out of {electorate} ({pct}), twice C: the reference turnout, until the group has voted on {min} proposals ({sample} so far).',
|
||||
'settings.usual.reference.other': '{n} voters out of {electorate} ({pct}), twice C: the reference turnout, until the group has voted on {min} proposals ({sample} so far).',
|
||||
'settings.usual.bounded': 'Kept between {lo} and {hi} of the electorate.',
|
||||
'settings.usual.when': 'Measured once a day, last on {date}.',
|
||||
'settings.usual.never': 'Measured once a day.',
|
||||
'settings.levels': 'The five levels',
|
||||
'settings.col.level': 'Level',
|
||||
'settings.col.share': 'Share asked',
|
||||
'settings.col.required': 'Votes for',
|
||||
'settings.current': 'this article',
|
||||
'settings.share.note.one': 'Share asked: the share of votes in favour beyond C when {n} person votes, as usual.',
|
||||
'settings.share.note.other': 'Share asked: the share of votes in favour beyond C when {n} people vote, as usual.',
|
||||
'settings.required.version.one': 'Votes for: those asked at the turnout of this version, {n} voter.',
|
||||
'settings.required.version.other': 'Votes for: those asked at the turnout of this version, {n} voters.',
|
||||
'settings.required.usual.one': 'Votes for: those asked at the usual turnout, {n} voter.',
|
||||
'settings.required.usual.other': 'Votes for: those asked at the usual turnout, {n} voters.',
|
||||
'settings.reading': 'At each level, the group asks for its share of votes in favour beyond C when it votes as usual; the more voters, the closer it comes to M. Nothing passes below C votes in favour.',
|
||||
|
||||
'history.title': 'Corrections',
|
||||
'history.initial': 'initial text',
|
||||
|
||||
+36
-10
@@ -134,7 +134,7 @@ export const es: Messages = {
|
||||
'deep.rival': 'reemplazaría «{title}»',
|
||||
'deep.addition': 'artículo nuevo',
|
||||
'deep.removal': 'retirada de «{title}»',
|
||||
'deep.inertia': 'inercia: {level}',
|
||||
'deep.inertia': 'inercia a {level}',
|
||||
'deep.blocked': 'bloqueada',
|
||||
'deep.mine': 'tu propuesta',
|
||||
'deep.stale': 'por reconfirmar',
|
||||
@@ -163,19 +163,19 @@ export const es: Messages = {
|
||||
'tally.missing.other': 'Faltan {n} votos a favor.',
|
||||
'tally.reached': 'Umbral alcanzado.',
|
||||
'tally.level': 'Nivel: {pct} % del umbral',
|
||||
'inertia.name.1': 'muy baja',
|
||||
'inertia.name.2': 'baja',
|
||||
'inertia.name.3': 'media',
|
||||
'inertia.name.4': 'alta',
|
||||
'inertia.name.5': 'muy alta',
|
||||
'inertia.level': '{k} / 5 · {name}',
|
||||
'inertia.badge': 'inercia {level}',
|
||||
|
||||
'inertia.low': 'inercia baja',
|
||||
'inertia.standard': 'inercia estándar',
|
||||
'inertia.high': 'inercia alta',
|
||||
'inertia.max': 'inercia máxima',
|
||||
'inertia.level.low': 'baja',
|
||||
'inertia.level.standard': 'estándar',
|
||||
'inertia.level.high': 'alta',
|
||||
'inertia.level.max': 'máxima',
|
||||
|
||||
'panel.vote': 'Votación',
|
||||
'panel.proposals': 'Propuestas',
|
||||
'panel.propose': 'Proponer',
|
||||
'panel.settings': 'Ajustes',
|
||||
'panel.close': 'Cerrar el panel',
|
||||
'panel.inforce': 'En vigor',
|
||||
'panel.proposal': 'Propuesta',
|
||||
@@ -205,9 +205,12 @@ export const es: Messages = {
|
||||
'inertia.explain': 'Cuanto más alta es la inercia, más votos hacen falta para cambiar este texto cuando vota poca gente.',
|
||||
'inertia.change': 'Proponer un cambio de inercia',
|
||||
'inertia.change.to': 'Nueva inercia',
|
||||
'inertia.change.note': 'Un cambio de inercia se vota a su vez con la inercia superior: {level}.',
|
||||
'inertia.change.note': 'Un cambio de inercia se vota a su vez con la inercia del artículo: {level}.',
|
||||
'inertia.change.submit': 'Someter a votación',
|
||||
'inertia.change.open': 'Cambio de inercia en curso: a {level}',
|
||||
'inertia.change.current': 'actual',
|
||||
'inertia.history.vote': 'Pasó de {from} a {to} el {date}, por votación.',
|
||||
'inertia.history.amendment': 'Pasó de {from} a {to} el {date}: ajuste inicial del texto.',
|
||||
|
||||
'formula.title': 'La fórmula del umbral',
|
||||
'formula.intro': 'El número de votos a favor exigido, R, depende de la participación:',
|
||||
@@ -221,6 +224,29 @@ export const es: Messages = {
|
||||
'formula.C': 'C — mínimo de votos a favor: {n}',
|
||||
'formula.R': 'R — umbral actual: {n} votos a favor',
|
||||
'formula.reading': 'Con poca participación, hace falta casi la unanimidad; cuando vota todo el mundo, basta la mayoría M. Nada pasa con menos de C votos a favor.',
|
||||
'common.pct': '{n} %',
|
||||
'settings.title': 'Ajustes de la fórmula',
|
||||
'settings.intro': 'Para cambiar un texto, el grupo pide un número de votos a favor que depende de su electorado, de su participación habitual y de la inercia del artículo.',
|
||||
'settings.usual': 'Participación habitual',
|
||||
'settings.usual.median.one': '{n} votante de {electorate} ({pct}): la mediana de las {sample} propuestas votadas en el último año.',
|
||||
'settings.usual.median.other': '{n} votantes de {electorate} ({pct}): la mediana de las {sample} propuestas votadas en el último año.',
|
||||
'settings.usual.reference.one': '{n} votante de {electorate} ({pct}), dos veces C: la participación de referencia, hasta que el grupo haya votado {min} propuestas ({sample} por ahora).',
|
||||
'settings.usual.reference.other': '{n} votantes de {electorate} ({pct}), dos veces C: la participación de referencia, hasta que el grupo haya votado {min} propuestas ({sample} por ahora).',
|
||||
'settings.usual.bounded': 'Mantenida entre el {lo} y el {hi} del electorado.',
|
||||
'settings.usual.when': 'Medida una vez al día, la última el {date}.',
|
||||
'settings.usual.never': 'Medida una vez al día.',
|
||||
'settings.levels': 'Los cinco niveles',
|
||||
'settings.col.level': 'Nivel',
|
||||
'settings.col.share': 'Proporción exigida',
|
||||
'settings.col.required': 'Votos a favor',
|
||||
'settings.current': 'este artículo',
|
||||
'settings.share.note.one': 'Proporción exigida: la parte de votos a favor más allá de C cuando vota {n} persona, como de costumbre.',
|
||||
'settings.share.note.other': 'Proporción exigida: la parte de votos a favor más allá de C cuando votan {n} personas, como de costumbre.',
|
||||
'settings.required.version.one': 'Votos a favor: los exigidos con la participación de esta versión, {n} votante.',
|
||||
'settings.required.version.other': 'Votos a favor: los exigidos con la participación de esta versión, {n} votantes.',
|
||||
'settings.required.usual.one': 'Votos a favor: los exigidos con la participación habitual, {n} votante.',
|
||||
'settings.required.usual.other': 'Votos a favor: los exigidos con la participación habitual, {n} votantes.',
|
||||
'settings.reading': 'En cada nivel, el grupo pide su parte de votos a favor más allá de C cuando vota como de costumbre; cuantos más votantes, más se acerca a M. Nada pasa con menos de C votos a favor.',
|
||||
|
||||
'history.title': 'Correcciones',
|
||||
'history.initial': 'texto inicial',
|
||||
|
||||
+36
-10
@@ -137,7 +137,7 @@ export const fr = {
|
||||
'deep.rival': 'remplacerait « {title} »',
|
||||
'deep.addition': 'nouvel article',
|
||||
'deep.removal': 'retrait de « {title} »',
|
||||
'deep.inertia': 'inertie : {level}',
|
||||
'deep.inertia': 'inertie vers {level}',
|
||||
'deep.blocked': 'bloquée',
|
||||
'deep.mine': 'ta proposition',
|
||||
'deep.stale': 'à reconfirmer',
|
||||
@@ -166,19 +166,19 @@ export const fr = {
|
||||
'tally.missing.other': 'Il manque {n} voix pour.',
|
||||
'tally.reached': 'Seuil atteint.',
|
||||
'tally.level': 'Niveau : {pct} % du seuil',
|
||||
'inertia.name.1': 'très basse',
|
||||
'inertia.name.2': 'basse',
|
||||
'inertia.name.3': 'moyenne',
|
||||
'inertia.name.4': 'haute',
|
||||
'inertia.name.5': 'très haute',
|
||||
'inertia.level': '{k} / 5 · {name}',
|
||||
'inertia.badge': 'inertie {level}',
|
||||
|
||||
'inertia.low': 'inertie faible',
|
||||
'inertia.standard': 'inertie standard',
|
||||
'inertia.high': 'inertie haute',
|
||||
'inertia.max': 'inertie maximale',
|
||||
'inertia.level.low': 'faible',
|
||||
'inertia.level.standard': 'standard',
|
||||
'inertia.level.high': 'haute',
|
||||
'inertia.level.max': 'maximale',
|
||||
|
||||
'panel.vote': 'Le vote',
|
||||
'panel.proposals': 'Propositions',
|
||||
'panel.propose': 'Proposer',
|
||||
'panel.settings': 'Réglages',
|
||||
'panel.close': 'Fermer le panneau',
|
||||
'panel.inforce': 'En vigueur',
|
||||
'panel.proposal': 'Proposition',
|
||||
@@ -208,9 +208,12 @@ export const fr = {
|
||||
'inertia.explain': "Plus l'inertie est haute, plus il faut de voix pour changer ce texte quand peu de gens votent.",
|
||||
'inertia.change': "Proposer un changement d'inertie",
|
||||
'inertia.change.to': 'Nouvelle inertie',
|
||||
'inertia.change.note': "Un changement d'inertie se vote lui-même avec l'inertie au-dessus : {level}.",
|
||||
'inertia.change.note': "Un changement d'inertie se vote lui-même à l'inertie de l'article : {level}.",
|
||||
'inertia.change.submit': 'Soumettre au vote',
|
||||
'inertia.change.open': "Changement d'inertie en cours : vers {level}",
|
||||
'inertia.change.current': 'actuelle',
|
||||
'inertia.history.vote': 'Passée de {from} à {to} le {date}, par un vote.',
|
||||
'inertia.history.amendment': 'Passée de {from} à {to} le {date} : réglage de départ du texte.',
|
||||
|
||||
'formula.title': 'La formule du seuil',
|
||||
'formula.intro': 'Le nombre de voix pour exigé, R, dépend de la participation :',
|
||||
@@ -224,6 +227,29 @@ export const fr = {
|
||||
'formula.C': 'C — minimum de voix pour : {n}',
|
||||
'formula.R': 'R — seuil actuel : {n} voix pour',
|
||||
'formula.reading': "À faible participation, il faut presque l'unanimité ; quand tout le monde vote, la majorité M suffit. Rien ne passe sous C voix pour.",
|
||||
'common.pct': '{n} %',
|
||||
'settings.title': 'Réglages de la formule',
|
||||
'settings.intro': "Pour changer un texte, le groupe demande un nombre de voix pour qui dépend de son électorat, de sa participation habituelle et de l'inertie de l'article.",
|
||||
'settings.usual': 'Participation habituelle',
|
||||
'settings.usual.median.one': '{n} votant sur {electorate} ({pct}) : la médiane des {sample} propositions votées depuis un an.',
|
||||
'settings.usual.median.other': '{n} votants sur {electorate} ({pct}) : la médiane des {sample} propositions votées depuis un an.',
|
||||
'settings.usual.reference.one': "{n} votant sur {electorate} ({pct}), deux fois C : la participation de référence, tant que le groupe n'a pas voté sur {min} propositions ({sample} pour l'instant).",
|
||||
'settings.usual.reference.other': "{n} votants sur {electorate} ({pct}), deux fois C : la participation de référence, tant que le groupe n'a pas voté sur {min} propositions ({sample} pour l'instant).",
|
||||
'settings.usual.bounded': "Ramenée entre {lo} et {hi} de l'électorat.",
|
||||
'settings.usual.when': 'Mesurée une fois par jour, la dernière fois le {date}.',
|
||||
'settings.usual.never': 'Mesurée une fois par jour.',
|
||||
'settings.levels': 'Les cinq crans',
|
||||
'settings.col.level': 'Cran',
|
||||
'settings.col.share': 'Part exigée',
|
||||
'settings.col.required': 'Voix pour',
|
||||
'settings.current': 'cet article',
|
||||
'settings.share.note.one': "Part exigée : la part de « pour » au-delà de C quand {n} personne vote, comme d'habitude.",
|
||||
'settings.share.note.other': "Part exigée : la part de « pour » au-delà de C quand {n} personnes votent, comme d'habitude.",
|
||||
'settings.required.version.one': 'Voix pour : celles exigées à la participation de cette version, {n} votant.',
|
||||
'settings.required.version.other': 'Voix pour : celles exigées à la participation de cette version, {n} votants.',
|
||||
'settings.required.usual.one': 'Voix pour : celles exigées à la participation habituelle, {n} votant.',
|
||||
'settings.required.usual.other': 'Voix pour : celles exigées à la participation habituelle, {n} votants.',
|
||||
'settings.reading': "À chaque cran, le groupe demande sa part de « pour » au-delà de C quand il vote comme d'habitude ; plus il y a de votants, plus elle se rapproche de M. Rien ne passe sous C voix pour.",
|
||||
|
||||
'history.title': 'Corrections',
|
||||
'history.initial': 'texte initial',
|
||||
|
||||
+12
-2
@@ -1,10 +1,20 @@
|
||||
import { t } from './i18n/index.svelte';
|
||||
import type { Proposal, Unit } from './types';
|
||||
import type { Inertia, Proposal, Unit } from './types';
|
||||
|
||||
/** « 3 / 5 · moyenne ». */
|
||||
export function inertiaLevel(k: Inertia): string {
|
||||
return t('inertia.level', { k, name: t(`inertia.name.${k}`) });
|
||||
}
|
||||
|
||||
/** « inertie 3 / 5 · moyenne ». */
|
||||
export function inertiaBadge(k: Inertia): string {
|
||||
return t('inertia.badge', { level: inertiaLevel(k) });
|
||||
}
|
||||
|
||||
/** What a proposal is, in words, whatever its kind. */
|
||||
export function titleOf(p: Proposal, unit: Unit | undefined): string {
|
||||
if (p.kind === 'inertia' && p.inertiaTarget) {
|
||||
return t('deep.inertia', { level: t(`inertia.level.${p.inertiaTarget}`) });
|
||||
return t('deep.inertia', { level: inertiaLevel(p.inertiaTarget) });
|
||||
}
|
||||
if (p.kind === 'remove') return t('deep.removal', { title: unit?.inForce?.title ?? p.title });
|
||||
return p.title;
|
||||
|
||||
+39
-2
@@ -1,6 +1,40 @@
|
||||
export type Lang = 'fr' | 'en' | 'es';
|
||||
export type LangMap = Partial<Record<Lang, string>>;
|
||||
export type Inertia = 'low' | 'standard' | 'high' | 'max';
|
||||
/** The five levels of inertia, 1 (lightest) to 5 (heaviest); 3 by default. */
|
||||
export type Inertia = 1 | 2 | 3 | 4 | 5;
|
||||
export const INERTIA_LEVELS: Inertia[] = [1, 2, 3, 4, 5];
|
||||
|
||||
/** One level on the group's scale: M and G of the formula, s_k, and the *for* votes asked at `cast` votes. */
|
||||
export interface LevelInfo {
|
||||
level: Inertia;
|
||||
majorityPct: number;
|
||||
gradient: number;
|
||||
sharePct: number;
|
||||
cast: number;
|
||||
required: number | null;
|
||||
}
|
||||
|
||||
/** p̃ — the usual turnout of the group, measured once a day (or the reference 2C/W). */
|
||||
export interface ParticipationInfo {
|
||||
ratio: number;
|
||||
source: 'median' | 'reference';
|
||||
sample: number;
|
||||
median: number | null;
|
||||
bounded: boolean;
|
||||
voters: number;
|
||||
minSample: number;
|
||||
windowDays: number;
|
||||
everyHours: number;
|
||||
bounds: [number, number];
|
||||
measuredAt: string | null;
|
||||
}
|
||||
|
||||
export interface InertiaChange {
|
||||
from: Inertia | null;
|
||||
to: Inertia | null;
|
||||
cause: 'vote' | 'amendment';
|
||||
at: string;
|
||||
}
|
||||
|
||||
export interface Me {
|
||||
id: string;
|
||||
@@ -75,6 +109,7 @@ export interface Unit {
|
||||
section: string | null;
|
||||
position: number;
|
||||
inertia: Inertia;
|
||||
inertiaChanges: InertiaChange[];
|
||||
inForce: Proposal | null;
|
||||
proposals: Proposal[];
|
||||
}
|
||||
@@ -103,10 +138,12 @@ export interface DocView {
|
||||
electorate: number;
|
||||
electorateSource: 'wot' | 'group';
|
||||
formula: { base: number };
|
||||
participation: ParticipationInfo;
|
||||
publication: Publication;
|
||||
};
|
||||
units: Unit[];
|
||||
inertia: { key: Inertia; majorityPct: number; gradient: number }[];
|
||||
/** The five levels, with the *for* votes asked at the usual turnout. */
|
||||
levels: LevelInfo[];
|
||||
}
|
||||
|
||||
export type DiffPart = { op: 'same' | 'added' | 'removed'; text: string };
|
||||
|
||||
Reference in New Issue
Block a user