forked from yvv/decision
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>
172 lines
4.8 KiB
Svelte
172 lines
4.8 KiB
Svelte
<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>
|