@kolkrabbi/kol-component 0.7.0 → 0.9.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 (131) hide show
  1. package/README.md +8 -0
  2. package/package.json +2 -5
  3. package/src/atoms/RotaryDial.jsx +35 -21
  4. package/src/hooks/colorMath.js +214 -0
  5. package/src/hooks/useEyedropper.js +124 -0
  6. package/src/index.js +15 -18
  7. package/src/molecules/ButtonGroup.jsx +45 -0
  8. package/src/molecules/ColorInputRow.jsx +146 -126
  9. package/src/molecules/PaletteHarmonyWheel.jsx +192 -0
  10. package/src/molecules/Slider.jsx +29 -14
  11. package/src/molecules/SplitToolButton.jsx +133 -0
  12. package/src/organisms/ContentFilters.jsx +2 -2
  13. package/src/organisms/GalleryCarousel.jsx +2 -1
  14. package/src/organisms/LoaderOverlay.jsx +11 -14
  15. package/src/organisms/MediaTileGallery.jsx +57 -0
  16. package/src/organisms/MediaViewer.jsx +89 -64
  17. package/src/atoms/PriceDisplay.jsx +0 -34
  18. package/src/atoms/TextPressure.jsx +0 -331
  19. package/src/atoms/TypeSample.jsx +0 -50
  20. package/src/atoms/TypeSpecCard.jsx +0 -42
  21. package/src/chess/ChessPiece.jsx +0 -191
  22. package/src/chess/apparatus/AlternativeControlsMock.jsx +0 -460
  23. package/src/chess/apparatus/ChessAnalysisLayout.jsx +0 -24
  24. package/src/chess/apparatus/ChessBoard.jsx +0 -210
  25. package/src/chess/apparatus/ChessBoardFullscreen.jsx +0 -49
  26. package/src/chess/apparatus/ChessBoardWithControls.jsx +0 -41
  27. package/src/chess/apparatus/ChessBoardWithSidebar.jsx +0 -122
  28. package/src/chess/apparatus/ChessSidebar.jsx +0 -131
  29. package/src/chess/apparatus/GameArchiveTable.jsx +0 -493
  30. package/src/chess/apparatus/GameSelector.jsx +0 -47
  31. package/src/chess/apparatus/NotationPanel.jsx +0 -70
  32. package/src/chess/apparatus/PlaybackControls.jsx +0 -52
  33. package/src/chess/apparatus/VariationTree.jsx +0 -137
  34. package/src/chess/assets/chess-extra-set/set-1/bishop-1.svg +0 -6
  35. package/src/chess/assets/chess-extra-set/set-1/bishop.svg +0 -6
  36. package/src/chess/assets/chess-extra-set/set-1/king-1.svg +0 -9
  37. package/src/chess/assets/chess-extra-set/set-1/king.svg +0 -7
  38. package/src/chess/assets/chess-extra-set/set-1/knight-1.svg +0 -8
  39. package/src/chess/assets/chess-extra-set/set-1/knight.svg +0 -7
  40. package/src/chess/assets/chess-extra-set/set-1/pawn-1.svg +0 -3
  41. package/src/chess/assets/chess-extra-set/set-1/pawn.svg +0 -3
  42. package/src/chess/assets/chess-extra-set/set-1/queen-1.svg +0 -11
  43. package/src/chess/assets/chess-extra-set/set-1/queen.svg +0 -6
  44. package/src/chess/assets/chess-extra-set/set-1/rook-1.svg +0 -7
  45. package/src/chess/assets/chess-extra-set/set-1/rook.svg +0 -11
  46. package/src/chess/assets/chess-extra-set/set-2/bishop-1.svg +0 -4
  47. package/src/chess/assets/chess-extra-set/set-2/bishop.svg +0 -4
  48. package/src/chess/assets/chess-extra-set/set-2/king-1.svg +0 -4
  49. package/src/chess/assets/chess-extra-set/set-2/king.svg +0 -4
  50. package/src/chess/assets/chess-extra-set/set-2/knight-1.svg +0 -5
  51. package/src/chess/assets/chess-extra-set/set-2/knight.svg +0 -5
  52. package/src/chess/assets/chess-extra-set/set-2/pawn-1.svg +0 -4
  53. package/src/chess/assets/chess-extra-set/set-2/pawn.svg +0 -4
  54. package/src/chess/assets/chess-extra-set/set-2/queen-1.svg +0 -4
  55. package/src/chess/assets/chess-extra-set/set-2/queen.svg +0 -4
  56. package/src/chess/assets/chess-extra-set/set-2/rook-1.svg +0 -5
  57. package/src/chess/assets/chess-extra-set/set-2/rook.svg +0 -5
  58. package/src/chess/assets/chess-extra-set/set-3/bishop-1.svg +0 -10
  59. package/src/chess/assets/chess-extra-set/set-3/bishop.svg +0 -3
  60. package/src/chess/assets/chess-extra-set/set-3/king-1.svg +0 -12
  61. package/src/chess/assets/chess-extra-set/set-3/king.svg +0 -3
  62. package/src/chess/assets/chess-extra-set/set-3/knight-1.svg +0 -3
  63. package/src/chess/assets/chess-extra-set/set-3/knight.svg +0 -10
  64. package/src/chess/assets/chess-extra-set/set-3/pawn-1.svg +0 -10
  65. package/src/chess/assets/chess-extra-set/set-3/pawn.svg +0 -10
  66. package/src/chess/assets/chess-extra-set/set-3/queen-1.svg +0 -10
  67. package/src/chess/assets/chess-extra-set/set-3/queen.svg +0 -3
  68. package/src/chess/assets/chess-extra-set/set-3/rook-1.svg +0 -3
  69. package/src/chess/assets/chess-extra-set/set-3/rook.svg +0 -11
  70. package/src/chess/assets/chess-vector-set/ABoard.svg +0 -164
  71. package/src/chess/assets/chess-vector-set/BBiskup.svg +0 -12
  72. package/src/chess/assets/chess-vector-set/BKing.svg +0 -15
  73. package/src/chess/assets/chess-vector-set/BKnight.svg +0 -13
  74. package/src/chess/assets/chess-vector-set/BPawn.svg +0 -7
  75. package/src/chess/assets/chess-vector-set/BQueen.svg +0 -6
  76. package/src/chess/assets/chess-vector-set/BRook.svg +0 -12
  77. package/src/chess/assets/chess-vector-set/WBiskup.svg +0 -14
  78. package/src/chess/assets/chess-vector-set/WKing.svg +0 -13
  79. package/src/chess/assets/chess-vector-set/WKnight.svg +0 -10
  80. package/src/chess/assets/chess-vector-set/WPawn.svg +0 -8
  81. package/src/chess/assets/chess-vector-set/WQueen.svg +0 -17
  82. package/src/chess/assets/chess-vector-set/WRook.svg +0 -13
  83. package/src/chess/context/ChessControlsContext.jsx +0 -379
  84. package/src/chess/dashboards/ChessHero.jsx +0 -303
  85. package/src/chess/index.js +0 -32
  86. package/src/chess/utils/parsePgnTree.js +0 -97
  87. package/src/dashboards/cards/DashAlertCard.jsx +0 -44
  88. package/src/dashboards/cards/DashChartCard.jsx +0 -44
  89. package/src/dashboards/cards/DashFeaturedCard.jsx +0 -42
  90. package/src/dashboards/cards/DashListCard.jsx +0 -87
  91. package/src/dashboards/cards/DashMetricCard.jsx +0 -59
  92. package/src/dashboards/cards/DashSlotCard.jsx +0 -45
  93. package/src/dashboards/cards/DashStackedBarCard.jsx +0 -70
  94. package/src/dashboards/cards/DashTableCard.jsx +0 -31
  95. package/src/dashboards/cards/_shared/CardHeader.jsx +0 -15
  96. package/src/dashboards/cards/_shared/LegendRow.jsx +0 -25
  97. package/src/dashboards/charts/Candlestick.jsx +0 -79
  98. package/src/dashboards/charts/DonutChart.jsx +0 -87
  99. package/src/dashboards/charts/Heatmap.jsx +0 -79
  100. package/src/dashboards/charts/Histogram.jsx +0 -56
  101. package/src/dashboards/charts/LineChart.jsx +0 -157
  102. package/src/dashboards/charts/ScatterPlot.jsx +0 -93
  103. package/src/dashboards/charts/Sparkline.jsx +0 -63
  104. package/src/dashboards/index.js +0 -28
  105. package/src/dashboards/layout/DashboardGrid.jsx +0 -30
  106. package/src/dashboards/layout/GridCard.jsx +0 -44
  107. package/src/dashboards/shared/DashTooltip.jsx +0 -6
  108. package/src/dashboards/shared/formatMetric.js +0 -26
  109. package/src/dashboards/shared/useChartTooltip.js +0 -25
  110. package/src/dashboards/shared/useCountUp.js +0 -107
  111. package/src/molecules/ArticleCard.jsx +0 -178
  112. package/src/molecules/WorkListItem.jsx +0 -83
  113. package/src/molecules/foundry/SpecimenSectionHeader.jsx +0 -89
  114. package/src/organisms/ArticleHeader.jsx +0 -95
  115. package/src/organisms/ColorLoader.jsx +0 -155
  116. package/src/organisms/DiagonalMarqueeRiver.jsx +0 -138
  117. package/src/organisms/ParallaxShelf.jsx +0 -141
  118. package/src/organisms/PortableTextRenderer.jsx +0 -115
  119. package/src/organisms/ProductDetailLayout.jsx +0 -189
  120. package/src/organisms/ScrollDriftGallery.jsx +0 -214
  121. package/src/organisms/StackHero.jsx +0 -83
  122. package/src/organisms/WorkCard.jsx +0 -120
  123. package/src/organisms/WorkViewToggle.jsx +0 -170
  124. package/src/organisms/foundry/FontPreviewSection.jsx +0 -187
  125. package/src/organisms/foundry/FoundryCharacterSets.jsx +0 -113
  126. package/src/organisms/foundry/GlyphMetricsGrid.jsx +0 -335
  127. package/src/organisms/foundry/TypefaceHero.jsx +0 -107
  128. package/src/organisms/foundry/TypefaceStyleSection.jsx +0 -163
  129. package/src/organisms/foundry/VariableFontSection.jsx +0 -158
  130. package/src/organisms/foundry/glyphData.js +0 -30
  131. package/src/organisms/foundry/index.js +0 -21
@@ -1,493 +0,0 @@
1
- // Data is consumer-provided via the `chessData` prop (no bundled dataset).
2
- // Required chessData functions: loadMonthGames(month), getMonthlySummary(),
3
- // getRandomMonth(), getGamePgnByIdAsync(id, month).
4
- import { useCallback, useEffect, useMemo, useState } from 'react'
5
- import Dropdown from '../../molecules/Dropdown.jsx'
6
- import Input from '../../atoms/Input.jsx'
7
- import Table from '../../organisms/Table.jsx'
8
- import Tag from '../../atoms/Tag.jsx'
9
- import Pill from '../../atoms/Pill.jsx'
10
-
11
- const MAX_VISIBLE_GAMES = 5
12
-
13
- const GameArchiveTable = ({ chessData, onGameLoad }) => {
14
- const {
15
- loadMonthGames = async () => [],
16
- getMonthlySummary = () => [],
17
- getRandomMonth = () => null,
18
- getGamePgnByIdAsync = async () => null
19
- } = chessData ?? {}
20
-
21
- const monthFormatter = useMemo(
22
- () =>
23
- new Intl.DateTimeFormat('en', {
24
- month: 'short',
25
- year: 'numeric'
26
- }),
27
- []
28
- )
29
-
30
- // Lightweight data - always loaded
31
- const monthlySummary = useMemo(() => getMonthlySummary(), [])
32
-
33
- // Heavy data - loaded on demand
34
- const [loadedMonths, setLoadedMonths] = useState(new Set())
35
- const [monthlyGames, setMonthlyGames] = useState({}) // { "2024-08": [...games] }
36
- const [isLoading, setIsLoading] = useState(false)
37
- const [selectedMonth, setSelectedMonth] = useState(null)
38
- const [selectedTimeClass, setSelectedTimeClass] = useState('all')
39
- const [selectedResult, setSelectedResult] = useState('all')
40
- const [searchTerm, setSearchTerm] = useState('')
41
- const [showAllGames, setShowAllGames] = useState(false)
42
-
43
- // Month options (from lightweight summary)
44
- const monthOptions = useMemo(() => {
45
- const formatted = (monthlySummary || [])
46
- .filter((entry) => entry?.month && entry.month !== 'unknown')
47
- .map((entry) => ({
48
- label: monthFormatter.format(new Date(`${entry.month}-01T00:00:00Z`)),
49
- value: entry.month,
50
- count: entry.total
51
- }))
52
- .reverse()
53
- return formatted
54
- }, [monthlySummary, monthFormatter])
55
-
56
- // Load random month on mount
57
- useEffect(() => {
58
- const loadInitialMonth = async () => {
59
- const randomMonth = getRandomMonth()
60
- setIsLoading(true)
61
- const games = await loadMonthGames(randomMonth)
62
- setMonthlyGames({ [randomMonth]: games.slice(0, 5) }) // Only first 5 games
63
- setLoadedMonths(new Set([randomMonth]))
64
- setSelectedMonth(randomMonth)
65
- setIsLoading(false)
66
- }
67
- loadInitialMonth()
68
- }, [])
69
-
70
- // Load month data when explicitly requested
71
- const handleLoadMonth = useCallback(async (month) => {
72
- if (loadedMonths.has(month)) {
73
- // Already loaded, just switch to it
74
- setSelectedMonth(month)
75
- return
76
- }
77
-
78
- setIsLoading(true)
79
- const games = await loadMonthGames(month)
80
- setMonthlyGames(prev => ({ ...prev, [month]: games }))
81
- setLoadedMonths(prev => new Set([...prev, month]))
82
- setSelectedMonth(month)
83
- setIsLoading(false)
84
- }, [loadedMonths])
85
-
86
- // Get current month's games
87
- const currentMonthGames = useMemo(() => {
88
- if (!selectedMonth) return []
89
- return monthlyGames[selectedMonth] || []
90
- }, [selectedMonth, monthlyGames])
91
-
92
- // Time class options (from current month only)
93
- const timeClassOptions = useMemo(() => {
94
- const unique = Array.from(
95
- new Set(currentMonthGames.map((game) => game.timeClass).filter(Boolean))
96
- )
97
- return [
98
- { label: 'All Time Classes', value: 'all' },
99
- ...unique.map((timeClass) => ({
100
- label: timeClass.charAt(0).toUpperCase() + timeClass.slice(1),
101
- value: timeClass
102
- }))
103
- ]
104
- }, [currentMonthGames])
105
-
106
- // Result options (from current month only)
107
- const resultOptions = useMemo(() => {
108
- const unique = Array.from(
109
- new Set(currentMonthGames.map((game) => game.playerResult).filter(Boolean))
110
- )
111
- return [
112
- { label: 'All Results', value: 'all' },
113
- ...unique.map((result) => ({
114
- label: result.charAt(0).toUpperCase() + result.slice(1),
115
- value: result
116
- }))
117
- ]
118
- }, [currentMonthGames])
119
-
120
- // Reset filters when month changes
121
- useEffect(() => {
122
- setShowAllGames(false)
123
- setSelectedTimeClass('all')
124
- setSelectedResult('all')
125
- setSearchTerm('')
126
- }, [selectedMonth])
127
-
128
- // Filter games
129
- const filteredGames = useMemo(() => {
130
- if (!currentMonthGames.length) return []
131
-
132
- const term = searchTerm.trim().toLowerCase()
133
-
134
- return currentMonthGames
135
- .filter((game) => {
136
- if (selectedTimeClass !== 'all' && game.timeClass !== selectedTimeClass) return false
137
- if (selectedResult !== 'all' && game.playerResult !== selectedResult) return false
138
-
139
- if (!term) return true
140
-
141
- const haystack = [
142
- game.player?.username,
143
- game.opponent?.username,
144
- game.timeControl,
145
- game.eco
146
- ]
147
- .filter(Boolean)
148
- .join(' ')
149
- .toLowerCase()
150
-
151
- return haystack.includes(term)
152
- })
153
- .sort((a, b) => (b.endTime ?? 0) - (a.endTime ?? 0))
154
- }, [currentMonthGames, selectedTimeClass, selectedResult, searchTerm])
155
-
156
- const tableRows = useMemo(() => {
157
- if (showAllGames || filteredGames.length <= MAX_VISIBLE_GAMES) {
158
- return filteredGames
159
- }
160
- return filteredGames.slice(0, MAX_VISIBLE_GAMES)
161
- }, [filteredGames, showAllGames])
162
-
163
- const canShowAll = filteredGames.length > MAX_VISIBLE_GAMES
164
-
165
- const monthLabel = useMemo(() => {
166
- if (!selectedMonth) return 'No month selected'
167
- try {
168
- return monthFormatter.format(new Date(`${selectedMonth}-01T00:00:00Z`))
169
- } catch {
170
- return selectedMonth
171
- }
172
- }, [monthFormatter, selectedMonth])
173
-
174
- const formatDate = useCallback((timestamp) => {
175
- if (!timestamp) return '—'
176
- const date = new Date(timestamp * 1000)
177
- return date.toLocaleDateString('en-US', {
178
- month: 'short',
179
- day: 'numeric',
180
- year: 'numeric'
181
- })
182
- }, [])
183
-
184
- const formatTime = useCallback((timestamp) => {
185
- if (!timestamp) return ''
186
- const date = new Date(timestamp * 1000)
187
- return date.toLocaleTimeString('en-US', {
188
- hour: '2-digit',
189
- minute: '2-digit'
190
- })
191
- }, [])
192
-
193
- const formatResult = useCallback((result) => {
194
- if (!result) return 'Pending'
195
- switch (result) {
196
- case 'win':
197
- return 'Win'
198
- case 'draw':
199
- return 'Draw'
200
- case 'stalemate':
201
- return 'Stalemate'
202
- case 'resigned':
203
- return 'Resigned'
204
- default:
205
- return result.charAt(0).toUpperCase() + result.slice(1)
206
- }
207
- }, [])
208
-
209
- const handleLoadGame = useCallback(async (game) => {
210
- // Pass month directly to avoid fetching full dataset
211
- const pgn = await getGamePgnByIdAsync(game.id, game.month)
212
- if (!pgn) {
213
- console.error('[GameArchiveTable] No PGN found for game:', game.id, 'month:', game.month)
214
- return
215
- }
216
- const loadedGame = {
217
- ...game,
218
- pgn
219
- }
220
- if (onGameLoad) {
221
- onGameLoad(loadedGame)
222
- }
223
- if (typeof window !== 'undefined') {
224
- window.scrollTo({ top: 0, behavior: 'smooth' })
225
- }
226
- }, [onGameLoad])
227
-
228
- const columns = useMemo(
229
- () => [
230
- {
231
- header: 'Date',
232
- accessor: 'date',
233
- className: 'kol-table-cell-text analysis-table__cell',
234
- render: (game) => (
235
- <div className="flex items-center gap-3">
236
- <span>{formatDate(game.endTime)}</span>
237
- {game.endTime ? (
238
- <span className="kol-table-token bg-fg-08 hidden md:inline-flex">{formatTime(game.endTime)}</span>
239
- ) : null}
240
- </div>
241
- )
242
- },
243
- {
244
- header: 'Opponent',
245
- accessor: 'opponent',
246
- className: 'kol-table-cell-text analysis-table__cell',
247
- render: (game) => <span>{game.opponent?.username ?? 'Opponent'}</span>
248
- },
249
- {
250
- header: 'Result',
251
- accessor: 'result',
252
- className: 'kol-table-cell-text analysis-table__cell',
253
- render: (game) => {
254
- const label = game.terminationCategory
255
- ? game.terminationCategory.replace('win-', 'win by ').replace(/-/g, ' ')
256
- : formatResult(game.playerResult)
257
- return <span className="kol-table-token bg-fg-08">{label}</span>
258
- }
259
- },
260
- {
261
- header: 'Color',
262
- accessor: 'color',
263
- className: 'kol-table-cell-text analysis-table__cell hidden lg:table-cell',
264
- headerClassName: 'kol-table-cell-title hidden lg:table-cell',
265
- render: (game) => (
266
- <span
267
- className={`kol-table-pill ${
268
- game.playerColor === 'white' ? 'kol-table-pill-light' : 'kol-table-pill-dark'
269
- }`}
270
- >
271
- {game.playerColor === 'white' ? 'White' : 'Black'}
272
- </span>
273
- )
274
- },
275
- {
276
- header: 'Time Control',
277
- accessor: 'timeControl',
278
- className: 'kol-table-cell-text analysis-table__cell hidden md:table-cell',
279
- headerClassName: 'kol-table-cell-title hidden md:table-cell',
280
- render: (game) => {
281
- const label = game.timeClass
282
- ? game.timeClass.charAt(0).toUpperCase() + game.timeClass.slice(1)
283
- : '—'
284
- return <span>{label}</span>
285
- }
286
- },
287
- {
288
- header: 'Rating',
289
- accessor: 'ratings',
290
- className: 'kol-table-cell-text analysis-table__cell hidden lg:table-cell',
291
- headerClassName: 'kol-table-cell-title hidden lg:table-cell',
292
- render: (game) => (
293
- <div className="flex items-center gap-3">
294
- <span className="kol-table-pill kol-table-pill-dark">
295
- {game.player?.rating ?? '—'}
296
- </span>
297
- <span>{game.opponent?.rating ?? '—'}</span>
298
- </div>
299
- )
300
- },
301
- {
302
- header: 'Link',
303
- accessor: 'url',
304
- className: 'kol-table-cell-text analysis-table__actions-cell',
305
- headerClassName: 'kol-table-cell-title analysis-table__actions-header',
306
- render: (game) => (
307
- <div className="analysis-table__actions">
308
- <button
309
- type="button"
310
- className="analysis-table__link analysis-table__link-button"
311
- onClick={(e) => {
312
- e.preventDefault()
313
- e.stopPropagation()
314
- handleLoadGame(game)
315
- }}
316
- >
317
- <span className="hidden sm:inline">Load here</span>
318
- <span className="inline sm:hidden">Load</span>
319
- </button>
320
- {game.url ? (
321
- <a
322
- href={game.url}
323
- className="analysis-table__link"
324
- target="_blank"
325
- rel="noreferrer"
326
- >
327
- Chess.com →
328
- </a>
329
- ) : (
330
- <span className="analysis-table__meta">No link</span>
331
- )}
332
- </div>
333
- )
334
- }
335
- ],
336
- [formatDate, formatTime, formatResult, handleLoadGame]
337
- )
338
-
339
- // Calculate memory usage
340
- const loadedGamesCount = useMemo(() => {
341
- return Object.values(monthlyGames).reduce((sum, games) => sum + games.length, 0)
342
- }, [monthlyGames])
343
-
344
- const selectedMonthInfo = useMemo(() => {
345
- return monthlySummary.find(entry => entry.month === selectedMonth)
346
- }, [monthlySummary, selectedMonth])
347
-
348
- const isMonthFullyLoaded = useMemo(() => {
349
- if (!selectedMonth || !selectedMonthInfo) return false
350
- const loadedCount = monthlyGames[selectedMonth]?.length || 0
351
- return loadedCount === selectedMonthInfo.total
352
- }, [selectedMonth, selectedMonthInfo, monthlyGames])
353
-
354
- return (
355
- <section className="space-y-6">
356
- {/* Memory Indicator */}
357
- <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 p-4 bg-surface-secondary border border-fg-08 rounded">
358
- <div className="space-y-1">
359
- <div className="flex items-center gap-2">
360
- <h3 className="kol-label-mono-xs uppercase">Game Archive Status</h3>
361
- {isLoading && <Pill variant="subtle" size="sm">Loading...</Pill>}
362
- </div>
363
- <p className="kol-mono-xs text-fg-64">
364
- {loadedMonths.size} of {monthOptions.length} months loaded · {loadedGamesCount.toLocaleString()} games in memory
365
- </p>
366
- </div>
367
- <div className="flex items-center gap-2">
368
- <Pill variant="subtle" size="sm">
369
- {monthLabel}
370
- </Pill>
371
- {selectedMonthInfo && (
372
- <Pill variant="subtle" size="sm">
373
- {selectedMonthInfo.total} total
374
- </Pill>
375
- )}
376
- </div>
377
- </div>
378
-
379
- <div className="flex flex-col gap-4">
380
- <div className="flex flex-wrap items-center justify-between gap-4">
381
- <h3 className="kol-heading-subsection">Browse Games</h3>
382
- <Tag>
383
- {`${tableRows.length} shown · ${filteredGames.length.toLocaleString()} filtered`}
384
- </Tag>
385
- </div>
386
- <p className="kol-text-md text-auto/70 leading-relaxed">
387
- Select a month to load games. Initially showing a random sample of 5 games.
388
- Use filters to narrow your search or load the full month.
389
- </p>
390
- </div>
391
-
392
- {/* Month Selector with Load Button */}
393
- <div className="flex flex-col gap-4">
394
- <div className="flex flex-col sm:flex-row gap-3 flex-wrap">
395
- <Dropdown
396
- options={monthOptions}
397
- value={selectedMonth}
398
- onChange={setSelectedMonth}
399
- className="analysis-control w-full sm:w-auto"
400
- placeholder="Select month..."
401
- />
402
-
403
- {selectedMonth && !loadedMonths.has(selectedMonth) && (
404
- <button
405
- onClick={() => handleLoadMonth(selectedMonth)}
406
- disabled={isLoading}
407
- className="px-4 py-2 bg-accent-primary text-surface-primary rounded kol-mono-sm hover:bg-accent-80 transition-colors disabled:opacity-50"
408
- >
409
- {isLoading ? 'Loading...' : `Load ${selectedMonthInfo?.total || 0} games from ${monthLabel}`}
410
- </button>
411
- )}
412
-
413
- {selectedMonth && loadedMonths.has(selectedMonth) && !isMonthFullyLoaded && (
414
- <button
415
- onClick={() => handleLoadMonth(selectedMonth)}
416
- disabled={isLoading}
417
- className="px-4 py-2 bg-surface-tertiary text-auto rounded kol-mono-sm border border-fg-08 hover:bg-surface-secondary transition-colors"
418
- >
419
- Load full month ({selectedMonthInfo?.total || 0} games)
420
- </button>
421
- )}
422
- </div>
423
- </div>
424
-
425
- {/* Filters - only show when month is loaded */}
426
- {loadedMonths.has(selectedMonth) && (
427
- <div className="flex flex-col gap-4 md:gap-0 md:flex-row md:items-center md:justify-between">
428
- <div className="flex flex-col sm:flex-row gap-3 flex-wrap">
429
- <Dropdown
430
- options={timeClassOptions}
431
- value={selectedTimeClass}
432
- onChange={setSelectedTimeClass}
433
- className="analysis-control w-full sm:w-auto"
434
- />
435
- <Dropdown
436
- options={resultOptions}
437
- value={selectedResult}
438
- onChange={setSelectedResult}
439
- className="analysis-control w-full sm:w-auto"
440
- />
441
- </div>
442
- <div className="w-full md:w-auto md:min-w-[280px]">
443
- <Input
444
- placeholder="Search opponent, ECO, or control…"
445
- value={searchTerm}
446
- onChange={(event) => setSearchTerm(event.target.value)}
447
- iconLeft="search-16"
448
- />
449
- </div>
450
- </div>
451
- )}
452
-
453
- {/* Table */}
454
- <div className="analysis-table-wrapper">
455
- {!selectedMonth || !loadedMonths.has(selectedMonth) ? (
456
- <div className="analysis-table-empty">
457
- <p className="kol-text-md text-auto/70">
458
- {selectedMonth
459
- ? 'Click "Load games" to view this month\'s games'
460
- : 'Select a month from the dropdown above to get started'}
461
- </p>
462
- </div>
463
- ) : tableRows.length === 0 ? (
464
- <div className="analysis-table-empty">
465
- <p className="kol-text-md text-auto/70">
466
- No games match the current filters. Try expanding your search criteria.
467
- </p>
468
- </div>
469
- ) : (
470
- <Table
471
- caption="Archive of chess games matching current filters"
472
- columns={columns}
473
- rows={tableRows}
474
- />
475
- )}
476
-
477
- {canShowAll && (
478
- <button
479
- type="button"
480
- className="analysis-table__toggle"
481
- onClick={() => setShowAllGames((value) => !value)}
482
- >
483
- {showAllGames
484
- ? 'Show fewer games'
485
- : `Show all ${filteredGames.length.toLocaleString()} games`}
486
- </button>
487
- )}
488
- </div>
489
- </section>
490
- )
491
- }
492
-
493
- export default GameArchiveTable
@@ -1,47 +0,0 @@
1
- import { useState } from 'react'
2
- import { Icon } from '@kolkrabbi/kol-icons'
3
- import { useChessControls } from '../context/ChessControlsContext'
4
-
5
- const GameSelector = () => {
6
- const { filteredGames, selectedGame, setSelectedGameId } = useChessControls()
7
- const [showGameSelector, setShowGameSelector] = useState(false)
8
-
9
- return (
10
- <div className="flex items-center gap-2 flex-1 relative">
11
- <div
12
- className="flex-1 rounded bg-oq-04 px-3 py-3 flex flex-col gap-1 cursor-pointer"
13
- onClick={() => setShowGameSelector(!showGameSelector)}
14
- >
15
- <div className="flex items-center justify-between">
16
- <span className="kol-mono-xs text-fg-80 uppercase tracking-[0.2em]">
17
- {selectedGame?.player?.username || 'Player'} vs {selectedGame?.opponent?.username || 'Opponent'}
18
- </span>
19
- <Icon name="chevron-down" size={16} className="text-fg-80" />
20
- </div>
21
- {selectedGame?.opening?.name && (
22
- <span className="kol-mono-xxs text-fg-64">
23
- {selectedGame.opening.name}
24
- </span>
25
- )}
26
- </div>
27
- {showGameSelector && (
28
- <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">
29
- {filteredGames.map((game) => (
30
- <div
31
- key={game.id}
32
- className="px-3 py-2 kol-mono-xs text-fg-80 hover:bg-oq-16 cursor-pointer"
33
- onClick={() => {
34
- setSelectedGameId(game.id)
35
- setShowGameSelector(false)
36
- }}
37
- >
38
- {game.player?.username} vs {game.opponent?.username}
39
- </div>
40
- ))}
41
- </div>
42
- )}
43
- </div>
44
- )
45
- }
46
-
47
- export default GameSelector
@@ -1,70 +0,0 @@
1
- import { useEffect, useRef } from 'react'
2
-
3
- const buttonClass = (isActive, hasMove) => {
4
- let base = 'text-left kol-mono-xs transition-colors'
5
- if (!hasMove) {
6
- return `${base} text-fg-40 cursor-not-allowed`
7
- }
8
- if (isActive) {
9
- return `${base} text-accent-primary font-semibold`
10
- }
11
- return `${base} text-fg-80 hover:text-fg-88`
12
- }
13
-
14
- const NotationPanel = ({ notationPairs = [], activePly = 0, onSelectPly = () => {}, isLoading = false }) => {
15
- const activeRef = useRef(null)
16
-
17
- useEffect(() => {
18
- if (activeRef.current) {
19
- activeRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
20
- }
21
- }, [activePly])
22
- if (isLoading) {
23
- return (
24
- <div className="flex flex-col gap-2 text-fg-48">
25
- <div className="h-3 bg-oq-04 rounded animate-pulse" />
26
- <div className="h-3 bg-oq-04 rounded animate-pulse" />
27
- <div className="h-3 bg-oq-04 rounded animate-pulse" />
28
- </div>
29
- )
30
- }
31
-
32
- if (!notationPairs.length) {
33
- return <p className="kol-mono-xs text-fg-64">No notation available for this game.</p>
34
- }
35
-
36
- return (
37
- <div className="flex flex-col gap-2">
38
- {notationPairs.map((pair) => {
39
- const isWhiteActive = pair.white?.ply === activePly
40
- const isBlackActive = pair.black?.ply === activePly
41
-
42
- return (
43
- <div key={pair.moveNumber} className="flex items-start gap-4" ref={isWhiteActive || isBlackActive ? activeRef : null}>
44
- <span className="kol-mono-xs text-fg-64 w-6 text-right">{pair.moveNumber}.</span>
45
- <div className="flex flex-1 gap-8">
46
- <button
47
- type="button"
48
- className={buttonClass(isWhiteActive, Boolean(pair.white))}
49
- disabled={!pair.white}
50
- onClick={() => pair.white && onSelectPly(pair.white.ply)}
51
- >
52
- {pair.white?.san ?? '—'}
53
- </button>
54
- <button
55
- type="button"
56
- className={buttonClass(isBlackActive, Boolean(pair.black))}
57
- disabled={!pair.black}
58
- onClick={() => pair.black && onSelectPly(pair.black.ply)}
59
- >
60
- {pair.black?.san ?? '—'}
61
- </button>
62
- </div>
63
- </div>
64
- )
65
- })}
66
- </div>
67
- )
68
- }
69
-
70
- export default NotationPanel
@@ -1,52 +0,0 @@
1
- import { Icon } from '@kolkrabbi/kol-icons'
2
- import { useChessControls } from '../context/ChessControlsContext'
3
-
4
- const PlaybackControls = () => {
5
- const {
6
- goToStart,
7
- goToEnd,
8
- stepBackward,
9
- stepForward,
10
- togglePlayback,
11
- isPlaying,
12
- moveIndex,
13
- notationPairs
14
- } = useChessControls()
15
-
16
- const playbackButtons = [
17
- { icon: 'play-arrow-start', label: 'Jump to start', action: goToStart },
18
- { icon: 'play-arrow-back', label: 'Step backward', action: stepBackward },
19
- {
20
- icon: isPlaying ? 'play-pause' : 'play-Play',
21
- label: isPlaying ? 'Pause playback' : 'Play moves',
22
- action: togglePlayback
23
- },
24
- { icon: 'play-arrow-forward', label: 'Step forward', action: stepForward },
25
- { icon: 'play-arrow-end', label: 'Jump to end', action: goToEnd }
26
- ]
27
-
28
- return (
29
- <div className="flex flex-col gap-3">
30
- <div className="flex items-center justify-center">
31
- <span className="kol-mono-xs text-fg-64 uppercase tracking-[0.2em]">
32
- Move {moveIndex}/{notationPairs.length || 0}
33
- </span>
34
- </div>
35
- <div className="grid grid-cols-5 gap-3">
36
- {playbackButtons.map(({ icon, label, action }) => (
37
- <button
38
- key={icon}
39
- type="button"
40
- aria-label={label}
41
- className="h-12 rounded bg-oq-02 border border-fg-08 text-fg-80 flex items-center justify-center hover:bg-oq-04 transition-colors"
42
- onClick={action}
43
- >
44
- <Icon name={icon} size={18} className="text-fg-80" />
45
- </button>
46
- ))}
47
- </div>
48
- </div>
49
- )
50
- }
51
-
52
- export default PlaybackControls