forked from yvv/decision
v3 : interface Svelte — accueil, groupes, lecteur du texte en vigueur, panneau contextuel, éditeur à assistant, trustinWallet
- 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>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
// State of the reader: the document, the view (in force / previews), the
|
||||
// language the text is read in, and the panel (which unit, which version,
|
||||
// which tab) — the panel lives in the URL so a link reopens it.
|
||||
|
||||
import { api, ApiError } from './api';
|
||||
import type { GroupView } from './group';
|
||||
import type { DocView, Lang, PreviewUnit, Proposal, Translation, Unit } from './types';
|
||||
|
||||
export type View = 'inforce' | 'wellrated' | 'all';
|
||||
export type Tab = 'vote' | 'proposals' | 'propose';
|
||||
|
||||
export interface PanelState {
|
||||
unitId: string | null;
|
||||
proposalId: string | null;
|
||||
tab: Tab;
|
||||
draftId?: string | null;
|
||||
prefill?: { title: string; body: string } | null;
|
||||
}
|
||||
|
||||
export interface Shown {
|
||||
title: string;
|
||||
body: string;
|
||||
/** The text is a translation (editorial). */
|
||||
translated: boolean;
|
||||
stale: boolean;
|
||||
/** Asked for a translation that does not exist yet: the source is shown. */
|
||||
missing: boolean;
|
||||
}
|
||||
|
||||
export function shown(
|
||||
v: { title: string; body: string; translations: Partial<Record<Lang, Translation>> },
|
||||
lang: Lang,
|
||||
source: Lang,
|
||||
): Shown {
|
||||
if (lang === source) return { title: v.title, body: v.body, translated: false, stale: false, missing: false };
|
||||
const tr = v.translations[lang];
|
||||
if (!tr) return { title: v.title, body: v.body, translated: false, stale: false, missing: true };
|
||||
return { title: tr.title, body: tr.body, translated: true, stale: tr.stale, missing: false };
|
||||
}
|
||||
|
||||
export class DocState {
|
||||
view = $state<DocView | null>(null);
|
||||
error = $state<string | null>(null);
|
||||
mode = $state<View>('inforce');
|
||||
preview = $state<PreviewUnit[] | null>(null);
|
||||
original = $state(false);
|
||||
|
||||
async load(group: string, slug: string) {
|
||||
try {
|
||||
const g = await api.get<GroupView>(`/groups/${group}`);
|
||||
const d = g.docs.find((x) => x.slug === slug);
|
||||
if (!d) {
|
||||
this.error = g.group.open ? 'not_found' : 'forbidden';
|
||||
return;
|
||||
}
|
||||
this.view = await api.get<DocView>(`/docs/${d.id}`);
|
||||
this.error = null;
|
||||
if (this.mode !== 'inforce') await this.loadPreview();
|
||||
} catch (e) {
|
||||
this.error = e instanceof ApiError ? e.code : 'internal';
|
||||
}
|
||||
}
|
||||
|
||||
async reload() {
|
||||
if (!this.view) return;
|
||||
this.view = await api.get<DocView>(`/docs/${this.view.doc.id}`);
|
||||
if (this.mode !== 'inforce') await this.loadPreview();
|
||||
}
|
||||
|
||||
async setMode(mode: View) {
|
||||
this.mode = mode;
|
||||
if (mode === 'inforce') this.preview = null;
|
||||
else await this.loadPreview();
|
||||
}
|
||||
|
||||
async loadPreview() {
|
||||
if (!this.view) return;
|
||||
const r = await api.get<{ units: PreviewUnit[] }>(`/docs/${this.view.doc.id}/preview?mode=${this.mode}`);
|
||||
this.preview = r.units;
|
||||
}
|
||||
|
||||
unit(id: string | null): Unit | undefined {
|
||||
return this.view?.units.find((u) => u.id === id);
|
||||
}
|
||||
|
||||
proposal(id: string | null): Proposal | undefined {
|
||||
if (!id || !this.view) return undefined;
|
||||
for (const u of this.view.units) {
|
||||
if (u.inForce?.id === id) return u.inForce;
|
||||
const p = u.proposals.find((x) => x.id === id);
|
||||
if (p) return p;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get inForce(): Unit[] {
|
||||
return (this.view?.units ?? []).filter((u) => u.inForce);
|
||||
}
|
||||
|
||||
/** Open proposals, in document order; drafts apart. */
|
||||
get open(): { unit: Unit; proposal: Proposal }[] {
|
||||
const out: { unit: Unit; proposal: Proposal }[] = [];
|
||||
for (const unit of this.view?.units ?? []) {
|
||||
for (const proposal of unit.proposals) if (proposal.status === 'open') out.push({ unit, proposal });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
get drafts(): { unit: Unit; proposal: Proposal }[] {
|
||||
const out: { unit: Unit; proposal: Proposal }[] = [];
|
||||
for (const unit of this.view?.units ?? []) {
|
||||
for (const proposal of unit.proposals) if (proposal.status === 'draft') out.push({ unit, proposal });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user