feat: NCWF-5/6/7/8/9 chess analysis page and engine integration (#11)
Co-authored-by: Janis Eccarius <eccariusjanis@gmail.com> Reviewed-on: #11
This commit was merged in pull request #11.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
padding: 12px 16px;
|
||||
font-family: var(--nc-mono, monospace);
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.move-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 28px 1fr 1fr;
|
||||
gap: 2px 4px;
|
||||
padding: 8px 12px;
|
||||
font-family: var(--nc-mono, monospace);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mv-num {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.mv {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
transition:
|
||||
background 0.12s,
|
||||
color 0.12s;
|
||||
}
|
||||
|
||||
.mv:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mv.active {
|
||||
background: rgba(255, 69, 200, 0.18);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mv-empty {
|
||||
cursor: default;
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.mv-empty:hover {
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.mv-san {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mv-placeholder {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* Quality badges */
|
||||
.mv-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.q-brilliant .mv-badge,
|
||||
.mv-badge.q-brilliant {
|
||||
color: #5ee5a1;
|
||||
background: rgba(94, 229, 161, 0.15);
|
||||
}
|
||||
|
||||
.q-best .mv-badge,
|
||||
.mv-badge.q-best {
|
||||
color: #5ee5a1;
|
||||
background: rgba(94, 229, 161, 0.1);
|
||||
}
|
||||
|
||||
.q-inaccuracy .mv-badge,
|
||||
.mv-badge.q-inaccuracy {
|
||||
color: #ffb13a;
|
||||
background: rgba(255, 177, 58, 0.15);
|
||||
}
|
||||
|
||||
.q-mistake .mv-badge,
|
||||
.mv-badge.q-mistake {
|
||||
color: #ff7a7a;
|
||||
background: rgba(255, 122, 122, 0.15);
|
||||
}
|
||||
|
||||
.q-blunder .mv-badge,
|
||||
.mv-badge.q-blunder {
|
||||
color: #ff4444;
|
||||
background: rgba(255, 68, 68, 0.18);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
@if (moves.length === 0) {
|
||||
<div class="empty">No annotated moves yet.</div>
|
||||
} @else {
|
||||
<div class="move-grid" role="list">
|
||||
@for (pair of pairs; track $index) {
|
||||
<div class="mv-num" role="presentation">{{ $index + 1 }}</div>
|
||||
|
||||
<div
|
||||
class="mv"
|
||||
[class.active]="isWhiteActive($index)"
|
||||
[class]="qualityClass(pair.white)"
|
||||
role="listitem"
|
||||
tabindex="0"
|
||||
(click)="selectWhite($index)"
|
||||
(keydown.enter)="selectWhite($index)"
|
||||
>
|
||||
<span class="mv-san">{{ pair.white?.san ?? '' }}</span>
|
||||
@if (qualityLabel(pair.white)) {
|
||||
<span class="mv-badge" [class]="qualityClass(pair.white)">{{
|
||||
qualityLabel(pair.white)
|
||||
}}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mv"
|
||||
[class.active]="isBlackActive($index)"
|
||||
[class.mv-empty]="!pair.black"
|
||||
[class]="qualityClass(pair.black)"
|
||||
role="listitem"
|
||||
[attr.tabindex]="pair.black ? 0 : null"
|
||||
(click)="selectBlack($index, pair.black)"
|
||||
(keydown.enter)="selectBlack($index, pair.black)"
|
||||
>
|
||||
@if (pair.black) {
|
||||
<span class="mv-san">{{ pair.black.san }}</span>
|
||||
@if (qualityLabel(pair.black)) {
|
||||
<span class="mv-badge" [class]="qualityClass(pair.black)">{{
|
||||
qualityLabel(pair.black)
|
||||
}}</span>
|
||||
}
|
||||
} @else {
|
||||
<span class="mv-san mv-placeholder">…</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { AnnotatedMove, MoveQuality } from '../../models/analysis.models';
|
||||
|
||||
interface AnnotatedPair {
|
||||
white: AnnotatedMove | null;
|
||||
black: AnnotatedMove | null;
|
||||
}
|
||||
|
||||
const QUALITY_LABELS: Record<MoveQuality, string> = {
|
||||
brilliant: '!!',
|
||||
best: '!',
|
||||
good: '',
|
||||
inaccuracy: '?!',
|
||||
mistake: '?',
|
||||
blunder: '??',
|
||||
};
|
||||
|
||||
const QUALITY_CLASSES: Record<MoveQuality, string> = {
|
||||
brilliant: 'q-brilliant',
|
||||
best: 'q-best',
|
||||
good: 'q-good',
|
||||
inaccuracy: 'q-inaccuracy',
|
||||
mistake: 'q-mistake',
|
||||
blunder: 'q-blunder',
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-annotated-move-list',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
templateUrl: './annotated-move-list.component.html',
|
||||
styleUrl: './annotated-move-list.component.css',
|
||||
})
|
||||
export class AnnotatedMoveListComponent {
|
||||
@Input({ required: true }) moves: AnnotatedMove[] = [];
|
||||
@Input() activePly: number | null = null;
|
||||
@Output() plySelected = new EventEmitter<number>();
|
||||
|
||||
get pairs(): AnnotatedPair[] {
|
||||
const result: AnnotatedPair[] = [];
|
||||
for (let i = 0; i < this.moves.length; i += 2) {
|
||||
result.push({
|
||||
white: this.moves[i] ?? null,
|
||||
black: this.moves[i + 1] ?? null,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
qualityLabel(move: AnnotatedMove | null): string {
|
||||
if (!move?.quality) return '';
|
||||
return QUALITY_LABELS[move.quality] ?? '';
|
||||
}
|
||||
|
||||
qualityClass(move: AnnotatedMove | null): string {
|
||||
if (!move?.quality) return '';
|
||||
return QUALITY_CLASSES[move.quality] ?? '';
|
||||
}
|
||||
|
||||
isWhiteActive(pairIndex: number): boolean {
|
||||
return this.activePly === pairIndex * 2;
|
||||
}
|
||||
|
||||
isBlackActive(pairIndex: number): boolean {
|
||||
return this.activePly === pairIndex * 2 + 1;
|
||||
}
|
||||
|
||||
selectWhite(pairIndex: number): void {
|
||||
this.plySelected.emit(pairIndex * 2);
|
||||
}
|
||||
|
||||
selectBlack(pairIndex: number, black: AnnotatedMove | null): void {
|
||||
if (!black) return;
|
||||
this.plySelected.emit(pairIndex * 2 + 1);
|
||||
}
|
||||
|
||||
formatEval(move: AnnotatedMove | null): string {
|
||||
if (!move || move.evalAfter === null) return '';
|
||||
const v = move.evalAfter;
|
||||
const sign = v > 0 ? '+' : '';
|
||||
return `${sign}${v.toFixed(2)}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user