v3 : les versions d'un article tiennent une seule ligne (L8)
- « Propositions en cours » : une ligne par article. Un article à plusieurs versions montre leur nombre et la meilleure en tête (niveau d'eau, pour / contre, « en tête » quand elle devance vraiment) ; la ligne se déplie (bouton à aria-expanded, clavier) sur ses versions, chacune avec son étiquette (variante, auteur), son niveau, ses marques, et un extrait qui part de là où les versions divergent ; un clic ouvre son vote comme avant. - Règle pure dans versions.ts (vitest) : ordre du document, meilleure d'abord (niveau d'eau, puis pour, puis contre ; une version bloquée jamais en tête), un article reste dans un filtre dès qu'une de ses versions passe. - e2e : C3 compte 30 lignes, C14 déplie « Connaissance suffisante » au clavier ; C11 passe par un article à une seule version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// The versions of an article, for the list of open proposals (Yvv, 2026-10-30:
|
||||
// « Quand un article a plusieurs versions, faut pas ajouter un article dans la
|
||||
// liste »): one line per article, its best version first, the others inside.
|
||||
|
||||
import type { Proposal, Unit } from './types';
|
||||
|
||||
export type Filter = 'all' | 'wellrated' | 'mine';
|
||||
|
||||
export interface Article {
|
||||
unit: Unit;
|
||||
/** The open proposals of the article, best first. */
|
||||
versions: Proposal[];
|
||||
/** The best version is strictly ahead of the next one (not a tie). */
|
||||
leads: boolean;
|
||||
}
|
||||
|
||||
/** How far a version has risen toward its threshold, from 0 to 1: its water level. */
|
||||
export function level(p: Proposal): number {
|
||||
const required = p.evaluation?.required ?? 0;
|
||||
return required > 0 ? Math.min(1, p.tally.for / required) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Negative when `a` comes before `b`: the highest water, then more for, then fewer
|
||||
* against. A blocked version comes after the others: it cannot enter into force.
|
||||
*/
|
||||
export function compare(a: Proposal, b: Proposal): number {
|
||||
return (
|
||||
Number(a.blocked) - Number(b.blocked) ||
|
||||
level(b) - level(a) ||
|
||||
b.tally.for - a.tally.for ||
|
||||
a.tally.against - b.tally.against
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plain excerpts of sibling versions, each starting a few words before the versions
|
||||
* part ways — two variants often share a long opening, and a one-line excerpt would
|
||||
* show only that.
|
||||
*/
|
||||
export function excerpts(texts: string[]): string[] {
|
||||
const LEAD = 3;
|
||||
const words = texts.map((t) => t.split(/\s+/).filter(Boolean));
|
||||
const [first] = words;
|
||||
if (!first || words.length < 2) return texts;
|
||||
let common = 0;
|
||||
while (common < first.length && words.every((w) => w[common] === first[common])) common++;
|
||||
const start = common - LEAD;
|
||||
// A short common start reads well as it is.
|
||||
if (start < 4) return texts;
|
||||
return words.map((w) => `… ${w.slice(start).join(' ')}`);
|
||||
}
|
||||
|
||||
export function passes(p: Proposal, filter: Filter): boolean {
|
||||
if (filter === 'wellrated') return !!p.evaluation?.wellRated;
|
||||
if (filter === 'mine') return p.mine;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* One article per unit, in the order of the document; an article is kept when at
|
||||
* least one of its versions passes the filter, and then shows all of them.
|
||||
*/
|
||||
export function articles(items: { unit: Unit; proposal: Proposal }[], filter: Filter = 'all'): Article[] {
|
||||
const byUnit = new Map<string, { unit: Unit; versions: Proposal[] }>();
|
||||
for (const { unit, proposal } of items) {
|
||||
const entry = byUnit.get(unit.id) ?? { unit, versions: [] };
|
||||
entry.versions.push(proposal);
|
||||
byUnit.set(unit.id, entry);
|
||||
}
|
||||
return [...byUnit.values()]
|
||||
.filter(({ versions }) => versions.some((p) => passes(p, filter)))
|
||||
.map(({ unit, versions }) => {
|
||||
// Array.prototype.sort is stable: equal versions keep the order of the document.
|
||||
const ranked = [...versions].sort(compare);
|
||||
const [best, next] = ranked;
|
||||
return { unit, versions: ranked, leads: !!best && !!next && compare(best, next) < 0 };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user