forked from yvv/decision
188 lines
5.4 KiB
Svelte
188 lines
5.4 KiB
Svelte
<script lang="ts">
|
|
// 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 { answerPanel } from '$lib/objections';
|
|
import type { BasketState } from '$lib/picks.svelte';
|
|
import { session } from '$lib/session.svelte';
|
|
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 ProposeView from './ProposeView.svelte';
|
|
import TitlePropose from './TitlePropose.svelte';
|
|
import VersionDetails from './VersionDetails.svelte';
|
|
|
|
let {
|
|
doc,
|
|
panel,
|
|
lang,
|
|
basket,
|
|
onchange,
|
|
onclose,
|
|
onvote,
|
|
onsign,
|
|
}: {
|
|
doc: DocState;
|
|
panel: PanelState;
|
|
lang: Lang;
|
|
basket: BasketState;
|
|
onchange: (p: PanelState) => void;
|
|
onclose: () => void;
|
|
/** 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 voteId = $derived(panel.proposalId ?? unit?.inForce?.id ?? unit?.proposals.find((p) => p.status === 'open')?.id ?? null);
|
|
/** 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();
|
|
|
|
$effect(() => {
|
|
void panel.tab;
|
|
void panel.proposalId;
|
|
body?.scrollTo({ top: 0 });
|
|
});
|
|
</script>
|
|
|
|
<aside class="panel card" aria-labelledby="panel-title">
|
|
<div class="bar">
|
|
<h2 id="panel-title">{heading}</h2>
|
|
<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 === 'basket'}
|
|
<BallotPanel {doc} {basket} section={panel.section ?? null} {onsign} onread={(id) => onvote(id, false)} />
|
|
{:else if panel.tab === 'details' && voteId}
|
|
<VersionDetails
|
|
{doc}
|
|
proposalId={voteId}
|
|
{lang}
|
|
edit={!!panel.edit}
|
|
onvote={(id) => onvote(id, true)}
|
|
onpropose={(prefill) => onchange({ ...panel, tab: 'propose', prefill, draftId: null, edit: false })}
|
|
onanswer={(o) => {
|
|
const next = answerPanel(o, session.me?.id ?? null);
|
|
if (next) onchange(next);
|
|
}}
|
|
/>
|
|
{:else if panel.tab === 'propose' && unit?.kind === 'title'}
|
|
<TitlePropose {doc} {unit} ondone={(id) => onvote(id, true)} />
|
|
{:else if panel.tab === 'propose'}
|
|
<ProposeView
|
|
{doc}
|
|
unitId={panel.unitId}
|
|
draftId={panel.draftId ?? null}
|
|
prefill={panel.prefill ?? null}
|
|
from={panel.objectionId ? (panel.proposalId ?? null) : null}
|
|
objectionId={panel.objectionId ?? null}
|
|
ondone={(r) =>
|
|
r.proposalId && r.submitted
|
|
? onvote(r.proposalId, true)
|
|
: 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} />
|
|
{/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.9rem 0.7rem 0.4rem 1.2rem;
|
|
}
|
|
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.75rem;
|
|
height: 2.75rem;
|
|
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;
|
|
}
|
|
@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;
|
|
}
|
|
}
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.panel {
|
|
animation: none;
|
|
}
|
|
}
|
|
</style>
|