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:
659
frontend/app/components/toolbox/ContextMapper.vue
Normal file
659
frontend/app/components/toolbox/ContextMapper.vue
Normal 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 + (1−M)·(1−(T/W)^G))·max(0,T−C)\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 + (1−M)·(1−(T/W)^G))·max(0,T−C)\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 + (1−M)·(1−(T/W)^G))·max(0,T−C)',
|
||||
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>
|
||||
666
frontend/app/components/toolbox/SocioElection.vue
Normal file
666
frontend/app/components/toolbox/SocioElection.vue
Normal file
@@ -0,0 +1,666 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* SocioElection — Guide processus d'élection sociocratique.
|
||||
* 6 étapes canoniques + advice process Laloux + clarté de rôle.
|
||||
* Référence : "La Sociocracie" (Robertson), "Reinventing Organizations" (Laloux).
|
||||
*/
|
||||
|
||||
interface Step {
|
||||
num: number
|
||||
title: string
|
||||
actor: string
|
||||
duration: string
|
||||
icon: string
|
||||
description: string
|
||||
tips: string[]
|
||||
pitfall?: string
|
||||
}
|
||||
|
||||
const steps: Step[] = [
|
||||
{
|
||||
num: 1,
|
||||
title: 'Clarifier le rôle',
|
||||
actor: 'Facilitateur + cercle',
|
||||
duration: '10-15 min',
|
||||
icon: 'i-lucide-clipboard-list',
|
||||
description: 'Définir ensemble la mission du rôle, ses domaines d\'autorité, ses redevabilités et la durée du mandat. Le rôle précède la personne.',
|
||||
tips: [
|
||||
'Distinguer redevabilités (obligations) et autorité (domaine de décision)',
|
||||
'Fixer une durée standard (ex: 1 an renouvelable)',
|
||||
'Identifier les compétences nécessaires — pas souhaitables',
|
||||
],
|
||||
pitfall: 'Ne pas définir le rôle sur mesure pour un candidat déjà imaginé.',
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
title: 'Nommer en silence',
|
||||
actor: 'Tous les membres',
|
||||
duration: '3-5 min',
|
||||
icon: 'i-lucide-pencil',
|
||||
description: 'Chacun écrit sur papier le nom d\'une personne (y compris soi-même) et la raison principale de son choix. En silence, sans influence mutuelle.',
|
||||
tips: [
|
||||
'Pas de discussion pendant cette étape',
|
||||
'S\'auto-nommer est bienvenu et valorisé',
|
||||
'Une seule nomination par personne',
|
||||
],
|
||||
},
|
||||
{
|
||||
num: 3,
|
||||
title: 'Recueillir les nominations',
|
||||
actor: 'Facilitateur',
|
||||
duration: '5-10 min',
|
||||
icon: 'i-lucide-list-checks',
|
||||
description: 'Le facilitateur lit chaque nomination à voix haute avec la raison. Pas de commentaire, pas de débat. Pure collecte.',
|
||||
tips: [
|
||||
'Lire nom + raison tels qu\'écrits',
|
||||
'Le facilitateur lit aussi sa propre nomination',
|
||||
'Compter et afficher les nominations',
|
||||
],
|
||||
},
|
||||
{
|
||||
num: 4,
|
||||
title: 'Argumenter',
|
||||
actor: 'Chaque membre',
|
||||
duration: '1-2 min / personne',
|
||||
icon: 'i-lucide-message-square',
|
||||
description: 'Chaque membre peut changer sa nomination et expliquer pourquoi (brièvement). Tour de table structuré, pas de croisements.',
|
||||
tips: [
|
||||
'1 minute maximum par personne',
|
||||
'Argumenter pour, pas contre',
|
||||
'Les candidats s\'expriment aussi brièvement',
|
||||
],
|
||||
pitfall: 'Éviter les longues plaidoiries — la clarté du rôle doit guider.',
|
||||
},
|
||||
{
|
||||
num: 5,
|
||||
title: 'Lever les objections',
|
||||
actor: 'Facilitateur + cercle',
|
||||
duration: '5-15 min',
|
||||
icon: 'i-lucide-shield-check',
|
||||
description: 'Le facilitateur propose l\'élection de la personne la plus nommée. Silence = consentement. Une objection grave peut être soulevée et traitée.',
|
||||
tips: [
|
||||
'Objection grave ≠ préférence — nuit-elle à la mission du cercle ?',
|
||||
'Une objection peut mener à reconsidérer une candidature',
|
||||
'L\'élu·e peut décliner — c\'est légitime',
|
||||
],
|
||||
pitfall: 'Une objection n\'est pas un veto — elle doit être travaillée collectivement.',
|
||||
},
|
||||
{
|
||||
num: 6,
|
||||
title: 'Célébrer',
|
||||
actor: 'Tous',
|
||||
duration: '2-3 min',
|
||||
icon: 'i-lucide-star',
|
||||
description: 'L\'élection est proclamée. L\'élu·e remercie et s\'engage publiquement. La communauté accueille le nouveau rôle.',
|
||||
tips: [
|
||||
'Documenter l\'élection (date, durée, personnes présentes)',
|
||||
'Annoncer à la communauté au sens large',
|
||||
'Fixer la prochaine évaluation du rôle',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const expandedStep = ref<number | null>(null)
|
||||
|
||||
function toggleStep(num: number) {
|
||||
expandedStep.value = expandedStep.value === num ? null : num
|
||||
}
|
||||
|
||||
// Advice process (Laloux)
|
||||
const adviceSteps = [
|
||||
{ icon: 'i-lucide-search', text: 'Identifier les personnes expertes ET impactées' },
|
||||
{ icon: 'i-lucide-message-circle', text: 'Les consulter — écouter vraiment' },
|
||||
{ icon: 'i-lucide-user-check', text: 'Décider seul·e, en intégrant les avis reçus' },
|
||||
{ icon: 'i-lucide-file-text', text: 'Documenter et communiquer la décision + raisons' },
|
||||
]
|
||||
|
||||
// Role clarity framework
|
||||
interface RoleAxis {
|
||||
label: string
|
||||
icon: string
|
||||
question: string
|
||||
example: string
|
||||
}
|
||||
|
||||
const roleAxes: RoleAxis[] = [
|
||||
{
|
||||
label: 'Mission',
|
||||
icon: 'i-lucide-target',
|
||||
question: 'Pourquoi ce rôle existe-t-il ?',
|
||||
example: 'Assurer la disponibilité des nœuds validateurs 24h/24',
|
||||
},
|
||||
{
|
||||
label: 'Domaine',
|
||||
icon: 'i-lucide-shield',
|
||||
question: 'Sur quoi a-t-il autorité exclusive ?',
|
||||
example: 'Configuration des serveurs de forge, rotation des clés',
|
||||
},
|
||||
{
|
||||
label: 'Redevabilités',
|
||||
icon: 'i-lucide-check-square',
|
||||
question: 'Quelles activités doit-il assurer ?',
|
||||
example: 'Publier un rapport mensuel, alerter en cas d\'incident',
|
||||
},
|
||||
{
|
||||
label: 'Durée',
|
||||
icon: 'i-lucide-calendar',
|
||||
question: 'Pour combien de temps ?',
|
||||
example: '1 an, renouvelable une fois, réévaluation à 6 mois',
|
||||
},
|
||||
]
|
||||
|
||||
const activeTab = ref<'election' | 'advice' | 'role'>('election')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="se">
|
||||
<!-- Tabs -->
|
||||
<div class="se__tabs">
|
||||
<button
|
||||
class="se__tab"
|
||||
:class="{ 'se__tab--active': activeTab === 'election' }"
|
||||
@click="activeTab = 'election'"
|
||||
>
|
||||
<UIcon name="i-lucide-users" />
|
||||
Élection
|
||||
</button>
|
||||
<button
|
||||
class="se__tab"
|
||||
:class="{ 'se__tab--active': activeTab === 'advice' }"
|
||||
@click="activeTab = 'advice'"
|
||||
>
|
||||
<UIcon name="i-lucide-message-circle" />
|
||||
Conseil
|
||||
</button>
|
||||
<button
|
||||
class="se__tab"
|
||||
:class="{ 'se__tab--active': activeTab === 'role' }"
|
||||
@click="activeTab = 'role'"
|
||||
>
|
||||
<UIcon name="i-lucide-clipboard-list" />
|
||||
Rôle
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Election sociocratique -->
|
||||
<div v-if="activeTab === 'election'" class="se__panel">
|
||||
<p class="se__intro">
|
||||
Processus en 6 étapes garantissant que l'élection repose sur la clarté du rôle
|
||||
et le consentement collectif — pas sur la popularité.
|
||||
</p>
|
||||
|
||||
<div class="se__steps">
|
||||
<div
|
||||
v-for="s in steps"
|
||||
:key="s.num"
|
||||
class="se__step"
|
||||
:class="{ 'se__step--open': expandedStep === s.num }"
|
||||
>
|
||||
<button class="se__step-head" @click="toggleStep(s.num)">
|
||||
<div class="se__step-num">{{ s.num }}</div>
|
||||
<div class="se__step-icon">
|
||||
<UIcon :name="s.icon" />
|
||||
</div>
|
||||
<div class="se__step-info">
|
||||
<span class="se__step-title">{{ s.title }}</span>
|
||||
<span class="se__step-meta">{{ s.actor }} · {{ s.duration }}</span>
|
||||
</div>
|
||||
<UIcon
|
||||
:name="expandedStep === s.num ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
||||
class="se__step-toggle"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<Transition name="expand">
|
||||
<div v-if="expandedStep === s.num" class="se__step-body">
|
||||
<p class="se__step-desc">{{ s.description }}</p>
|
||||
<ul class="se__step-tips">
|
||||
<li v-for="tip in s.tips" :key="tip">{{ tip }}</li>
|
||||
</ul>
|
||||
<div v-if="s.pitfall" class="se__step-pitfall">
|
||||
<UIcon name="i-lucide-alert-triangle" />
|
||||
{{ s.pitfall }}
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advice process -->
|
||||
<div v-if="activeTab === 'advice'" class="se__panel">
|
||||
<div class="se__advice-header">
|
||||
<span class="se__advice-tag">Laloux / Teal</span>
|
||||
<h4 class="se__advice-title">Processus de sollicitation d'avis</h4>
|
||||
<p class="se__advice-subtitle">
|
||||
Toute personne peut prendre une décision — à condition d'avoir d'abord
|
||||
consulté les experts et les impactés.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="se__advice-steps">
|
||||
<div v-for="(as, i) in adviceSteps" :key="i" class="se__advice-step">
|
||||
<div class="se__advice-dot">
|
||||
<UIcon :name="as.icon" />
|
||||
</div>
|
||||
<span class="se__advice-text">{{ as.text }}</span>
|
||||
<div v-if="i < adviceSteps.length - 1" class="se__advice-line" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="se__advice-rule">
|
||||
<UIcon name="i-lucide-lightbulb" class="se__advice-rule-icon" />
|
||||
<div>
|
||||
<strong>Règle d'or :</strong> plus la décision est impactante, plus il faut
|
||||
consulter largement. Mais la décision finale appartient toujours à celui ou
|
||||
celle qui l'a initiée.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="se__advice-when">
|
||||
<div class="se__advice-when-item se__advice-when-item--yes">
|
||||
<span class="se__advice-when-label">Adapter pour</span>
|
||||
<ul>
|
||||
<li>Décisions urgentes</li>
|
||||
<li>Rôles bien définis</li>
|
||||
<li>Culture de confiance</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="se__advice-when-item se__advice-when-item--no">
|
||||
<span class="se__advice-when-label">Éviter si</span>
|
||||
<ul>
|
||||
<li>Décision irréversible</li>
|
||||
<li>Groupe > 100 personnes</li>
|
||||
<li>Enjeu fondateur</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Role clarity -->
|
||||
<div v-if="activeTab === 'role'" class="se__panel">
|
||||
<p class="se__intro">
|
||||
Un rôle bien défini évite les zones grises, les conflits d'autorité
|
||||
et les mandats flous. Quatre axes suffisent.
|
||||
</p>
|
||||
|
||||
<div class="se__role-axes">
|
||||
<div v-for="axis in roleAxes" :key="axis.label" class="se__role-axis">
|
||||
<div class="se__role-axis-icon">
|
||||
<UIcon :name="axis.icon" />
|
||||
</div>
|
||||
<div class="se__role-axis-body">
|
||||
<span class="se__role-axis-label">{{ axis.label }}</span>
|
||||
<p class="se__role-axis-question">{{ axis.question }}</p>
|
||||
<p class="se__role-axis-example">ex: {{ axis.example }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="se__role-tip">
|
||||
<UIcon name="i-lucide-info" />
|
||||
<span>Un rôle n'est pas une fiche de poste. Il peut évoluer au prochain cycle
|
||||
de gouvernance sans changer la personne qui le tient.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.se { display: flex; flex-direction: column; gap: 1rem; }
|
||||
|
||||
/* Tabs */
|
||||
.se__tabs {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 12px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.se__tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-text-muted);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.se__tab--active {
|
||||
background: var(--mood-surface);
|
||||
color: var(--mood-accent);
|
||||
box-shadow: 0 1px 4px var(--mood-shadow);
|
||||
}
|
||||
|
||||
.se__panel { display: flex; flex-direction: column; gap: 0.875rem; }
|
||||
|
||||
.se__intro {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--mood-text-muted);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Steps */
|
||||
.se__steps { display: flex; flex-direction: column; gap: 0.375rem; }
|
||||
|
||||
.se__step {
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.se__step--open { background: var(--mood-surface); }
|
||||
|
||||
.se__step-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 0.875rem;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.se__step-num {
|
||||
width: 1.375rem;
|
||||
height: 1.375rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--mood-accent);
|
||||
color: var(--mood-accent-text);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.se__step-icon {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: var(--mood-surface);
|
||||
color: var(--mood-accent);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.se__step-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.se__step-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-text);
|
||||
}
|
||||
|
||||
.se__step-meta {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
|
||||
.se__step-toggle {
|
||||
color: var(--mood-text-muted);
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.se__step-body {
|
||||
padding: 0 0.875rem 0.875rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.se__step-desc {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--mood-text-muted);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.se__step-tips {
|
||||
margin: 0;
|
||||
padding: 0 0 0 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
list-style-type: disc;
|
||||
line-height: 1.6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
.se__step-tips li::marker { color: var(--mood-accent); }
|
||||
|
||||
.se__step-pitfall {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.625rem;
|
||||
background: color-mix(in srgb, var(--mood-error) 10%, transparent);
|
||||
border-radius: 8px;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--mood-error);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Advice */
|
||||
.se__advice-header { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
|
||||
.se__advice-tag {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
background: color-mix(in srgb, var(--mood-success) 15%, transparent);
|
||||
color: var(--mood-success);
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.se__advice-title {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 800;
|
||||
color: var(--mood-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.se__advice-subtitle {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--mood-text-muted);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.se__advice-steps { display: flex; flex-direction: column; gap: 0; }
|
||||
|
||||
.se__advice-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.625rem;
|
||||
position: relative;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.se__advice-dot {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--mood-accent-soft);
|
||||
color: var(--mood-accent);
|
||||
font-size: 0.875rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.se__advice-text {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-text);
|
||||
padding-top: 0.375rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.se__advice-line {
|
||||
position: absolute;
|
||||
left: calc(1rem - 1px);
|
||||
top: calc(0.5rem + 2rem);
|
||||
width: 2px;
|
||||
height: calc(100% - 2rem + 0.5rem);
|
||||
background: color-mix(in srgb, var(--mood-accent) 20%, transparent);
|
||||
}
|
||||
|
||||
.se__advice-rule {
|
||||
display: flex;
|
||||
gap: 0.625rem;
|
||||
align-items: flex-start;
|
||||
padding: 0.75rem;
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 10px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.se__advice-rule-icon { color: var(--mood-accent); flex-shrink: 0; margin-top: 0.1rem; }
|
||||
.se__advice-rule strong { color: var(--mood-text); }
|
||||
|
||||
.se__advice-when {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.se__advice-when-item {
|
||||
padding: 0.625rem;
|
||||
border-radius: 10px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.se__advice-when-item--yes {
|
||||
background: color-mix(in srgb, var(--mood-success) 10%, transparent);
|
||||
}
|
||||
|
||||
.se__advice-when-item--no {
|
||||
background: color-mix(in srgb, var(--mood-error) 8%, transparent);
|
||||
}
|
||||
|
||||
.se__advice-when-label {
|
||||
display: block;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.se__advice-when-item--yes .se__advice-when-label { color: var(--mood-success); }
|
||||
.se__advice-when-item--no .se__advice-when-label { color: var(--mood-error); }
|
||||
|
||||
.se__advice-when-item ul {
|
||||
margin: 0;
|
||||
padding: 0 0 0 0.875rem;
|
||||
color: var(--mood-text-muted);
|
||||
list-style-type: disc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
/* Role */
|
||||
.se__role-axes { display: flex; flex-direction: column; gap: 0.625rem; }
|
||||
|
||||
.se__role-axis {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
padding: 0.75rem;
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.se__role-axis-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: var(--mood-surface);
|
||||
color: var(--mood-accent);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.se__role-axis-body { flex: 1; min-width: 0; }
|
||||
|
||||
.se__role-axis-label {
|
||||
display: block;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--mood-accent);
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.se__role-axis-question {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.se__role-axis-example {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--mood-text-muted);
|
||||
margin: 0.125rem 0 0;
|
||||
line-height: 1.4;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.se__role-tip {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-start;
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
line-height: 1.5;
|
||||
padding: 0.625rem 0.75rem;
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Expand transition */
|
||||
.expand-enter-active, .expand-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.expand-enter-from, .expand-leave-to {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
.expand-enter-to, .expand-leave-from {
|
||||
max-height: 500px;
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
551
frontend/app/components/toolbox/WorkflowMilestones.vue
Normal file
551
frontend/app/components/toolbox/WorkflowMilestones.vue
Normal file
@@ -0,0 +1,551 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* WorkflowMilestones — 11 jalons de protocole de fonctionnement.
|
||||
* Sélectif et qualitatif : ce qui fait la différence entre un protocole
|
||||
* qui tient et un qui dérive.
|
||||
* Référence : g1vote, sociocracie, Laloux, Elinor Ostrom (gouvernance des communs).
|
||||
*/
|
||||
|
||||
interface Milestone {
|
||||
num: number
|
||||
name: string
|
||||
icon: string
|
||||
actor: string
|
||||
duration: { min: string; standard: string; major: string }
|
||||
description: string
|
||||
essential: boolean
|
||||
tip?: string
|
||||
ostrom?: string
|
||||
}
|
||||
|
||||
const milestones: Milestone[] = [
|
||||
{
|
||||
num: 1,
|
||||
name: 'Prise d\'initiative',
|
||||
icon: 'i-lucide-lightbulb',
|
||||
actor: 'Tout membre',
|
||||
duration: { min: '—', standard: '1-2j', major: '1-2j' },
|
||||
description: 'Formaliser l\'intention : quel problème, quel besoin, quelle cible visée. Nommer un·e porteur·euse responsable.',
|
||||
essential: true,
|
||||
tip: 'Une initiative sans porteur identifié ne décolle pas. La responsabilité individuelle est le premier jalon.',
|
||||
ostrom: 'Principe 1 — Frontières claires : qui est concerné, pourquoi.',
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
name: 'Processus d\'avis (advice)',
|
||||
icon: 'i-lucide-message-circle',
|
||||
actor: 'Porteur + experts + impactés',
|
||||
duration: { min: '1j', standard: '3-7j', major: '7-14j' },
|
||||
description: 'Consulter les personnes qui ont l\'expertise ET celles qui seront impactées. Écouter vraiment, intégrer ou expliquer pourquoi on n\'intègre pas.',
|
||||
essential: true,
|
||||
tip: 'Ce jalon est souvent escamoté. C\'est la principale cause d\'échec ou de résistance en implémentation.',
|
||||
ostrom: 'Principe 5 — Résolution des conflits accessible et peu coûteuse.',
|
||||
},
|
||||
{
|
||||
num: 3,
|
||||
name: 'Rédaction + amendements',
|
||||
icon: 'i-lucide-file-edit',
|
||||
actor: 'Porteur + communauté',
|
||||
duration: { min: '1-2j', standard: '3-7j', major: '7-21j' },
|
||||
description: 'Rédiger la proposition formelle. Ouvrir une période d\'amendements publics. Intégrer les modifications acceptées, rejeter les autres avec justification.',
|
||||
essential: true,
|
||||
tip: 'Distinguer amendements substantiels (re-vote possible) et de forme (porteur décide).',
|
||||
},
|
||||
{
|
||||
num: 4,
|
||||
name: 'Qualification technique',
|
||||
icon: 'i-lucide-shield-check',
|
||||
actor: 'Comité technique (si applicable)',
|
||||
duration: { min: '—', standard: '2-5j', major: '5-10j' },
|
||||
description: 'Pour les décisions techniques : revue par les experts désignés. Évaluation de faisabilité, risques, impact. Avis formel (non bloquant, sauf veto défini).',
|
||||
essential: false,
|
||||
tip: 'Optionnel selon la nature de la décision. Systématique pour les Runtime Upgrades.',
|
||||
},
|
||||
{
|
||||
num: 5,
|
||||
name: 'Ouverture du vote',
|
||||
icon: 'i-lucide-vote',
|
||||
actor: 'Porteur + plateforme',
|
||||
duration: { min: '—', standard: '1j', major: '1j' },
|
||||
description: 'Publier la proposition finale. Notifier la communauté. Ouvrir la session de vote avec les paramètres définis (protocole, formule, durée).',
|
||||
essential: true,
|
||||
tip: 'L\'ouverture doit être annoncée à l\'avance (délai de préavis selon règlement).',
|
||||
},
|
||||
{
|
||||
num: 6,
|
||||
name: 'Phase de vote',
|
||||
icon: 'i-lucide-bar-chart-2',
|
||||
actor: 'Membres habilités',
|
||||
duration: { min: '3j', standard: '7-14j', major: '21-30j' },
|
||||
description: 'Les membres habilités votent selon le protocole. Seuil de participation minimal surveillé. Résultats intermédiaires visibles (ou non, selon le protocole).',
|
||||
essential: true,
|
||||
ostrom: 'Principe 3 — Choix collectifs : ceux qui sont concernés participent aux décisions.',
|
||||
},
|
||||
{
|
||||
num: 7,
|
||||
name: 'Contrôle du quorum',
|
||||
icon: 'i-lucide-check-circle',
|
||||
actor: 'Plateforme + porteur',
|
||||
duration: { min: '—', standard: '—', major: '—' },
|
||||
description: 'Vérifier que le quorum minimum est atteint avant clôture. Si non atteint : prolonger, relancer, ou annuler selon les règles préétablies.',
|
||||
essential: true,
|
||||
tip: 'Définir à l\'avance le quorum et la procédure si non atteint — évite les ambiguïtés.',
|
||||
ostrom: 'Principe 4 — Supervision des règles par les membres.',
|
||||
},
|
||||
{
|
||||
num: 8,
|
||||
name: 'Proclamation des résultats',
|
||||
icon: 'i-lucide-megaphone',
|
||||
actor: 'Plateforme + porteur',
|
||||
duration: { min: '—', standard: '1j', major: '1j' },
|
||||
description: 'Annoncer le résultat officiel avec les chiffres détaillés (votes pour, contre, abstentions, taux participation, seuil requis). Archiver on-chain si adopté.',
|
||||
essential: true,
|
||||
tip: 'La transparence des résultats est aussi importante que le résultat lui-même.',
|
||||
ostrom: 'Principe 8 — Gouvernance emboîtée : résultats remontés aux niveaux supérieurs.',
|
||||
},
|
||||
{
|
||||
num: 9,
|
||||
name: 'Mise en application',
|
||||
icon: 'i-lucide-play-circle',
|
||||
actor: 'Porteur + implémenteurs',
|
||||
duration: { min: '—', standard: 'Variable', major: 'Variable' },
|
||||
description: 'Planifier l\'application effective de la décision. Désigner les responsables. Fixer des jalons d\'implémentation si complexe.',
|
||||
essential: true,
|
||||
tip: 'Une décision adoptée mais non implémentée érode la confiance dans le processus.',
|
||||
},
|
||||
{
|
||||
num: 10,
|
||||
name: 'Suivi et accountability',
|
||||
icon: 'i-lucide-activity',
|
||||
actor: 'Porteur + communauté',
|
||||
duration: { min: '—', standard: 'Continu', major: 'Continu' },
|
||||
description: 'Rapports réguliers sur l\'avancement. Signalement des écarts. Mécanisme de remontée si la décision produit des effets inattendus.',
|
||||
essential: false,
|
||||
tip: 'Intégrer dans le prochain cycle de gouvernance si des ajustements s\'imposent.',
|
||||
ostrom: 'Principe 4 — Surveillance continue des comportements et résultats.',
|
||||
},
|
||||
{
|
||||
num: 11,
|
||||
name: 'Rétrospective',
|
||||
icon: 'i-lucide-rotate-ccw',
|
||||
actor: 'Cercle concerné',
|
||||
duration: { min: '—', standard: '1-2h', major: '1-2j' },
|
||||
description: 'Évaluer : le processus a-t-il bien fonctionné ? La décision produit-elle les effets attendus ? Quoi améliorer pour la prochaine fois ?',
|
||||
essential: false,
|
||||
tip: 'La rétrospective est le moteur d\'amélioration du protocole lui-même (méta-gouvernance).',
|
||||
ostrom: 'Principe 7 — Reconnaissance externe de l\'organisation par des autorités supérieures.',
|
||||
},
|
||||
]
|
||||
|
||||
const showOstrom = ref(false)
|
||||
const activeDecisionType = ref<'minor' | 'standard' | 'major'>('standard')
|
||||
|
||||
const decisionTypes = [
|
||||
{ value: 'minor', label: 'Mineur', color: 'teal' },
|
||||
{ value: 'standard', label: 'Standard', color: 'accent' },
|
||||
{ value: 'major', label: 'Majeur', color: 'secondary' },
|
||||
]
|
||||
|
||||
const essentialMilestones = computed(() =>
|
||||
milestones.filter(m => m.essential),
|
||||
)
|
||||
|
||||
const optionalMilestones = computed(() =>
|
||||
milestones.filter(m => !m.essential),
|
||||
)
|
||||
|
||||
const totalDuration = computed(() => {
|
||||
const type = activeDecisionType.value
|
||||
const durations = {
|
||||
minor: '5-10 jours',
|
||||
standard: '14-30 jours',
|
||||
major: '45-90 jours',
|
||||
}
|
||||
return durations[type]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wm">
|
||||
<!-- Header -->
|
||||
<div class="wm__header">
|
||||
<h3 class="wm__title">Jalons de protocole</h3>
|
||||
<p class="wm__subtitle">
|
||||
11 jalons, dont 7 indispensables. Durées recommandées selon le type de décision.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Decision type selector -->
|
||||
<div class="wm__type-selector">
|
||||
<button
|
||||
v-for="dt in decisionTypes"
|
||||
:key="dt.value"
|
||||
class="wm__type-btn"
|
||||
:class="[
|
||||
`wm__type-btn--${dt.color}`,
|
||||
{ 'wm__type-btn--active': activeDecisionType === dt.value },
|
||||
]"
|
||||
@click="activeDecisionType = dt.value as 'minor' | 'standard' | 'major'"
|
||||
>
|
||||
{{ dt.label }}
|
||||
</button>
|
||||
<span class="wm__total-duration">≈ {{ totalDuration }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Essential milestones -->
|
||||
<div class="wm__section">
|
||||
<div class="wm__section-label">
|
||||
<span class="wm__section-badge wm__section-badge--essential">7 essentiels</span>
|
||||
</div>
|
||||
<div class="wm__milestones">
|
||||
<div
|
||||
v-for="m in essentialMilestones"
|
||||
:key="m.num"
|
||||
class="wm__milestone wm__milestone--essential"
|
||||
>
|
||||
<div class="wm__milestone-left">
|
||||
<div class="wm__milestone-num">{{ m.num }}</div>
|
||||
<div v-if="m.num < milestones.length" class="wm__milestone-line" />
|
||||
</div>
|
||||
<div class="wm__milestone-icon">
|
||||
<UIcon :name="m.icon" />
|
||||
</div>
|
||||
<div class="wm__milestone-body">
|
||||
<div class="wm__milestone-head">
|
||||
<span class="wm__milestone-name">{{ m.name }}</span>
|
||||
<span class="wm__milestone-duration">
|
||||
{{ m.duration[activeDecisionType] || '—' }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="wm__milestone-desc">{{ m.description }}</p>
|
||||
<div v-if="m.tip" class="wm__milestone-tip">
|
||||
<UIcon name="i-lucide-lightbulb" />
|
||||
{{ m.tip }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Optional milestones -->
|
||||
<div class="wm__section">
|
||||
<div class="wm__section-label">
|
||||
<span class="wm__section-badge wm__section-badge--optional">4 contextuels</span>
|
||||
</div>
|
||||
<div class="wm__milestones">
|
||||
<div
|
||||
v-for="m in optionalMilestones"
|
||||
:key="m.num"
|
||||
class="wm__milestone wm__milestone--optional"
|
||||
>
|
||||
<div class="wm__milestone-left">
|
||||
<div class="wm__milestone-num wm__milestone-num--optional">{{ m.num }}</div>
|
||||
</div>
|
||||
<div class="wm__milestone-icon wm__milestone-icon--optional">
|
||||
<UIcon :name="m.icon" />
|
||||
</div>
|
||||
<div class="wm__milestone-body">
|
||||
<div class="wm__milestone-head">
|
||||
<span class="wm__milestone-name">{{ m.name }}</span>
|
||||
<span class="wm__milestone-duration">
|
||||
{{ m.duration[activeDecisionType] || '—' }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="wm__milestone-desc">{{ m.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ostrom toggle -->
|
||||
<button class="wm__ostrom-toggle" @click="showOstrom = !showOstrom">
|
||||
<UIcon name="i-lucide-book-open" />
|
||||
<span>Principes Ostrom appliqués</span>
|
||||
<UIcon :name="showOstrom ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'" />
|
||||
</button>
|
||||
|
||||
<Transition name="expand">
|
||||
<div v-if="showOstrom" class="wm__ostrom">
|
||||
<p class="wm__ostrom-intro">
|
||||
Elinor Ostrom (Nobel 2009) a identifié 8 principes pour la gouvernance
|
||||
durable des communs. Les jalons ci-dessus les incarnent.
|
||||
</p>
|
||||
<div class="wm__ostrom-items">
|
||||
<div
|
||||
v-for="m in milestones.filter(x => x.ostrom)"
|
||||
:key="m.num"
|
||||
class="wm__ostrom-item"
|
||||
>
|
||||
<span class="wm__ostrom-jalon">Jalon {{ m.num }}</span>
|
||||
<span class="wm__ostrom-text">{{ m.ostrom }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wm { display: flex; flex-direction: column; gap: 1rem; }
|
||||
|
||||
.wm__header { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
|
||||
.wm__title {
|
||||
font-size: 1rem;
|
||||
font-weight: 800;
|
||||
color: var(--mood-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wm__subtitle {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--mood-text-muted);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Type selector */
|
||||
.wm__type-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.wm__type-btn {
|
||||
padding: 0.375rem 0.875rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
background: var(--mood-accent-soft);
|
||||
color: var(--mood-text-muted);
|
||||
transition: all 0.12s ease;
|
||||
}
|
||||
|
||||
.wm__type-btn--accent.wm__type-btn--active {
|
||||
background: var(--mood-accent);
|
||||
color: var(--mood-accent-text);
|
||||
}
|
||||
|
||||
.wm__type-btn--teal.wm__type-btn--active {
|
||||
background: color-mix(in srgb, var(--mood-success) 20%, transparent);
|
||||
color: var(--mood-success);
|
||||
}
|
||||
|
||||
.wm__type-btn--secondary.wm__type-btn--active {
|
||||
background: color-mix(in srgb, var(--mood-secondary, var(--mood-accent)) 20%, transparent);
|
||||
color: var(--mood-secondary, var(--mood-accent));
|
||||
}
|
||||
|
||||
.wm__total-duration {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-text-muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Section */
|
||||
.wm__section { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
|
||||
.wm__section-label { display: flex; align-items: center; gap: 0.5rem; }
|
||||
|
||||
.wm__section-badge {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 2px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.wm__section-badge--essential {
|
||||
background: var(--mood-accent-soft);
|
||||
color: var(--mood-accent);
|
||||
}
|
||||
|
||||
.wm__section-badge--optional {
|
||||
background: color-mix(in srgb, var(--mood-text-muted) 12%, transparent);
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
|
||||
/* Milestones */
|
||||
.wm__milestones { display: flex; flex-direction: column; gap: 0; }
|
||||
|
||||
.wm__milestone {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.625rem;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.wm__milestone-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 1.375rem;
|
||||
}
|
||||
|
||||
.wm__milestone-num {
|
||||
width: 1.375rem;
|
||||
height: 1.375rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--mood-accent);
|
||||
color: var(--mood-accent-text);
|
||||
font-size: 0.625rem;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.wm__milestone-num--optional {
|
||||
background: color-mix(in srgb, var(--mood-text-muted) 20%, transparent);
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
|
||||
.wm__milestone-line {
|
||||
width: 2px;
|
||||
flex: 1;
|
||||
min-height: 1.25rem;
|
||||
background: color-mix(in srgb, var(--mood-accent) 20%, transparent);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.wm__milestone-icon {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: var(--mood-accent-soft);
|
||||
color: var(--mood-accent);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.wm__milestone-icon--optional {
|
||||
background: color-mix(in srgb, var(--mood-text-muted) 10%, transparent);
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
|
||||
.wm__milestone-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.wm__milestone-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.wm__milestone-name {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-text);
|
||||
}
|
||||
|
||||
.wm__milestone--optional .wm__milestone-name {
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
|
||||
.wm__milestone-duration {
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||
color: var(--mood-accent);
|
||||
background: var(--mood-accent-soft);
|
||||
padding: 1px 6px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.wm__milestone-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
line-height: 1.5;
|
||||
margin: 0.125rem 0 0;
|
||||
}
|
||||
|
||||
.wm__milestone-tip {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.375rem;
|
||||
margin-top: 0.375rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
background: color-mix(in srgb, var(--mood-accent) 8%, transparent);
|
||||
border-radius: 8px;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--mood-accent);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Ostrom */
|
||||
.wm__ostrom-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.75rem;
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-text-muted);
|
||||
transition: color 0.12s ease;
|
||||
text-align: left;
|
||||
}
|
||||
.wm__ostrom-toggle:hover { color: var(--mood-text); }
|
||||
.wm__ostrom-toggle .i-lucide-book-open { color: var(--mood-accent); }
|
||||
|
||||
.wm__ostrom {
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 12px;
|
||||
padding: 0.875rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.wm__ostrom-intro {
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wm__ostrom-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.wm__ostrom-item {
|
||||
display: flex;
|
||||
gap: 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.wm__ostrom-jalon {
|
||||
font-weight: 700;
|
||||
color: var(--mood-accent);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wm__ostrom-text { color: var(--mood-text-muted); }
|
||||
|
||||
/* Expand transition */
|
||||
.expand-enter-active, .expand-leave-active { transition: all 0.2s ease; overflow: hidden; }
|
||||
.expand-enter-from, .expand-leave-to { max-height: 0; opacity: 0; }
|
||||
.expand-enter-to, .expand-leave-from { max-height: 1000px; opacity: 1; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user