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>
This commit is contained in:
Yvv
2026-03-17 00:13:08 +01:00
parent 316d205593
commit 290548703d
29 changed files with 4174 additions and 168 deletions

View File

@@ -0,0 +1,659 @@
<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>