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:
Yvv
2026-03-02 08:52:56 +01:00
parent 62808b974d
commit 0b230483d9
5 changed files with 469 additions and 149 deletions

View File

@@ -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...")

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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);
} }