Files
decision/frontend/app/components/toolbox/ContextMapper.vue
Yvv 290548703d Boîtes à outils enrichies : ContextMapper, SocioElection, WorkflowMilestones
- ContextMapper : 4 questions contexte → méthode de décision optimale
  (advice process Laloux, vote inertiel WoT, consentement sociocratique, Smith…)
- SocioElection : guide élection sociocratique 6 étapes + advice process + clarté de rôle
- WorkflowMilestones : 11 jalons de protocole (7 essentiels), durées recommandées, principes Ostrom
- WorkspaceSelector : sélecteur de collectif multi-site dans le header
- SectionLayout : toolbox en USlideover droit sur mobile, sidebar sticky desktop
- Décisions : ContextMapper intégré + guide consentement
- Mandats : SocioElection intégré + cycle de mandat
- Documents : guide inertie 4 niveaux + structure + IPFS
- Protocoles : WorkflowMilestones + protocole élection sociocratique ajouté
- Renommage projet Glibredecision → libreDecision (dossier + sources)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 00:13:08 +01:00

660 lines
18 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">
/**
* ContextMapper — Recommandeur de méthode de décision.
* 4 questions de contexte → méthode optimale + justification.
* Basé sur : Smith (WoT G1), Laloux (advice process), sociocracie.
*/
interface Option { value: string; label: string; icon: string }
interface Question { id: string; question: string; hint?: string; options: Option[] }
interface MethodRec {
name: string
icon: string
tag: string
tagColor: string
description: string
formula?: string
when: string
pros: string[]
cons: string[]
}
const questions: Question[] = [
{
id: 'urgency',
question: 'Quelle est l\'urgence ?',
hint: 'Le délai disponible avant que la décision soit nécessaire',
options: [
{ value: 'immediate', label: 'Immédiate', icon: 'i-lucide-zap' },
{ value: 'short', label: '< 48h', icon: 'i-lucide-clock' },
{ value: 'normal', label: 'Planifiable', icon: 'i-lucide-calendar' },
],
},
{
id: 'stakes',
question: 'Quel est l\'enjeu ?',
hint: 'L\'impact et la réversibilité de la décision',
options: [
{ value: 'irreversible', label: 'Irréversible', icon: 'i-lucide-lock' },
{ value: 'major', label: 'Majeur', icon: 'i-lucide-alert-triangle' },
{ value: 'moderate', label: 'Modéré', icon: 'i-lucide-minus-circle' },
{ value: 'minor', label: 'Mineur', icon: 'i-lucide-info' },
],
},
{
id: 'groupSize',
question: 'Taille du groupe ?',
hint: 'Nombre de personnes concernées ou habilitées à voter',
options: [
{ value: 'small', label: '< 10', icon: 'i-lucide-user' },
{ value: 'medium', label: '10 100', icon: 'i-lucide-users' },
{ value: 'large', label: '100+', icon: 'i-lucide-globe' },
],
},
{
id: 'nature',
question: 'Nature de la décision ?',
hint: 'Le type de compétence principalement sollicité',
options: [
{ value: 'technical', label: 'Technique', icon: 'i-lucide-cpu' },
{ value: 'political', label: 'Politique', icon: 'i-lucide-landmark' },
{ value: 'operational', label: 'Opérationnelle', icon: 'i-lucide-settings' },
],
},
]
const answers = ref<Record<string, string>>({})
const step = ref(0)
const animating = ref(false)
const currentQuestion = computed(() => questions[step.value])
const isComplete = computed(() => Object.keys(answers.value).length === questions.length)
const progress = computed(() => (step.value / questions.length) * 100)
function selectAnswer(questionId: string, value: string) {
answers.value = { ...answers.value, [questionId]: value }
if (step.value < questions.length - 1) {
animating.value = true
setTimeout(() => {
step.value++
animating.value = false
}, 160)
}
}
function goBack() {
if (step.value > 0) step.value--
}
function reset() {
answers.value = {}
step.value = 0
}
const recommendation = computed((): MethodRec | null => {
if (!isComplete.value) return null
const { urgency, stakes, groupSize, nature } = answers.value
// Immediate → Advice process (Laloux)
if (urgency === 'immediate') {
return {
name: 'Processus de sollicitation d\'avis',
icon: 'i-lucide-message-circle',
tag: 'Laloux / Teal',
tagColor: 'teal',
description: 'Le décideur identifié consulte les personnes expertes et impactées, puis décide seul et en rend compte. Rapide, non-bloquant, responsabilisant.',
formula: 'Pas de vote — consultation libre → décision documentée → compte-rendu',
when: 'Urgence opérationnelle, décision réversible, responsable clairement identifié.',
pros: ['Rapide (< 2h)', 'Non-bloquant', 'Responsabilise le décideur'],
cons: ['Requiert confiance dans le décideur', 'Pas de validation collective'],
}
}
// Technical + medium/large → Smith WoT
if (nature === 'technical' && groupSize !== 'small') {
return {
name: 'Vote inertiel WoT + critère Smith',
icon: 'i-lucide-network',
tag: 'G1 standard',
tagColor: 'accent',
description: 'Vote communautaire avec seuil adaptatif à la participation. Le critère Smith garantit que la décision reflète l\'expertise des validateurs.',
formula: 'R = C + B^W + (M + (1M)·(1(T/W)^G))·max(0,TC)\nSeuil Smith : ⌈SmithWoT^S⌉',
when: 'Décision technique nécessitant validation par les experts WoT (forgerons, CoTec).',
pros: ['Validé par expertise', 'Adaptatif à la participation', 'Tracé on-chain'],
cons: ['Durée minimum 7-30j', 'Complexité de la formule'],
}
}
// Irreversible + large → High threshold WoT
if (stakes === 'irreversible' && groupSize === 'large') {
return {
name: 'Vote inertiel WoT (inertie forte)',
icon: 'i-lucide-shield',
tag: 'G1 renforcé',
tagColor: 'secondary',
description: 'Pour les décisions irréversibles à fort impact : seuil de quasi-unanimité si faible participation, majorité qualifiée avec forte participation.',
formula: 'R = C + B^W + (M + (1M)·(1(T/W)^G))·max(0,TC)\nParamètres : M=67%, G=0.3 (inertie forte)',
when: 'Textes fondateurs, modifications structurelles, décisions irréversibles pour 100+ membres.',
pros: ['Protection maximale', 'Légitimité forte', 'Résistant aux minorités actives'],
cons: ['Durée longue (30+ jours)', 'Peut bloquer les évolutions nécessaires'],
}
}
// Small group → Sociocratic consent
if (groupSize === 'small') {
return {
name: 'Consentement sociocratique',
icon: 'i-lucide-check-circle-2',
tag: 'Sociocracie',
tagColor: 'tertiary',
description: 'Adoption si aucune objection grave n\'est soulevée. Une objection grave = la décision nuit à la mission commune, pas juste une préférence personnelle.',
formula: 'Adoptée si : aucune objection grave parmi les membres du cercle',
when: 'Cercle de travail (< 10 membres), enjeu modéré, décision réversible.',
pros: ['Rapide', 'Inclusif', 'Distingue objection grave et préférence'],
cons: ['Ne convient pas aux grands groupes', 'Risque de pression sociale'],
}
}
// Political + medium → WoT majority
if (nature === 'political') {
return {
name: 'Vote majoritaire WoT',
icon: 'i-lucide-vote',
tag: 'G1 standard',
tagColor: 'accent',
description: 'Vote binaire (Pour/Contre) avec seuil adaptatif à la participation WoT. Standard pour les décisions politiques de la communauté.',
formula: 'R = C + B^W + (M + (1M)·(1(T/W)^G))·max(0,TC)',
when: 'Décision politique communautaire, participation variable, groupe >10.',
pros: ['Standard WoT', 'Adaptatif', 'Tracé on-chain'],
cons: ['Durée 7-30j', 'Participation faible possible'],
}
}
// Default: minor/operational
return {
name: 'Advice process + validation légère',
icon: 'i-lucide-thumbs-up',
tag: 'Léger',
tagColor: 'teal',
description: 'Pour les décisions mineures ou opérationnelles : consultation des parties concernées, décision par le responsable désigné, notification de la communauté.',
formula: 'Consultation → Décision → Notification (sans vote formel)',
when: 'Décision opérationnelle de faible impact, facilement réversible.',
pros: ['Très rapide', 'Non-bloquant', 'Adapté à l\'opérationnel'],
cons: ['Légitimité limitée', 'Ne convient pas aux enjeux majeurs'],
}
})
const emit = defineEmits<{ use: [name: string] }>()
</script>
<template>
<div class="cmap">
<!-- Header -->
<div class="cmap__head">
<UIcon name="i-lucide-compass" class="cmap__head-icon" />
<div>
<h3 class="cmap__title">Choisir une méthode</h3>
<p class="cmap__subtitle">4 questions pour la méthode adaptée</p>
</div>
</div>
<!-- Result -->
<Transition name="fade-up" mode="out-in">
<div v-if="isComplete" key="result" class="cmap__result">
<div class="cmap__result-header">
<div class="cmap__result-icon">
<UIcon :name="recommendation!.icon" />
</div>
<div class="cmap__result-info">
<span class="cmap__result-tag" :class="`cmap__result-tag--${recommendation!.tagColor}`">
{{ recommendation!.tag }}
</span>
<h4 class="cmap__result-name">{{ recommendation!.name }}</h4>
</div>
</div>
<p class="cmap__result-desc">{{ recommendation!.description }}</p>
<div v-if="recommendation!.formula" class="cmap__formula">
<span class="cmap__formula-label">Formule</span>
<pre class="cmap__formula-code">{{ recommendation!.formula }}</pre>
</div>
<div class="cmap__pros-cons">
<div>
<span class="cmap__pros-label">Pour</span>
<ul class="cmap__list cmap__list--pro">
<li v-for="p in recommendation!.pros" :key="p">{{ p }}</li>
</ul>
</div>
<div>
<span class="cmap__cons-label">Contre</span>
<ul class="cmap__list cmap__list--con">
<li v-for="c in recommendation!.cons" :key="c">{{ c }}</li>
</ul>
</div>
</div>
<p class="cmap__when">
<UIcon name="i-lucide-lightbulb" />
{{ recommendation!.when }}
</p>
<div class="cmap__result-actions">
<button class="cmap__btn-reset" @click="reset">
<UIcon name="i-lucide-refresh-cw" />
Recommencer
</button>
<button class="cmap__btn-use" @click="emit('use', recommendation!.name)">
<UIcon name="i-lucide-play" />
Utiliser cette méthode
</button>
</div>
</div>
<!-- Quiz -->
<div v-else key="quiz" class="cmap__quiz">
<!-- Progress -->
<div class="cmap__progress">
<div class="cmap__progress-bar" :style="{ width: `${progress}%` }" />
</div>
<span class="cmap__step-label">{{ step + 1 }} / {{ questions.length }}</span>
<!-- Question -->
<Transition name="slide-right" mode="out-in">
<div :key="step" class="cmap__question-block">
<p class="cmap__question">{{ currentQuestion.question }}</p>
<p v-if="currentQuestion.hint" class="cmap__hint">{{ currentQuestion.hint }}</p>
<div class="cmap__options">
<button
v-for="opt in currentQuestion.options"
:key="opt.value"
class="cmap__option"
:class="{ 'cmap__option--selected': answers[currentQuestion.id] === opt.value }"
@click="selectAnswer(currentQuestion.id, opt.value)"
>
<div class="cmap__option-icon">
<UIcon :name="opt.icon" />
</div>
<span class="cmap__option-label">{{ opt.label }}</span>
</button>
</div>
</div>
</Transition>
<button v-if="step > 0" class="cmap__back" @click="goBack">
<UIcon name="i-lucide-chevron-left" />
Retour
</button>
</div>
</Transition>
</div>
</template>
<style scoped>
.cmap {
display: flex;
flex-direction: column;
gap: 1rem;
}
.cmap__head {
display: flex;
align-items: flex-start;
gap: 0.75rem;
}
.cmap__head-icon {
font-size: 1.375rem;
color: var(--mood-accent);
flex-shrink: 0;
margin-top: 0.125rem;
}
.cmap__title {
font-size: 1rem;
font-weight: 800;
color: var(--mood-text);
margin: 0;
}
.cmap__subtitle {
font-size: 0.8125rem;
color: var(--mood-text-muted);
margin: 0;
}
/* Progress */
.cmap__progress {
height: 4px;
background: var(--mood-accent-soft);
border-radius: 4px;
overflow: hidden;
}
.cmap__progress-bar {
height: 100%;
background: var(--mood-accent);
border-radius: 4px;
transition: width 0.3s ease;
}
.cmap__step-label {
font-size: 0.6875rem;
font-weight: 700;
color: var(--mood-text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
}
/* Question */
.cmap__question-block {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.cmap__question {
font-size: 0.9375rem;
font-weight: 700;
color: var(--mood-text);
margin: 0;
line-height: 1.4;
}
.cmap__hint {
font-size: 0.75rem;
color: var(--mood-text-muted);
margin: 0;
line-height: 1.5;
}
.cmap__options {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.cmap__option {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: var(--mood-accent-soft);
border-radius: 12px;
cursor: pointer;
transition: transform 0.1s ease, box-shadow 0.1s ease, background 0.1s ease;
text-align: left;
min-height: 2.75rem;
}
.cmap__option:hover {
transform: translateY(-1px);
box-shadow: 0 3px 10px var(--mood-shadow);
}
.cmap__option:active { transform: translateY(0); }
.cmap__option--selected {
background: var(--mood-accent);
}
.cmap__option--selected .cmap__option-icon,
.cmap__option--selected .cmap__option-label {
color: var(--mood-accent-text);
}
.cmap__option-icon {
width: 1.75rem;
height: 1.75rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
background: var(--mood-surface);
color: var(--mood-accent);
flex-shrink: 0;
font-size: 0.875rem;
}
.cmap__option--selected .cmap__option-icon {
background: rgba(255,255,255,0.2);
color: var(--mood-accent-text);
}
.cmap__option-label {
font-size: 0.875rem;
font-weight: 600;
color: var(--mood-text);
}
.cmap__back {
display: inline-flex;
align-items: center;
gap: 0.375rem;
font-size: 0.8125rem;
font-weight: 600;
color: var(--mood-text-muted);
background: none;
cursor: pointer;
padding: 0.375rem 0;
transition: color 0.1s ease;
}
.cmap__back:hover { color: var(--mood-text); }
/* Result */
.cmap__result {
display: flex;
flex-direction: column;
gap: 0.875rem;
}
.cmap__result-header {
display: flex;
align-items: flex-start;
gap: 0.75rem;
}
.cmap__result-icon {
width: 2.5rem;
height: 2.5rem;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
background: var(--mood-accent-soft);
color: var(--mood-accent);
font-size: 1.125rem;
}
.cmap__result-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.cmap__result-tag {
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 2px 8px;
border-radius: 20px;
width: fit-content;
}
.cmap__result-tag--accent {
background: var(--mood-accent-soft);
color: var(--mood-accent);
}
.cmap__result-tag--teal {
background: color-mix(in srgb, var(--mood-success) 15%, transparent);
color: var(--mood-success);
}
.cmap__result-tag--secondary {
background: color-mix(in srgb, var(--mood-secondary, var(--mood-accent)) 15%, transparent);
color: var(--mood-secondary, var(--mood-accent));
}
.cmap__result-tag--tertiary {
background: color-mix(in srgb, var(--mood-tertiary, var(--mood-accent)) 15%, transparent);
color: var(--mood-tertiary, var(--mood-accent));
}
.cmap__result-name {
font-size: 0.9375rem;
font-weight: 800;
color: var(--mood-text);
margin: 0;
line-height: 1.3;
}
.cmap__result-desc {
font-size: 0.8125rem;
color: var(--mood-text-muted);
line-height: 1.6;
margin: 0;
}
.cmap__formula {
background: var(--mood-accent-soft);
border-radius: 10px;
padding: 0.625rem 0.875rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.cmap__formula-label {
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--mood-accent);
}
.cmap__formula-code {
font-family: ui-monospace, SFMono-Regular, monospace;
font-size: 0.75rem;
color: var(--mood-text);
margin: 0;
white-space: pre-wrap;
line-height: 1.5;
}
.cmap__pros-cons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.cmap__pros-label,
.cmap__cons-label {
display: block;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 0.25rem;
}
.cmap__pros-label { color: var(--mood-success); }
.cmap__cons-label { color: var(--mood-error); }
.cmap__list {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.cmap__list li {
font-size: 0.6875rem;
color: var(--mood-text-muted);
padding-left: 0.875rem;
position: relative;
line-height: 1.4;
}
.cmap__list--pro li::before {
content: '✓';
position: absolute;
left: 0;
color: var(--mood-success);
font-weight: 700;
font-size: 0.5rem;
top: 0.2em;
}
.cmap__list--con li::before {
content: '·';
position: absolute;
left: 0;
color: var(--mood-error);
font-weight: 700;
}
.cmap__when {
display: flex;
align-items: flex-start;
gap: 0.5rem;
font-size: 0.75rem;
color: var(--mood-text-muted);
margin: 0;
line-height: 1.5;
font-style: italic;
}
.cmap__result-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.cmap__btn-reset {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
font-size: 0.8125rem;
font-weight: 600;
color: var(--mood-text-muted);
background: var(--mood-accent-soft);
border-radius: 20px;
cursor: pointer;
transition: transform 0.1s ease;
}
.cmap__btn-reset:hover { transform: translateY(-1px); color: var(--mood-text); }
.cmap__btn-use {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 1.125rem;
font-size: 0.8125rem;
font-weight: 700;
color: var(--mood-accent-text);
background: var(--mood-accent);
border-radius: 20px;
cursor: pointer;
transition: transform 0.1s ease, box-shadow 0.1s ease;
}
.cmap__btn-use:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px var(--mood-shadow);
}
/* Transitions */
.fade-up-enter-active, .fade-up-leave-active { transition: all 0.2s ease; }
.fade-up-enter-from { opacity: 0; transform: translateY(8px); }
.fade-up-leave-to { opacity: 0; transform: translateY(-4px); }
.slide-right-enter-active, .slide-right-leave-active { transition: all 0.16s ease; }
.slide-right-enter-from { opacity: 0; transform: translateX(12px); }
.slide-right-leave-to { opacity: 0; transform: translateX(-8px); }
</style>