Merge branch 'chantier/acte-certification' into lot/u-lecteur

This commit is contained in:
Yvv
2026-11-03 07:34:49 +01:00
87 changed files with 9932 additions and 895 deletions
+1
View File
@@ -78,6 +78,7 @@
{/snippet}
{#snippet marks(p: Proposal)}
{#if p.seedProposal}<span class="tag water" title={t('doc.seedProposal.long')}>{t('doc.seedProposal')}</span>{/if}
{#if p.evaluation?.wellRated}<span class="tag water">{t('deep.wellrated')}</span>{/if}
{#if p.blocked}<span class="tag ochre">{t('deep.held')}</span>{/if}
{#if p.myVoteStale}<span class="tag ochre">{t('deep.stale')}</span>{/if}
+1 -1
View File
@@ -117,7 +117,7 @@
<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') : '') : 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 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>
+10 -4
View File
@@ -33,12 +33,18 @@
<details class="formula">
<summary>{t('formula.title')}</summary>
<p class="muted">{t('formula.intro')}</p>
<!-- ADR 0013: judged on T′ = max(T, Q) voters; below the quorum, the voters missing count as against. -->
<math display="block" class="math">
<mrow>
<mi>R</mi><mo>=</mo><mi>C</mi><mo>+</mo><msup><mi>B</mi><mi>W</mi></msup><mo>+</mo>
<msup><mi>T</mi><mo>′</mo></msup><mo>=</mo><mi>max</mi><mo>(</mo><mi>T</mi><mo>,</mo><mi>Q</mi><mo>)</mo>
</mrow>
</math>
<math display="block" class="math">
<mrow>
<mi>R</mi><mo>=</mo><msup><mi>B</mi><mi>W</mi></msup><mo>+</mo>
<mrow><mo>(</mo><mi>M</mi><mo>+</mo><mo>(</mo><mn>1</mn><mo>−</mo><mi>M</mi><mo>)</mo><mo>(</mo><mn>1</mn><mo>−</mo>
<msup><mrow><mo>(</mo><mfrac><mi>T</mi><mi>W</mi></mfrac><mo>)</mo></mrow><mi>G</mi></msup><mo>)</mo><mo>)</mo></mrow>
<mo>·</mo><mi>max</mi><mo>(</mo><mn>0</mn><mo>,</mo><mi>T</mi><mo>−</mo><mi>C</mi><mo>)</mo>
<msup><mrow><mo>(</mo><mfrac><msup><mi>T</mi><mo>′</mo></msup><mi>W</mi></mfrac><mo>)</mo></mrow><mi>G</mi></msup><mo>)</mo><mo>)</mo></mrow>
<mo>·</mo><msup><mi>T</mi><mo>′</mo></msup>
</mrow>
</math>
<ul class="values">
@@ -49,7 +55,7 @@
<li>{t('formula.G', { n: i18n.number(Math.round(p.gradient * 1000) / 1000) })}</li>
{/if}
<li>{t('formula.B', { n: i18n.number(base) })}</li>
<li>{t('formula.C', { n: i18n.number(floor) })}</li>
<li>{t('formula.Q', { n: i18n.number(evaluation?.quorum ?? p?.quorum ?? floor) })}</li>
{#if evaluation}<li class="r">{t('formula.R', { n: evaluation.required })}</li>{/if}
</ul>
<p class="faint">{t('formula.reading')}</p>
+19 -5
View File
@@ -1,8 +1,9 @@
<script lang="ts">
// The settings of the formula, read only: W and its source, C, B, the usual
// The settings of the formula, read only: W and its source, B, the usual
// turnout of the group (median or reference) and the five levels on its
// scale. From the head of the document, the *for* votes asked are those of
// the usual turnout; from a version, those of its own turnout.
// scale, each with its quorum (ADR 0013). From the head of the document, the
// *for* votes asked are those of the usual turnout; from a version, those of
// its own turnout, on its article's quorum.
import { api } from '$lib/api';
import type { DocState } from '$lib/doc.svelte';
import { i18n, t } from '$lib/i18n/index.svelte';
@@ -50,7 +51,6 @@
<ul class="values">
<li>{t('formula.W', { n: i18n.number(view.doc.electorate), source: t(view.doc.electorateSource === 'wot' ? 'formula.W.wot' : 'formula.W.group') })}</li>
<li>{t('formula.C', { n: i18n.number(view.doc.voteFloor) })}</li>
<li>{t('formula.B', { n: i18n.number(view.doc.formula.base) })}</li>
</ul>
@@ -60,7 +60,7 @@
{#if p.source === 'median'}
{i18n.plural('settings.usual.median', shown?.voters ?? p.voters, { electorate: i18n.number(view.doc.electorate), pct: pct(shown?.ratio ?? p.ratio), sample: p.sample })}
{:else}
{i18n.plural('settings.usual.reference', shown?.voters ?? p.voters, { electorate: i18n.number(view.doc.electorate), pct: pct(shown?.ratio ?? p.ratio), min: p.minSample, sample: p.sample })}
{i18n.plural('settings.usual.reference', shown?.voters ?? p.voters, { electorate: i18n.number(view.doc.electorate), pct: pct(shown?.ratio ?? p.ratio), q: i18n.number(view.doc.voteFloor), min: p.minSample, sample: p.sample })}
{/if}
</p>
{#if p.bounded}<p class="faint small">{i18n.plural('settings.usual.bounded', p.voters, { lo: pct(p.bounds[0]), hi: pct(p.bounds[1]), pct: pct(p.ratio) })}</p>{/if}
@@ -76,6 +76,7 @@
<th scope="col">M</th>
<th scope="col">G</th>
<th scope="col">{t('settings.col.share')}</th>
<th scope="col">{t('settings.col.quorum')}</th>
<th scope="col">{t('settings.col.required')}</th>
</tr>
</thead>
@@ -92,6 +93,7 @@
<td>{whole(l.majorityPct)}</td>
<td>{gradient(l.gradient)}</td>
<td>{whole(l.sharePct)}</td>
<td>{l.quorum === undefined ? '—' : i18n.number(l.quorum)}</td>
<td class="required">{l.required === null ? '—' : i18n.number(l.required)}</td>
</tr>
{/each}
@@ -99,6 +101,7 @@
</table>
</div>
<p class="faint small">{i18n.plural('settings.share.note', p.voters)}</p>
<p class="faint small">{t('settings.quorum.note')}</p>
<p class="faint small">{i18n.plural(version ? 'settings.required.version' : 'settings.required.usual', cast)}</p>
<p class="faint small">{t('settings.reading')}</p>
</section>
@@ -194,4 +197,15 @@
.tag {
font-size: 0.68rem;
}
/* On a phone, six columns hold in the sheet: the headers and the level's name wrap. */
@media (max-width: 480px) {
th,
td {
padding: 0.4rem 0.3rem;
}
thead th,
th[scope='row'] {
white-space: normal;
}
}
</style>
@@ -238,6 +238,7 @@
<h3>{p.kind === 'text' || p.kind === 'title' ? text.title : titleOf(p, unit)}</h3>
<p class="meta faint">
{#if p.seedMinimal}<span class="tag water" title={t('doc.seed.long')}>{t('doc.seed')}</span>
{:else if p.seedProposal}<span class="tag water" title={t('doc.seedProposal.long')}>{t('doc.seedProposal')}</span>
{:else if p.confirmedAt}<span class="tag moss">{t('doc.confirmed')}</span>
{:else if p.enteredAt && p.status === 'in_force'}<span class="tag moss">{t('doc.adopted', { date: i18n.date(p.enteredAt) })}</span>{/if}
{#if p.blocked}<span class="tag ochre">{t('deep.held')}</span>{/if}
+5 -3
View File
@@ -133,7 +133,7 @@
<div class="level">
<WaterLevel tally={p.tally} evaluation={p.evaluation} blocked={p.blocked} size="large" />
<div class="numbers">
<p class="tally">{t('tally.short', { for: p.tally.for, against: p.tally.against })}</p>
<p class="tally">{t('tally.short', { for: p.tally.for, against: p.tally.against })}{#if p.tallySimulated?.for || p.tallySimulated?.against} <span class="faint small">{i18n.plural('vote.tally.simulated', p.tallySimulated.for + p.tallySimulated.against)}</span>{/if}</p>
{#if p.evaluation}
<p class="muted">{t('tally.required', { n: p.evaluation.required })}</p>
<p class="state">{p.evaluation.reached ? t('tally.reached') : i18n.plural('tally.missing', p.evaluation.missing)}</p>
@@ -186,8 +186,8 @@
{/if}
{#if p.myVoteStale}
<p class="warn">{t('vote.stale')}</p>
{:else if p.myVote}
<p class="voted {p.myVote}">{t(p.myVote === 'for' ? 'vote.mine.for' : 'vote.mine.against')} · <span class="how">{t(p.myVoteChain ? 'vote.mine.chain' : 'vote.mine.saved')}</span></p>
{:else if p.myVote && p.myVoteState !== 'pending'}
<p class="voted {p.myVote}">{t(p.myVote === 'for' ? 'vote.mine.for' : 'vote.mine.against')} · <span class="how">{t(p.myVoteChain ? 'vote.mine.chain' : p.myVoteState === 'simulated' ? 'vote.mine.simulated' : 'vote.mine.saved')}</span></p>
{:else if ballot?.status !== 'pending'}
<p class="muted">{t('vote.mine.none')}</p>
{/if}
@@ -215,6 +215,8 @@
{#if chain && access.chain}
<p class="faint small">{t('vote.chain.note', { amount: g1Amount(access.chain.amount) })}</p>
<p class="faint small">{t('vote.chain.counts')}</p>
{:else if access.simulated}
<p class="faint small">{t('vote.simulated.note')}</p>
{/if}
{/if}
<p class="faint small">{t('vote.private')}</p>
+25 -10
View File
@@ -30,7 +30,8 @@ export const en: Messages = {
'nav.menu': 'Menu',
'nav.close': 'Close',
'home.title': 'Making collective decisions possible',
'home.title.line1': 'Making it possible',
'home.title.line2': 'to decide together',
'home.leitmotiv.context': 'context and framing',
'home.leitmotiv.process': 'processes and methods',
'home.leitmotiv.tools': 'a toolbox',
@@ -122,6 +123,8 @@ export const en: Messages = {
'doc.seed.long': 'Came into force without a vote, so as not to start from a blank page. It remains open for voting, like any text.',
'doc.seed.votes.one': 'seed a minima · {n} vote',
'doc.seed.votes.other': 'seed a minima · {n} votes',
'doc.seedProposal': 'Seed proposal',
'doc.seedProposal.long': 'A starting text put forward to start the vote. It is not in force: it comes into force through the vote, like any proposal.',
'doc.confirmed': 'confirmed by vote',
'doc.adopted': 'adopted on {date}',
'doc.action.vote': 'Voting',
@@ -134,7 +137,7 @@ export const en: Messages = {
'doc.publish.pending': 'Publication pending',
'deep.title': 'Open proposals',
'deep.note': 'Not in force yet. Each one moves up as soon as it reaches its threshold: at least {floor} votes in favor, then the majority its inertia requires.',
'deep.note': 'Not in force yet. Each one moves up as soon as it reaches its threshold: the share of votes in favour its inertia requires, counted on at least its quorum of voters.',
'deep.new': 'Propose a new article',
'deep.drafts': 'Your drafts',
'deep.drafts.note': 'Not submitted to vote. Only you can see them.',
@@ -252,6 +255,10 @@ export const en: Messages = {
'vote.mine.chain': 'recorded on the Ğ1 chain',
'vote.mine.saved': 'saved',
'vote.mine.pending': 'Waiting for the Ğ1 chain: {choice}.',
'vote.mine.simulated': 'simulated',
'vote.simulated.note': 'The Ğ1 chain is not open yet: your vote is simulated. It counts as a vote, marked “simulated”, until it is erased when the chain opens.',
'vote.tally.simulated.one': 'of which {n} simulated',
'vote.tally.simulated.other': 'of which {n} simulated',
'vote.mine.refused': 'Your last vote was not counted: {reason}',
'vote.mine.expired': 'Your last vote was not seen on the Ğ1 chain in time: it does not count.',
'vote.mine.receipts': 'Your ballots and receipts',
@@ -266,6 +273,7 @@ export const en: Messages = {
'ballot.reason.not_member': 'the wallet that signed is not that of a member of the Ğ1 web of trust.',
'ballot.reason.no_identity': 'the wallet that signed is linked to no Ğ1 identity.',
'ballot.reason.transfer': 'the transfer to the vote account is missing, or its amount is too low.',
'ballot.reason.objection': 'an objection was holding this version back when the Ğ1 chain confirmed your vote; your earlier vote stands.',
'basket.title': 'Voting basket',
'basket.intro': 'Choose article by article, a whole section or the whole document: it all goes in a single gesture.',
'basket.doc': 'The whole document',
@@ -333,6 +341,7 @@ export const en: Messages = {
'ballots.lines.other': '{n} votes',
'ballots.status.pending': 'Waiting for the Ğ1 chain',
'ballots.status.confirmed': 'Counted for the Ğ1 identity “{name}”',
'ballots.status.simulated': 'Simulated: counted for “{name}” until the Ğ1 chain opens',
'ballots.status.refused': 'Not counted: {reason}',
'ballots.status.expired': 'Not seen on the Ğ1 chain in time: it does not count',
'ballots.block': 'block {n}',
@@ -372,17 +381,17 @@ export const en: Messages = {
'formula.M': 'M — majority at full turnout: {n}%',
'formula.G': 'G — gradient: {n}',
'formula.B': 'B — base: {n}',
'formula.C': 'C — minimum votes in favor: {n}',
'formula.Q': 'Q — quorum: {n}',
'formula.R': 'R — current threshold: {n} votes in favor',
'formula.reading': 'At low turnout, near-unanimity is needed; when everyone votes, majority M is enough. Nothing passes with fewer than C votes in favor.',
'formula.reading': 'At low turnout, near-unanimity is needed; when everyone votes, majority M is enough. Below the quorum Q, the missing voters count as against.',
'common.pct': '{n}%',
'settings.title': 'Formula settings',
'settings.intro': 'To change a text, the group asks for a number of votes in favour that depends on its electorate, its usual turnout and the inertia of the article.',
'settings.usual': 'Usual turnout',
'settings.usual.median.one': '{n} voter out of {electorate} ({pct}): the median of the {sample} proposals voted on over the past year.',
'settings.usual.median.other': '{n} voters out of {electorate} ({pct}): the median of the {sample} proposals voted on over the past year.',
'settings.usual.reference.one': '{n} voter out of {electorate} ({pct}), twice C: the reference turnout, until the group has voted on {min} proposals ({sample} so far).',
'settings.usual.reference.other': '{n} voters out of {electorate} ({pct}), twice C: the reference turnout, until the group has voted on {min} proposals ({sample} so far).',
'settings.usual.reference.one': '{n} voter out of {electorate} ({pct}), twice {q}, the reference quorum: the reference turnout, until the group has voted on {min} proposals ({sample} so far).',
'settings.usual.reference.other': '{n} voters out of {electorate} ({pct}), twice {q}, the reference quorum: the reference turnout, until the group has voted on {min} proposals ({sample} so far).',
'settings.usual.bounded.one': 'Kept between {lo} and {hi} of the electorate: the group asks for its votes in favour as if {n} person voted ({pct}).',
'settings.usual.bounded.other': 'Kept between {lo} and {hi} of the electorate: the group asks for its votes in favour as if {n} people voted ({pct}).',
'settings.usual.when': 'Measured once a day, last on {date}.',
@@ -390,15 +399,17 @@ export const en: Messages = {
'settings.levels': 'The five levels',
'settings.col.level': 'Level',
'settings.col.share': 'Share asked',
'settings.col.quorum': 'Quorum',
'settings.col.required': 'Votes for',
'settings.current': 'this article',
'settings.share.note.one': 'Share asked: the votes in favour asked beyond C, at {n} voter as usual.',
'settings.share.note.other': 'Share asked: the votes in favour asked beyond C, at {n} voters as usual.',
'settings.share.note.one': 'Share asked: the share of votes in favour asked, at {n} voter as usual.',
'settings.share.note.other': 'Share asked: the share of votes in favour asked, at {n} voters as usual.',
'settings.quorum.note': "Quorum: below it, the missing voters count as against. It is the level's; the author of an article may set another one before the first vote.",
'settings.required.version.one': "Votes for: at {n} voter, this version's turnout.",
'settings.required.version.other': "Votes for: at {n} voters, this version's turnout.",
'settings.required.usual.one': 'Votes for: at {n} voter, as usual.',
'settings.required.usual.other': 'Votes for: at {n} voters, as usual.',
'settings.reading': 'At each level, the group asks for its share of votes in favour beyond C when it votes as usual; the more voters, the closer it comes to M. Nothing passes below C votes in favour.',
'settings.reading': 'At each level, the group asks for its share of votes in favour when it votes as usual; the more voters, the closer it comes to M. Below the quorum of the level, the missing voters count as against.',
'history.title': 'Corrections',
'history.initial': 'initial text',
@@ -470,7 +481,7 @@ export const en: Messages = {
'objection.respond.contredit': 'Reword',
'objection.linked': 'This proposal answers an objection: once submitted, the person who raised it lifts it or reiterates it.',
'messages.title': 'Messages',
'messages.intro': 'What waits for a gesture from you: an objection to answer, a typo fix to validate. Each message carries its deadline; a message dealt with asks nothing more.',
'messages.intro': 'What waits for a gesture from you: an objection to answer, a typo fix to validate. Each message carries its deadline; a message dealt with asks nothing more. You also read here what became of a vote that was not counted.',
'messages.none': 'No messages for now.',
'messages.readAll': 'Mark all as read',
'messages.read': 'Mark as read',
@@ -479,12 +490,16 @@ export const en: Messages = {
'messages.open': 'See it in the text',
'messages.topic.objection': 'Objection',
'messages.topic.typo': 'Typo',
'messages.topic.ballot': 'Vote',
'messages.objection_raised.redit': '{name} raises an objection: your version “{held}” repeats “{other}”. Until it is lifted, nobody can vote for it any more. To answer, change the article it repeats.',
'messages.objection_raised.contredit': '{name} raises an objection: your version “{held}” contradicts “{other}”. Until it is lifted, nobody can vote for it any more. To answer, reword it.',
'messages.objection_reiterated': '{name} reiterates their objection: nobody can vote for “{held}” yet. Answer again.',
'messages.objection_answered': 'Your objection on “{held}” has been answered. Lift it if the answer suits you, or reiterate it. With no answer from you, it is lifted.',
'messages.typo_pending.author': '{name} suggests a typo fix in your version “{title}”.',
'messages.typo_pending.admin': '{name} suggests a typo fix in “{title}”, a seed text: an administrator validates it.',
'messages.ballot_expired': 'Your vote of {date} ({articles}) was not seen on the Ğ1 chain within 24 hours: it is set aside. You can vote again.',
'messages.ballot_refused': 'Your vote of {date} ({articles}) was not counted: {reason} You can vote again.',
'messages.ballot_objection': 'Your “for” on {articles} is set aside: an objection was holding this version back when the Ğ1 chain confirmed your vote. Your earlier vote stands.',
'messages.respond': 'Answer',
'messages.outcome.answered': 'answered',
'messages.outcome.reiterated': 'objection reiterated',
+25 -10
View File
@@ -30,7 +30,8 @@ export const es: Messages = {
'nav.menu': 'Menú',
'nav.close': 'Cerrar',
'home.title': 'Hacer posible la decisión colectiva',
'home.title.line1': 'Hacer posible',
'home.title.line2': 'la decisión colectiva',
'home.leitmotiv.context': 'contexto y encuadre',
'home.leitmotiv.process': 'procesos y modalidades',
'home.leitmotiv.tools': 'caja de herramientas',
@@ -122,6 +123,8 @@ export const es: Messages = {
'doc.seed.long': 'Entró en vigor sin votación, para no partir de una página en blanco. Sigue en votación, como todo texto.',
'doc.seed.votes.one': 'seed a minima · {n} voto',
'doc.seed.votes.other': 'seed a minima · {n} votos',
'doc.seedProposal': 'Propuesta seed',
'doc.seedProposal.long': 'Texto de partida destacado para lanzar la votación. No está en vigor: entra en vigor por la votación, como toda propuesta.',
'doc.confirmed': 'confirmado por la votación',
'doc.adopted': 'adoptado el {date}',
'doc.action.vote': 'Votación',
@@ -134,7 +137,7 @@ export const es: Messages = {
'doc.publish.pending': 'Publicación pendiente',
'deep.title': 'Propuestas en curso',
'deep.note': 'Aún no están en vigor. Cada una sube en cuanto alcanza su umbral: al menos {floor} votos a favor y, después, la mayoría que exige su inercia.',
'deep.note': 'Aún no están en vigor. Cada una sube en cuanto alcanza su umbral: la proporción de votos a favor que exige su inercia, contada sobre al menos su quórum de votantes.',
'deep.new': 'Proponer un artículo nuevo',
'deep.drafts': 'Tus borradores',
'deep.drafts.note': 'No sometidos a votación. Solo tú los ves.',
@@ -252,6 +255,10 @@ export const es: Messages = {
'vote.mine.chain': 'inscrito en la cadena Ğ1',
'vote.mine.saved': 'registrado',
'vote.mine.pending': 'Esperando a la cadena Ğ1: {choice}.',
'vote.mine.simulated': 'simulado',
'vote.simulated.note': 'La cadena Ğ1 aún no está abierta: tu voto es simulado. Cuenta como un voto, marcado «simulado», hasta que se borre al abrirse la cadena.',
'vote.tally.simulated.one': 'de ellos {n} simulado',
'vote.tally.simulated.other': 'de ellos {n} simulados',
'vote.mine.refused': 'Tu último voto no se contó: {reason}',
'vote.mine.expired': 'Tu último voto no se vio a tiempo en la cadena Ğ1: no cuenta.',
'vote.mine.receipts': 'Tus papeletas y recibos',
@@ -266,6 +273,7 @@ export const es: Messages = {
'ballot.reason.not_member': 'el monedero que firmó no es el de un miembro de la red de confianza Ğ1.',
'ballot.reason.no_identity': 'el monedero que firmó no está vinculado a ninguna identidad Ğ1.',
'ballot.reason.transfer': 'falta la transferencia a la cuenta de los votos, o su importe es demasiado bajo.',
'ballot.reason.objection': 'una objeción retenía esta versión cuando la cadena Ğ1 confirmó tu voto; tu voto anterior se mantiene.',
'basket.title': 'Cesta de votación',
'basket.intro': 'Elige artículo por artículo, una sección entera o todo el documento: todo sale en un solo gesto.',
'basket.doc': 'Todo el documento',
@@ -333,6 +341,7 @@ export const es: Messages = {
'ballots.lines.other': '{n} votos',
'ballots.status.pending': 'Esperando a la cadena Ğ1',
'ballots.status.confirmed': 'Contado para la identidad Ğ1 «{name}»',
'ballots.status.simulated': 'Simulado: contado para «{name}» hasta que se abra la cadena Ğ1',
'ballots.status.refused': 'No contado: {reason}',
'ballots.status.expired': 'No visto a tiempo en la cadena Ğ1: no cuenta',
'ballots.block': 'bloque {n}',
@@ -372,17 +381,17 @@ export const es: Messages = {
'formula.M': 'M — mayoría con participación plena: {n} %',
'formula.G': 'G — gradiente: {n}',
'formula.B': 'B — base: {n}',
'formula.C': 'C — mínimo de votos a favor: {n}',
'formula.Q': 'Q — quórum: {n}',
'formula.R': 'R — umbral actual: {n} votos a favor',
'formula.reading': 'Con poca participación, hace falta casi la unanimidad; cuando vota todo el mundo, basta la mayoría M. Nada pasa con menos de C votos a favor.',
'formula.reading': 'Con poca participación, hace falta casi la unanimidad; cuando vota todo el mundo, basta la mayoría M. Por debajo del quórum Q, los votantes que faltan cuentan como «no».',
'common.pct': '{n} %',
'settings.title': 'Ajustes de la fórmula',
'settings.intro': 'Para cambiar un texto, el grupo pide un número de votos a favor que depende de su electorado, de su participación habitual y de la inercia del artículo.',
'settings.usual': 'Participación habitual',
'settings.usual.median.one': '{n} votante de {electorate} ({pct}): la mediana de las {sample} propuestas votadas en el último año.',
'settings.usual.median.other': '{n} votantes de {electorate} ({pct}): la mediana de las {sample} propuestas votadas en el último año.',
'settings.usual.reference.one': '{n} votante de {electorate} ({pct}), dos veces C: la participación de referencia, hasta que el grupo haya votado {min} propuestas ({sample} por ahora).',
'settings.usual.reference.other': '{n} votantes de {electorate} ({pct}), dos veces C: la participación de referencia, hasta que el grupo haya votado {min} propuestas ({sample} por ahora).',
'settings.usual.reference.one': '{n} votante de {electorate} ({pct}), dos veces {q}, el quórum de referencia: la participación de referencia, hasta que el grupo haya votado {min} propuestas ({sample} por ahora).',
'settings.usual.reference.other': '{n} votantes de {electorate} ({pct}), dos veces {q}, el quórum de referencia: la participación de referencia, hasta que el grupo haya votado {min} propuestas ({sample} por ahora).',
'settings.usual.bounded.one': 'Mantenida entre el {lo} y el {hi} del electorado: el grupo pide sus votos a favor como si votara {n} persona ({pct}).',
'settings.usual.bounded.other': 'Mantenida entre el {lo} y el {hi} del electorado: el grupo pide sus votos a favor como si votaran {n} personas ({pct}).',
'settings.usual.when': 'Medida una vez al día, la última el {date}.',
@@ -390,15 +399,17 @@ export const es: Messages = {
'settings.levels': 'Los cinco niveles',
'settings.col.level': 'Nivel',
'settings.col.share': 'Proporción exigida',
'settings.col.quorum': 'Quórum',
'settings.col.required': 'Votos a favor',
'settings.current': 'este artículo',
'settings.share.note.one': 'Proporción exigida: los votos a favor pedidos más allá de C, con {n} votante como de costumbre.',
'settings.share.note.other': 'Proporción exigida: los votos a favor pedidos más allá de C, con {n} votantes como de costumbre.',
'settings.share.note.one': 'Proporción exigida: la parte de votos a favor pedida, con {n} votante como de costumbre.',
'settings.share.note.other': 'Proporción exigida: la parte de votos a favor pedida, con {n} votantes como de costumbre.',
'settings.quorum.note': 'Quórum: por debajo, los votantes que faltan cuentan como «no». Es el del nivel; el autor de un artículo puede fijar otro antes del primer voto.',
'settings.required.version.one': 'Votos a favor: con {n} votante, la participación de esta versión.',
'settings.required.version.other': 'Votos a favor: con {n} votantes, la participación de esta versión.',
'settings.required.usual.one': 'Votos a favor: con {n} votante, como de costumbre.',
'settings.required.usual.other': 'Votos a favor: con {n} votantes, como de costumbre.',
'settings.reading': 'En cada nivel, el grupo pide su parte de votos a favor más allá de C cuando vota como de costumbre; cuantos más votantes, más se acerca a M. Nada pasa con menos de C votos a favor.',
'settings.reading': 'En cada nivel, el grupo pide su parte de votos a favor cuando vota como de costumbre; cuantos más votantes, más se acerca a M. Por debajo del quórum del nivel, los votantes que faltan cuentan como «no».',
'history.title': 'Correcciones',
'history.initial': 'texto inicial',
@@ -470,7 +481,7 @@ export const es: Messages = {
'objection.respond.contredit': 'Reformular',
'objection.linked': 'Esta propuesta responde a una objeción: una vez enviada, la persona que la hizo la levanta o la reitera.',
'messages.title': 'Mensajes',
'messages.intro': 'Lo que espera un gesto tuyo: una objeción que responder, una corrección de errata que validar. Cada mensaje lleva su plazo; un mensaje tratado ya no pide nada.',
'messages.intro': 'Lo que espera un gesto tuyo: una objeción que responder, una corrección de errata que validar. Cada mensaje lleva su plazo; un mensaje tratado ya no pide nada. Aquí lees también qué fue de un voto que no se contó.',
'messages.none': 'Ningún mensaje por ahora.',
'messages.readAll': 'Marcar todo como leído',
'messages.read': 'Marcar como leído',
@@ -479,12 +490,16 @@ export const es: Messages = {
'messages.open': 'Verlo en el texto',
'messages.topic.objection': 'Objeción',
'messages.topic.typo': 'Errata',
'messages.topic.ballot': 'Voto',
'messages.objection_raised.redit': '{name} hace una objeción: tu versión «{held}» repite «{other}». Hasta que se levante, nadie puede votar a favor. Para responder, modifica el artículo que repite.',
'messages.objection_raised.contredit': '{name} hace una objeción: tu versión «{held}» contradice «{other}». Hasta que se levante, nadie puede votar a favor. Para responder, reformúlala.',
'messages.objection_reiterated': '{name} reitera su objeción: nadie puede votar aún a favor de «{held}». Responde de nuevo.',
'messages.objection_answered': 'Tu objeción sobre «{held}» recibió una respuesta. Levántala si te convence, o reitérala. Sin respuesta tuya, se levanta.',
'messages.typo_pending.author': '{name} propone una corrección de errata en tu versión «{title}».',
'messages.typo_pending.admin': '{name} propone una corrección de errata en «{title}», un texto del seed: un administrador la valida.',
'messages.ballot_expired': 'Tu voto del {date} ({articles}) no se vio en la cadena Ğ1 en 24 horas: queda descartado. Puedes votar de nuevo.',
'messages.ballot_refused': 'Tu voto del {date} ({articles}) no se contó: {reason} Puedes votar de nuevo.',
'messages.ballot_objection': 'Tu «a favor» sobre {articles} queda descartado: una objeción retenía esta versión cuando la cadena Ğ1 confirmó tu voto. Tu voto anterior se mantiene.',
'messages.respond': 'Responder',
'messages.outcome.answered': 'respuesta dada',
'messages.outcome.reiterated': 'objeción reiterada',
+26 -10
View File
@@ -33,7 +33,9 @@ export const fr = {
'nav.menu': 'Menu',
'nav.close': 'Fermer',
'home.title': 'Rendre possible la décision collective',
// The title on two lines, the break after « possible » (Yvv, 2026-11-02): one key per line.
'home.title.line1': 'Rendre possible',
'home.title.line2': 'la décision collective',
'home.leitmotiv.context': 'contexte et cadrage',
'home.leitmotiv.process': 'processus et modalités',
'home.leitmotiv.tools': 'boîte à outils',
@@ -125,6 +127,8 @@ export const fr = {
'doc.seed.long': "Entré en vigueur sans vote, pour ne pas partir d'une page blanche. Il reste sous vote, comme tout texte.",
'doc.seed.votes.one': 'seed a minima · {n} vote',
'doc.seed.votes.other': 'seed a minima · {n} votes',
'doc.seedProposal': 'Proposition seed',
'doc.seedProposal.long': "Texte de départ mis en avant pour lancer le vote. Il n'est pas en vigueur : il y entre par le vote, comme toute proposition.",
'doc.confirmed': 'confirmé par le vote',
'doc.adopted': 'adopté le {date}',
'doc.action.vote': 'Le vote',
@@ -137,7 +141,7 @@ export const fr = {
'doc.publish.pending': 'Publication en attente',
'deep.title': 'Propositions en cours',
'deep.note': "Pas encore en vigueur. Chacune remonte dès qu'elle atteint son seuil : au moins {floor} voix pour, puis la majorité que demande son inertie.",
'deep.note': "Pas encore en vigueur. Chacune remonte dès qu'elle atteint son seuil : la part de voix pour que demande son inertie, comptée sur au moins son quorum de votants.",
'deep.new': 'Proposer un nouvel article',
'deep.drafts': 'Tes brouillons',
'deep.drafts.note': 'Non soumis au vote. Toi seul les vois.',
@@ -255,6 +259,10 @@ export const fr = {
'vote.mine.chain': 'inscrit sur la chaîne Ğ1',
'vote.mine.saved': 'enregistré',
'vote.mine.pending': 'En attente de la chaîne Ğ1 : {choice}.',
'vote.mine.simulated': 'simulé',
'vote.simulated.note': "La chaîne Ğ1 n'est pas encore ouverte : ton vote est simulé. Il compte comme un vote, marqué « simulé », jusqu'à son effacement à l'ouverture de la chaîne.",
'vote.tally.simulated.one': 'dont {n} simulé',
'vote.tally.simulated.other': 'dont {n} simulés',
'vote.mine.refused': "Ton dernier vote n'a pas été compté : {reason}",
'vote.mine.expired': "Ton dernier vote n'a pas été vu sur la chaîne Ğ1 à temps : il ne compte pas.",
'vote.mine.receipts': 'Tes bulletins et reçus',
@@ -269,6 +277,7 @@ export const fr = {
'ballot.reason.not_member': "le portefeuille qui a signé n'est pas celui d'un membre de la toile de confiance Ğ1.",
'ballot.reason.no_identity': "le portefeuille qui a signé n'est rattaché à aucune identité Ğ1.",
'ballot.reason.transfer': 'le virement vers le compte des votes manque, ou son montant est trop faible.',
'ballot.reason.objection': "une objection retenait cette version quand la chaîne Ğ1 a confirmé ton vote ; ton vote d'avant reste.",
'basket.title': 'Panier de vote',
'basket.intro': 'Choisis article par article, une section entière ou tout le document : tout part en un seul geste.',
'basket.doc': 'Tout le document',
@@ -336,6 +345,7 @@ export const fr = {
'ballots.lines.other': '{n} votes',
'ballots.status.pending': 'En attente de la chaîne Ğ1',
'ballots.status.confirmed': "Compté pour l'identité Ğ1 « {name} »",
'ballots.status.simulated': "Simulé : compté pour « {name} » jusqu'à l'ouverture de la chaîne Ğ1",
'ballots.status.refused': 'Pas compté : {reason}',
'ballots.status.expired': 'Pas vu sur la chaîne Ğ1 à temps : il ne compte pas',
'ballots.block': 'bloc {n}',
@@ -375,17 +385,17 @@ export const fr = {
'formula.M': 'M — majorité à pleine participation : {n} %',
'formula.G': 'G — gradient : {n}',
'formula.B': 'B — base : {n}',
'formula.C': 'C — minimum de voix pour : {n}',
'formula.Q': 'Q — quorum : {n}',
'formula.R': 'R — seuil actuel : {n} voix pour',
'formula.reading': "À faible participation, il faut presque l'unanimité ; quand tout le monde vote, la majorité M suffit. Rien ne passe sous C voix pour.",
'formula.reading': "À faible participation, il faut presque l'unanimité ; quand tout le monde vote, la majorité M suffit. Sous le quorum Q, les votants qui manquent comptent comme « non ».",
'common.pct': '{n} %',
'settings.title': 'Réglages de la formule',
'settings.intro': "Pour changer un texte, le groupe demande un nombre de voix pour qui dépend de son électorat, de sa participation habituelle et de l'inertie de l'article.",
'settings.usual': 'Participation habituelle',
'settings.usual.median.one': '{n} votant sur {electorate} ({pct}) : la médiane des {sample} propositions votées depuis un an.',
'settings.usual.median.other': '{n} votants sur {electorate} ({pct}) : la médiane des {sample} propositions votées depuis un an.',
'settings.usual.reference.one': "{n} votant sur {electorate} ({pct}), deux fois C : la participation de référence, tant que le groupe n'a pas voté sur {min} propositions ({sample} pour l'instant).",
'settings.usual.reference.other': "{n} votants sur {electorate} ({pct}), deux fois C : la participation de référence, tant que le groupe n'a pas voté sur {min} propositions ({sample} pour l'instant).",
'settings.usual.reference.one': "{n} votant sur {electorate} ({pct}), deux fois {q}, le quorum de référence : la participation de référence, tant que le groupe n'a pas voté sur {min} propositions ({sample} pour l'instant).",
'settings.usual.reference.other': "{n} votants sur {electorate} ({pct}), deux fois {q}, le quorum de référence : la participation de référence, tant que le groupe n'a pas voté sur {min} propositions ({sample} pour l'instant).",
'settings.usual.bounded.one': "Ramenée entre {lo} et {hi} de l'électorat : le groupe demande ses voix pour comme si {n} personne votait ({pct}).",
'settings.usual.bounded.other': "Ramenée entre {lo} et {hi} de l'électorat : le groupe demande ses voix pour comme si {n} personnes votaient ({pct}).",
'settings.usual.when': 'Mesurée une fois par jour, la dernière fois le {date}.',
@@ -393,15 +403,17 @@ export const fr = {
'settings.levels': 'Les cinq crans',
'settings.col.level': 'Cran',
'settings.col.share': 'Part exigée',
'settings.col.quorum': 'Quorum',
'settings.col.required': 'Voix pour',
'settings.current': 'cet article',
'settings.share.note.one': "Part exigée : les « pour » demandés au-delà de C, à {n} votant comme d'habitude.",
'settings.share.note.other': "Part exigée : les « pour » demandés au-delà de C, à {n} votants comme d'habitude.",
'settings.share.note.one': "Part exigée : la part de « pour » demandée, à {n} votant comme d'habitude.",
'settings.share.note.other': "Part exigée : la part de « pour » demandée, à {n} votants comme d'habitude.",
'settings.quorum.note': "Quorum : en dessous, les votants qui manquent comptent comme « non ». C'est celui du cran ; l'auteur d'un article peut en fixer un autre avant le premier vote.",
'settings.required.version.one': 'Voix pour : à {n} votant, la participation de cette version.',
'settings.required.version.other': 'Voix pour : à {n} votants, la participation de cette version.',
'settings.required.usual.one': "Voix pour : à {n} votant, comme d'habitude.",
'settings.required.usual.other': "Voix pour : à {n} votants, comme d'habitude.",
'settings.reading': "À chaque cran, le groupe demande sa part de « pour » au-delà de C quand il vote comme d'habitude ; plus il y a de votants, plus elle se rapproche de M. Rien ne passe sous C voix pour.",
'settings.reading': "À chaque cran, le groupe demande sa part de « pour » quand il vote comme d'habitude ; plus il y a de votants, plus elle se rapproche de M. Sous le quorum du cran, les votants qui manquent comptent comme « non ».",
'history.title': 'Corrections',
'history.initial': 'texte initial',
@@ -473,7 +485,7 @@ export const fr = {
'objection.respond.contredit': 'Reformuler',
'objection.linked': "Cette proposition répond à une objection : une fois soumise, la personne qui l'a faite la lève ou la réitère.",
'messages.title': 'Messages',
'messages.intro': 'Ce qui attend un geste de toi : une objection à laquelle répondre, une correction de coquille à valider. Chaque message porte son échéance ; un message traité ne demande plus rien.',
'messages.intro': "Ce qui attend un geste de toi : une objection à laquelle répondre, une correction de coquille à valider. Chaque message porte son échéance ; un message traité ne demande plus rien. Tu y lis aussi ce qu'il advient d'un vote qui n'a pas été compté.",
'messages.none': "Aucun message pour l'instant.",
'messages.readAll': 'Tout marquer comme lu',
'messages.read': 'Marquer comme lu',
@@ -482,12 +494,16 @@ export const fr = {
'messages.open': 'Voir dans le texte',
'messages.topic.objection': 'Objection',
'messages.topic.typo': 'Coquille',
'messages.topic.ballot': 'Vote',
'messages.objection_raised.redit': "{name} fait une objection : ta version « {held} » redit « {other} ». Jusqu'à sa levée, personne ne peut plus voter pour elle. Pour répondre, modifie l'article visé.",
'messages.objection_raised.contredit': "{name} fait une objection : ta version « {held} » contredit « {other} ». Jusqu'à sa levée, personne ne peut plus voter pour elle. Pour répondre, reformule-la.",
'messages.objection_reiterated': '{name} réitère son objection : personne ne peut encore voter pour « {held} ». Réponds à nouveau.',
'messages.objection_answered': 'Ton objection sur « {held} » a reçu une réponse. Lève-la si elle te convient, ou réitère-la. Sans réponse de ta part, elle est levée.',
'messages.typo_pending.author': '{name} propose une correction de coquille dans ta version « {title} ».',
'messages.typo_pending.admin': '{name} propose une correction de coquille dans « {title} », un texte du seed : un administrateur la valide.',
'messages.ballot_expired': "Ton vote du {date} ({articles}) n'a pas été vu sur la chaîne Ğ1 en 24 heures : il est écarté. Tu peux voter à nouveau.",
'messages.ballot_refused': "Ton vote du {date} ({articles}) n'a pas été compté : {reason} Tu peux voter à nouveau.",
'messages.ballot_objection': "Ton « pour » sur {articles} est écarté : une objection retenait cette version quand la chaîne Ğ1 a confirmé ton vote. Ton vote d'avant reste.",
'messages.respond': 'Répondre',
'messages.outcome.answered': 'réponse donnée',
'messages.outcome.reiterated': 'objection réitérée',
+70 -8
View File
@@ -4,17 +4,19 @@ export type LangMap = Partial<Record<Lang, string>>;
export type Inertia = 1 | 2 | 3 | 4 | 5;
export const INERTIA_LEVELS: Inertia[] = [1, 2, 3, 4, 5];
/** One level on the group's scale: M and G of the formula, s_k, and the *for* votes asked at `cast` votes. */
/** One level on the group's scale: M and G of the formula, s_k, its quorum, and the *for* votes asked at `cast` votes. */
export interface LevelInfo {
level: Inertia;
majorityPct: number;
gradient: number;
sharePct: number;
/** Q — the quorum of the level (ADR 0013); from a version, its article's own if it has one. */
quorum?: number;
cast: number;
required: number | null;
}
/** p̃ — the usual turnout of the group, measured once a day (or the reference 2C/W). */
/** p̃ — the usual turnout of the group, measured once a day (or the reference 2Q₀/W, Q₀ = `voteFloor`). */
export interface ParticipationInfo {
ratio: number;
source: 'median' | 'reference';
@@ -60,11 +62,17 @@ export interface Tally {
export interface Evaluation {
cast: number;
/** Q — the quorum it is judged on (ADR 0013): below it, the voters missing count as against. */
quorum?: number;
required: number;
reached: boolean;
missing: number;
requiredRatio: number;
wellRated: boolean;
/** The total of *for* that passes with the *against* cast now. */
yesNeeded?: number;
/** The total of *against* that holds the *for* cast now in check (0: the quorum holds them). */
noNeeded?: number;
}
export interface Translation {
@@ -107,7 +115,16 @@ export interface Proposal {
enteredAt: string | null;
confirmedAt: string | null;
seedMinimal: boolean;
/** A seed proposal (ADR 0011): open, put forward by the seed to start the vote, not in force. */
seedProposal?: boolean;
/** The totals shown: recorded, pending and simulated votes, each intention in place of
* the vote its session sees as its own (ADR 0012). */
tally: Tally;
/** The part of `tally` still without proof (an intention pending on the chain). */
tallyPending: Tally;
/** The part of `tally` that comes from the simulation (ADR 0012). */
tallySimulated: Tally;
/** On the totals shown (`tally`), the quorum included (ADR 0013). */
evaluation: Evaluation | null;
votedAtInertia: Inertia;
/** Held back by a standing objection: the latest arrival of the two (never a text in force). */
@@ -116,11 +133,17 @@ export interface Proposal {
votable: boolean;
/** Standing objections around this version. */
objections: number;
/** What the person sees as her vote now: her latest gesture, an intention pending included. */
myVote: 'for' | 'against' | null;
/** Where « ton vote » stands (ADR 0012): `recorded` — proven by the chain, or a vote of
* the server in a managed group; `pending` — an intention waiting for the chain;
* `simulated` — a vote of the simulation, while the chain is closed. */
myVoteState: 'recorded' | 'pending' | 'simulated' | null;
myVoteStale: boolean;
/** « Ton vote » was cast on the Ğ1 chain (confirmed). */
myVoteChain: boolean;
/** A ballot of this session not counted yet: pending on the chain, or refused, or let go. */
/** A ballot of this session not counted yet: pending on the chain, or refused, or let go —
* or its *for* set aside by an objection (`refused`, reason `objection`, ADR 0012). */
myBallot: MyBallot | null;
translations: Partial<Record<Lang, Translation>>;
/** An order version: the order it gives its section now. */
@@ -144,7 +167,7 @@ export type Choice = 'for' | 'against' | 'none';
export interface MyBallot {
id: string;
status: 'pending' | 'refused' | 'expired';
reason: 'not_member' | 'no_identity' | 'transfer' | null;
reason: 'not_member' | 'no_identity' | 'transfer' | 'objection' | null;
choice: Choice;
}
@@ -164,8 +187,12 @@ export interface ChainSettings {
/** How this session votes in the group of the document, and whether it may now. */
export interface VoteAccess {
/** `server` too while the chain is closed: the vote is then simulated (`simulated`). */
way: 'server' | 'chain';
open: boolean;
/** The chain is closed: the vote leaves by a write of the server, marked as simulated,
* and counts until an act « supprime » sets it aside (ADR 0012). */
simulated: boolean;
reason: 'members_only' | 'chain_off' | null;
chain: ChainSettings | null;
/**
@@ -188,6 +215,8 @@ export interface Ballot {
voter: string | null;
entered: Entered[];
expiresAt: string;
/** `simulated`: a ballot of the simulation, proven by the server, never on the chain. */
mode: 'chain' | 'simulated';
}
/** A ballot of the person's history (`GET /api/ballots`): never anyone else's. */
@@ -202,6 +231,7 @@ export interface BallotEntry {
lines: number;
createdAt: string;
settledAt: string | null;
mode: Ballot['mode'];
}
export interface Unit {
@@ -219,6 +249,12 @@ export interface Unit {
members?: string[];
/** An order of fewer than two articles: nothing to decide, not shown. */
moot?: boolean;
/** Its own quorum, set by the author of its first version before the first vote; `null`: its level's (ADR 0013). */
quorum?: number | null;
/** The quorum of its level. */
quorumDefault?: number;
/** This session may set its quorum now (`PUT /units/{id}/quorum`). */
canSetQuorum?: boolean;
}
/** What a document decides besides its articles: its title, the order of each section. */
@@ -228,12 +264,16 @@ export interface Modules {
}
export interface Publication {
state: 'none' | 'pending' | 'done';
/** `suspended`: no article in force yet, the queue waits for the first text (ADR 0011). */
state: 'none' | 'pending' | 'suspended' | 'done';
configured: boolean;
since?: string;
doneAt?: string | null;
commit?: string | null;
attempts?: number;
/** Why it is suspended: no article in force yet (`text`, ADR 0011), or something in force
* entered by simulated votes that the proven votes do not carry yet (`proven`, ADR 0012). */
waitsFor?: 'text' | 'proven' | null;
}
export interface DocView {
@@ -248,7 +288,10 @@ export interface DocView {
title: LangMap;
summary: LangMap;
sourceLang: Lang;
/** Q₀ — the reference quorum (the C of before): the reference turnout is 2Q₀/W. */
voteFloor: number;
/** The quorum of each level, 1 to 5 (ADR 0013). */
quorums?: number[];
electorate: number;
electorateSource: 'wot' | 'group';
formula: { base: number };
@@ -342,11 +385,29 @@ export interface TypoFix {
version?: { id: string; title: string; status: Proposal['status']; unitId: string; code: string | null };
}
/** A message of one's box: about an objection or a typo fix, with its gestures now. */
/** A ballot set aside, as a message tells it (ADR 0012): line by line, what became of it. */
export interface BallotNotice {
id: string;
status: Ballot['status'];
reason: MyBallot['reason'];
mode: Ballot['mode'];
createdAt: string;
settledAt: string | null;
lines: { proposalId: string; unitId: string; code: string; title: string; choice: Choice; outcome: 'counted' | 'superseded' | 'objection' | null }[];
}
/** A message of one's box: about an objection, a typo fix or a ballot set aside, with its gestures now. */
export interface Message {
id: string;
topic: 'objection' | 'typo';
kind: 'objection_raised' | 'objection_answered' | 'objection_reiterated' | 'typo_pending';
topic: 'objection' | 'typo' | 'ballot';
kind:
| 'objection_raised'
| 'objection_answered'
| 'objection_reiterated'
| 'typo_pending'
| 'ballot_expired'
| 'ballot_refused'
| 'ballot_objection';
cycle: number;
createdAt: string;
dueAt: string | null;
@@ -358,4 +419,5 @@ export interface Message {
doc: { id: string; groupSlug: string; slug: string; title: LangMap };
objection?: Objection;
typo?: TypoFix;
ballot?: BallotNotice;
}
+121 -39
View File
@@ -1,17 +1,28 @@
<script lang="ts">
// The welcome (Yvv, 2026-11-02): the keystone arch, large enough to shelter the three points of
// the leitmotiv, the toolbox first; then the title, centred, broken after « possible ».
import sign from '$lib/assets/cle-de-voute.svg?raw';
import GroupChoice from '$lib/components/GroupChoice.svelte';
import { t } from '$lib/i18n/index.svelte';
import { session } from '$lib/session.svelte';
const LEITMOTIV = ['home.leitmotiv.tools', 'home.leitmotiv.context', 'home.leitmotiv.process'] as const;
</script>
<div class="home">
<header class="intro">
<h1>{t('home.title')}</h1>
<ul class="leitmotiv">
<li>{t('home.leitmotiv.context')}</li>
<li>{t('home.leitmotiv.process')}</li>
<li>{t('home.leitmotiv.tools')}</li>
</ul>
<div class="vault">
<span class="sign" aria-hidden="true">{@html sign}</span>
<ul class="leitmotiv">
{#each LEITMOTIV as key, i (key)}
<li style:--i={i}>{t(key)}</li>
{/each}
</ul>
</div>
<h1>
<span class="line">{t('home.title.line1')}</span>
<span class="line">{t('home.title.line2')}</span>
</h1>
</header>
{#if session.me}
@@ -28,56 +39,127 @@
.home {
max-width: 44rem;
margin: 0 auto;
padding: clamp(3rem, 10vh, 6.5rem) clamp(1.2rem, 5vw, 3.5rem) 5rem;
padding: clamp(2rem, 7vh, 4.5rem) clamp(1.2rem, 5vw, 3.5rem) 5rem;
display: grid;
gap: clamp(2.4rem, 6vh, 3.6rem);
}
.intro h1 {
font-family: var(--font-text);
font-weight: 500;
font-size: clamp(2.1rem, 1.4rem + 3vw, 3.4rem);
letter-spacing: -0.015em;
line-height: 1.08;
font-variation-settings: 'opsz' 72;
max-width: 15ch;
text-wrap: balance;
/* The arch, drawn by the sign of the logo (stones in the ink of the mood, the keystone in
`--cle`), never tilted. Its box is the stone itself: 92 units wide (the base slab, x from
−46 to 46) and 59 high (y from −40, the top of the keystone, to 19, under the slab); the SVG's
own 3-unit margin spills out above and below (clipped on the sides, where it is empty). On a
phone the arch reaches into the side padding, 0.75rem short of the screen's edges, so that the
opening keeps as much room as it can. */
.vault {
--w: min(34rem, 100vw - 1.5rem);
container-type: inline-size;
position: relative;
width: var(--w);
margin-inline: calc(50% - var(--w) / 2);
aspect-ratio: 92 / 59;
overflow-x: clip;
color: var(--ink);
}
/* The leitmotiv: three lines, each set on a small stone of the keystone's colour. */
.sign {
position: absolute;
inset: calc(-300% / 59) calc(-300% / 92);
}
.sign :global(svg) {
display: block;
width: 100%;
height: 100%;
}
/* The light under the vault: the opening (x from −18.5 to 18.5, from the keystone down to the
slab), its top a half circle, washed with the keystone's colour. */
.vault::before {
content: '';
position: absolute;
inset: calc(2150% / 59) calc(2750% / 92) calc(340% / 59);
border-radius: 50% 50% 0 0 / 54.25% 54.25% 0 0;
background: radial-gradient(
ellipse 75% 62% at 50% 60%,
color-mix(in srgb, var(--cle) 12%, transparent),
transparent
);
}
/* The three points, sheltered: centred in the opening (x from −19 to 19; a band from y = −19,
under the keystone, to 19, centred on the springing line y = 0), one line each. The type
follows the arch (cqi): the longest line of the three languages, 10.5 em (es), holds in the
38 units of the opening with room to spare. On a phone the italic is small (12px at 360):
its advances are kept exact, not rounded to the pixel, or the letters drift apart
(« pr ocessus ») and the word spaces close up. */
.leitmotiv {
list-style: none;
margin: 1.4rem 0 0;
position: absolute;
inset: calc(2100% / 59) calc(2700% / 92) 0;
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 0.2rem;
align-content: center;
justify-items: center;
gap: 0.2em;
font-family: var(--font-text);
font-style: italic;
font-size: clamp(1.12rem, 1rem + 0.55vw, 1.4rem);
line-height: 1.45;
font-size: 3.55cqi;
line-height: 1.3;
color: var(--ink-soft);
white-space: nowrap;
text-align: center;
text-rendering: geometricPrecision;
}
.leitmotiv li {
display: flex;
align-items: baseline;
gap: 0.7rem;
.intro h1 {
margin-top: clamp(1.4rem, 4vh, 2.4rem);
font-family: var(--font-text);
font-weight: 500;
font-size: min(3.3rem, 8.6vw);
letter-spacing: -0.015em;
line-height: 1.1;
font-variation-settings: 'opsz' 72;
text-align: center;
}
.leitmotiv li::before {
content: '';
flex: none;
width: 0.62em;
height: 0.34em;
border-radius: 0.1em;
background: var(--cle);
opacity: 0.75;
translate: 0 -0.12em;
.line {
display: block;
text-wrap: balance;
}
.enter {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.6rem 1.2rem;
display: grid;
justify-items: center;
gap: 0.8rem;
text-align: center;
}
.enter .btn {
min-height: 2.75rem;
padding-inline: 1.4rem;
}
.enter p {
font-size: 0.9rem;
max-width: 34ch;
}
/* The keystone sets the arch, then the three points come under it. */
@media (prefers-reduced-motion: no-preference) {
.sign :global(path[fill^='var(--cle']) {
transform-box: fill-box;
animation: settle 0.7s var(--ease) 0.1s backwards;
}
.leitmotiv li {
animation: appear 0.6s var(--ease) backwards;
animation-delay: calc(0.55s + var(--i) * 0.12s);
}
}
@keyframes settle {
from {
transform: translateY(-40%);
opacity: 0;
}
}
@keyframes appear {
from {
transform: translateY(0.4em);
opacity: 0;
}
}
</style>
+5 -2
View File
@@ -27,6 +27,8 @@
function standing(b: BallotEntry): string {
if (b.status === 'pending') return t('ballots.status.pending');
// A vote of the simulation (ADR 0012): counted for the session, never on the chain.
if (b.status === 'confirmed' && b.mode === 'simulated') return t('ballots.status.simulated', { name: b.voter ?? '' });
if (b.status === 'confirmed') return t('ballots.status.confirmed', { name: b.voter ?? '' });
if (b.status === 'refused') return t('ballots.status.refused', { reason: t(`ballot.reason.${b.reason ?? 'transfer'}`) });
return t('ballots.status.expired');
@@ -62,7 +64,7 @@
{#each data.value.ballots as b (b.id)}
<li class="card block ballot">
<p class="small faint">{i18n.date(b.createdAt)} · {titleOf(b.doc.title)} · {i18n.plural('ballots.lines', b.lines)}</p>
<p class="state {b.status}">
<p class="state {b.status}" class:simulated={b.mode === 'simulated'}>
{standing(b)}{#if b.block !== null}<span class="faint"> · {t('ballots.block', { n: i18n.number(b.block) })}</span>{/if}
</p>
<code class="comment">{b.comment}</code>
@@ -159,7 +161,8 @@
.state.confirmed {
color: var(--moss);
}
.state.pending {
.state.pending,
.state.confirmed.simulated {
color: var(--water);
}
.state.refused,
@@ -192,6 +192,8 @@
<section class="text" aria-label={t('doc.view.inforce')}>
{#if doc.mode === 'inforce' || !doc.preview}
<!-- No article in force (the seed proposals are not, ADR 0011): said once, above the orders. -->
{#if !doc.inForce.length}<p class="muted">{t('doc.empty')}</p>{/if}
{#each doc.inForceBySection as s, i (`${i}:${s.section}`)}
{#each s.units as u (u.id)}
{#if u.inForce}
@@ -224,8 +226,6 @@
{#snippet fold()}{@render foldOf(o, op)}{/snippet}
</OrderModule>
{/if}
{:else}
<p class="muted">{t('doc.empty')}</p>
{/each}
{:else}
{#each doc.preview as pu (pu.unitId)}
+22 -2
View File
@@ -2,7 +2,8 @@
// The box « Messages » of trustinWallet, in the application (2026-10-31, lot F,
// ADR 0010): what waits for a gesture of the person — an objection to answer,
// to lift or to reiterate, a typo fix to validate or refuse —, each with its
// deadline. A message dealt with offers no gesture any more. No mail.
// deadline. A message dealt with offers no gesture any more. No mail. A ballot set
// aside (ADR 0012) is told here too, without gesture: one votes again in the text.
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { api } from '$lib/api';
@@ -44,10 +45,27 @@
await reload();
}
/** The lines of a ballot a message is about: those set aside by an objection, or all. */
function linesOf(m: Message) {
const lines = m.ballot?.lines ?? [];
const held = lines.filter((l) => l.outcome === 'objection');
return m.kind === 'ballot_objection' && held.length ? held : lines;
}
/** The text of a message, in the language of the reader, from its subject. */
function textOf(m: Message): string {
const o = m.objection;
const fix = m.typo;
const b = m.ballot;
if (b) {
const articles = linesOf(m)
.map((l) => l.code)
.join(', ');
const date = i18n.date(b.createdAt);
if (m.kind === 'ballot_expired') return t('messages.ballot_expired', { date, articles });
if (m.kind === 'ballot_refused') return t('messages.ballot_refused', { date, articles, reason: t(`ballot.reason.${b.reason ?? 'transfer'}`) });
return t('messages.ballot_objection', { articles });
}
if (o) {
const held = o.heldVersion?.title ?? '';
if (m.kind === 'objection_raised') return t(`messages.objection_raised.${o.kind}`, { name: o.objector ?? '', held, other: o.other.title ?? '' });
@@ -73,6 +91,8 @@
function hrefOf(m: Message): string {
const v = m.objection?.heldVersion ?? m.typo?.version ?? null;
const base = `/g/${m.doc.groupSlug}/docs/${m.doc.slug}`;
const line = linesOf(m)[0];
if (line) return `${base}?${panelSearch({ tab: 'details', unitId: line.unitId, proposalId: line.proposalId }, null)}`;
if (!v) return base;
return `${base}?${panelSearch({ tab: 'details', unitId: v.unitId ?? null, proposalId: v.id }, null)}`;
}
@@ -149,7 +169,7 @@
<p class="text">{textOf(m)}</p>
{#if m.objection?.reason && m.kind !== 'objection_answered'}<p class="reason">{m.objection.reason}</p>{/if}
{#if m.typo}<DiffText parts={condense(m.typo.diff)} />{/if}
{#if m.done}
{#if m.done && m.topic !== 'ballot'}
<p class="outcome">{outcomeOf(m)}</p>
{:else if m.dueAt}
<p class="due">{t('messages.due', { date: i18n.date(m.dueAt) })}</p>