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>
This commit is contained in:
Yvv
2026-08-11 13:47:20 +02:00
co-authored by Claude Fable 5
parent d886302b59
commit e164b5f6c1
96 changed files with 14097 additions and 11777 deletions
@@ -0,0 +1,205 @@
<script setup lang="ts">
// <!-- ld-v2 --> Window block. Objection: « Ça me va / J'objecte » inline —
// silence counts as agreement ONLY when easily reversible, otherwise the
// explicit-agreement counter + « il manque un accord explicite ». Advice:
// three inline positions, the author then decides.
import type { Advice, Decision } from '~/types/domain'
import { ASSENT_MISSING, WINDOW_OBJECT, WINDOW_OK } from '~/lexicon'
import { useCollectiveStore } from '~/stores/collective'
import { useDecisionsStore } from '~/stores/decisions'
import { ADVICE_LABELS } from './decisionUi'
const props = defineProps<{ decision: Decision }>()
const col = useCollectiveStore()
const store = useDecisionsStore()
const meId = computed(() => col.me?.id ?? null)
const isAuthor = computed(() => meId.value === props.decision.authorId)
// ── Assents (« Ça me va » stocké) ──
const assents = computed(() =>
col.assents.filter(a => a.decisionId === props.decision.id))
const thirdPartyCount = computed(() =>
assents.value.filter(a => a.personId !== props.decision.authorId).length)
const iAssented = computed(() =>
meId.value !== null && assents.value.some(a => a.personId === meId.value))
function assent() { store.assentTo(props.decision.id) }
// ── Objection de fond ──
const objectOpen = ref(false)
const objectArg = ref('')
const feedback = ref('')
function object() {
const result = store.objectTo(props.decision.id, 'content', objectArg.value)
if ('ok' in result) { feedback.value = result.reason; return }
objectOpen.value = false
objectArg.value = ''
feedback.value = ''
}
// ── Avis ──
const myAdvice = computed(() =>
col.advices.find(a => a.decisionId === props.decision.id && a.personId === meId.value))
const adviceNote = ref('')
function advise(position: Advice['position']) {
const note = adviceNote.value.trim()
store.adviseOn(props.decision.id, position, note.length > 0 ? note : undefined)
adviceNote.value = ''
}
/** The author decides once instructed — the ONE state gate answers. */
function authorAdopts() {
const result = store.transition(props.decision.id, 'adopted')
feedback.value = result.ok ? '' : result.reason
}
</script>
<template>
<!-- ld-v2 -->
<section class="win">
<div class="win__head">
<h2 class="win__title">
{{ decision.status === 'advice' ? 'Fenêtre d\'avis' : 'Fenêtre d\'objection' }}
</h2>
<LdCountdown
v-if="decision.windowEndsAt"
:ends-at="decision.windowEndsAt"
:suspended-at="decision.windowSuspendedAt"
/>
</div>
<template v-if="decision.status === 'objection'">
<p v-if="decision.reversibility === 'easy'" class="win__rule">
À l'échéance sans objection, c'est adopté le silence vaut accord,
parce que c'est facilement réversible.
</p>
<template v-else>
<p class="win__rule">
{{ thirdPartyCount }} accord{{ thirdPartyCount > 1 ? 's' : '' }} explicite{{ thirdPartyCount > 1 ? 's' : '' }}
hors du réversible, l'accord est un geste, pas une absence.
</p>
<p v-if="thirdPartyCount === 0" class="win__missing">{{ ASSENT_MISSING }}</p>
</template>
<div class="win__actions no-print">
<button
type="button"
class="ld-btn"
:disabled="iAssented"
@click="assent()"
>
<UIcon name="i-lucide-check" />
<span>{{ iAssented ? `${WINDOW_OK} — déposé` : WINDOW_OK }}</span>
</button>
<button type="button" class="ld-btn ld-btn--ghost" @click="objectOpen = !objectOpen">
{{ WINDOW_OBJECT }}
</button>
</div>
<div v-if="objectOpen" class="win__form no-print">
<textarea
v-model="objectArg"
rows="2"
placeholder="Une objection s'argumente écris pourquoi."
/>
<button
type="button"
class="ld-btn"
:disabled="objectArg.trim().length === 0"
@click="object()"
>
Déposer l'objection
</button>
</div>
</template>
<template v-else-if="decision.status === 'advice'">
<p class="win__rule">
L'auteur·e écoute, puis décide — les avis restent annexés à la décision.
</p>
<div v-if="!myAdvice" class="win__form no-print">
<textarea
v-model="adviceNote"
rows="2"
placeholder="Un mot pour éclairer (optionnel)"
/>
<div class="win__actions">
<button type="button" class="ld-btn" @click="advise('favorable')">
{{ ADVICE_LABELS.favorable }}
</button>
<button type="button" class="ld-btn ld-btn--ghost" @click="advise('reserved')">
{{ ADVICE_LABELS.reserved }}
</button>
<button type="button" class="ld-btn ld-btn--ghost" @click="advise('unfavorable')">
{{ ADVICE_LABELS.unfavorable }}
</button>
</div>
</div>
<p v-else class="win__rule">Ton avis est déposé — {{ ADVICE_LABELS[myAdvice.position] }}.</p>
<div v-if="isAuthor" class="win__actions no-print">
<button type="button" class="ld-btn" @click="authorAdopts()">
<UIcon name="i-lucide-check-check" />
<span>Je décide — adopter</span>
</button>
</div>
</template>
<p v-if="feedback" class="win__feedback">{{ feedback }}</p>
</section>
</template>
<style scoped>
.win { display: flex; flex-direction: column; gap: 0.75rem; }
.win__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
flex-wrap: wrap;
}
.win__title {
margin: 0;
font-size: 1.0625rem;
font-weight: 800;
letter-spacing: -0.01em;
}
.win__rule {
margin: 0;
font-size: 0.9375rem;
color: var(--mood-text-muted);
}
.win__missing {
margin: 0;
width: fit-content;
padding: 4px 12px;
border-radius: var(--r-pill);
font-size: 0.875rem;
font-weight: 700;
color: var(--mood-surface);
background: var(--mood-status-fenetre);
}
.win__actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.win__form {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.win__form textarea {
width: 100%;
padding: 0.625rem 0.875rem;
font-size: 0.9375rem;
resize: vertical;
}
.win__feedback {
margin: 0;
font-size: 0.875rem;
font-weight: 600;
color: var(--mood-status-fenetre);
}
</style>