@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-component",
3
- "version": "0.6.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