forked from yvv/decision
- Accueil sobre, menu complet à gauche (Décisions, Docs de référence, Mandats, Protocoles), intro demandée, quatre sections, connexion Ğ1. - Lecteur : texte en vigueur en Literata, actions contextuelles par article (le vote, propositions, proposer), propositions en cours sous la « surface » avec leur niveau d'eau, aperçus « si les bien cotées passent » / « si tout passe », langue du texte fr/en/es (traductions éditoriales). - Panneau : vote pour/contre, seuil et formule (MathML), inertie et son changement (voté en inertie +), coquille sans vote, traduction, signalement de contradiction, historique des corrections. - trustinWallet (/acces) : groupes et membres, invitations à usage unique, administrateurs, retours d'usage. - Contrôles : vitest (markdown, catalogues, anti-lexique), Playwright C1–C10 + M1. - Sections du seed portées jusqu'à l'écran. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
210 lines
5.9 KiB
Svelte
210 lines
5.9 KiB
Svelte
<script lang="ts">
|
|
import type { DocState, PanelState } from '$lib/doc.svelte';
|
|
import { t } from '$lib/i18n/index.svelte';
|
|
import { relationOf, titleOf } from '$lib/labels';
|
|
import type { Lang } from '$lib/types';
|
|
import Icon from './Icon.svelte';
|
|
import ProposeView from './ProposeView.svelte';
|
|
import VoteView from './VoteView.svelte';
|
|
import WaterLevel from './WaterLevel.svelte';
|
|
|
|
let {
|
|
doc,
|
|
panel,
|
|
lang,
|
|
onchange,
|
|
onclose,
|
|
}: { doc: DocState; panel: PanelState; lang: Lang; onchange: (p: PanelState) => void; onclose: () => void } = $props();
|
|
|
|
const unit = $derived(doc.unit(panel.unitId));
|
|
const versions = $derived(
|
|
unit ? [unit.inForce, ...unit.proposals.filter((p) => p.status === 'open')].filter((p) => p !== null) : [],
|
|
);
|
|
const voteId = $derived(panel.proposalId ?? unit?.inForce?.id ?? unit?.proposals.find((p) => p.status === 'open')?.id ?? null);
|
|
const tabs = $derived(
|
|
(
|
|
[
|
|
['vote', t('panel.vote'), !!voteId],
|
|
['proposals', `${t('panel.proposals')}${versions.length > 1 ? ` · ${versions.length}` : ''}`, !!unit],
|
|
['propose', t('panel.propose'), true],
|
|
] as const
|
|
).filter(([, , show]) => show),
|
|
);
|
|
let body: HTMLElement | undefined = $state();
|
|
|
|
$effect(() => {
|
|
void panel.tab;
|
|
void panel.proposalId;
|
|
body?.scrollTo({ top: 0 });
|
|
});
|
|
</script>
|
|
|
|
<aside class="panel card" aria-label={t('panel.vote')}>
|
|
<div class="bar">
|
|
<div class="tabs" role="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}
|
|
</div>
|
|
<button class="close" type="button" onclick={onclose} aria-label={t('panel.close')}><Icon name="close" /></button>
|
|
</div>
|
|
|
|
<div class="body" bind:this={body}>
|
|
{#if panel.tab === 'vote' && voteId}
|
|
<VoteView
|
|
{doc}
|
|
proposalId={voteId}
|
|
{lang}
|
|
onopen={(id) => onchange({ ...panel, proposalId: id, tab: 'vote' })}
|
|
onpropose={(prefill) => onchange({ ...panel, tab: 'propose', prefill, draftId: null })}
|
|
/>
|
|
{:else if panel.tab === 'proposals' && unit}
|
|
<ul class="versions">
|
|
{#each versions as v (v.id)}
|
|
<li>
|
|
<button type="button" class="version" class:current={v.id === voteId} onclick={() => onchange({ ...panel, proposalId: v.id, tab: 'vote' })}>
|
|
<WaterLevel tally={v.tally} evaluation={v.evaluation} blocked={v.blocked} />
|
|
<span class="text">
|
|
<span class="eyebrow">{v.status === 'in_force' ? t('panel.inforce') : t('panel.proposal')}</span>
|
|
<span class="title">{titleOf(v, unit)}</span>
|
|
<span class="faint small">{v.status === 'in_force' ? (v.seedMinimal ? t('doc.seed') : '') : relationOf(v, unit)} · {t('tally.short', { for: v.tally.for, against: v.tally.against })}</span>
|
|
</span>
|
|
</button>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
<button class="btn small" type="button" onclick={() => onchange({ ...panel, tab: 'propose', prefill: null, draftId: null })}><Icon name="plus" size={16} />{t('doc.action.propose')}</button>
|
|
{:else if panel.tab === 'propose'}
|
|
<ProposeView
|
|
{doc}
|
|
unitId={panel.unitId}
|
|
draftId={panel.draftId ?? null}
|
|
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' })}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
</aside>
|
|
|
|
<style>
|
|
.panel {
|
|
position: fixed;
|
|
top: 0.8rem;
|
|
right: 0.8rem;
|
|
bottom: 0.8rem;
|
|
width: min(var(--panel), calc(100vw - 1.6rem));
|
|
display: flex;
|
|
flex-direction: column;
|
|
z-index: 35;
|
|
box-shadow: var(--shadow-strong);
|
|
animation: enter 0.24s var(--ease);
|
|
}
|
|
.bar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 0.5rem;
|
|
padding: 0.7rem 0.7rem 0.5rem 0.9rem;
|
|
}
|
|
.tabs {
|
|
display: flex;
|
|
gap: 0.2rem;
|
|
background: var(--surface-2);
|
|
padding: 3px;
|
|
border-radius: var(--r-pill);
|
|
overflow-x: auto;
|
|
}
|
|
.tabs button {
|
|
border: 0;
|
|
background: transparent;
|
|
border-radius: var(--r-pill);
|
|
padding: 0.35rem 0.8rem;
|
|
min-height: 2.1rem;
|
|
font-weight: 750;
|
|
font-size: 0.86rem;
|
|
color: var(--ink-soft);
|
|
white-space: nowrap;
|
|
}
|
|
.tabs button.on {
|
|
background: var(--surface);
|
|
color: var(--ink);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
.close {
|
|
display: grid;
|
|
place-items: center;
|
|
flex: none;
|
|
width: 2.25rem;
|
|
height: 2.25rem;
|
|
border: 0;
|
|
border-radius: var(--r-icon);
|
|
background: transparent;
|
|
color: var(--ink-soft);
|
|
}
|
|
.close:hover {
|
|
background: var(--water-wash);
|
|
}
|
|
.body {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 0.6rem 1.2rem 1.6rem;
|
|
overscroll-behavior: contain;
|
|
}
|
|
.versions {
|
|
list-style: none;
|
|
margin: 0 0 1rem;
|
|
padding: 0;
|
|
display: grid;
|
|
gap: 0.4rem;
|
|
}
|
|
.version {
|
|
display: flex;
|
|
gap: 0.8rem;
|
|
align-items: center;
|
|
width: 100%;
|
|
padding: 0.7rem 0.8rem;
|
|
border: 0;
|
|
border-radius: var(--r-input);
|
|
background: transparent;
|
|
text-align: left;
|
|
}
|
|
.version:hover,
|
|
.version.current {
|
|
background: var(--water-wash);
|
|
}
|
|
.text {
|
|
display: grid;
|
|
gap: 0.1rem;
|
|
}
|
|
.title {
|
|
font-weight: 750;
|
|
}
|
|
.small {
|
|
font-size: 0.82rem;
|
|
}
|
|
@keyframes enter {
|
|
from {
|
|
transform: translateX(16px);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
@media (max-width: 899px) {
|
|
.panel {
|
|
top: auto;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
width: 100%;
|
|
max-height: 86dvh;
|
|
border-radius: var(--r-card) var(--r-card) 0 0;
|
|
animation-name: sheet;
|
|
}
|
|
@keyframes sheet {
|
|
from {
|
|
transform: translateY(24px);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
}
|
|
</style>
|