Files
decision/frontend/app/components/votes/VoteInstruct.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

144 lines
6.9 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">
// <!-- ld-v2 --> Bloc « S'instruire » condensé — résumé toujours visible
// (titre, effets recherchés, ce que ça engage) + dépliables (texte, avis et
// objections, provenance). Auto-replié aux visites suivantes (localStorage).
import { useCollectiveStore } from '~/stores/collective'
import { BASELINE_PREFIX, ENGAGES_LABEL, INSTRUCT_BLOCK, ROUTE_LABELS } from '~/lexicon'
import type { Decision, Id } from '~/types/domain'
const props = defineProps<{ decision: Decision; secret?: boolean }>()
const col = useCollectiveStore()
const open = ref(true)
onMounted(() => {
const key = `ld2-instruct-${props.decision.id}`
if (localStorage.getItem(key)) open.value = false
else localStorage.setItem(key, '1')
})
const effects = computed(() => props.decision.brief?.effects ?? [])
const advices = computed(() => col.advices.filter(a => a.decisionId === props.decision.id))
const objections = computed(() => col.objections.filter(o => o.decisionId === props.decision.id))
const parent = computed(() => col.decisions.find(d => d.id === props.decision.parentDecisionId))
function name(id: Id): string {
return col.people.find(p => p.id === id)?.displayName ?? '—'
}
function day(iso: string): string {
return new Date(iso).toLocaleDateString('fr-FR')
}
const POSITION_LABELS = { favorable: 'favorable', reserved: 'réservé', unfavorable: 'défavorable' } as const
const OBJECTION_STATUS = { open: 'ouverte', withdrawn: 'levée', integrated: 'intégrée', escalated: 'escaladée' } as const
</script>
<template>
<!-- ld-v2 -->
<section class="ld-card vi">
<header class="vi__head">
<h2 class="vi__title">
<UIcon name="i-lucide-book-open" />
<span>{{ INSTRUCT_BLOCK }}</span>
</h2>
<button class="ld-btn ld-btn--quiet vi__toggle" type="button" @click="open = !open">
<span>{{ open ? 'Replier' : 'Voir le détail' }}</span>
<UIcon :name="open ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'" />
</button>
</header>
<!-- Résumé compact toujours visible -->
<p class="vi__sentence">{{ decision.title }}</p>
<ul v-if="effects.length" class="vi__effects">
<li v-for="(effect, i) in effects" :key="i">
<UIcon name="i-lucide-target" class="vi__effect-icon" />
<span>{{ effect.label }}</span>
<span v-if="effect.target" class="vi__target">{{ effect.target }}</span>
</li>
</ul>
<p v-if="decision.resources?.note" class="vi__engages">
<span class="vi__engages-label">{{ ENGAGES_LABEL }}</span>
<span>{{ decision.resources.note }}</span>
<strong v-if="decision.resources.amount !== undefined">
{{ decision.resources.amount.toLocaleString('fr-FR') }} {{ decision.resources.unit ?? '' }}
</strong>
</p>
<!-- Dépliables -->
<div v-if="open" class="vi__details">
<details v-if="decision.body || decision.baselineNote">
<summary>Le texte</summary>
<p v-if="decision.baselineNote" class="vi__baseline">
{{ BASELINE_PREFIX }} {{ decision.baselineNote }}
</p>
<p class="vi__body">{{ decision.body ?? decision.title }}</p>
</details>
<details v-if="advices.length || objections.length">
<summary>Avis et objections ({{ advices.length + objections.length }})</summary>
<ul class="vi__list">
<li v-for="advice in advices" :key="advice.id">
<span class="vi__who">{{ secret ? 'quelquun' : name(advice.personId) }}</span>
<span class="vi__pos">{{ POSITION_LABELS[advice.position] }}</span>
<span v-if="advice.note" class="vi__note">{{ advice.note }}</span>
</li>
<li v-for="objection in objections" :key="objection.id">
<span class="vi__who">{{ secret ? 'quelquun' : name(objection.personId) }}</span>
<span class="vi__pos vi__pos--objection">objection {{ OBJECTION_STATUS[objection.status] }}</span>
<span class="vi__note">{{ objection.argument }}</span>
</li>
</ul>
</details>
<details>
<summary>Provenance</summary>
<ul class="vi__list">
<li>Proposé par {{ name(decision.authorId) }} le {{ day(decision.createdAt) }}</li>
<li>Chemin : {{ ROUTE_LABELS[decision.route] }}</li>
<li v-if="parent">
Chaînée à
<NuxtLink :to="`/decisions/${parent.id}`" class="vi__link">{{ parent.title }}</NuxtLink>
</li>
<li v-if="decision.decidedHow">{{ decision.decidedHow }}</li>
</ul>
</details>
</div>
</section>
</template>
<style scoped>
.vi { padding: 1.1rem 1.25rem; display: flex; flex-direction: column; gap: 0.6rem; }
.vi__head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.vi__title {
display: inline-flex; align-items: center; gap: 0.45rem; margin: 0;
font-size: 0.9375rem; font-weight: 700; color: var(--mood-accent);
}
.vi__toggle { padding: 0.25rem 0.75rem; font-size: 0.8125rem; }
.vi__sentence { margin: 0; font-weight: 700; font-size: 1.0625rem; line-height: 1.35; }
.vi__effects { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; }
.vi__effects li { display: flex; align-items: baseline; gap: 0.45rem; font-size: 0.9375rem; }
.vi__effect-icon { color: var(--mood-tertiary); flex-shrink: 0; transform: translateY(2px); }
.vi__target {
font-size: 0.75rem; font-weight: 700; color: var(--mood-tertiary);
background: color-mix(in srgb, var(--mood-tertiary) 12%, transparent);
padding: 1px 8px; border-radius: var(--r-pill); white-space: nowrap;
}
.vi__engages {
margin: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4rem;
font-size: 0.9375rem; color: var(--mood-text);
background: var(--mood-accent-soft); border-radius: var(--r-input); padding: 0.5rem 0.75rem;
}
.vi__engages-label { font-size: 0.75rem; font-weight: 700; color: var(--mood-accent); text-transform: uppercase; letter-spacing: 0.04em; }
.vi__details { display: flex; flex-direction: column; gap: 0.35rem; }
.vi__details details { background: var(--mood-bg); border-radius: var(--r-input); padding: 0.55rem 0.8rem; }
.vi__details summary { cursor: pointer; font-weight: 700; font-size: 0.875rem; color: var(--mood-text-muted); user-select: none; }
.vi__baseline { font-size: 0.875rem; color: var(--mood-text-muted); font-style: italic; margin: 0.5rem 0 0.25rem; }
.vi__body { white-space: pre-wrap; font-size: 0.9375rem; line-height: 1.5; margin: 0.5rem 0 0; }
.vi__list { list-style: none; margin: 0.5rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; font-size: 0.875rem; }
.vi__who { font-weight: 700; margin-right: 0.35rem; }
.vi__pos { color: var(--mood-tertiary); font-weight: 600; margin-right: 0.35rem; }
.vi__pos--objection { color: var(--mood-warning); }
.vi__note { color: var(--mood-text-muted); }
.vi__link { color: var(--mood-accent); font-weight: 600; text-decoration: none; }
.vi__link:hover { text-decoration: underline; }
</style>