v3 : socle Rust (moteur pur, accès trustinWallet, serveur) ; maquette Nuxt rangée dans maquette/

- crates/ld-engine : formule inertielle (parité avec la maquette, garde C),
  coquille / changement de fond, assistant d'édition à règles, composition
  d'un document sous vote permanent (adoptions, aperçus, rivales).
- crates/trustin-access : qui voit, qui ouvre, qui gère les accès.
- crates/ld-server : API axum + SQLite, connexion Ğ1 (client OAuth2 de
  connect.monnaie-libre.fr), invitations, entrée en vigueur automatique,
  signalements de contradiction, traductions éditoriales, publication git,
  taille de la toile lue sur la chaîne.
- seeds/acte-engagement-certification.json : 33 articles, fr + en + es ;
  en vigueur : préambule, E1, E2 (seed a minima).
- maquette/ : frontend, backend et docker v2 déplacés tels quels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-21 02:04:42 +02:00
co-authored by Claude Opus 5.5
parent 4b1732916d
commit 67ddee28b2
275 changed files with 8979 additions and 0 deletions
+322
View File
@@ -0,0 +1,322 @@
<script setup lang="ts">
/**
* « / — Aujourd'hui » : capture + Fil personnel — la boîte de réception de la
* démocratie. Tout est sélecteur Pinia pur (useFeed) ; les sections vides sont
* masquées ; le compteur est nu — jamais de rouge.
*/
import type { Decision } from '~/types/domain'
import {
BOUNDARY_SUSPENDED,
CRYSTALLIZE_CARD,
DOSSIER_COMPLETE_CARD,
FEED_HEADER,
FIRST_DECISION,
ICONS,
MATURATION_CARD,
REVIEW_TITLE,
SCOPE_KEEP,
SCOPE_WIDEN,
} from '~/lexicon'
import { useCollectiveStore } from '~/stores/collective'
import { useDecisionsStore } from '~/stores/decisions'
import { useFeed } from '~/composables/useFeed'
const col = useCollectiveStore()
const decisions = useDecisionsStore()
const {
objectionWindows, adviceRequests, openVotes, toCrystallize, dossiersComplete,
tiesToBreak, reviewsDue, mandateReportsDue, boundaryObjections,
overflowingScopes, prioritiesAsked, suggestions, collectiveActivity, myCount,
} = useFeed()
const dossierError = ref('')
const allEmpty = computed(
() => myCount.value === 0 && suggestions.value.length === 0
&& collectiveActivity.value.length === 0,
)
/** The proposed founding decision, still in preparation. */
const pactPending = computed(() =>
col.decisions.some(d => d.title === FIRST_DECISION && d.status === 'draft'),
)
const decisionById = (id: string): Decision | undefined =>
col.decisions.find(d => d.id === id)
const personName = (id: string): string =>
col.people.find(p => p.id === id)?.displayName ?? 'Quelqu\'un'
const dateFr = (iso: string): string =>
new Date(iso).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long' })
/** Steward gesture: framing → closed once every element is terminal. */
function closeDossier(decision: Decision) {
const res = decisions.transition(decision.id, 'closed')
dossierError.value = res.ok ? '' : res.reason
}
</script>
<template>
<!-- ld-v2 -->
<div class="feed-page">
<FeedCapture />
<header class="feed-header">
<h1 class="feed-header__title">{{ FEED_HEADER }}</h1>
<span v-if="myCount > 0" class="feed-header__count">{{ myCount }}</span>
</header>
<!-- État vide global — invitation chaleureuse -->
<div v-if="allEmpty" class="ld-card feed-empty">
<UIcon name="i-lucide-sun-medium" class="feed-empty__icon" />
<p class="feed-empty__line">
Rien ne t'attend — c'est le bon moment pour capturer ta première décision.
</p>
<p class="feed-empty__hint">
Une phrase suffit, là-haut. Le chemin s'occupe du reste.
</p>
<NuxtLink
v-if="pactPending"
class="ld-btn"
:to="{ path: '/decider', query: { titre: FIRST_DECISION } }"
>
<UIcon name="i-lucide-feather" />
<span>{{ FIRST_DECISION }}</span>
</NuxtLink>
</div>
<template v-else>
<!-- Frontières contestées — priorité haute, en tête -->
<FeedSection
v-if="boundaryObjections.length > 0"
title="Frontières contestées — à traiter d'abord"
icon="i-lucide-octagon-pause"
priority
>
<FeedItemCard
v-for="o in boundaryObjections"
:key="o.id"
:title="decisionById(o.decisionId)?.title ?? 'Décision'"
icon="i-lucide-octagon-pause"
tint="var(--mood-status-fenetre)"
:to="`/decisions/${o.decisionId}`"
>
<template #meta>
<span class="status-pill status-suspended">{{ BOUNDARY_SUSPENDED }}</span>
</template>
<p class="feed-quote">{{ personName(o.personId) }} : « {{ o.argument }} »</p>
</FeedItemCard>
</FeedSection>
<!-- Fenêtres d'objection -->
<FeedSection
v-if="objectionWindows.length > 0"
title="Fenêtres d'objection"
icon="i-lucide-timer"
>
<FeedWindowCard v-for="d in objectionWindows" :key="d.id" :decision="d" />
</FeedSection>
<!-- Avis demandés -->
<FeedSection
v-if="adviceRequests.length > 0"
title="Avis demandés"
icon="i-lucide-message-circle"
>
<FeedAdviceCard v-for="d in adviceRequests" :key="d.id" :decision="d" />
</FeedSection>
<!-- Votes ouverts -->
<FeedSection v-if="openVotes.length > 0" title="Votes ouverts" icon="i-lucide-users">
<FeedVoteCard
v-for="item in openVotes"
:key="item.session.id"
:session="item.session"
:decision="item.decision"
/>
</FeedSection>
<!-- À cristalliser — le geste du garant -->
<FeedSection v-if="toCrystallize.length > 0" title="À cristalliser" :icon="ICONS.crystallize">
<FeedItemCard
v-for="item in toCrystallize"
:key="item.session.id"
:title="item.decision?.title ?? 'Réglage collectif'"
:icon="ICONS.crystallize"
tint="var(--mood-accent)"
:to="item.decision ? `/decisions/${item.decision.id}/vote` : undefined"
>
<p class="feed-line">{{ CRYSTALLIZE_CARD }}</p>
</FeedItemCard>
</FeedSection>
<!-- Dossiers complets — clore est un geste -->
<FeedSection v-if="dossiersComplete.length > 0" title="Dossiers complets" icon="i-lucide-folder-check">
<FeedItemCard
v-for="d in dossiersComplete"
:key="d.id"
:title="d.title"
icon="i-lucide-folder-check"
tint="var(--mood-status-vigueur)"
:to="`/decisions/${d.id}`"
>
<p class="feed-line">{{ DOSSIER_COMPLETE_CARD }}</p>
<p v-if="dossierError" class="feed-error">{{ dossierError }}</p>
<template #actions>
<button class="ld-btn feed-btn" type="button" @click="closeDossier(d)">
<UIcon name="i-lucide-stamp" />
<span>Clore et publier</span>
</button>
</template>
</FeedItemCard>
</FeedSection>
<!-- Égalités à départager — jamais par l'outil -->
<FeedSection v-if="tiesToBreak.length > 0" title="Égalités à départager" icon="i-lucide-scale">
<FeedItemCard
v-for="item in tiesToBreak"
:key="item.session.id"
:title="item.decision?.title ?? 'Élection'"
icon="i-lucide-scale"
tint="var(--mood-status-vote)"
:to="item.decision ? `/decisions/${item.decision.id}` : undefined"
>
<p class="feed-line">Égalité constatée — vous départagez, jamais l'outil.</p>
</FeedItemCard>
</FeedSection>
<!-- L'épreuve du réel -->
<FeedSection v-if="reviewsDue.length > 0" :title="REVIEW_TITLE" icon="i-lucide-flask-conical">
<FeedReviewCard v-for="d in reviewsDue" :key="d.id" :decision="d" />
</FeedSection>
<!-- Rapports de mandat -->
<FeedSection v-if="mandateReportsDue.length > 0" title="Rapports de mandat" icon="i-lucide-key-round">
<FeedItemCard
v-for="item in mandateReportsDue"
:key="item.mandate.id"
:title="item.mandate.title"
icon="i-lucide-key-round"
tint="var(--route-mandate)"
:to="`/mandats/${item.mandate.id}`"
>
<p class="feed-line">Un rapport est attendu depuis le {{ dateFr(item.dueAt) }}.</p>
</FeedItemCard>
</FeedSection>
<!-- Périmètres qui débordent -->
<FeedSection v-if="overflowingScopes.length > 0" title="Périmètres qui débordent" icon="i-lucide-expand">
<FeedItemCard
v-for="d in overflowingScopes"
:key="d.id"
:title="d.title"
icon="i-lucide-expand"
tint="var(--mood-status-fenetre)"
>
<p class="feed-line">Plus de monde se déclare concerné que prévu — décide du périmètre.</p>
<template #actions>
<NuxtLink class="ld-btn feed-btn" :to="`/decisions/${d.id}?geste=elargir`">
{{ SCOPE_WIDEN }}
</NuxtLink>
<NuxtLink class="ld-btn ld-btn--ghost feed-btn" :to="`/decisions/${d.id}?geste=maintenir`">
{{ SCOPE_KEEP }}
</NuxtLink>
</template>
</FeedItemCard>
</FeedSection>
<!-- Pondère tes enjeux -->
<FeedSection v-if="prioritiesAsked.length > 0" title="Pondère tes enjeux" icon="i-lucide-sliders-horizontal">
<FeedItemCard
v-for="c in prioritiesAsked"
:key="c.id"
:title="decisionById(c.decisionId)?.title ?? 'Élément de dossier'"
icon="i-lucide-sliders-horizontal"
tint="var(--mood-tertiary)"
:to="`/decisions/${c.decisionId}`"
>
<p class="feed-line">Dis au dossier combien cet élément compte pour toi (0–3).</p>
</FeedItemCard>
</FeedSection>
<!-- Boucle apprenante — cartes discrètes -->
<FeedSection v-if="suggestions.length > 0" title="Boucle apprenante" icon="i-lucide-repeat" discrete>
<template v-for="(s, i) in suggestions" :key="i">
<FeedItemCard
v-if="s.kind === 'claim-mandate'"
:title="`${s.count}ᵉ fois sur « ${s.tags[0]} » — réclame un mandat`"
:icon="ICONS.recurrence"
to="/mandats/nouveau"
/>
<FeedItemCard
v-else-if="s.kind === 'protocolize'"
:title="MATURATION_CARD"
icon="i-lucide-sprout"
to="/textes"
>
<p class="feed-line">{{ s.count }} consignations proches sur « {{ s.tags[0] }} ».</p>
</FeedItemCard>
<FeedItemCard
v-else
:title="`Le protocole « ${s.protocol.name} » n'a jamais servi — élague ?`"
icon="i-lucide-scissors"
to="/textes/formules"
/>
</template>
</FeedSection>
<!-- Activité du collectif -->
<FeedSection v-if="collectiveActivity.length > 0" title="Activité du collectif" icon="i-lucide-activity">
<FeedActivityCard v-for="d in collectiveActivity" :key="d.id" :decision="d" />
</FeedSection>
</template>
</div>
</template>
<style scoped>
.feed-page {
width: 100%;
max-width: 44rem;
margin: 0 auto;
}
.feed-header {
display: flex;
align-items: baseline;
gap: 0.625rem;
margin: 0.75rem 0 1.25rem;
}
.feed-header__title {
margin: 0;
font-size: clamp(1.375rem, 4vw, 1.75rem);
font-weight: 800;
letter-spacing: -0.02em;
}
.feed-header__count {
font-size: 1.0625rem;
font-weight: 700;
color: var(--mood-text-muted);
}
.feed-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
padding: clamp(2rem, 6vw, 3.5rem) 1.5rem;
text-align: center;
}
.feed-empty__icon { font-size: 2rem; color: var(--mood-accent); }
.feed-empty__line {
margin: 0;
font-size: 1.125rem;
font-weight: 700;
max-width: 26rem;
}
.feed-empty__hint {
margin: 0;
font-size: 0.9375rem;
color: var(--mood-text-muted);
}
.feed-line { margin: 0; font-size: 0.875rem; color: var(--mood-text-muted); }
.feed-quote { margin: 0; font-size: 0.875rem; font-style: italic; }
.feed-error { margin: 0; font-size: 0.8125rem; color: var(--mood-error); }
.feed-btn { font-size: 0.875rem; padding: 0.4rem 1rem; text-decoration: none; }
</style>