forked from yvv/decision
Lot U : le lecteur refait — l'ordre en une ligne au bout de sa section, poignées sur place (Reorder), le titre et l'ordre votés à leur place ; Propositions seed en tête quand rien n'est en vigueur ; Propositions en cours repliées par section ; panneau sans onglets ; oui pour passer · non pour bloquer ; quorum de l'auteur ; votes simulés dits simulés
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,30 +1,28 @@
|
||||
<script lang="ts">
|
||||
// The side panel: the basket of the vote, the versions of an article, a new
|
||||
// proposal, the details of a version, the settings of the formula. The vote of
|
||||
// one version unfolds under its article, not here.
|
||||
// The side panel, one thing at a time (lot U, UX-lecteur.md): what is written, or works
|
||||
// over several modules, or is read beside the text — the basket of the vote, a new
|
||||
// proposal, the details of a version, the settings of the formula. Named at its head,
|
||||
// with its cross; no tabs. What is read and decided on one version (its vote, its other
|
||||
// versions, its inertia, the order of its section) unfolds in place, never here.
|
||||
import type { DocState, PanelState } from '$lib/doc.svelte';
|
||||
import { t } from '$lib/i18n/index.svelte';
|
||||
import { relationOf, titleOf } from '$lib/labels';
|
||||
import { answerPanel } from '$lib/objections';
|
||||
import type { BasketState } from '$lib/picks.svelte';
|
||||
import { session } from '$lib/session.svelte';
|
||||
import type { Lang, Proposal } from '$lib/types';
|
||||
import type { Lang } from '$lib/types';
|
||||
import type { Line } from '$lib/vote';
|
||||
import BallotPanel from './BallotPanel.svelte';
|
||||
import FormulaSettings from './FormulaSettings.svelte';
|
||||
import Icon from './Icon.svelte';
|
||||
import OrderEditor from './OrderEditor.svelte';
|
||||
import ProposeView from './ProposeView.svelte';
|
||||
import TitlePropose from './TitlePropose.svelte';
|
||||
import VersionDetails from './VersionDetails.svelte';
|
||||
import WaterLevel from './WaterLevel.svelte';
|
||||
|
||||
let {
|
||||
doc,
|
||||
panel,
|
||||
lang,
|
||||
basket,
|
||||
vote,
|
||||
onchange,
|
||||
onclose,
|
||||
onvote,
|
||||
@@ -34,61 +32,41 @@
|
||||
panel: PanelState;
|
||||
lang: Lang;
|
||||
basket: BasketState;
|
||||
/** The version whose vote is unfolded under its article. */
|
||||
vote: string | null;
|
||||
onchange: (p: PanelState) => void;
|
||||
onclose: () => void;
|
||||
/** Unfolds the vote of a version under its article; `close` closes the panel. */
|
||||
/** Unfolds the vote of a version where it reads; `close` closes the panel. */
|
||||
onvote: (proposalId: string, close: boolean) => void;
|
||||
onsign: (lines: Line[]) => 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) : [],
|
||||
);
|
||||
// A module (ADR 0009) is proposed with its own gesture: another order, another title.
|
||||
const proposeLabel = $derived(unit?.kind === 'order' ? t('order.propose') : unit?.kind === 'title' ? t('title.propose') : t('doc.action.propose'));
|
||||
const nameOf = (id: string) => doc.articleTitle(id, lang, doc.view?.doc.sourceLang ?? 'fr');
|
||||
/** A version in the list: an order reads as its articles, in its order. */
|
||||
const nameOfVersion = (v: Proposal) => (v.kind === 'order' && v.order ? v.order.ids.map(nameOf).join(' · ') : titleOf(v, unit));
|
||||
const voteId = $derived(panel.proposalId ?? unit?.inForce?.id ?? unit?.proposals.find((p) => p.status === 'open')?.id ?? null);
|
||||
const tabs = $derived(
|
||||
(
|
||||
[
|
||||
['basket', basket.size ? `${t('panel.basket')} · ${basket.size}` : t('panel.basket'), !!doc.view?.vote.open || panel.tab === 'basket'],
|
||||
['proposals', `${t('panel.proposals')}${versions.length > 1 ? ` · ${versions.length}` : ''}`, !!unit],
|
||||
['propose', t('panel.propose'), true],
|
||||
// A version: reached from its vote, or from the list of versions.
|
||||
['details', t('panel.details'), !!panel.proposalId],
|
||||
// Reached from the head of the document or from a vote; shown while it is open.
|
||||
['settings', t('panel.settings'), panel.tab === 'settings'],
|
||||
] as const
|
||||
).filter(([, , show]) => show),
|
||||
/** One name per function, the same one that opened it. */
|
||||
const heading = $derived(
|
||||
panel.tab === 'basket'
|
||||
? t('basket.title')
|
||||
: panel.tab === 'settings'
|
||||
? t('settings.title')
|
||||
: panel.tab === 'details'
|
||||
? t('panel.details')
|
||||
: unit?.kind === 'title'
|
||||
? t('title.propose')
|
||||
: panel.unitId
|
||||
? t('doc.action.propose')
|
||||
: t('deep.new'),
|
||||
);
|
||||
let body: HTMLElement | undefined = $state();
|
||||
let tablist: HTMLElement | undefined = $state();
|
||||
|
||||
$effect(() => {
|
||||
void panel.tab;
|
||||
void panel.proposalId;
|
||||
body?.scrollTo({ top: 0 });
|
||||
});
|
||||
|
||||
// On a narrow sheet the tabs scroll: the open one stays in sight.
|
||||
$effect(() => {
|
||||
void panel.tab;
|
||||
tablist?.querySelector<HTMLElement>('[aria-selected="true"]')?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
});
|
||||
</script>
|
||||
|
||||
<aside class="panel card" aria-label={tabs.find(([key]) => key === panel.tab)?.[1] ?? t('panel.proposals')}>
|
||||
<aside class="panel card" aria-labelledby="panel-title">
|
||||
<div class="bar">
|
||||
<div class="tabs" role="tablist" bind:this={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, objectionId: null, edit: false })}>{label}</button>
|
||||
{/each}
|
||||
</div>
|
||||
<h2 id="panel-title">{heading}</h2>
|
||||
<button class="close" type="button" onclick={onclose} aria-label={t('panel.close')}><Icon name="close" /></button>
|
||||
</div>
|
||||
|
||||
@@ -108,24 +86,6 @@
|
||||
if (next) onchange(next);
|
||||
}}
|
||||
/>
|
||||
{: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 === vote} onclick={() => onvote(v.id, true)}>
|
||||
<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">{nameOfVersion(v)}</span>
|
||||
<span class="faint small">{v.status === 'in_force' ? (v.seedMinimal ? t('doc.seed') : '') : v.seedProposal ? t('doc.seedProposal') : relationOf(v, unit)} · {t('tally.short', { for: v.tally.for, against: v.tally.against })}{#if v.myVote && !v.myVoteStale} · {t('basket.mine', { choice: t(`vote.choice.${v.myVote}`) })}{/if}{#if v.blocked} · <b class="held">{t('deep.held')}</b>{/if}</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} />{proposeLabel}</button>
|
||||
{:else if panel.tab === 'propose' && unit?.kind === 'order'}
|
||||
<OrderEditor {doc} {unit} {nameOf} ondone={(id) => onvote(id, true)} oncancel={onclose} />
|
||||
{:else if panel.tab === 'propose' && unit?.kind === 'title'}
|
||||
<TitlePropose {doc} {unit} ondone={(id) => onvote(id, true)} />
|
||||
{:else if panel.tab === 'propose'}
|
||||
@@ -139,7 +99,9 @@
|
||||
ondone={(r) =>
|
||||
r.proposalId && r.submitted
|
||||
? onvote(r.proposalId, true)
|
||||
: onchange(r.proposalId ? { unitId: doc.proposal(r.proposalId)?.unitId ?? panel.unitId, proposalId: r.proposalId, tab: 'details' } : { ...panel, tab: 'proposals' })}
|
||||
: r.proposalId
|
||||
? onchange({ unitId: doc.proposal(r.proposalId)?.unitId ?? panel.unitId, proposalId: r.proposalId, tab: 'details' })
|
||||
: onclose()}
|
||||
/>
|
||||
{:else if panel.tab === 'settings'}
|
||||
<FormulaSettings {doc} proposalId={voteId} />
|
||||
@@ -165,38 +127,20 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.7rem 0.7rem 0.5rem 0.9rem;
|
||||
padding: 0.9rem 0.7rem 0.4rem 1.2rem;
|
||||
}
|
||||
.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);
|
||||
h2 {
|
||||
font-family: var(--font-text);
|
||||
font-weight: 500;
|
||||
font-size: 1.35rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.close {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
border: 0;
|
||||
border-radius: var(--r-icon);
|
||||
background: transparent;
|
||||
@@ -211,41 +155,6 @@
|
||||
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;
|
||||
}
|
||||
.held {
|
||||
color: var(--ochre);
|
||||
}
|
||||
@keyframes enter {
|
||||
from {
|
||||
transform: translateX(16px);
|
||||
@@ -270,4 +179,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.panel {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user