@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.
Files changed (91) hide show
  1. package/package.json +4 -2
  2. package/src/chess/ChessPiece.jsx +191 -0
  3. package/src/chess/apparatus/AlternativeControlsMock.jsx +460 -0
  4. package/src/chess/apparatus/ChessAnalysisLayout.jsx +24 -0
  5. package/src/chess/apparatus/ChessBoard.jsx +210 -0
  6. package/src/chess/apparatus/ChessBoardFullscreen.jsx +49 -0
  7. package/src/chess/apparatus/ChessBoardWithControls.jsx +41 -0
  8. package/src/chess/apparatus/ChessBoardWithSidebar.jsx +122 -0
  9. package/src/chess/apparatus/ChessSidebar.jsx +131 -0
  10. package/src/chess/apparatus/GameArchiveTable.jsx +493 -0
  11. package/src/chess/apparatus/GameSelector.jsx +47 -0
  12. package/src/chess/apparatus/NotationPanel.jsx +70 -0
  13. package/src/chess/apparatus/PlaybackControls.jsx +52 -0
  14. package/src/chess/apparatus/VariationTree.jsx +137 -0
  15. package/src/chess/assets/chess-extra-set/set-1/bishop-1.svg +6 -0
  16. package/src/chess/assets/chess-extra-set/set-1/bishop.svg +6 -0
  17. package/src/chess/assets/chess-extra-set/set-1/king-1.svg +9 -0
  18. package/src/chess/assets/chess-extra-set/set-1/king.svg +7 -0
  19. package/src/chess/assets/chess-extra-set/set-1/knight-1.svg +8 -0
  20. package/src/chess/assets/chess-extra-set/set-1/knight.svg +7 -0
  21. package/src/chess/assets/chess-extra-set/set-1/pawn-1.svg +3 -0
  22. package/src/chess/assets/chess-extra-set/set-1/pawn.svg +3 -0
  23. package/src/chess/assets/chess-extra-set/set-1/queen-1.svg +11 -0
  24. package/src/chess/assets/chess-extra-set/set-1/queen.svg +6 -0
  25. package/src/chess/assets/chess-extra-set/set-1/rook-1.svg +7 -0
  26. package/src/chess/assets/chess-extra-set/set-1/rook.svg +11 -0
  27. package/src/chess/assets/chess-extra-set/set-2/bishop-1.svg +4 -0
  28. package/src/chess/assets/chess-extra-set/set-2/bishop.svg +4 -0
  29. package/src/chess/assets/chess-extra-set/set-2/king-1.svg +4 -0
  30. package/src/chess/assets/chess-extra-set/set-2/king.svg +4 -0
  31. package/src/chess/assets/chess-extra-set/set-2/knight-1.svg +5 -0
  32. package/src/chess/assets/chess-extra-set/set-2/knight.svg +5 -0
  33. package/src/chess/assets/chess-extra-set/set-2/pawn-1.svg +4 -0
  34. package/src/chess/assets/chess-extra-set/set-2/pawn.svg +4 -0
  35. package/src/chess/assets/chess-extra-set/set-2/queen-1.svg +4 -0
  36. package/src/chess/assets/chess-extra-set/set-2/queen.svg +4 -0
  37. package/src/chess/assets/chess-extra-set/set-2/rook-1.svg +5 -0
  38. package/src/chess/assets/chess-extra-set/set-2/rook.svg +5 -0
  39. package/src/chess/assets/chess-extra-set/set-3/bishop-1.svg +10 -0
  40. package/src/chess/assets/chess-extra-set/set-3/bishop.svg +3 -0
  41. package/src/chess/assets/chess-extra-set/set-3/king-1.svg +12 -0
  42. package/src/chess/assets/chess-extra-set/set-3/king.svg +3 -0
  43. package/src/chess/assets/chess-extra-set/set-3/knight-1.svg +3 -0
  44. package/src/chess/assets/chess-extra-set/set-3/knight.svg +10 -0
  45. package/src/chess/assets/chess-extra-set/set-3/pawn-1.svg +10 -0
  46. package/src/chess/assets/chess-extra-set/set-3/pawn.svg +10 -0
  47. package/src/chess/assets/chess-extra-set/set-3/queen-1.svg +10 -0
  48. package/src/chess/assets/chess-extra-set/set-3/queen.svg +3 -0
  49. package/src/chess/assets/chess-extra-set/set-3/rook-1.svg +3 -0
  50. package/src/chess/assets/chess-extra-set/set-3/rook.svg +11 -0
  51. package/src/chess/assets/chess-vector-set/ABoard.svg +164 -0
  52. package/src/chess/assets/chess-vector-set/BBiskup.svg +12 -0
  53. package/src/chess/assets/chess-vector-set/BKing.svg +15 -0
  54. package/src/chess/assets/chess-vector-set/BKnight.svg +13 -0
  55. package/src/chess/assets/chess-vector-set/BPawn.svg +7 -0
  56. package/src/chess/assets/chess-vector-set/BQueen.svg +6 -0
  57. package/src/chess/assets/chess-vector-set/BRook.svg +12 -0
  58. package/src/chess/assets/chess-vector-set/WBiskup.svg +14 -0
  59. package/src/chess/assets/chess-vector-set/WKing.svg +13 -0
  60. package/src/chess/assets/chess-vector-set/WKnight.svg +10 -0
  61. package/src/chess/assets/chess-vector-set/WPawn.svg +8 -0
  62. package/src/chess/assets/chess-vector-set/WQueen.svg +17 -0
  63. package/src/chess/assets/chess-vector-set/WRook.svg +13 -0
  64. package/src/chess/context/ChessControlsContext.jsx +379 -0
  65. package/src/chess/dashboards/ChessHero.jsx +303 -0
  66. package/src/chess/index.js +32 -0
  67. package/src/chess/utils/parsePgnTree.js +97 -0
  68. package/src/dashboards/cards/DashAlertCard.jsx +44 -0
  69. package/src/dashboards/cards/DashChartCard.jsx +44 -0
  70. package/src/dashboards/cards/DashFeaturedCard.jsx +42 -0
  71. package/src/dashboards/cards/DashListCard.jsx +87 -0
  72. package/src/dashboards/cards/DashMetricCard.jsx +59 -0
  73. package/src/dashboards/cards/DashSlotCard.jsx +45 -0
  74. package/src/dashboards/cards/DashStackedBarCard.jsx +70 -0
  75. package/src/dashboards/cards/DashTableCard.jsx +31 -0
  76. package/src/dashboards/cards/_shared/CardHeader.jsx +15 -0
  77. package/src/dashboards/cards/_shared/LegendRow.jsx +25 -0
  78. package/src/dashboards/charts/Candlestick.jsx +79 -0
  79. package/src/dashboards/charts/DonutChart.jsx +87 -0
  80. package/src/dashboards/charts/Heatmap.jsx +79 -0
  81. package/src/dashboards/charts/Histogram.jsx +56 -0
  82. package/src/dashboards/charts/LineChart.jsx +157 -0
  83. package/src/dashboards/charts/ScatterPlot.jsx +93 -0
  84. package/src/dashboards/charts/Sparkline.jsx +63 -0
  85. package/src/dashboards/index.js +28 -0
  86. package/src/dashboards/layout/DashboardGrid.jsx +30 -0
  87. package/src/dashboards/layout/GridCard.jsx +44 -0
  88. package/src/dashboards/shared/DashTooltip.jsx +6 -0
  89. package/src/dashboards/shared/formatMetric.js +26 -0
  90. package/src/dashboards/shared/useChartTooltip.js +25 -0
  91. 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