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:
@@ -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>
|
||||
Reference in New Issue
Block a user