- Seed: restructure Engagement Forgeron (51→59 items) avec 3 nouvelles sections: Engagements fondamentaux (EF1-EF3), Engagements techniques (ET1-ET3), Qualification (Q0-Q1) liée au protocole Embarquement - Seed: ajout protocole Embarquement Forgeron (5 jalons: candidature, miroir, évaluation, certification Smith, mise en ligne) - GenesisBlock: fix lisibilité — fond mood-surface teinté accent au lieu de mood-text inversé, texte mood-aware au lieu de rgba blanc hardcodé - InertiaSlider: mini affiche "Inertie" sous le curseur, compact en width:fit-content pour s'adapter au label - Frontend: ajout section qualification dans SECTION_META/SECTION_ORDER - Pages, composants et tests des sprints précédents Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
420 lines
10 KiB
Vue
420 lines
10 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* Inertia slider — displays the inertia preset level for a section.
|
||
* Read-only indicator (voting on the preset uses the standard vote flow).
|
||
* In full mode: shows formula diagram with simplified curve visualization.
|
||
*/
|
||
const props = withDefaults(defineProps<{
|
||
preset: string
|
||
compact?: boolean
|
||
mini?: boolean
|
||
}>(), {
|
||
compact: false,
|
||
mini: false,
|
||
})
|
||
|
||
interface InertiaLevel {
|
||
label: string
|
||
gradient: number
|
||
majority: number
|
||
color: string
|
||
position: number // 0-100 for slider position
|
||
description: string
|
||
}
|
||
|
||
const LEVELS: Record<string, InertiaLevel> = {
|
||
low: {
|
||
label: 'Remplacement facile',
|
||
gradient: 0.1,
|
||
majority: 50,
|
||
color: '#22c55e',
|
||
position: 10,
|
||
description: 'Majorité simple suffit, même à faible participation',
|
||
},
|
||
standard: {
|
||
label: 'Inertie pour le remplacement',
|
||
gradient: 0.2,
|
||
majority: 50,
|
||
color: '#3b82f6',
|
||
position: 37,
|
||
description: 'Équilibre : consensus croissant avec la participation',
|
||
},
|
||
high: {
|
||
label: 'Remplacement difficile',
|
||
gradient: 0.4,
|
||
majority: 60,
|
||
color: '#f59e0b',
|
||
position: 63,
|
||
description: 'Forte mobilisation et super-majorité requises',
|
||
},
|
||
very_high: {
|
||
label: 'Remplacement très difficile',
|
||
gradient: 0.6,
|
||
majority: 66,
|
||
color: '#ef4444',
|
||
position: 90,
|
||
description: 'Quasi-unanimité requise à toute participation',
|
||
},
|
||
}
|
||
|
||
const level = computed((): InertiaLevel => LEVELS[props.preset] ?? LEVELS.standard!)
|
||
|
||
// Generate SVG curve points for the inertia function
|
||
// Formula simplified: Seuil% = M + (1-M) × (1 - (T/W)^G)
|
||
// Where T/W = participation rate, so Seuil% goes from ~100% at low participation to M at full participation
|
||
const curvePath = computed(() => {
|
||
const G = level.value.gradient
|
||
const M = level.value.majority / 100
|
||
const points: string[] = []
|
||
const steps = 40
|
||
|
||
for (let i = 0; i <= steps; i++) {
|
||
const participation = i / steps // T/W ratio 0..1
|
||
const threshold = M + (1 - M) * (1 - Math.pow(participation, G))
|
||
// SVG coordinates: x = participation (0..200), y = threshold inverted (0=100%, 80=20%)
|
||
const x = 30 + participation * 170
|
||
const y = 10 + (1 - threshold) * 70
|
||
points.push(`${x.toFixed(1)},${y.toFixed(1)}`)
|
||
}
|
||
|
||
return `M ${points.join(' L ')}`
|
||
})
|
||
|
||
// The 4 curve paths for the diagram overlay
|
||
const allCurves = computed(() => {
|
||
return Object.entries(LEVELS).map(([key, lvl]) => {
|
||
const G = lvl.gradient
|
||
const M = lvl.majority / 100
|
||
const points: string[] = []
|
||
const steps = 40
|
||
|
||
for (let i = 0; i <= steps; i++) {
|
||
const participation = i / steps
|
||
const threshold = M + (1 - M) * (1 - Math.pow(participation, G))
|
||
const x = 30 + participation * 170
|
||
const y = 10 + (1 - threshold) * 70
|
||
points.push(`${x.toFixed(1)},${y.toFixed(1)}`)
|
||
}
|
||
|
||
return {
|
||
key,
|
||
color: lvl.color,
|
||
path: `M ${points.join(' L ')}`,
|
||
active: key === props.preset,
|
||
}
|
||
})
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="inertia" :class="{ 'inertia--compact': compact, 'inertia--mini': mini }">
|
||
<!-- Slider track -->
|
||
<div class="inertia__track">
|
||
<div class="inertia__fill" :style="{ width: `${level.position}%`, background: level.color }" />
|
||
<div
|
||
class="inertia__thumb"
|
||
:style="{ left: `${level.position}%`, borderColor: level.color }"
|
||
/>
|
||
<!-- Level marks -->
|
||
<div
|
||
v-for="(lvl, key) in LEVELS"
|
||
:key="key"
|
||
class="inertia__mark"
|
||
:class="{ 'inertia__mark--active': key === preset }"
|
||
:style="{ left: `${lvl.position}%` }"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Label row -->
|
||
<div v-if="mini" class="inertia__info">
|
||
<span class="inertia__label inertia__label--mini" :style="{ color: level.color }">
|
||
Inertie
|
||
</span>
|
||
</div>
|
||
<div v-else class="inertia__info">
|
||
<span class="inertia__label" :style="{ color: level.color }">
|
||
{{ level.label }}
|
||
</span>
|
||
<span v-if="!compact" class="inertia__params">
|
||
G={{ level.gradient }} M={{ level.majority }}%
|
||
</span>
|
||
</div>
|
||
|
||
<!-- Description (not in compact mode) -->
|
||
<p v-if="!compact" class="inertia__desc">
|
||
{{ level.description }}
|
||
</p>
|
||
|
||
<!-- Formula diagram (not in compact mode) -->
|
||
<div v-if="!compact" class="inertia__diagram">
|
||
<svg viewBox="0 0 220 100" class="inertia__svg">
|
||
<!-- Grid -->
|
||
<line x1="30" y1="10" x2="30" y2="80" class="inertia__axis" />
|
||
<line x1="30" y1="80" x2="200" y2="80" class="inertia__axis" />
|
||
|
||
<!-- Grid lines -->
|
||
<line x1="30" y1="10" x2="200" y2="10" class="inertia__grid" />
|
||
<line x1="30" y1="45" x2="200" y2="45" class="inertia__grid" />
|
||
|
||
<!-- Majority line M -->
|
||
<line
|
||
x1="30"
|
||
:y1="10 + (1 - level.majority / 100) * 70"
|
||
x2="200"
|
||
:y2="10 + (1 - level.majority / 100) * 70"
|
||
class="inertia__majority-line"
|
||
/>
|
||
<text
|
||
x="203"
|
||
:y="13 + (1 - level.majority / 100) * 70"
|
||
class="inertia__axis-label"
|
||
style="fill: var(--mood-accent)"
|
||
>M={{ level.majority }}%</text>
|
||
|
||
<!-- Background curves (ghosted) -->
|
||
<path
|
||
v-for="curve in allCurves"
|
||
:key="curve.key"
|
||
:d="curve.path"
|
||
fill="none"
|
||
:stroke="curve.color"
|
||
:stroke-width="curve.active ? 0 : 1"
|
||
:opacity="curve.active ? 0 : 0.15"
|
||
stroke-dasharray="3 3"
|
||
/>
|
||
|
||
<!-- Active curve -->
|
||
<path
|
||
:d="curvePath"
|
||
fill="none"
|
||
:stroke="level.color"
|
||
stroke-width="2.5"
|
||
stroke-linecap="round"
|
||
/>
|
||
|
||
<!-- Axis labels -->
|
||
<text x="15" y="14" class="inertia__axis-label">100%</text>
|
||
<text x="15" y="49" class="inertia__axis-label">50%</text>
|
||
<text x="15" y="84" class="inertia__axis-label">0%</text>
|
||
|
||
<text x="28" y="95" class="inertia__axis-label">0%</text>
|
||
<text x="105" y="95" class="inertia__axis-label">50%</text>
|
||
<text x="185" y="95" class="inertia__axis-label">100%</text>
|
||
|
||
<!-- Axis titles -->
|
||
<text x="3" y="50" class="inertia__axis-title" transform="rotate(-90, 6, 50)">Seuil</text>
|
||
<text x="110" y="100" class="inertia__axis-title">Participation (T/W)</text>
|
||
</svg>
|
||
|
||
<!-- Simplified formula -->
|
||
<div class="inertia__formula">
|
||
<span class="inertia__formula-label">Formule :</span>
|
||
<code class="inertia__formula-code">Seuil = M + (1-M) × (1 - (T/W)<sup>G</sup>)</code>
|
||
</div>
|
||
<div class="inertia__formula-legend">
|
||
<span><strong>T</strong> = votes exprimés</span>
|
||
<span><strong>W</strong> = taille WoT</span>
|
||
<span><strong>M</strong> = majorité cible</span>
|
||
<span><strong>G</strong> = gradient d'inertie</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.inertia {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.375rem;
|
||
}
|
||
|
||
.inertia--compact {
|
||
gap: 0.25rem;
|
||
width: fit-content;
|
||
}
|
||
|
||
.inertia--mini {
|
||
gap: 0.125rem;
|
||
width: fit-content;
|
||
min-width: 3rem;
|
||
}
|
||
|
||
.inertia--mini .inertia__track {
|
||
height: 3px;
|
||
}
|
||
|
||
.inertia--mini .inertia__thumb {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-width: 2px;
|
||
}
|
||
|
||
.inertia__track {
|
||
position: relative;
|
||
height: 6px;
|
||
background: color-mix(in srgb, var(--mood-text) 10%, transparent);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.inertia--compact .inertia__track {
|
||
height: 4px;
|
||
}
|
||
|
||
.inertia__fill {
|
||
position: absolute;
|
||
inset: 0;
|
||
right: auto;
|
||
border-radius: 3px;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.inertia__thumb {
|
||
position: absolute;
|
||
top: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: 14px;
|
||
height: 14px;
|
||
border-radius: 50%;
|
||
background: var(--mood-bg);
|
||
border: 3px solid;
|
||
transition: left 0.3s ease;
|
||
z-index: 2;
|
||
}
|
||
|
||
.inertia--compact .inertia__thumb {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-width: 2px;
|
||
}
|
||
|
||
.inertia__mark {
|
||
position: absolute;
|
||
top: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: 4px;
|
||
height: 4px;
|
||
border-radius: 50%;
|
||
background: color-mix(in srgb, var(--mood-text) 20%, transparent);
|
||
z-index: 1;
|
||
}
|
||
|
||
.inertia__mark--active {
|
||
background: transparent;
|
||
}
|
||
|
||
.inertia__info {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.inertia__label {
|
||
font-size: 0.6875rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
.inertia--compact .inertia__label {
|
||
font-size: 0.625rem;
|
||
}
|
||
|
||
.inertia__label--mini {
|
||
font-size: 0.5625rem;
|
||
font-weight: 600;
|
||
text-transform: none;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.inertia__params {
|
||
font-size: 0.625rem;
|
||
font-family: monospace;
|
||
color: var(--mood-text-muted);
|
||
}
|
||
|
||
.inertia__desc {
|
||
font-size: 0.6875rem;
|
||
color: var(--mood-text-muted);
|
||
line-height: 1.3;
|
||
}
|
||
|
||
/* Diagram */
|
||
.inertia__diagram {
|
||
margin-top: 0.5rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.inertia__svg {
|
||
width: 100%;
|
||
max-width: 320px;
|
||
height: auto;
|
||
}
|
||
|
||
.inertia__axis {
|
||
stroke: color-mix(in srgb, var(--mood-text) 25%, transparent);
|
||
stroke-width: 1;
|
||
}
|
||
|
||
.inertia__grid {
|
||
stroke: color-mix(in srgb, var(--mood-text) 8%, transparent);
|
||
stroke-width: 0.5;
|
||
stroke-dasharray: 2 4;
|
||
}
|
||
|
||
.inertia__majority-line {
|
||
stroke: var(--mood-accent);
|
||
stroke-width: 0.75;
|
||
stroke-dasharray: 4 3;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.inertia__axis-label {
|
||
font-size: 5px;
|
||
fill: var(--mood-text-muted);
|
||
font-family: monospace;
|
||
}
|
||
|
||
.inertia__axis-title {
|
||
font-size: 5px;
|
||
fill: var(--mood-text-muted);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.inertia__formula {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.375rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.inertia__formula-label {
|
||
font-size: 0.625rem;
|
||
font-weight: 600;
|
||
color: var(--mood-text-muted);
|
||
}
|
||
|
||
.inertia__formula-code {
|
||
font-size: 0.6875rem;
|
||
font-family: monospace;
|
||
color: var(--mood-text);
|
||
padding: 0.125rem 0.375rem;
|
||
border-radius: 4px;
|
||
background: color-mix(in srgb, var(--mood-accent) 6%, var(--mood-bg));
|
||
}
|
||
|
||
.inertia__formula-legend {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.75rem;
|
||
font-size: 0.5625rem;
|
||
color: var(--mood-text-muted);
|
||
}
|
||
|
||
.inertia__formula-legend strong {
|
||
color: var(--mood-text);
|
||
font-weight: 700;
|
||
}
|
||
</style>
|