mirror of
https://codeberg.org/likwid/likwid.git
synced 2026-02-09 21:13:09 +00:00
ui: polish voting results chart
This commit is contained in:
parent
5adce4a066
commit
826ffd9022
1 changed files with 108 additions and 62 deletions
|
|
@ -41,6 +41,7 @@ const { proposalId, apiBase = '' } = Astro.props;
|
||||||
private data: any;
|
private data: any;
|
||||||
private apiBase: string;
|
private apiBase: string;
|
||||||
private enabled = false;
|
private enabled = false;
|
||||||
|
private tooltip?: HTMLElement;
|
||||||
|
|
||||||
constructor(container: HTMLElement) {
|
constructor(container: HTMLElement) {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
|
|
@ -161,9 +162,10 @@ const { proposalId, apiBase = '' } = Astro.props;
|
||||||
const isWinner = index === 0;
|
const isWinner = index === 0;
|
||||||
|
|
||||||
const safeLabel = this.escapeHtml(result.label);
|
const safeLabel = this.escapeHtml(result.label);
|
||||||
|
const safeValue = this.escapeHtml(this.formatScore(score, method));
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="result-bar ${isWinner ? 'winner' : ''}" style="--bar-delay: ${index * 0.1}s">
|
<div class="result-bar ${isWinner ? 'winner' : ''}" style="--bar-delay: ${index * 0.1}s" tabindex="0" data-label="${safeLabel}" data-value="${safeValue}">
|
||||||
<div class="bar-header">
|
<div class="bar-header">
|
||||||
<span class="bar-rank">#${result.rank || index + 1}</span>
|
<span class="bar-rank">#${result.rank || index + 1}</span>
|
||||||
<span class="bar-label">${safeLabel}</span>
|
<span class="bar-label">${safeLabel}</span>
|
||||||
|
|
@ -187,8 +189,11 @@ const { proposalId, apiBase = '' } = Astro.props;
|
||||||
<div class="bars-container">
|
<div class="bars-container">
|
||||||
${barsHtml}
|
${barsHtml}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="chart-tooltip" id="chart-tooltip" aria-hidden="true"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
this.attachChartInteractions(chartArea);
|
||||||
|
|
||||||
// Trigger animation
|
// Trigger animation
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
chartArea.querySelectorAll<HTMLElement>('.bar-fill').forEach((bar) => {
|
chartArea.querySelectorAll<HTMLElement>('.bar-fill').forEach((bar) => {
|
||||||
|
|
@ -197,6 +202,54 @@ const { proposalId, apiBase = '' } = Astro.props;
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachChartInteractions(chartArea: HTMLElement): void {
|
||||||
|
const tooltip = chartArea.querySelector<HTMLElement>('#chart-tooltip');
|
||||||
|
if (!tooltip) return;
|
||||||
|
this.tooltip = tooltip;
|
||||||
|
|
||||||
|
const show = (bar: HTMLElement) => {
|
||||||
|
if (!this.tooltip) return;
|
||||||
|
const label = bar.dataset.label || '';
|
||||||
|
const value = bar.dataset.value || '';
|
||||||
|
this.tooltip.textContent = `${label} • ${value}`;
|
||||||
|
this.tooltip.setAttribute('aria-hidden', 'false');
|
||||||
|
this.tooltip.classList.add('is-visible');
|
||||||
|
|
||||||
|
const rect = bar.getBoundingClientRect();
|
||||||
|
const left = Math.max(12, Math.min(window.innerWidth - 12, rect.left + rect.width * 0.35));
|
||||||
|
const top = Math.max(12, rect.top - 10);
|
||||||
|
this.tooltip.style.left = `${left}px`;
|
||||||
|
this.tooltip.style.top = `${top}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
if (!this.tooltip) return;
|
||||||
|
this.tooltip.setAttribute('aria-hidden', 'true');
|
||||||
|
this.tooltip.classList.remove('is-visible');
|
||||||
|
};
|
||||||
|
|
||||||
|
chartArea.addEventListener('mouseover', (e) => {
|
||||||
|
const target = e.target as HTMLElement | null;
|
||||||
|
const bar = target?.closest?.('.result-bar') as HTMLElement | null;
|
||||||
|
if (bar) show(bar);
|
||||||
|
});
|
||||||
|
|
||||||
|
chartArea.addEventListener('mouseout', (e) => {
|
||||||
|
const related = (e as MouseEvent).relatedTarget as HTMLElement | null;
|
||||||
|
if (!related || !related.closest?.('.result-bar')) hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
chartArea.addEventListener('focusin', (e) => {
|
||||||
|
const target = e.target as HTMLElement | null;
|
||||||
|
const bar = target?.closest?.('.result-bar') as HTMLElement | null;
|
||||||
|
if (bar) show(bar);
|
||||||
|
});
|
||||||
|
|
||||||
|
chartArea.addEventListener('focusout', () => {
|
||||||
|
hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
renderDetails(): void {
|
renderDetails(): void {
|
||||||
const detailsArea = this.container.querySelector<HTMLElement>('#details-area');
|
const detailsArea = this.container.querySelector<HTMLElement>('#details-area');
|
||||||
const roundsArea = this.container.querySelector<HTMLElement>('#rounds-area');
|
const roundsArea = this.container.querySelector<HTMLElement>('#rounds-area');
|
||||||
|
|
@ -366,7 +419,7 @@ const { proposalId, apiBase = '' } = Astro.props;
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.results-chart {
|
.results-chart {
|
||||||
background: var(--color-surface);
|
background: rgba(255, 255, 255, 0.03);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
|
@ -411,6 +464,7 @@ const { proposalId, apiBase = '' } = Astro.props;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card svg {
|
.stat-card svg {
|
||||||
|
|
@ -421,6 +475,7 @@ const { proposalId, apiBase = '' } = Astro.props;
|
||||||
.stat-card.winner {
|
.stat-card.winner {
|
||||||
background: linear-gradient(135deg, var(--color-warning) 0%, #f97316 100%);
|
background: linear-gradient(135deg, var(--color-warning) 0%, #f97316 100%);
|
||||||
color: white;
|
color: white;
|
||||||
|
border-color: rgba(255, 255, 255, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card.winner svg {
|
.stat-card.winner svg {
|
||||||
|
|
@ -443,82 +498,31 @@ const { proposalId, apiBase = '' } = Astro.props;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-header svg {
|
.chart-header svg {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bars-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-bar {
|
|
||||||
animation: fadeInUp 0.3s ease forwards;
|
|
||||||
animation-delay: var(--bar-delay, 0s);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeInUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-rank {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
min-width: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-label {
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--color-text);
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.winner-icon {
|
|
||||||
color: var(--color-warning);
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-track {
|
.bar-track {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
border-radius: 8px;
|
border-radius: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar-fill {
|
.bar-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-secondary) 100%);
|
||||||
|
border-radius: 16px;
|
||||||
width: 0;
|
width: 0;
|
||||||
background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
|
transition: width 0.8s ease;
|
||||||
border-radius: 8px;
|
box-shadow: 0 10px 24px rgba(59, 130, 246, 0.14);
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding-right: 0.75rem;
|
padding-right: 0.75rem;
|
||||||
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
@ -530,6 +534,48 @@ const { proposalId, apiBase = '' } = Astro.props;
|
||||||
|
|
||||||
.result-bar.winner .bar-fill {
|
.result-bar.winner .bar-fill {
|
||||||
background: linear-gradient(90deg, var(--color-warning) 0%, #f97316 100%);
|
background: linear-gradient(90deg, var(--color-warning) 0%, #f97316 100%);
|
||||||
|
box-shadow: 0 10px 24px rgba(249, 115, 22, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-tooltip {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 50;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(0, 0, 0, 0.72);
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-tooltip.is-visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.results-chart {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-track {
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-header {
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar-value {
|
.bar-value {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue