Lot U, travail en cours : icône poignée, crans dessinés (Crans), champs annoncés de S et V, canCast et byChain

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 07:34:49 +01:00
co-authored by Claude Opus 5.5
parent d4531da9be
commit 0a63bd7876
4 changed files with 76 additions and 2 deletions
+42
View File
@@ -0,0 +1,42 @@
<script lang="ts">
// The inertia of a module, drawn: five notches side by side, the level filled
// (ADR 0007, « cinq crans »). The water level of a vote stands upright; the
// weight of a text lies flat — two shapes one does not confuse. The words stay
// beside it: « inertie 3 / 5 · moyenne », or only « moyenne » when `short`
// (a screen reader still hears the whole).
import { t } from '$lib/i18n/index.svelte';
import { inertiaBadge } from '$lib/labels';
import type { Inertia } from '$lib/types';
let { level, short = false }: { level: Inertia; short?: boolean } = $props();
const full = $derived(inertiaBadge(level));
</script>
<span class="crans" title={full}>
<span class="notches" aria-hidden="true">
{#each [1, 2, 3, 4, 5] as k (k)}<span class="notch" class:on={k <= level}></span>{/each}
</span>
{#if short}<span class="visually-hidden">{full}</span><span aria-hidden="true">{t(`inertia.name.${level}`)}</span>{:else}<span>{full}</span>{/if}
</span>
<style>
.crans {
display: inline-flex;
align-items: center;
gap: 0.4rem;
white-space: nowrap;
}
.notches {
display: inline-flex;
gap: 2px;
}
.notch {
width: 5px;
height: 0.62rem;
border-radius: 2px;
background: color-mix(in srgb, var(--ink-faint) 28%, transparent);
}
.notch.on {
background: var(--ink-soft);
}
</style>
+3 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
// A few line icons, drawn here rather than pulled from a library.
let { name, size = 18 }: { name: 'lock' | 'close' | 'menu' | 'moon' | 'sun' | 'chevron' | 'message' | 'key' | 'plus' | 'inbox'; size?: number } = $props();
let { name, size = 18 }: { name: 'lock' | 'close' | 'menu' | 'moon' | 'sun' | 'chevron' | 'message' | 'key' | 'plus' | 'inbox' | 'grip'; size?: number } = $props();
</script>
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
@@ -22,6 +22,8 @@
<circle cx="8" cy="15" r="4" /><path d="M11 12l8-8M16 7l2 2M14 9l2 2" />
{:else if name === 'plus'}
<path d="M12 5v14M5 12h14" />
{:else if name === 'grip'}
<g fill="currentColor" stroke="none"><circle cx="9" cy="6" r="1.6" /><circle cx="15" cy="6" r="1.6" /><circle cx="9" cy="12" r="1.6" /><circle cx="15" cy="12" r="1.6" /><circle cx="9" cy="18" r="1.6" /><circle cx="15" cy="18" r="1.6" /></g>
{:else if name === 'inbox'}
<path d="M4 13l2.2-7.3A1 1 0 0 1 7.2 5h9.6a1 1 0 0 1 1 .7L20 13v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1z" /><path d="M4 13h4.5l1 2h5l1-2H20" />
{/if}
+17
View File
@@ -125,6 +125,18 @@ export interface Proposal {
translations: Partial<Record<Lang, Translation>>;
/** An order version: the order it gives its section now. */
order?: OrderInfo;
/**
* Lot S: a « Proposition seed », proposed at the start to set the vote going — not in
* force. Absent before lot S: read as false.
*/
seedProposal?: boolean;
/**
* Lot V: where this session's vote stands — recorded, waiting (the chain, the buffer of
* intentions), or simulated while the chain is closed. Absent before lot V.
*/
myVoteState?: 'recorded' | 'pending' | 'simulated' | null;
/** Lot V: votes cast and not counted yet (the buffer of intentions). */
tallyPending?: Tally;
}
export type Choice = 'for' | 'against' | 'none';
@@ -156,6 +168,11 @@ export interface VoteAccess {
open: boolean;
reason: 'members_only' | 'chain_off' | null;
chain: ChainSettings | null;
/**
* Lot V: the vote is simulated while the chain is closed — the buttons show as if it
* were open, marked « vote simulé ». Absent before lot V.
*/
simulated?: true;
}
/** A ballot of the chain, as `GET /api/ballots/{id}` tells it to the session that asked. */
+14 -1
View File
@@ -6,7 +6,7 @@ import type { DocState } from './doc.svelte';
import { i18n, t, type Key } from './i18n/index.svelte';
import { toasts } from './toast.svelte';
import { moduleName, sectionLabel } from './labels';
import type { Entered } from './types';
import type { Entered, VoteAccess } from './types';
/** A line of a vote: one version, for or against, or null to withdraw one's vote. */
export interface Line {
@@ -14,6 +14,19 @@ export interface Line {
choice: 'for' | 'against' | null;
}
/**
* Whether this session sees the buttons of the vote: open, or simulated while the chain is
* closed (lot V: « les boutons de vote apparaissent partout comme si le vote était ouvert »).
*/
export function canCast(access: VoteAccess | null | undefined): boolean {
return !!access && (access.open || access.simulated === true);
}
/** A vote that goes through the person's Ğ1 wallet; a simulated vote never does. */
export function byChain(access: VoteAccess | null | undefined): boolean {
return access?.way === 'chain' && access.simulated !== true && !!access.chain;
}
/** Cents of Ğ1, as the person reads them: « 0,1 » in French. */
export function g1Amount(cents: number): string {
return i18n.number(cents / 100);