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