Files
decision/maquette/frontend/app/pages/index.vue
T
YvvandClaude Opus 5.5 67ddee28b2 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>
2026-10-21 02:04:42 +02:00

323 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>