v3 : le vote déplié sous l'article, le panier, la signature par le portefeuille Ğ1

- Le vote se déplie sous l'article, façon accordéon, l'URL pour état (?vote=) :
  niveau d'eau, totaux, seuil et voix manquantes, ton vote et où il en est
  (enregistré, inscrit sur la chaîne, en attente, à reconfirmer, non compté),
  inertie et formule. Le reste d'une version (coquilles, traductions,
  signalements, historique) passe dans l'onglet « Détails » du panneau.
- Le panier : article par article, « tout pour / tout contre » par section ou
  pour tout le document, jamais pour un article à plusieurs versions ; un seul
  geste (une écriture, ou une transaction).
- La signature : le portefeuille de la page (injectedWeb3), polkadot-api chargé
  au moment de signer ; sans portefeuille, Ğecko par lien et QR ; la page
  attend la confirmation. Un bulletin que le portefeuille n'a pas signé est
  lâché aussitôt (DELETE /api/ballots/{id}).
- Pastilles d'ambiance : flèches dans le groupe, anneau de focus pointillé à
  la couleur d'accent de la page (3:1 au moins), distinct du choix.
- e2e : chaîne simulée (indexeur et nœud), faux portefeuille ; C21–C24, et
  C4, C5, C6, C11, C17, C19, C20, M1 mis à jour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-30 22:47:26 +01:00
co-authored by Claude Opus 5.5
parent 3541e1b1a6
commit b260596cfd
36 changed files with 4353 additions and 234 deletions
@@ -0,0 +1,73 @@
<script lang="ts">
// One choice of the basket: for, against, or withdraw one's vote (when there is
// one). Pressing the chosen one again takes the version out of the basket.
import { t } from '$lib/i18n/index.svelte';
import type { Choice } from '$lib/types';
let {
value,
voted,
label,
disabled = false,
onpick,
}: { value: Choice | undefined; voted: boolean; label: string; disabled?: boolean; onpick: (c: Choice) => void } = $props();
</script>
<span class="picker" role="group" aria-label={label}>
<button type="button" class="for" aria-pressed={value === 'for'} {disabled} onclick={() => onpick('for')}>{t('vote.for')}</button>
<button type="button" class="against" aria-pressed={value === 'against'} {disabled} onclick={() => onpick('against')}>{t('vote.against')}</button>
{#if voted || value === 'none'}
<button type="button" class="none" aria-pressed={value === 'none'} {disabled} onclick={() => onpick('none')}>{t('basket.pick.retract')}</button>
{/if}
</span>
<style>
.picker {
display: inline-flex;
flex-wrap: wrap;
gap: 0.25rem;
background: var(--surface-2);
padding: 3px;
border-radius: var(--r-pill);
}
button {
border: 0;
background: transparent;
border-radius: var(--r-pill);
padding: 0.3rem 0.75rem;
min-height: 2.25rem;
min-width: 2.75rem;
font-weight: 750;
font-size: 0.84rem;
color: var(--ink-soft);
transition: background-color 0.15s var(--ease), color 0.15s var(--ease);
}
button:hover:not(:disabled) {
color: var(--ink);
background: var(--surface);
}
.for[aria-pressed='true'],
.for[aria-pressed='true']:hover {
background: var(--moss);
color: var(--on-water);
}
.against[aria-pressed='true'],
.against[aria-pressed='true']:hover {
background: var(--ochre);
color: var(--on-water);
}
.none[aria-pressed='true'],
.none[aria-pressed='true']:hover {
background: var(--ink-soft);
color: var(--surface);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media (prefers-reduced-motion: reduce) {
button {
transition: none;
}
}
</style>