Files
decision/frontend/app/pages/index.vue
T
YvvandClaude Fable 5 e164b5f6c1 v2 : les 14 écrans — le tour complet de la démocratie d'exercice
- Aujourd'hui (Fil 13 sections + capture sticky) + Le chemin (tunnel 2 gestes,
  Q0 inline, 3 chips, dérogation asymétrique, alternatives réglage/consignation)
- Registre + fiche décision (timeline, périmètre premier/second lieu auditable,
  affluence non-ignorable, S'instruire condensé, éléments + cartographie de
  clôture, épreuve du réel, Remettre en question, PV A4, gravure)
- Salle de vote 5 modalités (consentement, nuancé+histogramme, binaire hérité
  avec jauge inertielle, Réglage collectif complet — faisceau, médiane basse,
  Pour moi, Explorer, cristallisation-geste —, élection à départage humain)
- Textes (Pacte en clair, document vivant, diff, vue projetée, Atelier des
  formules porté du v1 sur le moteur unique) + Mandats (faits comptés, feux
  de la rampe, wizard 3 étapes) + Observatoire (consigner→observer→protocoliser)
- Onboarding 7 gabarits + Données locales (export/import, attributs, atelier)
- voting→framing gardé (Reformuler d'un réglage figé non cristallisé)
- Pages v1 supprimées (login, documents, mandates, protocols, sanctuary,
  tools, decisions/new) — 342 tests verts, build zéro erreur

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 13:47:20 +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, -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 (03).</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>