UX: texte valorise, vote discret, inertie visuelle, genese repliable
- EngagementCard: texte agrandi (15-16px), vote board discret (opacity, scale) - MiniVoteBoard: badge Adopte/En attente apres "Vote permanent :", board compact - InertiaSlider: labels descriptifs (inertie pour le remplacement), schema SVG avec courbe de seuil, formule simplifiee et legende parametres - GenesisBlock: toggle repliement individuel par section (source, outils, forum, processus, contributeurs) - Votes varies dans Conseils et bonnes pratiques (non-adoptes inclus) - Seed: Certification responsable → Reciprocite, ordonnancement inertie standard, notes variables K1/K2 (vote porte sur l'inclusion, pas les valeurs), init_db() dans seed.py pour DB vierge Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,7 @@ from datetime import datetime, timedelta, timezone
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.database import async_session, engine, Base
|
from app.database import async_session, engine, Base, init_db
|
||||||
from app.models.protocol import FormulaConfig, VotingProtocol
|
from app.models.protocol import FormulaConfig, VotingProtocol
|
||||||
from app.models.document import Document, DocumentItem
|
from app.models.document import Document, DocumentItem
|
||||||
from app.models.decision import Decision, DecisionStep
|
from app.models.decision import Decision, DecisionStep
|
||||||
@@ -351,7 +351,7 @@ ENGAGEMENT_CERTIFICATION_ITEMS: list[dict] = [
|
|||||||
{
|
{
|
||||||
"position": "E2",
|
"position": "E2",
|
||||||
"item_type": "clause",
|
"item_type": "clause",
|
||||||
"title": "Certification responsable",
|
"title": "Reciprocite",
|
||||||
"sort_order": 5,
|
"sort_order": 5,
|
||||||
"section_tag": "fondamental",
|
"section_tag": "fondamental",
|
||||||
"inertia_preset": "standard",
|
"inertia_preset": "standard",
|
||||||
@@ -534,7 +534,10 @@ ENGAGEMENT_CERTIFICATION_ITEMS: list[dict] = [
|
|||||||
"- Sentinelle : membre ayant recu et emis >= Y[N] certifs "
|
"- Sentinelle : membre ayant recu et emis >= Y[N] certifs "
|
||||||
"(Y = ceil(N^(1/5)))\n"
|
"(Y = ceil(N^(1/5)))\n"
|
||||||
"- Certifications actives valables **2 ans**\n"
|
"- Certifications actives valables **2 ans**\n"
|
||||||
"- Renouvellement de l'accord tous les **12 mois**"
|
"- Renouvellement de l'accord tous les **12 mois**\n\n"
|
||||||
|
"*Note : le vote porte sur l'inclusion de ces regles dans le document, "
|
||||||
|
"pas sur les valeurs des variables protocolaires elles-memes, "
|
||||||
|
"qui sont fixees par le protocole Duniter.*"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -549,7 +552,10 @@ ENGAGEMENT_CERTIFICATION_ITEMS: list[dict] = [
|
|||||||
"- 1 Dividende Universel (DU) par personne par jour\n"
|
"- 1 Dividende Universel (DU) par personne par jour\n"
|
||||||
"- Reevaluation a chaque equinoxe : "
|
"- Reevaluation a chaque equinoxe : "
|
||||||
"`DU(n+1) = DU(n) + c² × (M/N) / 182.625` avec c = 4.88%\n"
|
"`DU(n+1) = DU(n) + c² × (M/N) / 182.625` avec c = 4.88%\n"
|
||||||
"- DU(0) = 10.00 G1"
|
"- DU(0) = 10.00 G1\n\n"
|
||||||
|
"*Note : le vote porte sur l'inclusion de ces parametres dans le document, "
|
||||||
|
"pas sur les valeurs monetaires elles-memes, "
|
||||||
|
"qui decoulent de la TRM et du bloc 0.*"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
@@ -790,11 +796,11 @@ ENGAGEMENT_CERTIFICATION_ITEMS: list[dict] = [
|
|||||||
"title": "Ordonnancement du document",
|
"title": "Ordonnancement du document",
|
||||||
"sort_order": 33,
|
"sort_order": 33,
|
||||||
"section_tag": "ordonnancement",
|
"section_tag": "ordonnancement",
|
||||||
"inertia_preset": "high",
|
"inertia_preset": "standard",
|
||||||
"current_text": (
|
"current_text": (
|
||||||
"L'ordre de presentation des items dans le document est "
|
"L'ordre de presentation des items dans le document est "
|
||||||
"lui-meme soumis au vote. Toute proposition de reorganisation "
|
"lui-meme soumis au vote. Toute proposition de reorganisation "
|
||||||
"doit atteindre le seuil d'adoption avec l'inertie haute."
|
"doit atteindre le seuil d'adoption avec l'inertie standard."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -1442,6 +1448,10 @@ async def run_seed():
|
|||||||
print("Glibredecision - Seed Database")
|
print("Glibredecision - Seed Database")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Ensure tables exist
|
||||||
|
await init_db()
|
||||||
|
print("[0/7] Tables created.\n")
|
||||||
|
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
async with session.begin():
|
async with session.begin():
|
||||||
print("\n[1/7] Formula Configs...")
|
print("\n[1/7] Formula Configs...")
|
||||||
|
|||||||
@@ -46,6 +46,23 @@ const itemTypeLabel = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Mock vote data varies by item for demo — items in "bonnes pratiques" (E8-E11) get lower/mixed votes
|
||||||
|
const mockVotes = computed(() => {
|
||||||
|
const order = props.item.sort_order
|
||||||
|
const pos = props.item.position
|
||||||
|
|
||||||
|
// Conseils et bonnes pratiques: varied votes, some non-adopted
|
||||||
|
if (pos === 'E8') return { votesFor: 4, votesAgainst: 3 } // contested
|
||||||
|
if (pos === 'E9') return { votesFor: 2, votesAgainst: 5 } // rejected
|
||||||
|
if (pos === 'E10') return { votesFor: 6, votesAgainst: 2 } // borderline
|
||||||
|
if (pos === 'E11') return { votesFor: 3, votesAgainst: 4 } // rejected
|
||||||
|
|
||||||
|
// Default: well-adopted items
|
||||||
|
const base = ((order * 7 + 13) % 5) + 8 // 8-12
|
||||||
|
const against = (order % 3) // 0-2
|
||||||
|
return { votesFor: base, votesAgainst: against }
|
||||||
|
})
|
||||||
|
|
||||||
function navigateToItem() {
|
function navigateToItem() {
|
||||||
navigateTo(`/documents/${props.documentSlug}/items/${props.item.id}`)
|
navigateTo(`/documents/${props.documentSlug}/items/${props.item.id}`)
|
||||||
}
|
}
|
||||||
@@ -102,8 +119,8 @@ function navigateToItem() {
|
|||||||
<!-- Mini vote board -->
|
<!-- Mini vote board -->
|
||||||
<div v-if="showVoteBoard" class="engagement-card__vote">
|
<div v-if="showVoteBoard" class="engagement-card__vote">
|
||||||
<MiniVoteBoard
|
<MiniVoteBoard
|
||||||
:votes-for="10"
|
:votes-for="mockVotes.votesFor"
|
||||||
:votes-against="1"
|
:votes-against="mockVotes.votesAgainst"
|
||||||
:wot-size="7224"
|
:wot-size="7224"
|
||||||
:is-permanent="item.is_permanent_vote"
|
:is-permanent="item.is_permanent_vote"
|
||||||
:inertia-preset="item.inertia_preset"
|
:inertia-preset="item.inertia_preset"
|
||||||
@@ -227,19 +244,40 @@ function navigateToItem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.engagement-card__body {
|
.engagement-card__body {
|
||||||
padding: 0.5rem 1rem 0.75rem;
|
padding: 0.75rem 1rem 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.9375rem;
|
||||||
line-height: 1.6;
|
line-height: 1.7;
|
||||||
color: var(--mood-text);
|
color: var(--mood-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.engagement-card__body {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.engagement-card__vote {
|
.engagement-card__vote {
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: scale(0.92);
|
||||||
|
transform-origin: left center;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-card:hover .engagement-card__vote {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.engagement-card__inertia {
|
.engagement-card__inertia {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.375rem 1rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-card:hover .engagement-card__inertia {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.engagement-card__actions {
|
.engagement-card__actions {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* Genesis block: displays source documents, repos, forum synthesis, and formula trigger
|
* Genesis block: displays source documents, repos, forum synthesis, and formula trigger
|
||||||
* for a reference document. Collapsible by default.
|
* for a reference document. Main block collapsible, each sub-section independently collapsible.
|
||||||
*/
|
*/
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
genesisJson: string
|
genesisJson: string
|
||||||
@@ -9,6 +9,19 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const expanded = ref(false)
|
const expanded = ref(false)
|
||||||
|
|
||||||
|
// Individual section toggles
|
||||||
|
const sectionOpen = reactive<Record<string, boolean>>({
|
||||||
|
source: true,
|
||||||
|
tools: false,
|
||||||
|
forum: true,
|
||||||
|
process: false,
|
||||||
|
contributors: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleSection(key: string) {
|
||||||
|
sectionOpen[key] = !sectionOpen[key]
|
||||||
|
}
|
||||||
|
|
||||||
interface GenesisData {
|
interface GenesisData {
|
||||||
source_document: {
|
source_document: {
|
||||||
title: string
|
title: string
|
||||||
@@ -88,10 +101,18 @@ const statusLabel = (status: string) => {
|
|||||||
<div v-if="expanded" class="genesis-block__body">
|
<div v-if="expanded" class="genesis-block__body">
|
||||||
<!-- Source document -->
|
<!-- Source document -->
|
||||||
<div class="genesis-section">
|
<div class="genesis-section">
|
||||||
|
<button class="genesis-section__toggle" @click="toggleSection('source')">
|
||||||
<h4 class="genesis-section__title">
|
<h4 class="genesis-section__title">
|
||||||
<UIcon name="i-lucide-file-text" />
|
<UIcon name="i-lucide-file-text" />
|
||||||
Document source
|
Document source
|
||||||
</h4>
|
</h4>
|
||||||
|
<UIcon
|
||||||
|
:name="sectionOpen.source ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
||||||
|
class="text-sm"
|
||||||
|
style="color: var(--mood-text-muted)"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div v-if="sectionOpen.source" class="genesis-section__content">
|
||||||
<div class="genesis-card">
|
<div class="genesis-card">
|
||||||
<p class="font-medium text-sm" style="color: var(--mood-text)">
|
<p class="font-medium text-sm" style="color: var(--mood-text)">
|
||||||
{{ genesis.source_document.title }}
|
{{ genesis.source_document.title }}
|
||||||
@@ -118,13 +139,22 @@ const statusLabel = (status: string) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Reference tools -->
|
<!-- Reference tools -->
|
||||||
<div class="genesis-section">
|
<div class="genesis-section">
|
||||||
|
<button class="genesis-section__toggle" @click="toggleSection('tools')">
|
||||||
<h4 class="genesis-section__title">
|
<h4 class="genesis-section__title">
|
||||||
<UIcon name="i-lucide-wrench" />
|
<UIcon name="i-lucide-wrench" />
|
||||||
Outils de reference
|
Outils de reference
|
||||||
</h4>
|
</h4>
|
||||||
|
<UIcon
|
||||||
|
:name="sectionOpen.tools ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
||||||
|
class="text-sm"
|
||||||
|
style="color: var(--mood-text-muted)"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div v-if="sectionOpen.tools" class="genesis-section__content">
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<a
|
<a
|
||||||
v-for="(url, name) in genesis.reference_tools"
|
v-for="(url, name) in genesis.reference_tools"
|
||||||
@@ -141,13 +171,22 @@ const statusLabel = (status: string) => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Forum synthesis -->
|
<!-- Forum synthesis -->
|
||||||
<div class="genesis-section">
|
<div class="genesis-section">
|
||||||
|
<button class="genesis-section__toggle" @click="toggleSection('forum')">
|
||||||
<h4 class="genesis-section__title">
|
<h4 class="genesis-section__title">
|
||||||
<UIcon name="i-lucide-messages-square" />
|
<UIcon name="i-lucide-messages-square" />
|
||||||
Synthese des discussions
|
Synthese des discussions
|
||||||
</h4>
|
</h4>
|
||||||
|
<UIcon
|
||||||
|
:name="sectionOpen.forum ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
||||||
|
class="text-sm"
|
||||||
|
style="color: var(--mood-text-muted)"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div v-if="sectionOpen.forum" class="genesis-section__content">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<a
|
<a
|
||||||
v-for="topic in genesis.forum_synthesis"
|
v-for="topic in genesis.forum_synthesis"
|
||||||
@@ -176,26 +215,44 @@ const statusLabel = (status: string) => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Formula trigger -->
|
<!-- Formula trigger -->
|
||||||
<div class="genesis-section">
|
<div class="genesis-section">
|
||||||
|
<button class="genesis-section__toggle" @click="toggleSection('process')">
|
||||||
<h4 class="genesis-section__title">
|
<h4 class="genesis-section__title">
|
||||||
<UIcon name="i-lucide-zap" />
|
<UIcon name="i-lucide-zap" />
|
||||||
Processus de depot
|
Processus de depot
|
||||||
</h4>
|
</h4>
|
||||||
|
<UIcon
|
||||||
|
:name="sectionOpen.process ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
||||||
|
class="text-sm"
|
||||||
|
style="color: var(--mood-text-muted)"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div v-if="sectionOpen.process" class="genesis-section__content">
|
||||||
<div class="genesis-card">
|
<div class="genesis-card">
|
||||||
<p class="text-xs leading-relaxed" style="color: var(--mood-text)">
|
<p class="text-xs leading-relaxed" style="color: var(--mood-text)">
|
||||||
{{ genesis.formula_trigger }}
|
{{ genesis.formula_trigger }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Contributors -->
|
<!-- Contributors -->
|
||||||
<div class="genesis-section">
|
<div class="genesis-section">
|
||||||
|
<button class="genesis-section__toggle" @click="toggleSection('contributors')">
|
||||||
<h4 class="genesis-section__title">
|
<h4 class="genesis-section__title">
|
||||||
<UIcon name="i-lucide-users" />
|
<UIcon name="i-lucide-users" />
|
||||||
Contributeurs
|
Contributeurs
|
||||||
</h4>
|
</h4>
|
||||||
|
<UIcon
|
||||||
|
:name="sectionOpen.contributors ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
||||||
|
class="text-sm"
|
||||||
|
style="color: var(--mood-text-muted)"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div v-if="sectionOpen.contributors" class="genesis-section__content">
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<div
|
<div
|
||||||
v-for="c in genesis.contributors"
|
v-for="c in genesis.contributors"
|
||||||
@@ -208,6 +265,7 @@ const statusLabel = (status: string) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -250,7 +308,29 @@ const statusLabel = (status: string) => {
|
|||||||
padding: 0 1.25rem 1.25rem;
|
padding: 0 1.25rem 1.25rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.25rem;
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-section {
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: color-mix(in srgb, var(--mood-accent) 2%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-section__toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-section__toggle:hover {
|
||||||
|
background: color-mix(in srgb, var(--mood-accent) 5%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.genesis-section__title {
|
.genesis-section__title {
|
||||||
@@ -262,7 +342,11 @@ const statusLabel = (status: string) => {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
color: var(--mood-accent);
|
color: var(--mood-accent);
|
||||||
margin-bottom: 0.5rem;
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-section__content {
|
||||||
|
padding: 0 0.75rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.genesis-card {
|
.genesis-card {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* Inertia slider — displays the inertia preset level for a section.
|
* Inertia slider — displays the inertia preset level for a section.
|
||||||
* Read-only indicator (voting on the preset uses the standard vote flow).
|
* Read-only indicator (voting on the preset uses the standard vote flow).
|
||||||
* Shows the formula parameters underneath.
|
* In full mode: shows formula diagram with simplified curve visualization.
|
||||||
*/
|
*/
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
preset: string
|
preset: string
|
||||||
@@ -22,40 +22,86 @@ interface InertiaLevel {
|
|||||||
|
|
||||||
const LEVELS: Record<string, InertiaLevel> = {
|
const LEVELS: Record<string, InertiaLevel> = {
|
||||||
low: {
|
low: {
|
||||||
label: 'Basse',
|
label: 'Remplacement facile',
|
||||||
gradient: 0.1,
|
gradient: 0.1,
|
||||||
majority: 50,
|
majority: 50,
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
position: 10,
|
position: 10,
|
||||||
description: 'Facile a remplacer',
|
description: 'Majorite simple suffit, meme a faible participation',
|
||||||
},
|
},
|
||||||
standard: {
|
standard: {
|
||||||
label: 'Standard',
|
label: 'Inertie pour le remplacement',
|
||||||
gradient: 0.2,
|
gradient: 0.2,
|
||||||
majority: 50,
|
majority: 50,
|
||||||
color: '#3b82f6',
|
color: '#3b82f6',
|
||||||
position: 37,
|
position: 37,
|
||||||
description: 'Equilibre participation/consensus',
|
description: 'Equilibre : consensus croissant avec la participation',
|
||||||
},
|
},
|
||||||
high: {
|
high: {
|
||||||
label: 'Haute',
|
label: 'Remplacement difficile',
|
||||||
gradient: 0.4,
|
gradient: 0.4,
|
||||||
majority: 60,
|
majority: 60,
|
||||||
color: '#f59e0b',
|
color: '#f59e0b',
|
||||||
position: 63,
|
position: 63,
|
||||||
description: 'Forte mobilisation requise',
|
description: 'Forte mobilisation et super-majorite requises',
|
||||||
},
|
},
|
||||||
very_high: {
|
very_high: {
|
||||||
label: 'Tres haute',
|
label: 'Remplacement tres difficile',
|
||||||
gradient: 0.6,
|
gradient: 0.6,
|
||||||
majority: 66,
|
majority: 66,
|
||||||
color: '#ef4444',
|
color: '#ef4444',
|
||||||
position: 90,
|
position: 90,
|
||||||
description: 'Quasi-unanimite requise',
|
description: 'Quasi-unanimite requise a toute participation',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const level = computed((): InertiaLevel => LEVELS[props.preset] ?? LEVELS.standard!)
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -91,6 +137,80 @@ const level = computed((): InertiaLevel => LEVELS[props.preset] ?? LEVELS.standa
|
|||||||
<p v-if="!compact" class="inertia__desc">
|
<p v-if="!compact" class="inertia__desc">
|
||||||
{{ level.description }}
|
{{ level.description }}
|
||||||
</p>
|
</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 exprimes</span>
|
||||||
|
<span><strong>W</strong> = taille WoT</span>
|
||||||
|
<span><strong>M</strong> = majorite cible</span>
|
||||||
|
<span><strong>G</strong> = gradient d'inertie</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -119,14 +239,11 @@ const level = computed((): InertiaLevel => LEVELS[props.preset] ?? LEVELS.standa
|
|||||||
.inertia__fill {
|
.inertia__fill {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
right: auto;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inertia__fill {
|
|
||||||
right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inertia__thumb {
|
.inertia__thumb {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -189,4 +306,83 @@ const level = computed((): InertiaLevel => LEVELS[props.preset] ?? LEVELS.standa
|
|||||||
color: var(--mood-text-muted);
|
color: var(--mood-text-muted);
|
||||||
line-height: 1.3;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -81,37 +81,29 @@ function formatDate(d: string): string {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mini-board">
|
<div class="mini-board">
|
||||||
<!-- Vote type badge -->
|
<!-- Vote type + status on same line -->
|
||||||
<div class="mini-board__header">
|
<div class="mini-board__header">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
<UBadge
|
<template v-if="isPermanent">
|
||||||
v-if="isPermanent"
|
<UIcon name="i-lucide-infinity" class="text-xs" style="color: var(--mood-accent)" />
|
||||||
color="primary"
|
<span class="text-xs font-semibold" style="color: var(--mood-text-muted)">Vote permanent :</span>
|
||||||
variant="subtle"
|
</template>
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
<UIcon name="i-lucide-infinity" class="mr-1" />
|
|
||||||
Vote permanent
|
|
||||||
</UBadge>
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<UBadge color="info" variant="subtle" size="xs">
|
<UIcon name="i-lucide-clock" class="text-xs" style="color: var(--mood-accent)" />
|
||||||
<UIcon name="i-lucide-clock" class="mr-1" />
|
<span class="text-xs font-semibold" style="color: var(--mood-text-muted)">Vote temporaire :</span>
|
||||||
Vote temporaire
|
|
||||||
</UBadge>
|
|
||||||
<span v-if="startsAt && endsAt" class="text-xs" style="color: var(--mood-text-muted)">
|
<span v-if="startsAt && endsAt" class="text-xs" style="color: var(--mood-text-muted)">
|
||||||
{{ formatDate(startsAt) }} - {{ formatDate(endsAt) }}
|
{{ formatDate(startsAt) }} - {{ formatDate(endsAt) }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
|
||||||
|
|
||||||
<UBadge
|
<UBadge
|
||||||
:color="isPassing ? 'success' : 'neutral'"
|
:color="isPassing ? 'success' : 'warning'"
|
||||||
:variant="isPassing ? 'solid' : 'subtle'"
|
:variant="isPassing ? 'solid' : 'subtle'"
|
||||||
size="xs"
|
size="xs"
|
||||||
>
|
>
|
||||||
{{ isPassing ? 'Adopte' : 'En attente' }}
|
{{ isPassing ? 'Adopte' : 'En attente' }}
|
||||||
</UBadge>
|
</UBadge>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Progress bar -->
|
<!-- Progress bar -->
|
||||||
<div class="mini-board__bar">
|
<div class="mini-board__bar">
|
||||||
@@ -165,9 +157,9 @@ function formatDate(d: string): string {
|
|||||||
.mini-board {
|
.mini-board {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.375rem;
|
||||||
padding: 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
background: color-mix(in srgb, var(--mood-accent) 3%, var(--mood-bg));
|
background: color-mix(in srgb, var(--mood-accent) 3%, var(--mood-bg));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +215,7 @@ function formatDate(d: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mini-board__stat {
|
.mini-board__stat {
|
||||||
font-size: 0.6875rem;
|
font-size: 0.625rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--mood-text-muted);
|
color: var(--mood-text-muted);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user