Files
decision/web/src/lib/components/InertiaPicker.svelte
T
YvvandClaude Opus 5.5 db64405e1e 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>
2026-10-30 16:42:11 +01:00

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>