@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,87 @@
1
+ import { useMemo } from 'react'
2
+ import useChartTooltip from '../shared/useChartTooltip'
3
+ import DashTooltip from '../shared/DashTooltip'
4
+
5
+ const DonutChart = ({
6
+ segments = [],
7
+ size = 120,
8
+ thickness = 20,
9
+ centerLabel,
10
+ showLegend = false
11
+ }) => {
12
+ const total = useMemo(() => segments.reduce((sum, s) => sum + s.value, 0), [segments])
13
+ const { activeIndex, handlers, containerHandlers, containerRef } = useChartTooltip()
14
+
15
+ const radius = (size - thickness) / 2
16
+ const circumference = 2 * Math.PI * radius
17
+ const center = size / 2
18
+
19
+ const arcs = useMemo(() => {
20
+ let offset = 0
21
+ return segments.map((seg) => {
22
+ const pct = total > 0 ? seg.value / total : 0
23
+ const dash = pct * circumference
24
+ const gap = circumference - dash
25
+ const rotation = (offset / total) * 360
26
+ offset += seg.value
27
+ return { ...seg, dash, gap, rotation, pct }
28
+ })
29
+ }, [segments, total, circumference])
30
+
31
+ return (
32
+ <div className="inline-flex flex-col items-center gap-3">
33
+ <div
34
+ className="relative"
35
+ style={{ width: size, height: size }}
36
+ ref={containerRef}
37
+ {...containerHandlers}
38
+ >
39
+ <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
40
+ {arcs.map((arc, i) => (
41
+ <circle
42
+ key={i}
43
+ className={`dash-donut-segment ${activeIndex === i ? 'dash-chart-element--active' : ''}`}
44
+ cx={center}
45
+ cy={center}
46
+ r={radius}
47
+ fill="none"
48
+ stroke={arc.color}
49
+ strokeWidth={thickness}
50
+ strokeDasharray={`${arc.dash} ${arc.gap}`}
51
+ transform={`rotate(${arc.rotation - 90} ${center} ${center})`}
52
+ style={{ cursor: 'default' }}
53
+ {...handlers(i)}
54
+ />
55
+ ))}
56
+ </svg>
57
+
58
+ {centerLabel && (
59
+ <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
60
+ <span className="dash-body text-fg-88">{centerLabel}</span>
61
+ </div>
62
+ )}
63
+
64
+ <DashTooltip visible={activeIndex !== null}>
65
+ {activeIndex !== null && arcs[activeIndex] && (
66
+ <span className="dash-caption">
67
+ {arcs[activeIndex].label ? `${arcs[activeIndex].label}: ` : ''}{arcs[activeIndex].value} ({(arcs[activeIndex].pct * 100).toFixed(1)}%)
68
+ </span>
69
+ )}
70
+ </DashTooltip>
71
+ </div>
72
+
73
+ {showLegend && (
74
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1 justify-center">
75
+ {segments.map((seg, i) => (
76
+ <div key={i} className="flex items-center gap-1.5">
77
+ <span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: seg.color }} />
78
+ <span className="dash-caption text-fg-64">{seg.label}</span>
79
+ </div>
80
+ ))}
81
+ </div>
82
+ )}
83
+ </div>
84
+ )
85
+ }
86
+
87
+ export default DonutChart
@@ -0,0 +1,79 @@
1
+ import useChartTooltip from '../shared/useChartTooltip'
2
+ import DashTooltip from '../shared/DashTooltip'
3
+
4
+ const Heatmap = ({
5
+ data = [],
6
+ rows = [],
7
+ cols = [],
8
+ colorScale = ['var(--kol-palette-blue)', 'var(--kol-palette-yellow)'],
9
+ cellSize = 32,
10
+ fill = false
11
+ }) => {
12
+ const { activeIndex, handlers, containerHandlers, containerRef } = useChartTooltip()
13
+
14
+ const cells = Array.isArray(data[0])
15
+ ? data.flatMap((row, r) => row.map((value, c) => ({ row: r, col: c, value })))
16
+ : data
17
+
18
+ const maxValue = Math.max(...cells.map(c => c.value), 1)
19
+ const colCount = cols.length || (Array.isArray(data[0]) ? data[0].length : Math.max(...cells.map(c => c.col)) + 1)
20
+
21
+ return (
22
+ <div className={`flex gap-2 min-w-0${fill ? ' h-full' : ''}`} ref={containerRef} {...containerHandlers}>
23
+ {rows.length > 0 && (
24
+ <div
25
+ className="grid shrink-0 items-center"
26
+ style={{ gridTemplateRows: `repeat(${rows.length}, ${fill ? '1fr' : `${cellSize}px`})`, gap: '4px' }}
27
+ >
28
+ {rows.map((label, i) => (
29
+ <span key={i} className="dash-caption text-fg-64 text-right">{label}</span>
30
+ ))}
31
+ </div>
32
+ )}
33
+
34
+ <div className="flex-1 flex flex-col gap-2 min-w-0">
35
+ <div
36
+ className={`grid gap-1${fill ? ' flex-1' : ''}`}
37
+ style={{
38
+ gridTemplateColumns: `repeat(${colCount}, 1fr)`,
39
+ gridAutoRows: fill ? '1fr' : `${cellSize}px`
40
+ }}
41
+ >
42
+ {cells.map((cell, idx) => {
43
+ const pct = Math.round((cell.value / maxValue) * 100)
44
+ return (
45
+ <div
46
+ key={idx}
47
+ className={`dash-heatmap-cell rounded-sm ${activeIndex === idx ? 'dash-chart-element--active' : ''}`}
48
+ style={{
49
+ backgroundColor: `color-mix(in srgb, ${colorScale[1]} ${pct}%, ${colorScale[0]})`
50
+ }}
51
+ {...handlers(idx)}
52
+ />
53
+ )
54
+ })}
55
+ </div>
56
+
57
+ {cols.length > 0 && (
58
+ <div className="flex justify-around">
59
+ {cols.map((label, i) => (
60
+ <span key={i} className="dash-caption text-fg-64 text-center">{label}</span>
61
+ ))}
62
+ </div>
63
+ )}
64
+
65
+ <DashTooltip visible={activeIndex !== null}>
66
+ {activeIndex !== null && cells[activeIndex] && (
67
+ <span className="dash-caption">
68
+ {rows[cells[activeIndex].row] ?? `R${cells[activeIndex].row}`},{' '}
69
+ {cols[cells[activeIndex].col] ?? `C${cells[activeIndex].col}`}:{' '}
70
+ {cells[activeIndex].value}
71
+ </span>
72
+ )}
73
+ </DashTooltip>
74
+ </div>
75
+ </div>
76
+ )
77
+ }
78
+
79
+ export default Heatmap
@@ -0,0 +1,56 @@
1
+ import useChartTooltip from '../shared/useChartTooltip'
2
+ import DashTooltip from '../shared/DashTooltip'
3
+
4
+ const Histogram = ({
5
+ data = [],
6
+ barColor = 'var(--kol-accent-primary)'
7
+ }) => {
8
+ const maxCount = Math.max(...data.map(d => d.count), 1)
9
+ const { activeIndex, handlers, containerHandlers, containerRef } = useChartTooltip()
10
+
11
+ return (
12
+ <div className="flex min-w-0 flex-1 min-h-0">
13
+ <div className="flex flex-col justify-between pr-2 shrink-0">
14
+ <span className="dash-caption text-fg-64 text-right">{maxCount}</span>
15
+ <span className="dash-caption text-fg-64 text-right">{Math.round(maxCount / 2)}</span>
16
+ <span className="dash-caption text-fg-64 text-right">0</span>
17
+ </div>
18
+
19
+ <div className="flex-1 flex flex-col gap-2 min-w-0" ref={containerRef} {...containerHandlers}>
20
+ <div className="relative flex items-end gap-1 flex-1">
21
+ {data.map((bucket, idx) => {
22
+ const pct = Math.round((bucket.count / maxCount) * 100)
23
+ return (
24
+ <div
25
+ key={idx}
26
+ className={`flex-1 flex items-end min-w-0 h-full ${activeIndex === idx ? 'dash-chart-element--active' : ''}`}
27
+ {...handlers(idx)}
28
+ >
29
+ <div
30
+ className="w-full rounded-t-sm"
31
+ style={{ height: `${pct}%`, backgroundColor: barColor }}
32
+ />
33
+ </div>
34
+ )
35
+ })}
36
+
37
+ <DashTooltip visible={activeIndex !== null}>
38
+ {activeIndex !== null && data[activeIndex] && (
39
+ <span className="dash-caption">{data[activeIndex].range}: {data[activeIndex].count}</span>
40
+ )}
41
+ </DashTooltip>
42
+ </div>
43
+
44
+ <div className="flex items-start gap-1">
45
+ {data.map((bucket, idx) => (
46
+ <div key={idx} className="flex-1 flex flex-col items-center min-w-0 overflow-hidden">
47
+ <span className="dash-caption text-fg-64 truncate w-full text-center">{bucket.range}</span>
48
+ </div>
49
+ ))}
50
+ </div>
51
+ </div>
52
+ </div>
53
+ )
54
+ }
55
+
56
+ export default Histogram
@@ -0,0 +1,157 @@
1
+ import { useMemo } from 'react'
2
+ import useChartTooltip from '../shared/useChartTooltip'
3
+ import DashTooltip from '../shared/DashTooltip'
4
+
5
+ const SVG_HEIGHT = 100
6
+
7
+ const normalizeAll = (seriesArr) => {
8
+ const allValues = seriesArr.flatMap(s => s.data.map(d => d.y))
9
+ const min = Math.min(...allValues)
10
+ const max = Math.max(...allValues)
11
+ const range = max - min || 1
12
+
13
+ return seriesArr.map(s => ({
14
+ ...s,
15
+ points: s.data.map((d, i) => ({
16
+ x: (i / Math.max(s.data.length - 1, 1)) * 100,
17
+ y: SVG_HEIGHT - ((d.y - min) / range) * (SVG_HEIGHT * 0.8) - SVG_HEIGHT * 0.1,
18
+ raw: d
19
+ }))
20
+ }))
21
+ }
22
+
23
+ const buildPath = (points) =>
24
+ points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(' ')
25
+
26
+ const buildAreaPath = (linePath) =>
27
+ `${linePath} L 100 ${SVG_HEIGHT} L 0 ${SVG_HEIGHT} Z`
28
+
29
+ const LineChart = ({
30
+ data,
31
+ series,
32
+ height = 240,
33
+ fill = false,
34
+ xLabels,
35
+ yLabels,
36
+ showArea = false,
37
+ showDots = false
38
+ }) => {
39
+ const resolvedSeries = useMemo(() => {
40
+ if (series) return series
41
+ if (data) return [{ data, color: 'var(--kol-accent-primary)' }]
42
+ return []
43
+ }, [data, series])
44
+
45
+ const normalizedSeries = useMemo(() => normalizeAll(resolvedSeries), [resolvedSeries])
46
+
47
+ const { activeIndex, handlers, containerHandlers, containerRef } = useChartTooltip()
48
+
49
+ const gradientIds = normalizedSeries.map((_, i) => `line-area-grad-${i}`)
50
+
51
+ return (
52
+ <div className={`flex min-w-0${fill ? ' h-full' : ''}`}>
53
+ {yLabels && (
54
+ <div className="flex flex-col justify-between pr-2 shrink-0" style={fill ? undefined : { height }}>
55
+ {[...yLabels].reverse().map((label, i) => (
56
+ <span key={i} className="dash-caption text-fg-64 text-right">{label}</span>
57
+ ))}
58
+ </div>
59
+ )}
60
+
61
+ <div className="flex-1 flex flex-col gap-2 min-w-0" ref={containerRef} {...containerHandlers}>
62
+ <div className={`relative${fill ? ' flex-1' : ''}`} style={fill ? undefined : { height }}>
63
+ <svg
64
+ className="absolute inset-0 w-full h-full"
65
+ viewBox={`0 0 100 ${SVG_HEIGHT}`}
66
+ preserveAspectRatio="none"
67
+ >
68
+ <defs>
69
+ {normalizedSeries.map((s, i) => (showArea || s.fill) && (
70
+ <linearGradient key={i} id={gradientIds[i]} x1="0%" y1="0%" x2="0%" y2="100%">
71
+ <stop offset="0%" stopColor={s.color} stopOpacity="0.3" />
72
+ <stop offset="100%" stopColor={s.color} stopOpacity="0" />
73
+ </linearGradient>
74
+ ))}
75
+ </defs>
76
+
77
+ {normalizedSeries.map((s, i) => {
78
+ const path = buildPath(s.points)
79
+ return (
80
+ <g key={i}>
81
+ {(showArea || s.fill) && (
82
+ <path
83
+ className="dash-line-area"
84
+ d={buildAreaPath(path)}
85
+ fill={`url(#${gradientIds[i]})`}
86
+ />
87
+ )}
88
+ <path
89
+ className="dash-line-path"
90
+ d={path}
91
+ fill="none"
92
+ stroke={s.color}
93
+ strokeWidth="2"
94
+ strokeLinejoin="round"
95
+ strokeLinecap="round"
96
+ vectorEffect="non-scaling-stroke"
97
+ />
98
+ </g>
99
+ )
100
+ })}
101
+ </svg>
102
+
103
+ {showDots && normalizedSeries.map((s, si) =>
104
+ s.points.map((p, pi) => (
105
+ <span
106
+ key={`${si}-${pi}`}
107
+ className={`absolute block w-2 h-2 rounded-full ${activeIndex === pi ? 'dash-chart-element--active' : ''}`}
108
+ style={{
109
+ backgroundColor: s.color,
110
+ left: `${p.x}%`,
111
+ top: `${p.y}%`,
112
+ transform: 'translate(-50%, -50%)'
113
+ }}
114
+ {...handlers(pi)}
115
+ />
116
+ ))
117
+ )}
118
+
119
+ {!showDots && normalizedSeries[0]?.points.map((p, pi) => (
120
+ <span
121
+ key={pi}
122
+ className="absolute top-0 bottom-0"
123
+ style={{ left: `${p.x}%`, width: `${100 / Math.max(normalizedSeries[0].points.length - 1, 1)}%`, transform: 'translateX(-50%)' }}
124
+ {...handlers(pi)}
125
+ />
126
+ ))}
127
+
128
+ <DashTooltip visible={activeIndex !== null}>
129
+ {activeIndex !== null && (
130
+ <div className="flex flex-col gap-0.5">
131
+ {normalizedSeries.map((s, si) => {
132
+ const pt = s.points[activeIndex]
133
+ if (!pt) return null
134
+ return (
135
+ <span key={si} className="dash-caption">
136
+ {pt.raw.x != null ? `${pt.raw.x}: ` : ''}{pt.raw.y}
137
+ </span>
138
+ )
139
+ })}
140
+ </div>
141
+ )}
142
+ </DashTooltip>
143
+ </div>
144
+
145
+ {xLabels && (
146
+ <div className="flex justify-between">
147
+ {xLabels.map((label, i) => (
148
+ <span key={i} className="dash-caption text-fg-64">{label}</span>
149
+ ))}
150
+ </div>
151
+ )}
152
+ </div>
153
+ </div>
154
+ )
155
+ }
156
+
157
+ export default LineChart
@@ -0,0 +1,93 @@
1
+ import useChartTooltip from '../shared/useChartTooltip'
2
+ import DashTooltip from '../shared/DashTooltip'
3
+
4
+ const ScatterPlot = ({
5
+ data = [],
6
+ maxX = 200,
7
+ maxY = 3000,
8
+ xLabels = [],
9
+ yLabels = [],
10
+ pointColor = 'var(--kol-palette-blue)',
11
+ height = 320
12
+ }) => {
13
+ const formatY = (v) => v === 0 ? '0' : v >= 1000 ? `${(v / 1000).toFixed(1).replace('.0', '')}K` : v
14
+ const { activeIndex, handlers, containerHandlers, containerRef } = useChartTooltip()
15
+
16
+ const minY = yLabels.length > 0 ? Math.min(...yLabels) : 0
17
+ const rangeY = maxY - minY || 1
18
+ const scaleY = (v) => ((v - minY) / rangeY) * 90 + 5
19
+ const scaleX = (v) => (v / maxX) * 100
20
+
21
+ return (
22
+ <div className="flex">
23
+ <div className="relative pr-2 shrink-0 w-10" style={{ height }}>
24
+ {yLabels.map((yValue) => (
25
+ <span
26
+ key={yValue}
27
+ className="absolute right-2 dash-caption text-fg-64"
28
+ style={{ bottom: `${scaleY(yValue)}%`, transform: 'translateY(50%)' }}
29
+ >
30
+ {formatY(yValue)}
31
+ </span>
32
+ ))}
33
+ </div>
34
+
35
+ <div className="flex-1 flex flex-col min-w-0">
36
+ <div className="relative rounded" style={{ height }} ref={containerRef} {...containerHandlers}>
37
+ {yLabels.map((yValue) => (
38
+ <div
39
+ key={yValue}
40
+ className="absolute left-0 right-0 border-t border-fg-08"
41
+ style={{ bottom: `${scaleY(yValue)}%` }}
42
+ />
43
+ ))}
44
+
45
+ {xLabels.map((xValue) => (
46
+ <div
47
+ key={xValue}
48
+ className="absolute top-0 bottom-0 border-l border-fg-08"
49
+ style={{ left: `${scaleX(xValue)}%` }}
50
+ />
51
+ ))}
52
+
53
+ {data.map(point => (
54
+ <span
55
+ key={point.id}
56
+ className={`absolute block dash-scatter-point ${activeIndex === point.id ? 'dash-chart-element--active' : ''}`}
57
+ style={{
58
+ background: pointColor,
59
+ left: `${scaleX(point.x)}%`,
60
+ bottom: `${scaleY(point.y)}%`,
61
+ transform: 'translate(-50%, 50%)'
62
+ }}
63
+ {...handlers(point.id)}
64
+ />
65
+ ))}
66
+
67
+ <DashTooltip visible={activeIndex !== null}>
68
+ {activeIndex !== null && (() => {
69
+ const point = data.find(p => p.id === activeIndex)
70
+ if (!point) return null
71
+ return (
72
+ <div className="flex flex-col gap-0.5">
73
+ {point.id != null && <span className="dash-caption text-fg-64">{point.id}</span>}
74
+ <span className="dash-caption">x: {point.x}</span>
75
+ <span className="dash-caption">y: {point.y}</span>
76
+ </div>
77
+ )
78
+ })()}
79
+ </DashTooltip>
80
+ </div>
81
+
82
+ <div className="flex justify-between pt-2">
83
+ <span className="dash-caption text-fg-64">0</span>
84
+ {xLabels.map((xValue) => (
85
+ <span key={xValue} className="dash-caption text-fg-64">{xValue}</span>
86
+ ))}
87
+ </div>
88
+ </div>
89
+ </div>
90
+ )
91
+ }
92
+
93
+ export default ScatterPlot
@@ -0,0 +1,63 @@
1
+ import { useMemo } from 'react'
2
+
3
+ const Sparkline = ({
4
+ data = [],
5
+ color = 'var(--kol-surface-on-primary)',
6
+ height = 24,
7
+ fill = false
8
+ }) => {
9
+ const path = useMemo(() => {
10
+ if (data.length < 2) return ''
11
+ const min = Math.min(...data)
12
+ const max = Math.max(...data)
13
+ const range = max - min || 1
14
+
15
+ return data
16
+ .map((v, i) => {
17
+ const x = (i / (data.length - 1)) * 100
18
+ const y = 100 - ((v - min) / range) * 80 - 10
19
+ return `${i === 0 ? 'M' : 'L'} ${x.toFixed(2)} ${y.toFixed(2)}`
20
+ })
21
+ .join(' ')
22
+ }, [data])
23
+
24
+ if (!path) return null
25
+
26
+ const id = useMemo(() => `spark-${Math.random().toString(36).slice(2, 8)}`, [])
27
+
28
+ return (
29
+ <svg
30
+ width="100%"
31
+ height={height}
32
+ viewBox="0 0 100 100"
33
+ preserveAspectRatio="none"
34
+ style={{ display: 'block' }}
35
+ >
36
+ {fill && (
37
+ <>
38
+ <defs>
39
+ <linearGradient id={id} x1="0%" y1="0%" x2="0%" y2="100%">
40
+ <stop offset="0%" stopColor={color} stopOpacity="0.25" />
41
+ <stop offset="100%" stopColor={color} stopOpacity="0" />
42
+ </linearGradient>
43
+ </defs>
44
+ <path
45
+ d={`${path} L 100 100 L 0 100 Z`}
46
+ fill={`url(#${id})`}
47
+ />
48
+ </>
49
+ )}
50
+ <path
51
+ d={path}
52
+ fill="none"
53
+ stroke={color}
54
+ strokeWidth="2"
55
+ strokeLinejoin="round"
56
+ strokeLinecap="round"
57
+ vectorEffect="non-scaling-stroke"
58
+ />
59
+ </svg>
60
+ )
61
+ }
62
+
63
+ export default Sparkline
@@ -0,0 +1,28 @@
1
+ // Cards
2
+ export { default as DashMetricCard } from './cards/DashMetricCard'
3
+ export { default as DashStackedBarCard } from './cards/DashStackedBarCard'
4
+ export { default as DashChartCard } from './cards/DashChartCard'
5
+ export { default as DashListCard } from './cards/DashListCard'
6
+ export { default as DashFeaturedCard } from './cards/DashFeaturedCard'
7
+ export { default as DashAlertCard } from './cards/DashAlertCard'
8
+ export { default as DashSlotCard } from './cards/DashSlotCard'
9
+ export { default as DashTableCard } from './cards/DashTableCard'
10
+
11
+ // Chart visualizations
12
+ export { default as Histogram } from './charts/Histogram'
13
+ export { default as Candlestick } from './charts/Candlestick'
14
+ export { default as ScatterPlot } from './charts/ScatterPlot'
15
+ export { default as LineChart } from './charts/LineChart'
16
+ export { default as DonutChart } from './charts/DonutChart'
17
+ export { default as Sparkline } from './charts/Sparkline'
18
+ export { default as Heatmap } from './charts/Heatmap'
19
+
20
+ // Layout
21
+ export { default as DashboardGrid } from './layout/DashboardGrid'
22
+ export { default as GridCard } from './layout/GridCard'
23
+
24
+ // Shared
25
+ export { default as DashTooltip } from './shared/DashTooltip'
26
+ export { default as useChartTooltip } from './shared/useChartTooltip'
27
+ export { default as useCountUp } from './shared/useCountUp'
28
+ export { default as formatMetric, formatCompact, formatPercent, formatDelta } from './shared/formatMetric'
@@ -0,0 +1,30 @@
1
+ import React from 'react'
2
+
3
+ /**
4
+ * DashboardGrid - Flexible grid system for chess dashboard layouts
5
+ *
6
+ * Layouts:
7
+ * - '4-col': 4 columns × 200px rows (default)
8
+ * - '6-col': 6 columns × 180px rows (dense)
9
+ * - 'board-focus': 3fr/2fr columns × 240px rows (board emphasis)
10
+ * - '3-col': 3 columns × 200px rows (simple)
11
+ *
12
+ * @param {string} layout - Grid configuration preset
13
+ * @param {string} className - Additional CSS classes
14
+ * @param {React.ReactNode} children - GridCard components
15
+ */
16
+ const DashboardGrid = ({
17
+ layout = '4-col',
18
+ className = '',
19
+ children
20
+ }) => {
21
+ return (
22
+ <div style={{ containerType: 'inline-size' }}>
23
+ <div className={`dash-grid ${className}`.trim()}>
24
+ {children}
25
+ </div>
26
+ </div>
27
+ )
28
+ }
29
+
30
+ export default DashboardGrid
@@ -0,0 +1,44 @@
1
+ import React from 'react'
2
+
3
+ /**
4
+ * GridCard - Wrapper for components in the dashboard grid
5
+ *
6
+ * Span configurations:
7
+ * - '1x1': Single cell (default)
8
+ * - '2x1': 2 columns × 1 row
9
+ * - '2x2': 2 columns × 2 rows
10
+ * - '3x2': 3 columns × 2 rows
11
+ * - '4x2': 4 columns × 2 rows (full width, double height)
12
+ * - '1x2': 1 column × 2 rows (tall card)
13
+ * - '3x1': 3 columns × 1 row
14
+ * - '4x1': 4 columns × 1 row (full width strip)
15
+ *
16
+ * @param {string} span - Grid span configuration
17
+ * @param {string} className - Additional CSS classes
18
+ * @param {React.ReactNode} children - Component to display
19
+ * @param {boolean} asCard - Wrap in card styling (for non-component content)
20
+ */
21
+ const GridCard = ({
22
+ span = '1x1',
23
+ className = '',
24
+ children,
25
+ asCard = false
26
+ }) => {
27
+ const [cols, rows] = (span || '1x1').split('x').map(Number)
28
+
29
+ const style = {
30
+ gridColumn: `span ${cols || 1}`,
31
+ gridRow: `span ${rows || 1}`
32
+ }
33
+
34
+ // If asCard, add card styling; otherwise just grid positioning
35
+ const cardClass = asCard ? 'dash-card' : ''
36
+
37
+ return (
38
+ <div data-cols={cols || 1} style={style} className={`h-full ${cardClass} ${className}`.trim()}>
39
+ {children}
40
+ </div>
41
+ )
42
+ }
43
+
44
+ export default GridCard
@@ -0,0 +1,6 @@
1
+ const DashTooltip = ({ visible, children }) => {
2
+ if (!visible) return null
3
+ return <div className="dash-tooltip">{children}</div>
4
+ }
5
+
6
+ export default DashTooltip
@@ -0,0 +1,26 @@
1
+ export const formatCompact = (n) => {
2
+ if (n >= 1000000) {
3
+ return `${(n / 1000000).toFixed(1).replace(/\.0$/, '')}M`
4
+ }
5
+ if (n >= 1000) {
6
+ const formatted = (n / 1000).toFixed(n >= 10000 ? 0 : 1)
7
+ return `${formatted.replace(/\.0$/, '')}K`
8
+ }
9
+ return n.toLocaleString()
10
+ }
11
+
12
+ export const formatPercent = (n) => {
13
+ if (!Number.isFinite(n)) return '0'
14
+ const formatted = n >= 10 ? n.toFixed(0) : n.toFixed(1)
15
+ return `${formatted.replace(/\.0$/, '')}%`
16
+ }
17
+
18
+ export const formatDelta = (n) => {
19
+ if (!Number.isFinite(n)) return '0%'
20
+ const sign = n > 0 ? '+' : ''
21
+ const formatted = Math.abs(n) >= 10 ? n.toFixed(0) : n.toFixed(1)
22
+ return `${sign}${formatted.replace(/\.0$/, '')}%`
23
+ }
24
+
25
+ const formatMetric = { formatCompact, formatPercent, formatDelta }
26
+ export default formatMetric