forked from yvv/decision
- overlays Nuxt UI (UModal/USlideover) : géométrie posée en CSS (assets/css/overlays.css) — les utilitaires tailwind attendus par @nuxt/ui ne sont pas générés (UnoCSS ne scanne pas node_modules) : drawer mobile et modales de confirmation se rendaient hors écran - a11y : title/description sur les 3 overlays (DialogTitle/Description rendus, warnings reka-ui éteints), fermeture du drawer ancrée et testée - suppression du collectif actif : bascule sur un collectif restant — jamais d'accueil orphelin (test store ajouté, 343 vitest verts) - observatoire : la route « transmis » entre dans les barres (base 100 %) - overflows mobiles à zéro : formules KaTeX (défilement), table vigueur (défilement < 768px), pill cible (retour à la ligne), inputs decider et mandat (border-box) - vérifié navigateur : drawer, modale, 14 routes sans débordement, zéro erreur console Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
144 lines
6.9 KiB
Vue
144 lines
6.9 KiB
Vue
<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 ? 'quelqu’un' : 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 ? 'quelqu’un' : 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); overflow-wrap: anywhere;
|
||
}
|
||
.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>
|