@kolkrabbi/kol-component 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/src/chess/ChessPiece.jsx +191 -0
- package/src/chess/apparatus/AlternativeControlsMock.jsx +460 -0
- package/src/chess/apparatus/ChessAnalysisLayout.jsx +24 -0
- package/src/chess/apparatus/ChessBoard.jsx +210 -0
- package/src/chess/apparatus/ChessBoardFullscreen.jsx +49 -0
- package/src/chess/apparatus/ChessBoardWithControls.jsx +41 -0
- package/src/chess/apparatus/ChessBoardWithSidebar.jsx +122 -0
- package/src/chess/apparatus/ChessSidebar.jsx +131 -0
- package/src/chess/apparatus/GameArchiveTable.jsx +493 -0
- package/src/chess/apparatus/GameSelector.jsx +47 -0
- package/src/chess/apparatus/NotationPanel.jsx +70 -0
- package/src/chess/apparatus/PlaybackControls.jsx +52 -0
- package/src/chess/apparatus/VariationTree.jsx +137 -0
- package/src/chess/assets/chess-extra-set/set-1/bishop-1.svg +6 -0
- package/src/chess/assets/chess-extra-set/set-1/bishop.svg +6 -0
- package/src/chess/assets/chess-extra-set/set-1/king-1.svg +9 -0
- package/src/chess/assets/chess-extra-set/set-1/king.svg +7 -0
- package/src/chess/assets/chess-extra-set/set-1/knight-1.svg +8 -0
- package/src/chess/assets/chess-extra-set/set-1/knight.svg +7 -0
- package/src/chess/assets/chess-extra-set/set-1/pawn-1.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-1/pawn.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-1/queen-1.svg +11 -0
- package/src/chess/assets/chess-extra-set/set-1/queen.svg +6 -0
- package/src/chess/assets/chess-extra-set/set-1/rook-1.svg +7 -0
- package/src/chess/assets/chess-extra-set/set-1/rook.svg +11 -0
- package/src/chess/assets/chess-extra-set/set-2/bishop-1.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/bishop.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/king-1.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/king.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/knight-1.svg +5 -0
- package/src/chess/assets/chess-extra-set/set-2/knight.svg +5 -0
- package/src/chess/assets/chess-extra-set/set-2/pawn-1.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/pawn.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/queen-1.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/queen.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/rook-1.svg +5 -0
- package/src/chess/assets/chess-extra-set/set-2/rook.svg +5 -0
- package/src/chess/assets/chess-extra-set/set-3/bishop-1.svg +10 -0
- package/src/chess/assets/chess-extra-set/set-3/bishop.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-3/king-1.svg +12 -0
- package/src/chess/assets/chess-extra-set/set-3/king.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-3/knight-1.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-3/knight.svg +10 -0
- package/src/chess/assets/chess-extra-set/set-3/pawn-1.svg +10 -0
- package/src/chess/assets/chess-extra-set/set-3/pawn.svg +10 -0
- package/src/chess/assets/chess-extra-set/set-3/queen-1.svg +10 -0
- package/src/chess/assets/chess-extra-set/set-3/queen.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-3/rook-1.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-3/rook.svg +11 -0
- package/src/chess/assets/chess-vector-set/ABoard.svg +164 -0
- package/src/chess/assets/chess-vector-set/BBiskup.svg +12 -0
- package/src/chess/assets/chess-vector-set/BKing.svg +15 -0
- package/src/chess/assets/chess-vector-set/BKnight.svg +13 -0
- package/src/chess/assets/chess-vector-set/BPawn.svg +7 -0
- package/src/chess/assets/chess-vector-set/BQueen.svg +6 -0
- package/src/chess/assets/chess-vector-set/BRook.svg +12 -0
- package/src/chess/assets/chess-vector-set/WBiskup.svg +14 -0
- package/src/chess/assets/chess-vector-set/WKing.svg +13 -0
- package/src/chess/assets/chess-vector-set/WKnight.svg +10 -0
- package/src/chess/assets/chess-vector-set/WPawn.svg +8 -0
- package/src/chess/assets/chess-vector-set/WQueen.svg +17 -0
- package/src/chess/assets/chess-vector-set/WRook.svg +13 -0
- package/src/chess/context/ChessControlsContext.jsx +379 -0
- package/src/chess/dashboards/ChessHero.jsx +303 -0
- package/src/chess/index.js +32 -0
- package/src/chess/utils/parsePgnTree.js +97 -0
- package/src/dashboards/cards/DashAlertCard.jsx +44 -0
- package/src/dashboards/cards/DashChartCard.jsx +44 -0
- package/src/dashboards/cards/DashFeaturedCard.jsx +42 -0
- package/src/dashboards/cards/DashListCard.jsx +87 -0
- package/src/dashboards/cards/DashMetricCard.jsx +59 -0
- package/src/dashboards/cards/DashSlotCard.jsx +45 -0
- package/src/dashboards/cards/DashStackedBarCard.jsx +70 -0
- package/src/dashboards/cards/DashTableCard.jsx +31 -0
- package/src/dashboards/cards/_shared/CardHeader.jsx +15 -0
- package/src/dashboards/cards/_shared/LegendRow.jsx +25 -0
- package/src/dashboards/charts/Candlestick.jsx +79 -0
- package/src/dashboards/charts/DonutChart.jsx +87 -0
- package/src/dashboards/charts/Heatmap.jsx +79 -0
- package/src/dashboards/charts/Histogram.jsx +56 -0
- package/src/dashboards/charts/LineChart.jsx +157 -0
- package/src/dashboards/charts/ScatterPlot.jsx +93 -0
- package/src/dashboards/charts/Sparkline.jsx +63 -0
- package/src/dashboards/index.js +28 -0
- package/src/dashboards/layout/DashboardGrid.jsx +30 -0
- package/src/dashboards/layout/GridCard.jsx +44 -0
- package/src/dashboards/shared/DashTooltip.jsx +6 -0
- package/src/dashboards/shared/formatMetric.js +26 -0
- package/src/dashboards/shared/useChartTooltip.js +25 -0
- package/src/dashboards/shared/useCountUp.js +107 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "KOL design-system components — atoms through organisms, emitting canonical kol-* classes. Pairs with @kolkrabbi/kol-theme for styling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"module": "./src/index.js",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./src/index.js",
|
|
11
|
-
"./foundry": "./src/organisms/foundry/index.js"
|
|
11
|
+
"./foundry": "./src/organisms/foundry/index.js",
|
|
12
|
+
"./dashboards": "./src/dashboards/index.js",
|
|
13
|
+
"./chess": "./src/chess/index.js"
|
|
12
14
|
},
|
|
13
15
|
"files": [
|
|
14
16
|
"src",
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Load default chess vector set
|
|
2
|
+
const svgModules = import.meta.glob('./assets/chess-vector-set/*.svg', {
|
|
3
|
+
eager: true,
|
|
4
|
+
query: '?raw',
|
|
5
|
+
import: 'default'
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
// Load extra chess sets (set-1, set-2, set-3)
|
|
9
|
+
const extraSet1Modules = import.meta.glob('./assets/chess-extra-set/set-1/*.svg', {
|
|
10
|
+
eager: true,
|
|
11
|
+
query: '?raw',
|
|
12
|
+
import: 'default'
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const extraSet2Modules = import.meta.glob('./assets/chess-extra-set/set-2/*.svg', {
|
|
16
|
+
eager: true,
|
|
17
|
+
query: '?raw',
|
|
18
|
+
import: 'default'
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const extraSet3Modules = import.meta.glob('./assets/chess-extra-set/set-3/*.svg', {
|
|
22
|
+
eager: true,
|
|
23
|
+
query: '?raw',
|
|
24
|
+
import: 'default'
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const PIECE_KEY_MAP = {
|
|
28
|
+
white: {
|
|
29
|
+
king: 'WKing',
|
|
30
|
+
queen: 'WQueen',
|
|
31
|
+
rook: 'WRook',
|
|
32
|
+
bishop: 'WBiskup',
|
|
33
|
+
knight: 'WKnight',
|
|
34
|
+
pawn: 'WPawn'
|
|
35
|
+
},
|
|
36
|
+
black: {
|
|
37
|
+
king: 'BKing',
|
|
38
|
+
queen: 'BQueen',
|
|
39
|
+
rook: 'BRook',
|
|
40
|
+
bishop: 'BBiskup',
|
|
41
|
+
knight: 'BKnight',
|
|
42
|
+
pawn: 'BPawn'
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Map for extra sets (use simpler naming convention)
|
|
47
|
+
const EXTRA_SET_PIECE_MAP = {
|
|
48
|
+
white: {
|
|
49
|
+
king: 'king-1',
|
|
50
|
+
queen: 'queen-1',
|
|
51
|
+
rook: 'rook-1',
|
|
52
|
+
bishop: 'bishop-1',
|
|
53
|
+
knight: 'knight-1',
|
|
54
|
+
pawn: 'pawn-1'
|
|
55
|
+
},
|
|
56
|
+
black: {
|
|
57
|
+
king: 'king',
|
|
58
|
+
queen: 'queen',
|
|
59
|
+
rook: 'rook',
|
|
60
|
+
bishop: 'bishop',
|
|
61
|
+
knight: 'knight',
|
|
62
|
+
pawn: 'pawn'
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const PIECE_CACHE = Object.entries(svgModules).reduce((acc, [path, svgContent]) => {
|
|
67
|
+
const fileName = path.split('/').pop() || ''
|
|
68
|
+
const key = fileName.replace('.svg', '')
|
|
69
|
+
acc[key] = svgContent
|
|
70
|
+
return acc
|
|
71
|
+
}, {})
|
|
72
|
+
|
|
73
|
+
// Cache for extra sets
|
|
74
|
+
const EXTRA_SET_1_CACHE = Object.entries(extraSet1Modules).reduce((acc, [path, svgContent]) => {
|
|
75
|
+
const fileName = path.split('/').pop() || ''
|
|
76
|
+
const key = fileName.replace('.svg', '')
|
|
77
|
+
acc[key] = svgContent
|
|
78
|
+
return acc
|
|
79
|
+
}, {})
|
|
80
|
+
|
|
81
|
+
const EXTRA_SET_2_CACHE = Object.entries(extraSet2Modules).reduce((acc, [path, svgContent]) => {
|
|
82
|
+
const fileName = path.split('/').pop() || ''
|
|
83
|
+
const key = fileName.replace('.svg', '')
|
|
84
|
+
acc[key] = svgContent
|
|
85
|
+
return acc
|
|
86
|
+
}, {})
|
|
87
|
+
|
|
88
|
+
const EXTRA_SET_3_CACHE = Object.entries(extraSet3Modules).reduce((acc, [path, svgContent]) => {
|
|
89
|
+
const fileName = path.split('/').pop() || ''
|
|
90
|
+
const key = fileName.replace('.svg', '')
|
|
91
|
+
acc[key] = svgContent
|
|
92
|
+
return acc
|
|
93
|
+
}, {})
|
|
94
|
+
|
|
95
|
+
const normalizeSize = (value) => {
|
|
96
|
+
if (typeof value === 'number') {
|
|
97
|
+
return `${value}px`
|
|
98
|
+
}
|
|
99
|
+
if (typeof value === 'string') {
|
|
100
|
+
return value
|
|
101
|
+
}
|
|
102
|
+
return '64px'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const applySizeToMarkup = (markup) => {
|
|
106
|
+
let updated = markup
|
|
107
|
+
updated = updated.replace(/width="[^"]*"/i, 'width="100%"')
|
|
108
|
+
updated = updated.replace(/height="[^"]*"/i, 'height="100%"')
|
|
109
|
+
return updated
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const ChessPieceBase = ({
|
|
113
|
+
size = 64,
|
|
114
|
+
className = '',
|
|
115
|
+
style = {},
|
|
116
|
+
children
|
|
117
|
+
}) => {
|
|
118
|
+
const dimension = normalizeSize(size)
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div
|
|
122
|
+
className={`relative inline-flex aspect-square items-center justify-center ${className}`}
|
|
123
|
+
style={{
|
|
124
|
+
width: dimension,
|
|
125
|
+
height: dimension,
|
|
126
|
+
...style
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
{children}
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const ChessPiece = ({
|
|
135
|
+
piece,
|
|
136
|
+
color = 'white',
|
|
137
|
+
size = 64,
|
|
138
|
+
className = '',
|
|
139
|
+
style = {},
|
|
140
|
+
pieceSet = 'default'
|
|
141
|
+
}) => {
|
|
142
|
+
const normalizedColor = color === 'black' ? 'black' : 'white'
|
|
143
|
+
|
|
144
|
+
let pieceKey
|
|
145
|
+
let cache
|
|
146
|
+
|
|
147
|
+
// Select piece set and cache
|
|
148
|
+
if (pieceSet === 'set-1') {
|
|
149
|
+
pieceKey = EXTRA_SET_PIECE_MAP[normalizedColor]?.[piece]
|
|
150
|
+
cache = EXTRA_SET_1_CACHE
|
|
151
|
+
} else if (pieceSet === 'set-2') {
|
|
152
|
+
pieceKey = EXTRA_SET_PIECE_MAP[normalizedColor]?.[piece]
|
|
153
|
+
cache = EXTRA_SET_2_CACHE
|
|
154
|
+
} else if (pieceSet === 'set-3') {
|
|
155
|
+
pieceKey = EXTRA_SET_PIECE_MAP[normalizedColor]?.[piece]
|
|
156
|
+
cache = EXTRA_SET_3_CACHE
|
|
157
|
+
} else {
|
|
158
|
+
// Default set
|
|
159
|
+
pieceKey = PIECE_KEY_MAP[normalizedColor]?.[piece]
|
|
160
|
+
cache = PIECE_CACHE
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const markup = pieceKey ? cache[pieceKey] : null
|
|
164
|
+
|
|
165
|
+
if (!markup) {
|
|
166
|
+
console.warn(`ChessPiece: No asset found for ${normalizedColor} ${piece} in set ${pieceSet}`)
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<ChessPieceBase
|
|
172
|
+
size={size}
|
|
173
|
+
className={`text-current ${className}`}
|
|
174
|
+
style={style}
|
|
175
|
+
>
|
|
176
|
+
<span
|
|
177
|
+
className="block h-full w-full"
|
|
178
|
+
style={{
|
|
179
|
+
display: 'inline-flex',
|
|
180
|
+
alignItems: 'center',
|
|
181
|
+
justifyContent: 'center'
|
|
182
|
+
}}
|
|
183
|
+
dangerouslySetInnerHTML={{ __html: applySizeToMarkup(markup) }}
|
|
184
|
+
/>
|
|
185
|
+
</ChessPieceBase>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
ChessPiece.Base = ChessPieceBase
|
|
190
|
+
|
|
191
|
+
export default ChessPiece
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { Icon } from '@kolkrabbi/kol-icons'
|
|
3
|
+
import { ChessPiece } from '../index.js'
|
|
4
|
+
import { useChessControls } from '../context/ChessControlsContext'
|
|
5
|
+
import NotationPanel from './NotationPanel'
|
|
6
|
+
import VariationTree from './VariationTree'
|
|
7
|
+
|
|
8
|
+
const PIECE_MAP = {
|
|
9
|
+
p: 'pawn',
|
|
10
|
+
r: 'rook',
|
|
11
|
+
n: 'knight',
|
|
12
|
+
b: 'bishop',
|
|
13
|
+
q: 'queen',
|
|
14
|
+
k: 'king'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const AlternativeControlsMock = () => {
|
|
18
|
+
const palettePieces = ['rook', 'knight', 'bishop', 'queen', 'king', 'bishop', 'knight', 'rook']
|
|
19
|
+
const {
|
|
20
|
+
searchQuery,
|
|
21
|
+
setSearchQuery,
|
|
22
|
+
filteredGames,
|
|
23
|
+
selectedGame,
|
|
24
|
+
selectedGameId,
|
|
25
|
+
setSelectedGameId,
|
|
26
|
+
notationPairs,
|
|
27
|
+
moveIndex,
|
|
28
|
+
setMoveIndex,
|
|
29
|
+
goToStart,
|
|
30
|
+
goToEnd,
|
|
31
|
+
stepBackward,
|
|
32
|
+
stepForward,
|
|
33
|
+
togglePlayback,
|
|
34
|
+
isPlaying,
|
|
35
|
+
isLoading,
|
|
36
|
+
activeColor,
|
|
37
|
+
orientation,
|
|
38
|
+
toggleOrientation,
|
|
39
|
+
loadEmptyPosition,
|
|
40
|
+
addUserVariation,
|
|
41
|
+
userVariations,
|
|
42
|
+
getPgnWithUserVariations,
|
|
43
|
+
isEditMode,
|
|
44
|
+
toggleEditMode,
|
|
45
|
+
capturedPieces,
|
|
46
|
+
pieceSet,
|
|
47
|
+
setPieceSet,
|
|
48
|
+
boardTheme,
|
|
49
|
+
setBoardTheme
|
|
50
|
+
} = useChessControls()
|
|
51
|
+
|
|
52
|
+
const [selectedPalettePiece, setSelectedPalettePiece] = useState(null)
|
|
53
|
+
const [showSearch, setShowSearch] = useState(false)
|
|
54
|
+
const [showGameSelector, setShowGameSelector] = useState(false)
|
|
55
|
+
const [showNotation, setShowNotation] = useState(false)
|
|
56
|
+
const [showGameInfo, setShowGameInfo] = useState(false)
|
|
57
|
+
|
|
58
|
+
const playbackButtons = [
|
|
59
|
+
{ icon: 'play-arrow-start', label: 'Jump to start', action: goToStart },
|
|
60
|
+
{ icon: 'play-arrow-back', label: 'Step backward', action: stepBackward },
|
|
61
|
+
{
|
|
62
|
+
icon: isPlaying ? 'play-pause' : 'play-Play',
|
|
63
|
+
label: isPlaying ? 'Pause playback' : 'Play moves',
|
|
64
|
+
action: togglePlayback
|
|
65
|
+
},
|
|
66
|
+
{ icon: 'play-arrow-forward', label: 'Step forward', action: stepForward },
|
|
67
|
+
{ icon: 'play-arrow-end', label: 'Jump to end', action: goToEnd }
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
const notationLabel = selectedGame?.opening?.name || 'notation'
|
|
71
|
+
|
|
72
|
+
// Calculate material advantage
|
|
73
|
+
const materialEvaluation = (() => {
|
|
74
|
+
const whitePieces = capturedPieces?.white || []
|
|
75
|
+
const blackPieces = capturedPieces?.black || []
|
|
76
|
+
|
|
77
|
+
const values = { p: 1, n: 3, b: 3, r: 5, q: 9, k: 0 }
|
|
78
|
+
const whiteValue = whitePieces.reduce((sum, piece) => sum + (values[piece] || 0), 0)
|
|
79
|
+
const blackValue = blackPieces.reduce((sum, piece) => sum + (values[piece] || 0), 0)
|
|
80
|
+
|
|
81
|
+
const diff = whiteValue - blackValue
|
|
82
|
+
const percentage = Math.min(Math.max((diff + 15) / 30, 0), 1) * 100
|
|
83
|
+
|
|
84
|
+
return { diff, percentage, advantage: diff > 0 ? 'white' : diff < 0 ? 'black' : 'equal' }
|
|
85
|
+
})()
|
|
86
|
+
|
|
87
|
+
const handleAddVariation = () => {
|
|
88
|
+
const label = window.prompt('Name this variation:', `Variation ${userVariations.length + 1}`)
|
|
89
|
+
if (label === null) return
|
|
90
|
+
const sanInput = window.prompt('Enter SAN moves separated by spaces:', 'Nc3 Bb5 d4')
|
|
91
|
+
if (!sanInput) return
|
|
92
|
+
const sanSequence = sanInput.split(' ').map((san) => san.trim()).filter(Boolean)
|
|
93
|
+
if (!sanSequence.length) return
|
|
94
|
+
addUserVariation(label, sanSequence)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const handleExportPgn = async () => {
|
|
98
|
+
const value = getPgnWithUserVariations()
|
|
99
|
+
try {
|
|
100
|
+
await navigator.clipboard.writeText(value)
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.warn('Unable to copy PGN to clipboard', error)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const handlePaletteClick = (color, piece) => {
|
|
107
|
+
if (!isEditMode) return
|
|
108
|
+
setSelectedPalettePiece({ color, piece })
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Keyboard shortcuts
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const handleKeyDown = (event) => {
|
|
114
|
+
// Don't intercept if user is typing in an input
|
|
115
|
+
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
switch (event.key) {
|
|
120
|
+
case 'ArrowLeft':
|
|
121
|
+
event.preventDefault()
|
|
122
|
+
stepBackward()
|
|
123
|
+
break
|
|
124
|
+
case 'ArrowRight':
|
|
125
|
+
event.preventDefault()
|
|
126
|
+
stepForward()
|
|
127
|
+
break
|
|
128
|
+
case ' ':
|
|
129
|
+
event.preventDefault()
|
|
130
|
+
togglePlayback()
|
|
131
|
+
break
|
|
132
|
+
case 'Home':
|
|
133
|
+
event.preventDefault()
|
|
134
|
+
goToStart()
|
|
135
|
+
break
|
|
136
|
+
case 'End':
|
|
137
|
+
event.preventDefault()
|
|
138
|
+
goToEnd()
|
|
139
|
+
break
|
|
140
|
+
case 'f':
|
|
141
|
+
case 'F':
|
|
142
|
+
event.preventDefault()
|
|
143
|
+
toggleOrientation()
|
|
144
|
+
break
|
|
145
|
+
default:
|
|
146
|
+
break
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
151
|
+
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
152
|
+
}, [stepBackward, stepForward, togglePlayback, goToStart, goToEnd, toggleOrientation])
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<div className="w-full h-full bg-oq-02 flex flex-col text-fg-88">
|
|
156
|
+
<div className="flex flex-col gap-3 p-3">
|
|
157
|
+
<div className="flex items-center justify-between">
|
|
158
|
+
<div className="flex items-center gap-3">
|
|
159
|
+
<span className="kol-mono-xs text-fg-64">Setup Position</span>
|
|
160
|
+
</div>
|
|
161
|
+
<div className="flex items-center gap-2 text-fg-64">
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
onClick={() => setShowSearch(!showSearch)}
|
|
165
|
+
title="Search games"
|
|
166
|
+
className="p-1 hover:text-fg transition-colors"
|
|
167
|
+
>
|
|
168
|
+
<Icon name="search-16" size={20} />
|
|
169
|
+
</button>
|
|
170
|
+
<button type="button" onClick={toggleOrientation} title="Flip board" className="p-1 hover:text-fg transition-colors">
|
|
171
|
+
<Icon name="rotate" size={20} />
|
|
172
|
+
</button>
|
|
173
|
+
<button type="button" onClick={loadEmptyPosition} title="Clear board" className="p-1 hover:text-fg transition-colors">
|
|
174
|
+
<Icon name="bucket" size={20} />
|
|
175
|
+
</button>
|
|
176
|
+
<button type="button" onClick={handleAddVariation} title="New variation" className="p-1 hover:text-fg transition-colors">
|
|
177
|
+
<Icon name="component" size={20} />
|
|
178
|
+
</button>
|
|
179
|
+
<button type="button" onClick={handleExportPgn} title="Copy PGN" className="p-1 hover:text-fg transition-colors">
|
|
180
|
+
<Icon name="copy" size={20} />
|
|
181
|
+
</button>
|
|
182
|
+
<button type="button" onClick={toggleEditMode} title="Toggle edit mode" className={`p-1 hover:text-fg transition-colors ${isEditMode ? 'text-accent-primary' : ''}`}>
|
|
183
|
+
<Icon name={isEditMode ? 'stat-cycle' : 'pills'} size={20} />
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{showSearch && (
|
|
189
|
+
<input
|
|
190
|
+
type="text"
|
|
191
|
+
value={searchQuery}
|
|
192
|
+
onChange={(event) => setSearchQuery(event.target.value)}
|
|
193
|
+
placeholder="Search games"
|
|
194
|
+
className="bg-oq-02 border border-oq-16 rounded px-2 py-1 kol-mono-xxs text-fg-80"
|
|
195
|
+
/>
|
|
196
|
+
)}
|
|
197
|
+
|
|
198
|
+
{/* Piece Set Selector */}
|
|
199
|
+
<div className="flex items-center gap-2">
|
|
200
|
+
<span className="kol-mono-xxs text-fg-64 uppercase tracking-[0.3em]">Piece Set</span>
|
|
201
|
+
<select
|
|
202
|
+
value={pieceSet}
|
|
203
|
+
onChange={(e) => setPieceSet(e.target.value)}
|
|
204
|
+
className="bg-oq-04 border border-oq-16 rounded px-2 py-1 kol-mono-xxs text-fg-80"
|
|
205
|
+
>
|
|
206
|
+
<option value="default">Default</option>
|
|
207
|
+
<option value="set-1">Set 1</option>
|
|
208
|
+
<option value="set-2">Set 2</option>
|
|
209
|
+
<option value="set-3">Set 3</option>
|
|
210
|
+
</select>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* Board Theme Selector */}
|
|
214
|
+
<div className="flex items-center gap-2">
|
|
215
|
+
<span className="kol-mono-xxs text-fg-64 uppercase tracking-[0.3em]">Board Theme</span>
|
|
216
|
+
<select
|
|
217
|
+
value={boardTheme}
|
|
218
|
+
onChange={(e) => setBoardTheme(e.target.value)}
|
|
219
|
+
className="bg-oq-04 border border-oq-16 rounded px-2 py-1 kol-mono-xxs text-fg-80"
|
|
220
|
+
>
|
|
221
|
+
<option value="green-white">Green & White</option>
|
|
222
|
+
<option value="blue-gray">Blue & Gray</option>
|
|
223
|
+
<option value="gray">Gray</option>
|
|
224
|
+
<option value="olive">Olive</option>
|
|
225
|
+
<option value="brown">Brown</option>
|
|
226
|
+
<option value="dark">Dark</option>
|
|
227
|
+
</select>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<div className="p-3 border-t border-oq-08 bg-fg-02">
|
|
232
|
+
{isEditMode && selectedPalettePiece ? (
|
|
233
|
+
<div className="kol-mono-xs text-fg-64 mb-2">
|
|
234
|
+
Placing: {selectedPalettePiece.color} {selectedPalettePiece.piece}
|
|
235
|
+
</div>
|
|
236
|
+
) : null}
|
|
237
|
+
<div className="flex flex-col gap-3">
|
|
238
|
+
{['white', 'black'].map((color) => (
|
|
239
|
+
<div key={color} className="flex items-center justify-between gap-1 w-full">
|
|
240
|
+
{palettePieces.map((piece, index) => (
|
|
241
|
+
<div
|
|
242
|
+
key={`${color}-${piece}-${index}`}
|
|
243
|
+
className={`flex items-center justify-center bg-oq-02 rounded transition ring-offset-1 flex-1 aspect-square ${
|
|
244
|
+
isEditMode
|
|
245
|
+
? 'cursor-pointer border border-dashed border-oq-12'
|
|
246
|
+
: ''
|
|
247
|
+
} ${
|
|
248
|
+
selectedPalettePiece &&
|
|
249
|
+
selectedPalettePiece.color === color &&
|
|
250
|
+
selectedPalettePiece.piece === piece
|
|
251
|
+
? 'ring-2 ring-accent-primary'
|
|
252
|
+
: ''
|
|
253
|
+
}`}
|
|
254
|
+
onClick={() => handlePaletteClick(color, piece)}
|
|
255
|
+
>
|
|
256
|
+
<ChessPiece piece={piece} color={color} size="32px" pieceSet={pieceSet} />
|
|
257
|
+
</div>
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
))}
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<div className="flex flex-col gap-4 flex-1 p-3 min-h-0">
|
|
265
|
+
<div className="flex items-center justify-between gap-2 flex-shrink-0">
|
|
266
|
+
<div className="flex items-center gap-2 flex-1 relative">
|
|
267
|
+
<div
|
|
268
|
+
className="flex-1 rounded bg-oq-04 px-3 py-3 flex flex-col gap-1 cursor-pointer"
|
|
269
|
+
onClick={() => setShowGameSelector(!showGameSelector)}
|
|
270
|
+
>
|
|
271
|
+
<div className="flex items-center justify-between">
|
|
272
|
+
<span className="kol-mono-xs text-fg-80 uppercase tracking-[0.2em]">
|
|
273
|
+
{selectedGame?.player?.username || 'Player'} vs {selectedGame?.opponent?.username || 'Opponent'}
|
|
274
|
+
</span>
|
|
275
|
+
<Icon name="chevron-down" size={16} className="text-fg-80" />
|
|
276
|
+
</div>
|
|
277
|
+
{selectedGame?.opening?.name && (
|
|
278
|
+
<span className="kol-mono-xxs text-fg-64">
|
|
279
|
+
{selectedGame.opening.name}
|
|
280
|
+
</span>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
{showGameSelector && (
|
|
284
|
+
<div className="absolute top-full left-0 right-0 mt-1 bg-oq-08 border border-oq-16 rounded max-h-60 overflow-auto z-10">
|
|
285
|
+
{filteredGames.map((game) => (
|
|
286
|
+
<div
|
|
287
|
+
key={game.id}
|
|
288
|
+
className="px-3 py-2 kol-mono-xs text-fg-80 hover:bg-oq-16 cursor-pointer"
|
|
289
|
+
onClick={() => {
|
|
290
|
+
setSelectedGameId(game.id)
|
|
291
|
+
setShowGameSelector(false)
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
294
|
+
{game.player?.username} vs {game.opponent?.username}
|
|
295
|
+
</div>
|
|
296
|
+
))}
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
</div>
|
|
300
|
+
<div className="flex items-center gap-2">
|
|
301
|
+
{['stat-chart-a', 'stat-chart-b', 'stat-chart-c'].map((icon) => (
|
|
302
|
+
<span key={icon} className="w-7 h-7 flex items-center justify-center">
|
|
303
|
+
<Icon name={icon} size={16} className="text-fg-80" />
|
|
304
|
+
</span>
|
|
305
|
+
))}
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
{/* Material Evaluation Bar */}
|
|
310
|
+
<div className="flex flex-col gap-2 flex-shrink-0">
|
|
311
|
+
<div className="flex items-center justify-between">
|
|
312
|
+
<span className="kol-mono-xxs text-fg-64 uppercase tracking-[0.3em]">Material</span>
|
|
313
|
+
{materialEvaluation.advantage !== 'equal' && (
|
|
314
|
+
<span className="kol-mono-xxs text-fg-80">
|
|
315
|
+
{materialEvaluation.advantage === 'white' ? '+' : ''}{materialEvaluation.diff}
|
|
316
|
+
</span>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
<div className="h-2 bg-fg-04 rounded-full overflow-hidden relative">
|
|
320
|
+
<div
|
|
321
|
+
className="absolute top-0 left-0 h-full bg-white transition-all duration-300"
|
|
322
|
+
style={{ width: `${materialEvaluation.percentage}%` }}
|
|
323
|
+
/>
|
|
324
|
+
<div
|
|
325
|
+
className="absolute top-0 right-0 h-full bg-[#0a682a] transition-all duration-300"
|
|
326
|
+
style={{ width: `${100 - materialEvaluation.percentage}%` }}
|
|
327
|
+
/>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<div className="flex flex-row gap-6 flex-shrink-0">
|
|
332
|
+
{['white', 'black'].map((color) => {
|
|
333
|
+
const pieces = capturedPieces?.[color] || []
|
|
334
|
+
const materialValue = pieces.reduce((sum, piece) => {
|
|
335
|
+
const values = { p: 1, n: 3, b: 3, r: 5, q: 9, k: 0 }
|
|
336
|
+
return sum + (values[piece] || 0)
|
|
337
|
+
}, 0)
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<div key={color} className="flex flex-col gap-2 flex-1">
|
|
341
|
+
<div className="flex items-center justify-between">
|
|
342
|
+
<span className="kol-mono-xxs text-fg-64 uppercase tracking-[0.3em]">{color}</span>
|
|
343
|
+
{materialValue > 0 && (
|
|
344
|
+
<span className="kol-mono-xxs text-fg-80">+{materialValue}</span>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
<div className="flex flex-wrap gap-1 min-h-[24px]">
|
|
348
|
+
{pieces.map((piece, index) => (
|
|
349
|
+
<div key={`${color}-captured-${index}`} className="flex items-center justify-center">
|
|
350
|
+
<ChessPiece piece={PIECE_MAP[piece]} color={color === 'white' ? 'black' : 'white'} size="20px" pieceSet={pieceSet} />
|
|
351
|
+
</div>
|
|
352
|
+
))}
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
)
|
|
356
|
+
})}
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
{selectedGame && (
|
|
360
|
+
<div className="flex flex-col gap-2 flex-shrink-0 border-t border-oq-08 pt-4">
|
|
361
|
+
<div className="flex-1 rounded bg-oq-04 px-3 py-3 flex items-center justify-between cursor-pointer" onClick={() => setShowGameInfo(!showGameInfo)}>
|
|
362
|
+
<span className="kol-mono-xs text-fg-80 uppercase tracking-[0.2em]">Game Info</span>
|
|
363
|
+
<Icon name={showGameInfo ? 'chevron-up' : 'chevron-down'} size={16} className="text-fg-80" />
|
|
364
|
+
</div>
|
|
365
|
+
{showGameInfo && (
|
|
366
|
+
<div className="rounded bg-oq-04 p-3">
|
|
367
|
+
<div className="flex flex-col gap-3">
|
|
368
|
+
<div className="flex items-center justify-between">
|
|
369
|
+
<span className="kol-mono-xxs text-fg-64 uppercase tracking-[0.3em]">Players</span>
|
|
370
|
+
<span className="kol-mono-xs text-fg-80">
|
|
371
|
+
{selectedGame.player?.rating || '—'} vs {selectedGame.opponent?.rating || '—'}
|
|
372
|
+
</span>
|
|
373
|
+
</div>
|
|
374
|
+
<div className="flex items-center justify-between">
|
|
375
|
+
<span className="kol-mono-xxs text-fg-64 uppercase tracking-[0.3em]">Result</span>
|
|
376
|
+
<span className="kol-mono-xs text-fg-80 capitalize">
|
|
377
|
+
{selectedGame.playerResult || '—'}
|
|
378
|
+
</span>
|
|
379
|
+
</div>
|
|
380
|
+
<div className="flex items-center justify-between">
|
|
381
|
+
<span className="kol-mono-xxs text-fg-64 uppercase tracking-[0.3em]">Time Control</span>
|
|
382
|
+
<span className="kol-mono-xs text-fg-80 capitalize">
|
|
383
|
+
{selectedGame.timeClass || '—'}
|
|
384
|
+
</span>
|
|
385
|
+
</div>
|
|
386
|
+
{selectedGame.endTime && (
|
|
387
|
+
<div className="flex items-center justify-between">
|
|
388
|
+
<span className="kol-mono-xxs text-fg-64 uppercase tracking-[0.3em]">Date</span>
|
|
389
|
+
<span className="kol-mono-xs text-fg-80">
|
|
390
|
+
{new Date(selectedGame.endTime * 1000).toLocaleDateString()}
|
|
391
|
+
</span>
|
|
392
|
+
</div>
|
|
393
|
+
)}
|
|
394
|
+
{selectedGame.opening?.name && (
|
|
395
|
+
<div className="flex flex-col gap-1">
|
|
396
|
+
<span className="kol-mono-xxs text-fg-64 uppercase tracking-[0.3em]">Opening</span>
|
|
397
|
+
<span className="kol-mono-xs text-fg-80">
|
|
398
|
+
{selectedGame.opening.name}
|
|
399
|
+
</span>
|
|
400
|
+
</div>
|
|
401
|
+
)}
|
|
402
|
+
{selectedGame.termination && (
|
|
403
|
+
<div className="flex flex-col gap-1">
|
|
404
|
+
<span className="kol-mono-xxs text-fg-64 uppercase tracking-[0.3em]">Termination</span>
|
|
405
|
+
<span className="kol-mono-xs text-fg-80">
|
|
406
|
+
{selectedGame.termination}
|
|
407
|
+
</span>
|
|
408
|
+
</div>
|
|
409
|
+
)}
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
)}
|
|
413
|
+
</div>
|
|
414
|
+
)}
|
|
415
|
+
|
|
416
|
+
<div className="flex flex-col gap-2 flex-1 min-h-0">
|
|
417
|
+
<div className="flex items-center gap-2 pt-4 border-t border-oq-08 flex-shrink-0">
|
|
418
|
+
<div className="flex-1 rounded bg-oq-04 px-3 py-3 flex items-center justify-between cursor-pointer" onClick={() => setShowNotation(!showNotation)}>
|
|
419
|
+
<span className="kol-mono-xs text-fg-80 uppercase tracking-[0.2em]">{notationLabel}</span>
|
|
420
|
+
<Icon name={showNotation ? 'chevron-up' : 'chevron-down'} size={16} className="text-fg-80" />
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
{showNotation && (
|
|
424
|
+
<div className="rounded bg-oq-04 p-3 overflow-auto flex-1 min-h-0">
|
|
425
|
+
<NotationPanel
|
|
426
|
+
notationPairs={notationPairs}
|
|
427
|
+
activePly={moveIndex}
|
|
428
|
+
onSelectPly={setMoveIndex}
|
|
429
|
+
isLoading={isLoading}
|
|
430
|
+
/>
|
|
431
|
+
</div>
|
|
432
|
+
)}
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
<div className="flex flex-col gap-3 flex-shrink-0">
|
|
436
|
+
<div className="flex items-center justify-center">
|
|
437
|
+
<span className="kol-mono-xs text-fg-64 uppercase tracking-[0.2em]">
|
|
438
|
+
Move {moveIndex}/{notationPairs.length || 0}
|
|
439
|
+
</span>
|
|
440
|
+
</div>
|
|
441
|
+
<div className="grid grid-cols-5 gap-3">
|
|
442
|
+
{playbackButtons.map(({ icon, label, action }) => (
|
|
443
|
+
<button
|
|
444
|
+
key={icon}
|
|
445
|
+
type="button"
|
|
446
|
+
aria-label={label}
|
|
447
|
+
className="h-12 rounded bg-oq-02 border border-fg-08 text-fg-80 flex items-center justify-center"
|
|
448
|
+
onClick={action}
|
|
449
|
+
>
|
|
450
|
+
<Icon name={icon} size={18} className="text-fg-80" />
|
|
451
|
+
</button>
|
|
452
|
+
))}
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export default AlternativeControlsMock
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Pass a `chessData` adapter (see chess/index.js) — forwarded to the table + board.
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import GameArchiveTable from './GameArchiveTable'
|
|
4
|
+
import ChessBoardWithControls from './ChessBoardWithControls'
|
|
5
|
+
|
|
6
|
+
const ChessAnalysisLayout = ({ chessData }) => {
|
|
7
|
+
const [loadedGame, setLoadedGame] = useState(null)
|
|
8
|
+
|
|
9
|
+
const handleGameLoad = (game) => {
|
|
10
|
+
setLoadedGame(game)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="space-y-8 md:space-y-12 lg:max-w-[1232px]">
|
|
15
|
+
<GameArchiveTable chessData={chessData} onGameLoad={handleGameLoad} />
|
|
16
|
+
|
|
17
|
+
<section>
|
|
18
|
+
<ChessBoardWithControls externalGame={loadedGame} chessData={chessData} />
|
|
19
|
+
</section>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default ChessAnalysisLayout
|