UI-Improvements
This commit is contained in:
@@ -8,44 +8,64 @@ import { EventLogRow, formatSolveDateTime } from './scoreboard.pure';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule],
|
||||
styles: [`
|
||||
:host { display: block; }
|
||||
.empty { padding: 32px; text-align: center; opacity: 0.7; }
|
||||
:host { display: block; max-width: 840px; margin: 0 auto; }
|
||||
.empty { padding: 32px; text-align: center; color: #777788; font-family: var(--font-mono); }
|
||||
ol {
|
||||
list-style: none; padding: 0; margin: 0;
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
max-height: 70vh; overflow: auto;
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
max-height: 75vh; overflow-y: auto;
|
||||
}
|
||||
li {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 8px 10px;
|
||||
background: var(--color-surface, #1c1f26);
|
||||
border: 1px solid var(--color-border, #2a2f3a);
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
font-size: 13px;
|
||||
padding: 12px 16px;
|
||||
background: #000000;
|
||||
border: 2px solid #1a1a24;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9rem;
|
||||
color: #ffffff;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
li:hover {
|
||||
border-color: var(--neon-purple);
|
||||
box-shadow: 0 0 10px var(--neon-purple-glow);
|
||||
}
|
||||
.icon { font-size: 1.1rem; min-width: 24px; text-align: center; }
|
||||
.icon-gold { color: #ffb700; }
|
||||
.icon-silver { color: #c0c0c0; }
|
||||
.icon-bronze { color: #cd7f32; }
|
||||
.icon-check { color: var(--neon-green); }
|
||||
.time { color: #777788; font-size: 0.8rem; }
|
||||
.who {
|
||||
font-family: var(--font-heading, 'Chakra Petch', sans-serif);
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
font-size: 1rem;
|
||||
color: #ffffff;
|
||||
}
|
||||
.action { color: #aaaaee; }
|
||||
.cat { color: var(--neon-purple); font-weight: bold; }
|
||||
.challenge { color: var(--neon-green); font-weight: bold; }
|
||||
.pts-box {
|
||||
margin-left: auto;
|
||||
border: 2px solid var(--neon-purple);
|
||||
background: #000000;
|
||||
color: #ffffff;
|
||||
font-weight: 800;
|
||||
padding: 3px 10px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.icon { font-size: 18px; min-width: 22px; text-align: center; }
|
||||
.icon-gold { color: #d4a017; }
|
||||
.icon-silver { color: #9aa3b0; }
|
||||
.icon-bronze { color: #b87333; }
|
||||
.icon-check { color: var(--color-success, #22c55e); }
|
||||
.time { opacity: 0.7; font-variant-numeric: tabular-nums; min-width: 170px; }
|
||||
.who { font-weight: 600; min-width: 120px; }
|
||||
.what { flex: 1; min-width: 0; }
|
||||
.abbrev { opacity: 0.65; margin-right: 6px; font-size: 11px; }
|
||||
.pts { font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
`],
|
||||
template: `
|
||||
<ng-container *ngIf="rows?.length; else empty">
|
||||
<ol data-testid="scoreboard-event-log">
|
||||
<li *ngFor="let r of rows; trackBy: trackBySolve" [attr.data-testid]="'event-log-row-' + r.solveId">
|
||||
<span [class]="iconClass(r)" class="icon" aria-hidden="true">{{ icon(r) }}</span>
|
||||
<span class="time">{{ format(r.awardedAtUtc) }}</span>
|
||||
<span class="time">[{{ format(r.awardedAtUtc) }}]</span>
|
||||
<span class="who">{{ r.playerName }}</span>
|
||||
<span class="what">
|
||||
<span class="abbrev" *ngIf="r.categoryAbbreviation">{{ r.categoryAbbreviation }}</span>
|
||||
{{ r.challengeName }}
|
||||
</span>
|
||||
<span class="pts">+{{ r.awardedPoints }}</span>
|
||||
<span class="action">solved</span>
|
||||
<span class="cat" *ngIf="r.categoryAbbreviation">{{ r.categoryAbbreviation }} -</span>
|
||||
<span class="challenge">{{ r.challengeName }}</span>
|
||||
<div class="pts-box">{{ r.awardedPoints }} points</div>
|
||||
</li>
|
||||
</ol>
|
||||
</ng-container>
|
||||
@@ -60,9 +80,9 @@ export class EventLogComponent {
|
||||
format = formatSolveDateTime;
|
||||
|
||||
icon(r: EventLogRow): string {
|
||||
if (r.position === 1) return '★';
|
||||
if (r.position === 2) return '★';
|
||||
if (r.position === 3) return '★';
|
||||
if (r.position === 1) return '🏆';
|
||||
if (r.position === 2) return '🥈';
|
||||
if (r.position === 3) return '🥉';
|
||||
return '✓';
|
||||
}
|
||||
|
||||
|
||||
@@ -8,65 +8,93 @@ import { PLAYER_COLOR_PALETTE, MatrixView } from './scoreboard.pure';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule],
|
||||
styles: [`
|
||||
:host { display: block; }
|
||||
.empty { padding: 32px; text-align: center; opacity: 0.7; }
|
||||
.scroll {
|
||||
:host { display: block; width: 100%; }
|
||||
.empty { padding: 32px; text-align: center; color: #777788; font-family: var(--font-mono); }
|
||||
.matrix-container {
|
||||
background: #000000;
|
||||
border: 3px solid var(--neon-red);
|
||||
box-shadow: 0 0 20px rgba(255, 0, 51, 0.25);
|
||||
overflow: auto;
|
||||
max-height: 70vh;
|
||||
max-width: 100%;
|
||||
border: 1px solid var(--color-border, #2a2f3a);
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
background: var(--color-surface, #1c1f26);
|
||||
max-height: 75vh;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
th, td {
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--color-border, #2a2f3a);
|
||||
border-right: 1px solid var(--color-border, #2a2f3a);
|
||||
font-size: 13px;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid #1a1a24;
|
||||
border-right: 1px solid #1a1a24;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
min-width: 56px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
th.sticky-x, td.sticky-x {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
background: var(--color-surface, #1c1f26);
|
||||
z-index: 1;
|
||||
text-align: left;
|
||||
min-width: 160px;
|
||||
min-width: 48px;
|
||||
}
|
||||
thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--color-surface, #1c1f26);
|
||||
background: #0b0b10;
|
||||
z-index: 2;
|
||||
vertical-align: bottom;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 12px;
|
||||
height: 180px;
|
||||
}
|
||||
thead th.sticky-x { z-index: 3; }
|
||||
.abbrev { opacity: 0.65; margin-right: 6px; font-size: 11px; }
|
||||
.swatch { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }
|
||||
.cell-gold { color: #d4a017; }
|
||||
.cell-silver { color: #9aa3b0; }
|
||||
.cell-bronze { color: #b87333; }
|
||||
.cell-check { color: var(--color-success, #22c55e); }
|
||||
.th-vertical {
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
font-weight: 800;
|
||||
color: var(--neon-green);
|
||||
letter-spacing: 0.5px;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
margin: 0 auto;
|
||||
max-height: 160px;
|
||||
}
|
||||
th.sticky-x, td.sticky-x {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
background: #050508;
|
||||
z-index: 5;
|
||||
text-align: left;
|
||||
min-width: 180px;
|
||||
font-weight: 800;
|
||||
}
|
||||
thead th.sticky-x {
|
||||
z-index: 10;
|
||||
vertical-align: middle;
|
||||
height: auto;
|
||||
writing-mode: horizontal-tb;
|
||||
transform: none;
|
||||
font-family: var(--font-heading, 'Chakra Petch', sans-serif);
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
color: #888899;
|
||||
}
|
||||
.swatch {
|
||||
display: inline-block; width: 10px; height: 10px; border-radius: 50%;
|
||||
margin-right: 8px; vertical-align: middle;
|
||||
}
|
||||
.cell-gold { color: #ffb700; font-size: 14px; font-weight: bold; }
|
||||
.cell-silver { color: #c0c0c0; font-size: 14px; font-weight: bold; }
|
||||
.cell-bronze { color: #cd7f32; font-size: 14px; font-weight: bold; }
|
||||
.cell-check { color: var(--neon-green); font-size: 14px; font-weight: bold; }
|
||||
`],
|
||||
template: `
|
||||
<ng-container *ngIf="view && view.players.length; else empty">
|
||||
<div class="scroll" data-testid="scoreboard-matrix-scroll">
|
||||
<div class="matrix-container" data-testid="scoreboard-matrix-scroll">
|
||||
<table data-testid="scoreboard-matrix">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sticky-x" data-testid="matrix-player-header">Player</th>
|
||||
<th class="sticky-x" data-testid="matrix-player-header">PLAYER / CHALLENGE</th>
|
||||
<th
|
||||
*ngFor="let ch of view.challenges; trackBy: trackByCh"
|
||||
[attr.data-testid]="'matrix-col-' + ch.id"
|
||||
>
|
||||
<span class="abbrev" *ngIf="ch.categoryAbbreviation">{{ ch.categoryAbbreviation }}</span>
|
||||
<span>{{ ch.name }}</span>
|
||||
<div class="th-vertical">
|
||||
<span>{{ ch.name }}</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -84,11 +112,11 @@ import { PLAYER_COLOR_PALETTE, MatrixView } from './scoreboard.pure';
|
||||
[attr.data-testid]="'matrix-cell-' + p.playerId + '-' + ch.id"
|
||||
>
|
||||
<ng-container [ngSwitch]="view.cells[p.playerId]?.[ch.id]">
|
||||
<span *ngSwitchCase="1" class="cell-gold" title="1st solver">★</span>
|
||||
<span *ngSwitchCase="2" class="cell-silver" title="2nd solver">★</span>
|
||||
<span *ngSwitchCase="3" class="cell-bronze" title="3rd solver">★</span>
|
||||
<span *ngSwitchCase="1" class="cell-gold" title="1st solver">🏆</span>
|
||||
<span *ngSwitchCase="2" class="cell-silver" title="2nd solver">🥈</span>
|
||||
<span *ngSwitchCase="3" class="cell-bronze" title="3rd solver">🥉</span>
|
||||
<span *ngSwitchCase="'solved'" class="cell-check" title="solved">✓</span>
|
||||
<span *ngSwitchDefault></span>
|
||||
<span *ngSwitchDefault>·</span>
|
||||
</ng-container>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -8,53 +8,115 @@ import { PLAYER_COLOR_PALETTE, RankingRow } from './scoreboard.pure';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule],
|
||||
styles: [`
|
||||
:host { display: block; }
|
||||
.empty { padding: 32px; text-align: center; opacity: 0.7; }
|
||||
:host { display: block; max-width: 860px; margin: 0 auto; }
|
||||
.empty { padding: 32px; text-align: center; color: #777788; font-family: var(--font-mono); }
|
||||
.ranking-container {
|
||||
background: #000000;
|
||||
border: 3px solid var(--neon-red);
|
||||
box-shadow: 0 0 20px rgba(255, 0, 51, 0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--color-surface, #1c1f26);
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
overflow: hidden;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
th, td {
|
||||
padding: 8px 12px;
|
||||
thead th {
|
||||
background: #111118;
|
||||
color: #ffffff;
|
||||
font-family: var(--font-heading, 'Chakra Petch', sans-serif);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
padding: 14px 20px;
|
||||
border-bottom: 2px solid var(--neon-red);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--color-border, #2a2f3a);
|
||||
}
|
||||
th { font-weight: 600; opacity: 0.85; }
|
||||
.rank { width: 64px; }
|
||||
.points { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.solved { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.swatch { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 8px; vertical-align: middle; }
|
||||
tbody tr {
|
||||
border-bottom: 1px solid #1a1a24;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
tbody tr:hover {
|
||||
background: #0d0d14;
|
||||
}
|
||||
td {
|
||||
padding: 14px 20px;
|
||||
font-size: 1rem;
|
||||
color: #ffffff;
|
||||
}
|
||||
.col-rank { width: 90px; }
|
||||
.rank-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-family: var(--font-heading, 'Chakra Petch', sans-serif);
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
font-size: 1.1rem;
|
||||
background: #1a1a24;
|
||||
color: #ffffff;
|
||||
transform: skewX(-10deg);
|
||||
}
|
||||
.rank-1 { background: #ffb700; color: #000000; }
|
||||
.rank-2 { background: #c0c0c0; color: #000000; }
|
||||
.rank-3 { background: #cd7f32; color: #000000; }
|
||||
.col-player { font-weight: 800; font-style: italic; font-size: 1.1rem; }
|
||||
.swatch {
|
||||
display: inline-block; width: 10px; height: 10px; border-radius: 50%;
|
||||
margin-right: 10px; vertical-align: middle;
|
||||
}
|
||||
.col-solved {
|
||||
text-align: center;
|
||||
color: var(--neon-purple);
|
||||
font-weight: 800;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.col-points {
|
||||
text-align: right;
|
||||
font-family: var(--font-heading, 'Chakra Petch', sans-serif);
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
font-size: 1.25rem;
|
||||
color: #ffffff;
|
||||
}
|
||||
`],
|
||||
template: `
|
||||
<ng-container *ngIf="rows?.length; else empty">
|
||||
<table data-testid="scoreboard-ranking">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="rank" aria-sort="none" data-testid="ranking-rank-header">Rank</th>
|
||||
<th>Player</th>
|
||||
<th class="solved" data-testid="ranking-solved-header">Solved</th>
|
||||
<th class="points" data-testid="ranking-points-header">Points</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
*ngFor="let row of rows; trackBy: trackById"
|
||||
[attr.data-testid]="'ranking-row-' + row.playerId"
|
||||
[attr.data-rank]="row.rank"
|
||||
>
|
||||
<td class="rank">{{ row.rank }}</td>
|
||||
<td>
|
||||
<span class="swatch" [style.background-color]="color(row.colorIndex)" aria-hidden="true"></span>
|
||||
{{ row.playerName }}
|
||||
</td>
|
||||
<td class="solved">{{ row.solvedCount }}</td>
|
||||
<td class="points">{{ row.points }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="ranking-container">
|
||||
<table data-testid="scoreboard-ranking">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-rank" aria-sort="none" data-testid="ranking-rank-header">RANK</th>
|
||||
<th class="col-player">PLAYER</th>
|
||||
<th class="col-solved" style="text-align:center;" data-testid="ranking-solved-header">SOLVES</th>
|
||||
<th class="col-points" style="text-align:right;" data-testid="ranking-points-header">POINTS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
*ngFor="let row of rows; trackBy: trackById"
|
||||
[attr.data-testid]="'ranking-row-' + row.playerId"
|
||||
[attr.data-rank]="row.rank"
|
||||
>
|
||||
<td class="col-rank">
|
||||
<span class="rank-badge" [class.rank-1]="row.rank===1" [class.rank-2]="row.rank===2" [class.rank-3]="row.rank===3">
|
||||
{{ row.rank }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="col-player">
|
||||
<span class="swatch" [style.background-color]="color(row.colorIndex)" aria-hidden="true"></span>
|
||||
{{ row.playerName }}
|
||||
</td>
|
||||
<td class="col-solved">{{ row.solvedCount }}</td>
|
||||
<td class="col-points">{{ row.points }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #empty>
|
||||
<p class="empty" data-testid="ranking-empty">No players yet</p>
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
projectGraphPoints,
|
||||
} from './scoreboard.pure';
|
||||
|
||||
interface Point { x: number; y: number; }
|
||||
interface RenderedSeries extends GraphSeries {
|
||||
pointsAttr: string;
|
||||
}
|
||||
@@ -20,22 +19,28 @@ interface RenderedSeries extends GraphSeries {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule],
|
||||
styles: [`
|
||||
:host { display: block; }
|
||||
:host { display: block; max-width: 880px; margin: 0 auto; }
|
||||
.empty, .not-started {
|
||||
padding: 32px; text-align: center; opacity: 0.7;
|
||||
padding: 32px; text-align: center; color: #777788; font-family: var(--font-mono);
|
||||
}
|
||||
.legend {
|
||||
display: flex; flex-wrap: wrap; gap: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
display: flex; flex-wrap: wrap; justify-content: center; gap: 10px 16px;
|
||||
margin-top: 16px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px; font-weight: bold;
|
||||
}
|
||||
.legend-item { display: flex; align-items: center; gap: 6px; }
|
||||
.legend-item { display: flex; align-items: center; gap: 6px; color: #ffffff; }
|
||||
.swatch { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
|
||||
.frame {
|
||||
border: 1px solid var(--color-border, #2a2f3a);
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
background: var(--color-surface, #1c1f26);
|
||||
padding: 8px;
|
||||
border: 3px solid var(--neon-red);
|
||||
background: #000000;
|
||||
padding: 20px;
|
||||
box-shadow: 0 0 20px rgba(255, 0, 51, 0.25);
|
||||
}
|
||||
.graph-title {
|
||||
font-family: var(--font-heading, 'Chakra Petch', sans-serif);
|
||||
font-size: 1.1rem; font-weight: 900; font-style: italic;
|
||||
text-transform: uppercase; color: #ffffff; margin-bottom: 16px;
|
||||
}
|
||||
svg { display: block; width: 100%; height: auto; }
|
||||
`],
|
||||
@@ -50,6 +55,32 @@ interface RenderedSeries extends GraphSeries {
|
||||
<ng-container *ngSwitchDefault>
|
||||
<ng-container *ngIf="renderedSeries().length; else emptySeries">
|
||||
<div class="frame" data-testid="score-graph">
|
||||
<h3 class="graph-title">TOP PLAYERS - SCORE PROGRESSION</h3>
|
||||
<svg
|
||||
[attr.viewBox]="'0 0 ' + width + ' ' + height"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
data-testid="score-graph-svg"
|
||||
>
|
||||
<g class="axes">
|
||||
<line [attr.x1]="padL" [attr.y1]="height - padB" [attr.x2]="width - padR" [attr.y2]="height - padB" stroke="#333344" stroke-width="2" />
|
||||
<line [attr.x1]="padL" [attr.y1]="padT" [attr.x2]="padL" [attr.y2]="height - padB" stroke="#333344" stroke-width="2" />
|
||||
<text [attr.x]="padL" [attr.y]="padT - 6" font-size="11" font-weight="bold" fill="#888899">Points</text>
|
||||
<text [attr.x]="width - padR" [attr.y]="height - 6" text-anchor="end" font-size="11" font-weight="bold" fill="#888899">Time</text>
|
||||
<text [attr.x]="padL - 6" [attr.y]="height - padB + 4" text-anchor="end" font-size="11" fill="#777788">0</text>
|
||||
<text [attr.x]="padL - 6" [attr.y]="padT + 4" text-anchor="end" font-size="11" fill="#ffffff" font-weight="bold">{{ maxValue() }}</text>
|
||||
</g>
|
||||
<polyline
|
||||
*ngFor="let s of renderedSeries(); trackBy: trackByPlayer"
|
||||
[attr.points]="s.pointsAttr"
|
||||
fill="none"
|
||||
[attr.stroke]="color(s.colorIndex)"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
[attr.data-testid]="'score-graph-line-' + s.playerId"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div class="legend">
|
||||
<span
|
||||
*ngFor="let s of renderedSeries(); trackBy: trackByPlayer"
|
||||
@@ -60,28 +91,6 @@ interface RenderedSeries extends GraphSeries {
|
||||
{{ s.playerName }}
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
[attr.viewBox]="'0 0 ' + width + ' ' + height"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
data-testid="score-graph-svg"
|
||||
>
|
||||
<g class="axes">
|
||||
<line [attr.x1]="padL" [attr.y1]="height - padB" [attr.x2]="width - padR" [attr.y2]="height - padB" stroke="currentColor" stroke-opacity="0.3" />
|
||||
<line [attr.x1]="padL" [attr.y1]="padT" [attr.x2]="padL" [attr.y2]="height - padB" stroke="currentColor" stroke-opacity="0.3" />
|
||||
<text [attr.x]="padL" [attr.y]="padT - 4" font-size="10" fill="currentColor" fill-opacity="0.6">pts</text>
|
||||
<text [attr.x]="width - padR" [attr.y]="height - 4" text-anchor="end" font-size="10" fill="currentColor" fill-opacity="0.6">time</text>
|
||||
<text [attr.x]="padL - 4" [attr.y]="height - padB" text-anchor="end" font-size="10" fill="currentColor" fill-opacity="0.6">0</text>
|
||||
<text [attr.x]="padL - 4" [attr.y]="padT + 4" text-anchor="end" font-size="10" fill="currentColor" fill-opacity="0.6">{{ maxValue() }}</text>
|
||||
</g>
|
||||
<polyline
|
||||
*ngFor="let s of renderedSeries(); trackBy: trackByPlayer"
|
||||
[attr.points]="s.pointsAttr"
|
||||
fill="none"
|
||||
[attr.stroke]="color(s.colorIndex)"
|
||||
stroke-width="2"
|
||||
[attr.data-testid]="'score-graph-line-' + s.playerId"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #emptySeries>
|
||||
@@ -100,11 +109,11 @@ export class ScoreGraphComponent {
|
||||
}
|
||||
|
||||
readonly width = 800;
|
||||
readonly height = 320;
|
||||
readonly padL = 40;
|
||||
readonly padR = 16;
|
||||
readonly padT = 16;
|
||||
readonly padB = 28;
|
||||
readonly height = 340;
|
||||
readonly padL = 50;
|
||||
readonly padR = 20;
|
||||
readonly padT = 24;
|
||||
readonly padB = 30;
|
||||
|
||||
private readonly _view = signal<GraphView | null>(null);
|
||||
|
||||
|
||||
@@ -5,13 +5,14 @@ import { SCOREBOARD_TABS, ScoreboardTab } from './scoreboard.pure';
|
||||
interface TabSpec {
|
||||
id: ScoreboardTab;
|
||||
label: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const TAB_LABELS: Readonly<Record<ScoreboardTab, string>> = {
|
||||
ranking: 'Ranking',
|
||||
matrix: 'Matrix',
|
||||
'event-log': 'Event Log',
|
||||
graph: 'Score Graph',
|
||||
const TAB_SPECS: Readonly<Record<ScoreboardTab, { label: string; icon: string }>> = {
|
||||
ranking: { label: 'RANKINGS', icon: '🏆' },
|
||||
matrix: { label: 'MATRIX', icon: '▦' },
|
||||
'event-log': { label: 'EVENT LOG', icon: '≡' },
|
||||
graph: { label: 'SCORE GRAPH', icon: '📈' },
|
||||
};
|
||||
|
||||
@Component({
|
||||
@@ -22,25 +23,32 @@ const TAB_LABELS: Readonly<Record<ScoreboardTab, string>> = {
|
||||
styles: [`
|
||||
:host { display: block; }
|
||||
nav {
|
||||
display: flex; gap: 6px;
|
||||
border-bottom: 1px solid var(--color-border, #2a2f3a);
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 12px;
|
||||
overflow-x: auto;
|
||||
display: flex; gap: 10px; justify-content: center;
|
||||
margin-bottom: 24px; flex-wrap: wrap;
|
||||
}
|
||||
button {
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
background: var(--color-surface, #1c1f26);
|
||||
color: inherit;
|
||||
border: 1px solid var(--color-border, #2a2f3a);
|
||||
padding: 10px 20px;
|
||||
background: #000000;
|
||||
color: #aaaaee;
|
||||
border: 2px solid #22222e;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 800;
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
button:hover:not(.active) {
|
||||
border-color: var(--neon-purple);
|
||||
color: #ffffff;
|
||||
}
|
||||
button.active {
|
||||
background: var(--color-primary, #3b82f6);
|
||||
color: var(--color-primary-contrast, #fff);
|
||||
border-color: var(--color-primary, #3b82f6);
|
||||
background: var(--neon-red);
|
||||
color: #000000;
|
||||
border-color: var(--neon-red);
|
||||
box-shadow: 0 0 15px rgba(255, 0, 51, 0.4);
|
||||
}
|
||||
`],
|
||||
template: `
|
||||
@@ -51,7 +59,10 @@ const TAB_LABELS: Readonly<Record<ScoreboardTab, string>> = {
|
||||
[class.active]="t.id === active"
|
||||
[attr.data-testid]="'scoreboard-tab-' + t.id"
|
||||
(click)="select.emit(t.id)"
|
||||
>{{ t.label }}</button>
|
||||
>
|
||||
<span>{{ t.icon }}</span>
|
||||
<span>{{ t.label }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
`,
|
||||
})
|
||||
@@ -59,5 +70,9 @@ export class ScoreboardTabsComponent {
|
||||
@Input({ required: true }) active!: ScoreboardTab;
|
||||
@Output() select = new EventEmitter<ScoreboardTab>();
|
||||
|
||||
readonly tabs: TabSpec[] = SCOREBOARD_TABS.map((id) => ({ id, label: TAB_LABELS[id] }));
|
||||
readonly tabs: TabSpec[] = SCOREBOARD_TABS.map((id) => ({
|
||||
id,
|
||||
label: TAB_SPECS[id].label,
|
||||
icon: TAB_SPECS[id].icon,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user