likwid/frontend/src/components/voting/VotingMethodCard.astro

298 lines
7.3 KiB
Text
Raw Normal View History

---
/**
* VotingMethodCard - Visual explainer card for a voting method
* Shows icon, name, description, and interactive example
*/
interface Props {
method: 'approval' | 'ranked_choice' | 'schulze' | 'star' | 'quadratic';
2026-02-06 18:31:06 +00:00
compact?: boolean;
selected?: boolean;
interactive?: boolean;
}
const { method, compact = false, selected = false, interactive = false } = Astro.props;
const methodData = {
approval: {
name: 'Approval Voting',
icon: 'icon-approval',
color: '#22c55e',
shortDesc: 'Select all options you approve',
fullDesc: 'Vote for as many options as you like. The option with the most approvals wins. Simple and reduces strategic voting.',
complexity: 1,
pros: ['Simple to understand', 'No spoiler effect', 'Encourages honest voting'],
cons: ['No preference intensity', 'May produce ties'],
},
ranked_choice: {
name: 'Ranked Choice',
icon: 'icon-ranked-choice',
color: '#3b82f6',
shortDesc: 'Rank options in order of preference',
fullDesc: 'Rank candidates from first to last choice. If no majority, lowest candidate eliminated and votes redistributed until majority.',
complexity: 2,
pros: ['Eliminates spoiler effect', 'Majority winner', 'Expresses preferences'],
cons: ['More complex counting', 'Non-monotonic edge cases'],
},
schulze: {
name: 'Schulze Method',
icon: 'icon-schulze',
color: '#8b5cf6',
shortDesc: 'Pairwise comparison tournament',
fullDesc: 'Condorcet-consistent method using strongest paths. Compares every pair of options and finds the winner who beats all others.',
complexity: 3,
pros: ['Condorcet winner guaranteed', 'Handles cycles', 'Clone-proof'],
cons: ['Complex to explain', 'Requires all rankings'],
},
star: {
name: 'STAR Voting',
icon: 'icon-star',
color: '#f59e0b',
shortDesc: 'Score options 0-5, top two face runoff',
fullDesc: 'Score Then Automatic Runoff: Rate each option 0-5 stars. Top two scoring options go to automatic runoff based on preferences.',
complexity: 2,
pros: ['Express intensity', 'Automatic runoff', 'Reduces strategy'],
cons: ['Two-phase complexity', 'Score interpretation varies'],
},
quadratic: {
name: 'Quadratic Voting',
icon: 'icon-quadratic',
color: '#ec4899',
shortDesc: 'Spend credits where cost = votes²',
fullDesc: 'Allocate credits to options. Cost grows quadratically: 1 vote = 1 credit, 2 votes = 4 credits, 3 = 9. Express intensity of preference.',
complexity: 3,
pros: ['Intensity expression', 'Prevents vote buying', 'Fair allocation'],
cons: ['Budget strategy', 'Math complexity'],
},
};
const data = methodData[method];
const complexityBars = Array(3).fill(0).map((_, i) => i < data.complexity);
---
<div class:list={['method-card', { compact, selected, interactive }]} data-method={method}>
<div class="method-header">
<div class="method-icon" style={`--method-color: ${data.color}`}>
<svg class="icon icon-lg"><use href={`#${data.icon}`}/></svg>
</div>
<div class="method-title">
<h3>{data.name}</h3>
<div class="complexity">
<span class="complexity-label">Complexity:</span>
<div class="complexity-bars">
{complexityBars.map((filled) => (
<div class:list={['complexity-bar', { filled }]} />
))}
</div>
</div>
</div>
{selected && (
<div class="selected-badge">
<svg class="icon"><use href="#icon-check"/></svg>
</div>
)}
</div>
{!compact && (
<>
<p class="method-desc">{data.fullDesc}</p>
<div class="method-traits">
<div class="pros">
<span class="trait-label">Advantages</span>
<ul>
{data.pros.map(pro => (
<li>
<svg class="icon icon-sm"><use href="#icon-check"/></svg>
{pro}
</li>
))}
</ul>
</div>
<div class="cons">
<span class="trait-label">Considerations</span>
<ul>
{data.cons.map(con => (
<li>
<svg class="icon icon-sm"><use href="#icon-info"/></svg>
{con}
</li>
))}
</ul>
</div>
</div>
</>
)}
{compact && (
<p class="method-short-desc">{data.shortDesc}</p>
)}
</div>
<style>
.method-card {
background: var(--color-surface);
border: 2px solid var(--color-border);
border-radius: 12px;
padding: 1.25rem;
transition: all 0.2s ease;
}
.method-card.interactive {
cursor: pointer;
}
.method-card.interactive:hover {
border-color: var(--method-color, var(--color-primary));
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.method-card.selected {
border-color: var(--method-color, var(--color-primary));
background: color-mix(in srgb, var(--method-color, var(--color-primary)) 5%, var(--color-surface));
}
.method-header {
display: flex;
align-items: flex-start;
gap: 1rem;
margin-bottom: 1rem;
}
.method-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, var(--method-color) 15%, transparent);
border-radius: 10px;
color: var(--method-color);
flex-shrink: 0;
}
.method-title {
flex: 1;
}
.method-title h3 {
margin: 0 0 0.25rem;
font-size: 1.125rem;
color: var(--color-text);
}
.complexity {
display: flex;
align-items: center;
gap: 0.5rem;
}
.complexity-label {
font-size: 0.75rem;
color: var(--color-text-muted);
}
.complexity-bars {
display: flex;
gap: 3px;
}
.complexity-bar {
width: 16px;
height: 6px;
border-radius: 3px;
background: var(--color-border);
}
.complexity-bar.filled {
background: var(--method-color, var(--color-primary));
}
.selected-badge {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-success);
border-radius: 50%;
color: white;
}
.method-desc {
color: var(--color-text);
font-size: 0.9375rem;
line-height: 1.6;
margin: 0 0 1rem;
}
.method-short-desc {
color: var(--color-text-muted);
font-size: 0.875rem;
margin: 0;
}
.method-traits {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.trait-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
display: block;
margin-bottom: 0.5rem;
}
.method-traits ul {
list-style: none;
padding: 0;
margin: 0;
}
.method-traits li {
display: flex;
align-items: flex-start;
gap: 0.5rem;
font-size: 0.8125rem;
color: var(--color-text);
margin-bottom: 0.375rem;
}
.method-traits li svg {
flex-shrink: 0;
margin-top: 2px;
}
.pros li svg {
color: var(--color-success);
}
.cons li svg {
color: var(--color-warning);
}
.compact .method-header {
margin-bottom: 0.5rem;
}
.compact .method-icon {
width: 40px;
height: 40px;
}
.compact .method-title h3 {
font-size: 1rem;
}
@media (max-width: 640px) {
.method-traits {
grid-template-columns: 1fr;
}
}
</style>