@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
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { Chess } from 'chess.js'
|
|
3
|
+
import { ChessPiece } from '../index.js'
|
|
4
|
+
|
|
5
|
+
const FILES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
|
|
6
|
+
const PIECE_TYPE_MAP = {
|
|
7
|
+
p: 'pawn',
|
|
8
|
+
r: 'rook',
|
|
9
|
+
n: 'knight',
|
|
10
|
+
b: 'bishop',
|
|
11
|
+
q: 'queen',
|
|
12
|
+
k: 'king'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const BOARD_THEMES = {
|
|
16
|
+
'blue-gray': { light: '#E8EDF9', dark: '#B7C0D8' },
|
|
17
|
+
'gray': { light: '#EFEFEF', dark: '#A6A6A6' },
|
|
18
|
+
'green-white': { light: '#ffffff', dark: '#0a682a' },
|
|
19
|
+
'olive': { light: '#EFEED2', dark: '#779557' },
|
|
20
|
+
'dark': { light: '#4D4D51', dark: '#242427' },
|
|
21
|
+
'brown': { light: '#F0D9B5', dark: '#B58863' }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const buildBoardState = (fen) => {
|
|
25
|
+
const chess = new Chess()
|
|
26
|
+
if (fen) {
|
|
27
|
+
/* chess.js v1 load() returns void and THROWS on invalid FENs — and it
|
|
28
|
+
* validates playability (kings required), not renderability. Display
|
|
29
|
+
* states like the cleared board are legitimate here, so on throw we
|
|
30
|
+
* place the FEN's board portion piece-by-piece instead of reverting
|
|
31
|
+
* to the start position. */
|
|
32
|
+
try {
|
|
33
|
+
chess.load(fen)
|
|
34
|
+
} catch {
|
|
35
|
+
chess.clear()
|
|
36
|
+
const ranks = fen.split(' ')[0].split('/')
|
|
37
|
+
ranks.forEach((rank, r) => {
|
|
38
|
+
let file = 0
|
|
39
|
+
for (const ch of rank) {
|
|
40
|
+
if (/\d/.test(ch)) { file += Number(ch); continue }
|
|
41
|
+
const square = 'abcdefgh'[file] + (8 - r)
|
|
42
|
+
try {
|
|
43
|
+
chess.put({ type: ch.toLowerCase(), color: ch === ch.toLowerCase() ? 'b' : 'w' }, square)
|
|
44
|
+
} catch { /* unplaceable char — leave square empty */ }
|
|
45
|
+
file += 1
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return chess.board()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const getSquareTone = (fileIndex, rankIndex) =>
|
|
54
|
+
(fileIndex + rankIndex) % 2 === 0 ? 'light' : 'dark'
|
|
55
|
+
|
|
56
|
+
const getBoardSize = (size) => {
|
|
57
|
+
if (size === 'fluid') return '100%'
|
|
58
|
+
const sizeMap = {
|
|
59
|
+
mobile: '384px',
|
|
60
|
+
tablet: '520px',
|
|
61
|
+
desktop: '760px'
|
|
62
|
+
}
|
|
63
|
+
return sizeMap[size] || size
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const getPieceSize = (size) => {
|
|
67
|
+
if (size === 'fluid') return '75%'
|
|
68
|
+
const sizeMap = {
|
|
69
|
+
sm: '44px',
|
|
70
|
+
md: '64px',
|
|
71
|
+
lg: '76px',
|
|
72
|
+
mobile: '40px',
|
|
73
|
+
tablet: '52px',
|
|
74
|
+
desktop: '76px'
|
|
75
|
+
}
|
|
76
|
+
return sizeMap[size] || sizeMap.lg
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const ChessBoard = ({ fen, size = 'desktop', orientation = 'white', showPieces = true, lastMove = null, pieceSet = 'default', boardTheme = 'green-white' }) => {
|
|
80
|
+
const boardState = useMemo(() => buildBoardState(fen), [fen])
|
|
81
|
+
const isFluid = size === 'fluid'
|
|
82
|
+
const boardPixelSize = getBoardSize(size)
|
|
83
|
+
const piecePixelSize = getPieceSize(size)
|
|
84
|
+
const squarePixelSize = isFluid ? '100%' : `${parseInt(boardPixelSize) / 8}px`
|
|
85
|
+
const coordinatePaddingClass = size === 'mobile' ? 'p-1' : size === 'tablet' ? '!p-1.5' : 'p-2'
|
|
86
|
+
const coordinateTypographyClass = size === 'mobile' ? 'kol-helper-xxxs' : 'kol-helper-xs'
|
|
87
|
+
const rankIndices =
|
|
88
|
+
orientation === 'white'
|
|
89
|
+
? [...Array(boardState.length).keys()]
|
|
90
|
+
: [...Array(boardState.length).keys()].reverse()
|
|
91
|
+
const fileIndices =
|
|
92
|
+
orientation === 'white'
|
|
93
|
+
? [...Array(FILES.length).keys()]
|
|
94
|
+
: [...Array(FILES.length).keys()].reverse()
|
|
95
|
+
|
|
96
|
+
const filesForOrientation = orientation === 'white' ? FILES : [...FILES].reverse()
|
|
97
|
+
const ranksForOrientation =
|
|
98
|
+
orientation === 'white'
|
|
99
|
+
? [...Array(8).keys()].map((i) => 8 - i)
|
|
100
|
+
: [...Array(8).keys()].map((i) => i + 1)
|
|
101
|
+
|
|
102
|
+
const theme = BOARD_THEMES[boardTheme] || BOARD_THEMES['green-white']
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="chess-board-wrapper">
|
|
106
|
+
<div
|
|
107
|
+
className={`chess-board${isFluid ? ' chess-board--fluid' : ''}`}
|
|
108
|
+
style={isFluid ? undefined : { width: boardPixelSize, height: boardPixelSize }}
|
|
109
|
+
>
|
|
110
|
+
<div className="chess-board__grid">
|
|
111
|
+
{rankIndices.map((rankIndex) =>
|
|
112
|
+
fileIndices.map((fileIndex) => {
|
|
113
|
+
const square = boardState[rankIndex][fileIndex]
|
|
114
|
+
const tone = getSquareTone(fileIndex, rankIndex)
|
|
115
|
+
const coordinate = `${FILES[fileIndex]}${8 - rankIndex}`
|
|
116
|
+
const pieceType = square?.type ?? null
|
|
117
|
+
const pieceColor = square?.color === 'b' ? 'black' : 'white'
|
|
118
|
+
const pieceName = pieceType ? PIECE_TYPE_MAP[pieceType] : null
|
|
119
|
+
|
|
120
|
+
const isLastMoveSquare = lastMove && (coordinate === lastMove.from || coordinate === lastMove.to)
|
|
121
|
+
|
|
122
|
+
const squareClass =
|
|
123
|
+
tone === 'light'
|
|
124
|
+
? 'chess-square chess-square--light'
|
|
125
|
+
: 'chess-square chess-square--dark'
|
|
126
|
+
|
|
127
|
+
const highlightClass = isLastMoveSquare ? 'chess-square--highlighted' : ''
|
|
128
|
+
|
|
129
|
+
const rankNum = 8 - rankIndex
|
|
130
|
+
const fileLetter = FILES[fileIndex]
|
|
131
|
+
const isDarkSquare = tone === 'dark'
|
|
132
|
+
const textColor = isDarkSquare ? 'text-white' : 'text-[#166534]'
|
|
133
|
+
const isFirstRank = rankNum === 1
|
|
134
|
+
const isFirstRankFromBlack = rankNum === 8
|
|
135
|
+
const isAFile = fileIndex === 0
|
|
136
|
+
|
|
137
|
+
const squareColor = tone === 'light' ? theme.light : theme.dark
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div
|
|
141
|
+
key={coordinate}
|
|
142
|
+
className={`${squareClass} ${highlightClass}`}
|
|
143
|
+
data-square={coordinate}
|
|
144
|
+
style={{ backgroundColor: squareColor }}
|
|
145
|
+
>
|
|
146
|
+
{showPieces && pieceName ? (
|
|
147
|
+
<div className="relative z-10 flex items-center justify-center" style={{ width: squarePixelSize, height: squarePixelSize }}>
|
|
148
|
+
<ChessPiece piece={pieceName} color={pieceColor} size={piecePixelSize} pieceSet={pieceSet} />
|
|
149
|
+
</div>
|
|
150
|
+
) : null}
|
|
151
|
+
|
|
152
|
+
{/* Coordinate labels - absolutely positioned to overlay on pieces */}
|
|
153
|
+
<div className="absolute inset-0 pointer-events-none">
|
|
154
|
+
{orientation === 'white' ? (
|
|
155
|
+
// White orientation labels
|
|
156
|
+
isFirstRank ? (
|
|
157
|
+
coordinate === 'a1' ? (
|
|
158
|
+
<div className={`${coordinatePaddingClass} flex justify-between w-full h-full`}>
|
|
159
|
+
<div className={`flex items-start text-white ${coordinateTypographyClass} uppercase`}>
|
|
160
|
+
<span>1</span>
|
|
161
|
+
</div>
|
|
162
|
+
<div className={`flex items-end text-white ${coordinateTypographyClass} uppercase`}>
|
|
163
|
+
<span>A</span>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
) : (
|
|
167
|
+
<div className={`${coordinatePaddingClass} flex justify-end items-end ${textColor} ${coordinateTypographyClass} uppercase w-full h-full`}>
|
|
168
|
+
<span>{fileLetter}</span>
|
|
169
|
+
</div>
|
|
170
|
+
)
|
|
171
|
+
) : isAFile ? (
|
|
172
|
+
<div className={`${coordinatePaddingClass} flex justify-start items-start ${textColor} ${coordinateTypographyClass} uppercase w-full h-full`}>
|
|
173
|
+
<span>{rankNum}</span>
|
|
174
|
+
</div>
|
|
175
|
+
) : null
|
|
176
|
+
) : (
|
|
177
|
+
// Black orientation labels (flipped)
|
|
178
|
+
isFirstRankFromBlack ? (
|
|
179
|
+
coordinate === 'a8' ? (
|
|
180
|
+
<div className={`${coordinatePaddingClass} flex justify-between w-full h-full`}>
|
|
181
|
+
<div className={`flex items-start text-white ${coordinateTypographyClass} uppercase`}>
|
|
182
|
+
<span>8</span>
|
|
183
|
+
</div>
|
|
184
|
+
<div className={`flex items-end text-white ${coordinateTypographyClass} uppercase`}>
|
|
185
|
+
<span>A</span>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
) : (
|
|
189
|
+
<div className={`${coordinatePaddingClass} flex justify-end items-end ${textColor} ${coordinateTypographyClass} uppercase w-full h-full`}>
|
|
190
|
+
<span>{fileLetter}</span>
|
|
191
|
+
</div>
|
|
192
|
+
)
|
|
193
|
+
) : isAFile ? (
|
|
194
|
+
<div className={`${coordinatePaddingClass} flex justify-start items-start ${textColor} ${coordinateTypographyClass} uppercase w-full h-full`}>
|
|
195
|
+
<span>{rankNum}</span>
|
|
196
|
+
</div>
|
|
197
|
+
) : null
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
})
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export default ChessBoard
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import ChessBoard from './ChessBoard'
|
|
2
|
+
import ChessSidebar from './ChessSidebar'
|
|
3
|
+
|
|
4
|
+
const ChessBoardFullscreen = ({
|
|
5
|
+
boardProps = {},
|
|
6
|
+
sidebarProps = {},
|
|
7
|
+
wrapperClassName = '',
|
|
8
|
+
sidebarWidth = '400px'
|
|
9
|
+
}) => {
|
|
10
|
+
const {
|
|
11
|
+
className: boardContainerClassName = '',
|
|
12
|
+
size: boardSize = 'desktop',
|
|
13
|
+
...restBoardProps
|
|
14
|
+
} = boardProps
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
className: sidebarClassName = 'w-full h-full bg-fg-04 text-auto',
|
|
18
|
+
size: sidebarSize = 'desktop',
|
|
19
|
+
variant: sidebarVariant = 'minimal',
|
|
20
|
+
...restSidebarProps
|
|
21
|
+
} = sidebarProps
|
|
22
|
+
|
|
23
|
+
const rootClassName = ['relative w-full', wrapperClassName].filter(Boolean).join(' ')
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className={rootClassName} style={{ minHeight: '100dvh' }}>
|
|
27
|
+
<div
|
|
28
|
+
className={`flex h-full w-full items-center justify-center ${boardContainerClassName}`}
|
|
29
|
+
style={{ paddingRight: sidebarWidth, paddingLeft: 0, minHeight: '100dvh' }}
|
|
30
|
+
>
|
|
31
|
+
<ChessBoard size={boardSize} {...restBoardProps} />
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div
|
|
35
|
+
className="absolute inset-y-0 right-0 flex"
|
|
36
|
+
style={{ width: sidebarWidth }}
|
|
37
|
+
>
|
|
38
|
+
<ChessSidebar
|
|
39
|
+
size={sidebarSize}
|
|
40
|
+
variant={sidebarVariant}
|
|
41
|
+
className={`h-full w-full ${sidebarClassName}`}
|
|
42
|
+
{...restSidebarProps}
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default ChessBoardFullscreen
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useChessControls, ChessControlsProvider } from '../context/ChessControlsContext'
|
|
2
|
+
import ChessBoard from './ChessBoard'
|
|
3
|
+
import AlternativeControlsMock from './AlternativeControlsMock'
|
|
4
|
+
import PlaybackControls from './PlaybackControls'
|
|
5
|
+
|
|
6
|
+
const ChessBoardView = () => {
|
|
7
|
+
const { snapshots, moveIndex, orientation, lastMove, pieceSet, boardTheme } = useChessControls()
|
|
8
|
+
return (
|
|
9
|
+
<ChessBoard
|
|
10
|
+
fen={snapshots[moveIndex]?.fen}
|
|
11
|
+
size="fluid"
|
|
12
|
+
orientation={orientation}
|
|
13
|
+
lastMove={lastMove}
|
|
14
|
+
pieceSet={pieceSet}
|
|
15
|
+
boardTheme={boardTheme}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ChessBoardWithControlsContent = () => {
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex flex-col lg:flex-row gap-6 lg:gap-8">
|
|
23
|
+
<div className="flex-1 min-w-0">
|
|
24
|
+
<ChessBoardView />
|
|
25
|
+
</div>
|
|
26
|
+
<div className="w-full lg:w-[440px] lg:flex-shrink-0">
|
|
27
|
+
<AlternativeControlsMock />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ChessBoardWithControls = ({ externalGame = null, chessData }) => {
|
|
34
|
+
return (
|
|
35
|
+
<ChessControlsProvider externalGame={externalGame} chessData={chessData}>
|
|
36
|
+
<ChessBoardWithControlsContent />
|
|
37
|
+
</ChessControlsProvider>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default ChessBoardWithControls
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ChessControlsProvider, useChessControls } from '../context/ChessControlsContext'
|
|
2
|
+
import ChessBoard from './ChessBoard'
|
|
3
|
+
import ChessBoardFullscreen from './ChessBoardFullscreen'
|
|
4
|
+
import ChessSidebar from './ChessSidebar'
|
|
5
|
+
|
|
6
|
+
const ToolbarButton = ({ label, onClick }) => (
|
|
7
|
+
<button type="button" onClick={onClick} className="board-toolbar__button">
|
|
8
|
+
{label}
|
|
9
|
+
</button>
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
const ChessBoardWithSidebarContent = ({ className = '', onToggleFullscreen = null, isFullscreen = false }) => {
|
|
13
|
+
const {
|
|
14
|
+
snapshots,
|
|
15
|
+
moveIndex,
|
|
16
|
+
orientation,
|
|
17
|
+
selectedGame,
|
|
18
|
+
selectedGameId,
|
|
19
|
+
filteredGames,
|
|
20
|
+
setSelectedGameId,
|
|
21
|
+
goToStart,
|
|
22
|
+
goToEnd,
|
|
23
|
+
stepBackward,
|
|
24
|
+
stepForward,
|
|
25
|
+
togglePlayback
|
|
26
|
+
} = useChessControls()
|
|
27
|
+
|
|
28
|
+
const handleSelectGame = (event) => {
|
|
29
|
+
setSelectedGameId(event.target.value || null)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (isFullscreen) {
|
|
33
|
+
const fullscreenWrapperClassName = ['min-h-dvh', className].filter(Boolean).join(' ')
|
|
34
|
+
return (
|
|
35
|
+
<ChessBoardFullscreen
|
|
36
|
+
wrapperClassName={fullscreenWrapperClassName}
|
|
37
|
+
boardProps={{
|
|
38
|
+
size: 'desktop',
|
|
39
|
+
fen: snapshots[moveIndex]?.fen,
|
|
40
|
+
orientation
|
|
41
|
+
}}
|
|
42
|
+
sidebarProps={{
|
|
43
|
+
selectedGame,
|
|
44
|
+
selectedGameId,
|
|
45
|
+
sampleGames: filteredGames,
|
|
46
|
+
moveIndex,
|
|
47
|
+
onSelectGame: handleSelectGame,
|
|
48
|
+
onGoToStart: goToStart,
|
|
49
|
+
onStepBackward: stepBackward,
|
|
50
|
+
onStepForward: stepForward,
|
|
51
|
+
onGoToEnd: goToEnd,
|
|
52
|
+
onTogglePlayback: togglePlayback,
|
|
53
|
+
size: 'desktop',
|
|
54
|
+
variant: 'minimal',
|
|
55
|
+
className: 'w-full h-full bg-fg-04 text-auto',
|
|
56
|
+
onToggleFullscreen,
|
|
57
|
+
isFullscreen
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const rootClassName = ['board-playback', className].filter(Boolean).join(' ')
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className={rootClassName}>
|
|
67
|
+
<div className="board-playback__toolbar board-playback__toolbar--left">
|
|
68
|
+
<ToolbarButton label="⏮" onClick={goToStart} />
|
|
69
|
+
<ToolbarButton label="▶" onClick={togglePlayback} />
|
|
70
|
+
<ToolbarButton label="⏭" onClick={goToEnd} />
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div className="board-playback__toolbar board-playback__toolbar--right">
|
|
74
|
+
<ToolbarButton label="⏮" onClick={goToStart} />
|
|
75
|
+
<ToolbarButton label="◀" onClick={stepBackward} />
|
|
76
|
+
<ToolbarButton label="▶" onClick={togglePlayback} />
|
|
77
|
+
<ToolbarButton label="▸" onClick={stepForward} />
|
|
78
|
+
<ToolbarButton label="⏭" onClick={goToEnd} />
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div className="board-playback__content">
|
|
82
|
+
<ChessBoard fen={snapshots[moveIndex]?.fen} size="fluid" orientation={orientation} />
|
|
83
|
+
|
|
84
|
+
<ChessSidebar
|
|
85
|
+
selectedGame={selectedGame}
|
|
86
|
+
selectedGameId={selectedGameId}
|
|
87
|
+
sampleGames={filteredGames}
|
|
88
|
+
moveIndex={moveIndex}
|
|
89
|
+
onSelectGame={handleSelectGame}
|
|
90
|
+
onGoToStart={goToStart}
|
|
91
|
+
onStepBackward={stepBackward}
|
|
92
|
+
onStepForward={stepForward}
|
|
93
|
+
onGoToEnd={goToEnd}
|
|
94
|
+
onTogglePlayback={togglePlayback}
|
|
95
|
+
size="desktop"
|
|
96
|
+
onToggleFullscreen={onToggleFullscreen}
|
|
97
|
+
isFullscreen={isFullscreen}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const ChessBoardWithSidebar = ({
|
|
105
|
+
className = '',
|
|
106
|
+
onToggleFullscreen = null,
|
|
107
|
+
isFullscreen = false,
|
|
108
|
+
externalGame = null,
|
|
109
|
+
chessData
|
|
110
|
+
}) => {
|
|
111
|
+
return (
|
|
112
|
+
<ChessControlsProvider externalGame={externalGame} chessData={chessData}>
|
|
113
|
+
<ChessBoardWithSidebarContent
|
|
114
|
+
className={className}
|
|
115
|
+
onToggleFullscreen={onToggleFullscreen}
|
|
116
|
+
isFullscreen={isFullscreen}
|
|
117
|
+
/>
|
|
118
|
+
</ChessControlsProvider>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default ChessBoardWithSidebar
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { ChessPiece } from '../index.js'
|
|
2
|
+
|
|
3
|
+
const getPieceSize = (size) => {
|
|
4
|
+
const sizeMap = {
|
|
5
|
+
sm: '28px',
|
|
6
|
+
md: '40px',
|
|
7
|
+
lg: '52px'
|
|
8
|
+
}
|
|
9
|
+
return sizeMap[size] || sizeMap.lg
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ChessSidebar = ({
|
|
13
|
+
selectedGame = null,
|
|
14
|
+
selectedGameId = null,
|
|
15
|
+
sampleGames = [],
|
|
16
|
+
moveIndex = 0,
|
|
17
|
+
onSelectGame = () => {},
|
|
18
|
+
onGoToStart = () => {},
|
|
19
|
+
onStepBackward = () => {},
|
|
20
|
+
onStepForward = () => {},
|
|
21
|
+
onGoToEnd = () => {},
|
|
22
|
+
onTogglePlayback = () => {},
|
|
23
|
+
size = 'desktop',
|
|
24
|
+
className = '',
|
|
25
|
+
variant = 'default',
|
|
26
|
+
onToggleFullscreen = null,
|
|
27
|
+
isFullscreen = false
|
|
28
|
+
}) => {
|
|
29
|
+
const piecePixelSize = getPieceSize(size)
|
|
30
|
+
const getSidebarClass = (variant) => {
|
|
31
|
+
switch (variant) {
|
|
32
|
+
case 'minimal':
|
|
33
|
+
return 'board-playback__sidebar--minimal'
|
|
34
|
+
case 'plain':
|
|
35
|
+
return 'board-playback__sidebar--plain'
|
|
36
|
+
default:
|
|
37
|
+
return 'board-playback__sidebar'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const sidebarClass = getSidebarClass(variant)
|
|
41
|
+
const showFullscreenToggle = typeof onToggleFullscreen === 'function'
|
|
42
|
+
const fullscreenLabel = isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'
|
|
43
|
+
const toolbarButton = (label, onClick) => (
|
|
44
|
+
<button type="button" onClick={onClick} className="board-toolbar__button">
|
|
45
|
+
{label}
|
|
46
|
+
</button>
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<aside className={`${sidebarClass} ${className}`}>
|
|
51
|
+
<header className="board-playback__sidebar-header">
|
|
52
|
+
<div className="flex items-center justify-between gap-3">
|
|
53
|
+
<span className="board-playback__badge">{'{}'} Setup Position</span>
|
|
54
|
+
{showFullscreenToggle ? (
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
onClick={onToggleFullscreen}
|
|
58
|
+
className="board-toolbar__button board-toolbar__button--compact"
|
|
59
|
+
>
|
|
60
|
+
{fullscreenLabel}
|
|
61
|
+
</button>
|
|
62
|
+
) : null}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div className="space-y-2">
|
|
66
|
+
<h3 className="board-playback__title">
|
|
67
|
+
{selectedGame?.player?.username ?? 'Player'} vs{' '}
|
|
68
|
+
{selectedGame?.opponent?.username ?? 'Opponent'}
|
|
69
|
+
</h3>
|
|
70
|
+
<p className="board-playback__meta">
|
|
71
|
+
{selectedGame?.timeClass ?? 'blitz'} · {selectedGame?.timeControl ?? '180'} · move{' '}
|
|
72
|
+
{moveIndex}
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<select
|
|
77
|
+
value={selectedGameId ?? ''}
|
|
78
|
+
onChange={(e) => onSelectGame(e.target.value || null)}
|
|
79
|
+
className="board-playback__select"
|
|
80
|
+
>
|
|
81
|
+
{(sampleGames || []).map((game) => (
|
|
82
|
+
<option value={game.id} key={game.id}>
|
|
83
|
+
{game.player?.username} vs {game.opponent?.username}
|
|
84
|
+
</option>
|
|
85
|
+
))}
|
|
86
|
+
</select>
|
|
87
|
+
</header>
|
|
88
|
+
|
|
89
|
+
<div className="board-playback__palette">
|
|
90
|
+
<div className="board-playback__palette-row">
|
|
91
|
+
{['rook', 'knight', 'bishop', 'queen', 'king', 'bishop', 'knight', 'rook'].map(
|
|
92
|
+
(piece, index) => (
|
|
93
|
+
<ChessPiece key={`white-${piece}-${index}`} piece={piece} color="white" size={piecePixelSize} />
|
|
94
|
+
)
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
<div className="board-playback__palette-row">
|
|
98
|
+
{['rook', 'knight', 'bishop', 'queen', 'king', 'bishop', 'knight', 'rook'].map(
|
|
99
|
+
(piece, index) => (
|
|
100
|
+
<ChessPiece key={`black-${piece}-${index}`} piece={piece} color="black" size={piecePixelSize} />
|
|
101
|
+
)
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div className="board-playback__controls">
|
|
107
|
+
{toolbarButton('⏮', onGoToStart)}
|
|
108
|
+
{toolbarButton('◀', onStepBackward)}
|
|
109
|
+
{toolbarButton('▶', onTogglePlayback)}
|
|
110
|
+
{toolbarButton('▸', onStepForward)}
|
|
111
|
+
{toolbarButton('⏭', onGoToEnd)}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="board-playback__footer">
|
|
115
|
+
<span>⚙</span>
|
|
116
|
+
<span>♜</span>
|
|
117
|
+
<span>♘</span>
|
|
118
|
+
<span>♞</span>
|
|
119
|
+
<div className="board-playback__avatar">
|
|
120
|
+
<div className="board-playback__avatar-img" />
|
|
121
|
+
<div>
|
|
122
|
+
<div className="board-playback__avatar-name">Tór Grimsson</div>
|
|
123
|
+
<div className="board-playback__avatar-meta">Oct 11, 2025</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</aside>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default ChessSidebar
|