@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,32 @@
1
+ // @kolkrabbi/kol-component/chess — the chess analysis apparatus.
2
+ // Styling ships in @kolkrabbi/kol-theme (kol-components-chess.css, layer components).
3
+ // Game data is NOT bundled: pass a `chessData` adapter to ChessAnalysisLayout /
4
+ // GameArchiveTable / ChessHero / ChessControlsProvider. Required functions:
5
+ // getSampleGames(), getManifest(), getMonthlySummary(),
6
+ // getRandomMonth(), loadMonthGames(month), getGamePgnByIdAsync(id, month)
7
+
8
+ // Apparatus
9
+ export { default as ChessAnalysisLayout } from './apparatus/ChessAnalysisLayout.jsx'
10
+ export { default as ChessBoard } from './apparatus/ChessBoard.jsx'
11
+ export { default as ChessBoardWithControls } from './apparatus/ChessBoardWithControls.jsx'
12
+ export { default as ChessBoardWithSidebar } from './apparatus/ChessBoardWithSidebar.jsx'
13
+ export { default as ChessBoardFullscreen } from './apparatus/ChessBoardFullscreen.jsx'
14
+ export { default as ChessSidebar } from './apparatus/ChessSidebar.jsx'
15
+ export { default as GameArchiveTable } from './apparatus/GameArchiveTable.jsx'
16
+ export { default as GameSelector } from './apparatus/GameSelector.jsx'
17
+ export { default as NotationPanel } from './apparatus/NotationPanel.jsx'
18
+ export { default as PlaybackControls } from './apparatus/PlaybackControls.jsx'
19
+ export { default as VariationTree } from './apparatus/VariationTree.jsx'
20
+ export { default as AlternativeControlsMock } from './apparatus/AlternativeControlsMock.jsx'
21
+
22
+ // Piece renderer
23
+ export { default as ChessPiece } from './ChessPiece.jsx'
24
+
25
+ // Dashboard
26
+ export { default as ChessHero } from './dashboards/ChessHero.jsx'
27
+
28
+ // State
29
+ export { ChessControlsProvider, useChessControls, createSnapshotsFromPgn } from './context/ChessControlsContext.jsx'
30
+
31
+ // Utils
32
+ export { default as buildMoveTree } from './utils/parsePgnTree.js'
@@ -0,0 +1,97 @@
1
+ const cleanPgn = (pgn = '') => {
2
+ return pgn
3
+ .replace(/\[[^\]]*\]/g, '') // remove headers
4
+ .replace(/\{[^}]*\}/g, '') // remove comments
5
+ .replace(/\$[0-9]+/g, '') // remove NAGs
6
+ .replace(/\n/g, ' ')
7
+ .replace(/\s+/g, ' ')
8
+ .trim()
9
+ }
10
+
11
+ const tokenize = (pgn) => {
12
+ if (!pgn) return []
13
+ const prepared = cleanPgn(pgn)
14
+ .replace(/\(/g, ' ( ')
15
+ .replace(/\)/g, ' ) ')
16
+ return prepared
17
+ .split(/\s+/)
18
+ .filter(Boolean)
19
+ }
20
+
21
+ const isMoveNumber = (token = '') => /^[0-9]+\.{1,3}$/.test(token)
22
+ const isResultToken = (token = '') => ['1-0', '0-1', '1/2-1/2', '*'].includes(token)
23
+
24
+ const createStackEntry = (startPly = 0) => ({
25
+ moves: [],
26
+ lastNode: null,
27
+ currentPly: startPly
28
+ })
29
+
30
+ const pushMove = (entry, san) => {
31
+ entry.currentPly += 1
32
+ const ply = entry.currentPly
33
+ const node = {
34
+ id: `${ply}-${san}`,
35
+ ply,
36
+ san,
37
+ color: ply % 2 === 1 ? 'white' : 'black',
38
+ moveNumber: Math.ceil(ply / 2),
39
+ variations: []
40
+ }
41
+ entry.moves.push(node)
42
+ entry.lastNode = node
43
+ }
44
+
45
+ const attachVariation = (parentEntry, variationMoves) => {
46
+ if (!parentEntry.lastNode) {
47
+ return
48
+ }
49
+ parentEntry.lastNode.variations.push(variationMoves)
50
+ }
51
+
52
+ export const buildMoveTree = (pgn) => {
53
+ const tokens = tokenize(pgn)
54
+ if (!tokens.length) {
55
+ return []
56
+ }
57
+
58
+ const root = createStackEntry(0)
59
+ const stack = [root]
60
+
61
+ tokens.forEach((tokenRaw) => {
62
+ const token = tokenRaw.trim()
63
+ const top = stack[stack.length - 1]
64
+
65
+ if (token === '(') {
66
+ const parent = stack[stack.length - 1]
67
+ if (!parent.lastNode) {
68
+ return
69
+ }
70
+ const variationEntry = createStackEntry(parent.lastNode.ply)
71
+ attachVariation(parent, variationEntry.moves)
72
+ stack.push(variationEntry)
73
+ return
74
+ }
75
+
76
+ if (token === ')') {
77
+ if (stack.length > 1) {
78
+ stack.pop()
79
+ }
80
+ return
81
+ }
82
+
83
+ if (isMoveNumber(token) || isResultToken(token)) {
84
+ return
85
+ }
86
+
87
+ if (!token) {
88
+ return
89
+ }
90
+
91
+ pushMove(top, token)
92
+ })
93
+
94
+ return root.moves
95
+ }
96
+
97
+ export default buildMoveTree
@@ -0,0 +1,44 @@
1
+ import { Badge } from '@kolkrabbi/kol-component'
2
+
3
+ const DashAlertCard = ({
4
+ label,
5
+ value,
6
+ trend = 'up',
7
+ trendValue,
8
+ alerts = [],
9
+ footer,
10
+ className = ''
11
+ }) => {
12
+ const isPositive = trend === 'up'
13
+
14
+ return (
15
+ <div className={`dash-card ${className}`.trim()}>
16
+ <div className="flex items-start justify-between gap-4">
17
+ <div className="flex flex-col gap-1">
18
+ <span className="dash-detail text-fg-64">{label}</span>
19
+ <span className="dash-value">{value}</span>
20
+ </div>
21
+ {trendValue && (
22
+ <Badge icon={isPositive ? 'trending-up' : 'trending-down'}>{trendValue}</Badge>
23
+ )}
24
+ </div>
25
+
26
+ <div className="flex flex-col gap-4 flex-1">
27
+ {alerts.map((alert, idx) => (
28
+ <div key={idx} className="flex flex-col gap-1">
29
+ <span className="dash-subtitle">{alert.title}</span>
30
+ <span className="dash-body text-fg-64">{alert.description}</span>
31
+ </div>
32
+ ))}
33
+ </div>
34
+
35
+ {footer && (
36
+ <div className="dash-card__footer">
37
+ <span className="dash-detail text-fg-64">{footer}</span>
38
+ </div>
39
+ )}
40
+ </div>
41
+ )
42
+ }
43
+
44
+ export default DashAlertCard
@@ -0,0 +1,44 @@
1
+ import { Badge } from '@kolkrabbi/kol-component'
2
+ import CardHeader from './_shared/CardHeader'
3
+ import LegendRow from './_shared/LegendRow'
4
+
5
+ const DashChartCard = ({
6
+ title,
7
+ subtitle,
8
+ icon,
9
+ badge,
10
+ metricLabel,
11
+ currentValue,
12
+ legends = [],
13
+ footer,
14
+ children,
15
+ className = ''
16
+ }) => {
17
+ return (
18
+ <div className={`dash-card ${className}`.trim()}>
19
+ <div className="flex items-start justify-between gap-4">
20
+ <CardHeader icon={icon} title={title} subtitle={subtitle} />
21
+ {(badge || metricLabel || currentValue) && (
22
+ <div className="flex flex-col items-end gap-4">
23
+ {badge && <Badge>{badge}</Badge>}
24
+ {currentValue && <span className="dash-detail text-fg-64">{currentValue}</span>}
25
+ </div>
26
+ )}
27
+ </div>
28
+
29
+ <div className="relative w-full flex-1 min-h-0 flex flex-col">
30
+ {children}
31
+ </div>
32
+
33
+ <LegendRow legends={legends} />
34
+
35
+ {footer && (
36
+ <div className="dash-card__footer">
37
+ <span className="dash-detail text-fg-64">{footer}</span>
38
+ </div>
39
+ )}
40
+ </div>
41
+ )
42
+ }
43
+
44
+ export default DashChartCard
@@ -0,0 +1,42 @@
1
+ import { Badge } from '@kolkrabbi/kol-component'
2
+ import CardHeader from './_shared/CardHeader'
3
+ import LegendRow from './_shared/LegendRow'
4
+
5
+ const DashFeaturedCard = ({
6
+ badge,
7
+ title,
8
+ icon,
9
+ description,
10
+ metricLabel,
11
+ metricValue,
12
+ chart,
13
+ legends = [],
14
+ className = ''
15
+ }) => {
16
+ return (
17
+ <div className={`dash-card ${className}`.trim()}>
18
+ <div className="flex items-start justify-between">
19
+ <div className="flex flex-col gap-2">
20
+ {badge && <Badge className="self-start">{badge}</Badge>}
21
+ <CardHeader icon={icon} title={title} subtitle={description} />
22
+ </div>
23
+ {metricValue && (
24
+ <div className="text-right">
25
+ <span className="dash-detail text-fg-64">{metricLabel}</span>
26
+ <p className="dash-value">{metricValue}</p>
27
+ </div>
28
+ )}
29
+ </div>
30
+
31
+ {chart && (
32
+ <div className="flex-1 flex flex-col gap-3">
33
+ {chart}
34
+ </div>
35
+ )}
36
+
37
+ <LegendRow legends={legends} wrap />
38
+ </div>
39
+ )
40
+ }
41
+
42
+ export default DashFeaturedCard
@@ -0,0 +1,87 @@
1
+ import { Badge } from '@kolkrabbi/kol-component'
2
+ import CardHeader from './_shared/CardHeader'
3
+
4
+ const DashListCard = ({
5
+ title,
6
+ subtitle,
7
+ icon,
8
+ badge,
9
+ items = [],
10
+ variant = 'text',
11
+ barColor = 'var(--kol-palette-yellow)',
12
+ footer,
13
+ className = ''
14
+ }) => {
15
+ return (
16
+ <div className={`dash-card ${className}`.trim()}>
17
+ <div className="flex justify-between items-start">
18
+ <CardHeader icon={icon} title={title} subtitle={subtitle} />
19
+ {badge && <Badge>{badge}</Badge>}
20
+ </div>
21
+
22
+ <div className="flex flex-col gap-3">
23
+ {items.map((item, idx) => (
24
+ <div key={idx} className="dash-list-item">
25
+ <ListItem item={item} variant={variant} barColor={barColor} />
26
+ </div>
27
+ ))}
28
+ </div>
29
+
30
+ {footer && (
31
+ <div className="dash-card__footer">
32
+ <span className="dash-detail text-fg-64">{footer}</span>
33
+ </div>
34
+ )}
35
+ </div>
36
+ )
37
+ }
38
+
39
+ const ListItem = ({ item, variant, barColor }) => {
40
+ if (variant === 'meter') {
41
+ return (
42
+ <div className="flex flex-col gap-1">
43
+ <div className="flex items-center justify-between">
44
+ <span className="dash-body text-fg-80">{item.label}</span>
45
+ <span className="dash-body text-fg-88">{item.value}</span>
46
+ </div>
47
+ <div className="h-2 bg-fg-08 rounded-full overflow-hidden">
48
+ <div
49
+ className="h-full rounded-full"
50
+ style={{ width: `${item.percent}%`, background: item.color || barColor }}
51
+ />
52
+ </div>
53
+ {item.detail && (
54
+ <span className="dash-list-item__detail dash-caption text-fg-64">{item.detail}</span>
55
+ )}
56
+ </div>
57
+ )
58
+ }
59
+
60
+ if (variant === 'ratings') {
61
+ return (
62
+ <div className="flex items-center justify-between">
63
+ <div className="flex items-center gap-3">
64
+ {item.color && <div className="w-3 h-3 rounded-full shrink-0" style={{ backgroundColor: item.color }} />}
65
+ <span className="dash-body text-fg-80">{item.label}</span>
66
+ </div>
67
+ <div className="flex items-baseline gap-2">
68
+ <span className="dash-value">{item.value}</span>
69
+ {item.detail && <span className="dash-detail text-fg-64">{item.detail}</span>}
70
+ </div>
71
+ </div>
72
+ )
73
+ }
74
+
75
+ // variant === 'text' (default)
76
+ return (
77
+ <div className="flex justify-between items-center">
78
+ <span className="dash-body text-fg-80">{item.label}</span>
79
+ <span className="dash-body text-fg-88">{item.value}</span>
80
+ {item.detail && (
81
+ <span className="dash-list-item__detail dash-caption text-fg-64">{item.detail}</span>
82
+ )}
83
+ </div>
84
+ )
85
+ }
86
+
87
+ export default DashListCard
@@ -0,0 +1,59 @@
1
+ import { useState } from 'react'
2
+ import useCountUp from '../shared/useCountUp'
3
+
4
+ const DashMetricCard = ({
5
+ label,
6
+ value,
7
+ delta,
8
+ borderColor,
9
+ metrics,
10
+ sparkline,
11
+ className = ''
12
+ }) => {
13
+ const [activeIdx, setActiveIdx] = useState(0)
14
+ const [fading, setFading] = useState(false)
15
+
16
+ const isInteractive = Array.isArray(metrics) && metrics.length > 1
17
+ const current = isInteractive ? metrics[activeIdx] : { label, value, delta, borderColor }
18
+
19
+ const borderStyle = current.borderColor
20
+ ? { borderLeftWidth: '3px', borderLeftColor: current.borderColor }
21
+ : undefined
22
+
23
+ const { displayValue, ref: countRef } = useCountUp(current.value, {
24
+ enabled: current.value != null
25
+ })
26
+
27
+ const handleClick = () => {
28
+ if (!isInteractive) return
29
+ setFading(true)
30
+ setTimeout(() => {
31
+ setActiveIdx((activeIdx + 1) % metrics.length)
32
+ setFading(false)
33
+ }, 150)
34
+ }
35
+
36
+ return (
37
+ <div
38
+ className={`dash-card ${isInteractive ? 'dash-metric--interactive' : ''} ${className}`.trim()}
39
+ style={borderStyle}
40
+ onClick={handleClick}
41
+ >
42
+ <span className="dash-detail text-fg-64">{current.label}</span>
43
+ <span className={`dash-value dash-metric-value ${fading ? 'dash-metric-value--fading' : ''}`} ref={countRef}>
44
+ {displayValue}
45
+ </span>
46
+ {current.delta && <span className="dash-body text-fg-80">{current.delta}</span>}
47
+ {sparkline && <div className="mt-auto">{sparkline}</div>}
48
+ {isInteractive && (
49
+ <div className="dash-metric-dots">
50
+ {metrics.map((_, idx) => (
51
+ <span key={idx} className={`dash-metric-dot ${idx === activeIdx ? 'dash-metric-dot--active' : ''}`} />
52
+ ))}
53
+ </div>
54
+ )}
55
+ </div>
56
+ )
57
+ }
58
+
59
+ export default DashMetricCard
@@ -0,0 +1,45 @@
1
+ import CardHeader from './_shared/CardHeader'
2
+
3
+ const DashSlotCard = ({
4
+ title,
5
+ subtitle,
6
+ icon,
7
+ chart,
8
+ items = [],
9
+ footer,
10
+ className = ''
11
+ }) => {
12
+ return (
13
+ <div className={`dash-card ${className}`.trim()}>
14
+ <CardHeader icon={icon} title={title} subtitle={subtitle} />
15
+
16
+ {chart && (
17
+ <div className="relative w-full flex-1 min-h-0 overflow-hidden">
18
+ {chart}
19
+ </div>
20
+ )}
21
+
22
+ {items.length > 0 && (
23
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
24
+ {items.map((item, idx) => (
25
+ <div key={idx} className="flex flex-col gap-1">
26
+ <span className="dash-caption text-fg-80">{item.label}</span>
27
+ <span className="dash-caption text-fg-88">{item.value}</span>
28
+ </div>
29
+ ))}
30
+ </div>
31
+ )}
32
+
33
+ {footer && (
34
+ <div className="dash-card__footer">
35
+ <div className="flex justify-between items-center">
36
+ <span className="dash-detail text-fg-80">{footer.label}</span>
37
+ <span className="dash-detail text-fg-88">{footer.value}</span>
38
+ </div>
39
+ </div>
40
+ )}
41
+ </div>
42
+ )
43
+ }
44
+
45
+ export default DashSlotCard
@@ -0,0 +1,70 @@
1
+ import { Icon } from '@kolkrabbi/kol-icons'
2
+
3
+ const DashStackedBarCard = ({
4
+ title,
5
+ value,
6
+ data = [],
7
+ icon,
8
+ label,
9
+ trend,
10
+ footerLeft,
11
+ footerRight,
12
+ className = ''
13
+ }) => {
14
+ const compact = !!icon
15
+
16
+ return (
17
+ <div className={`dash-card ${className}`.trim()}>
18
+ {compact ? (
19
+ <>
20
+ <div className="flex justify-between items-center">
21
+ <span className="dash-detail text-fg-88">{title}</span>
22
+ <Icon name={icon} size={24} className="text-fg-88" />
23
+ </div>
24
+ <div className="flex items-end gap-3">
25
+ <span className="dash-subtitle">{value}</span>
26
+ {label && <span className="dash-detail text-fg-64 mb-1">{label}</span>}
27
+ {trend && (
28
+ <Icon
29
+ name={trend === 'up' ? 'trending-up' : 'trending-down'}
30
+ size={16}
31
+ className="text-fg-64 mb-1"
32
+ />
33
+ )}
34
+ </div>
35
+ </>
36
+ ) : (
37
+ <>
38
+ <span className="dash-detail text-fg-88">{title}</span>
39
+ <span className="dash-subtitle">{value}</span>
40
+ </>
41
+ )}
42
+
43
+ <div className="flex-1 flex items-stretch gap-1 min-h-[80px]">
44
+ {data.map((item, i) => {
45
+ const winRate = item.total > 0 ? (item.win / item.total) * 100 : 0
46
+ const drawRate = item.total > 0 ? (item.draw / item.total) * 100 : 0
47
+ const lossRate = item.total > 0 ? (item.loss / item.total) * 100 : 0
48
+
49
+ return (
50
+ <div key={i} className="flex-1 flex flex-col gap-0.5">
51
+ <div className="flex-1" />
52
+ <div className="rounded-sm" style={{ flex: `${winRate} 0 0`, background: 'var(--kol-palette-green)' }} />
53
+ <div className="rounded-sm" style={{ flex: `${drawRate} 0 0`, background: 'var(--kol-palette-blue)' }} />
54
+ <div className="rounded-sm" style={{ flex: `${lossRate} 0 0`, background: 'var(--kol-palette-red)' }} />
55
+ </div>
56
+ )
57
+ })}
58
+ </div>
59
+
60
+ {(footerLeft || footerRight) && (
61
+ <div className="flex justify-between items-center">
62
+ {footerLeft && <span className="dash-detail text-fg-64">{footerLeft}</span>}
63
+ {footerRight && <span className="dash-detail text-fg-64">{footerRight}</span>}
64
+ </div>
65
+ )}
66
+ </div>
67
+ )
68
+ }
69
+
70
+ export default DashStackedBarCard
@@ -0,0 +1,31 @@
1
+ import CardHeader from './_shared/CardHeader'
2
+ import { Table } from '@kolkrabbi/kol-component'
3
+
4
+ const DashTableCard = ({
5
+ title,
6
+ subtitle,
7
+ icon,
8
+ badge,
9
+ columns,
10
+ rows,
11
+ footer,
12
+ className = ''
13
+ }) => {
14
+ return (
15
+ <div className={`dash-card ${className}`.trim()}>
16
+ <CardHeader icon={icon} title={title} subtitle={subtitle} />
17
+
18
+ <div className="flex-1 min-h-0">
19
+ <Table columns={columns} rows={rows} />
20
+ </div>
21
+
22
+ {footer && (
23
+ <div className="dash-card__footer">
24
+ <span className="dash-detail text-fg-64">{footer}</span>
25
+ </div>
26
+ )}
27
+ </div>
28
+ )
29
+ }
30
+
31
+ export default DashTableCard
@@ -0,0 +1,15 @@
1
+ import { Icon } from '@kolkrabbi/kol-icons'
2
+
3
+ const CardHeader = ({ icon, title, subtitle }) => {
4
+ return (
5
+ <div className="flex flex-col gap-2">
6
+ <div className="flex items-center gap-2">
7
+ {icon && <Icon name={icon} size={24} className="text-fg-88" />}
8
+ <span className="dash-title">{title}</span>
9
+ </div>
10
+ {subtitle && <span className="dash-detail text-fg-64">{subtitle}</span>}
11
+ </div>
12
+ )
13
+ }
14
+
15
+ export default CardHeader
@@ -0,0 +1,25 @@
1
+ const LegendRow = ({ legends = [], wrap = false }) => {
2
+ if (legends.length === 0) return null
3
+
4
+ return (
5
+ <div className={`flex items-center gap-3 ${wrap ? 'flex-wrap' : ''}`.trim()}>
6
+ {legends.map((legend, idx) => (
7
+ <div key={idx} className="flex items-center gap-2">
8
+ <span
9
+ className="w-2 h-2 rounded-full shrink-0"
10
+ style={{ backgroundColor: legend.color || (idx === 0 ? 'var(--kol-palette-yellow)' : 'var(--kol-palette-purple)') }}
11
+ aria-hidden="true"
12
+ />
13
+ <div className="flex flex-col">
14
+ <span className="dash-caption text-fg-88">{legend.label}</span>
15
+ {legend.detail && (
16
+ <span className="dash-detail text-fg-64">{legend.detail}</span>
17
+ )}
18
+ </div>
19
+ </div>
20
+ ))}
21
+ </div>
22
+ )
23
+ }
24
+
25
+ export default LegendRow
@@ -0,0 +1,79 @@
1
+ import useChartTooltip from '../shared/useChartTooltip'
2
+ import DashTooltip from '../shared/DashTooltip'
3
+
4
+ const candlestickColors = {
5
+ accent: 'var(--kol-palette-yellow)',
6
+ neutral: 'var(--kol-palette-purple)'
7
+ }
8
+
9
+ const Candlestick = ({
10
+ data = [],
11
+ height = 288
12
+ }) => {
13
+ const allValues = data.flatMap(d => [d.high, d.low, d.open, d.close])
14
+ const minValue = Math.min(...allValues)
15
+ const maxValue = Math.max(...allValues)
16
+ const range = maxValue - minValue || 1
17
+ const { activeIndex, handlers, containerHandlers, containerRef } = useChartTooltip()
18
+
19
+ const scale = (value) => ((value - minValue) / range) * 80 + 10
20
+
21
+ return (
22
+ <div className="relative w-full rounded" style={{ height }} ref={containerRef} {...containerHandlers}>
23
+ {[20, 40, 60, 80].map((y) => (
24
+ <div
25
+ key={y}
26
+ className="absolute left-0 right-0 border-t border-fg-08"
27
+ style={{ top: `${100 - y}%` }}
28
+ />
29
+ ))}
30
+
31
+ <div className="absolute inset-0 flex items-end gap-3">
32
+ {data.map((item, idx) => {
33
+ const color = candlestickColors[item.variant] || candlestickColors.accent
34
+ const scaledHigh = scale(item.high)
35
+ const scaledLow = scale(item.low)
36
+ const scaledOpen = scale(item.open)
37
+ const scaledClose = scale(item.close)
38
+ const wickHeight = Math.max(scaledHigh - scaledLow, 1)
39
+ const bodyHeight = Math.max(Math.abs(scaledClose - scaledOpen), 3)
40
+ const bodyBottom = Math.min(scaledOpen, scaledClose)
41
+
42
+ return (
43
+ <div
44
+ key={idx}
45
+ className={`relative flex-1 h-full ${activeIndex === idx ? 'dash-chart-element--active' : ''}`}
46
+ {...handlers(idx)}
47
+ >
48
+ <span
49
+ className="absolute left-1/2 -translate-x-1/2 w-[1.5px]"
50
+ style={{ height: `${wickHeight}%`, bottom: `${scaledLow}%`, background: color }}
51
+ />
52
+ <span
53
+ className="absolute left-1/2 -translate-x-1/2 w-2 rounded-sm"
54
+ style={{ height: `${bodyHeight}%`, bottom: `${bodyBottom}%`, background: color }}
55
+ />
56
+ </div>
57
+ )
58
+ })}
59
+ </div>
60
+
61
+ <DashTooltip visible={activeIndex !== null}>
62
+ {activeIndex !== null && data[activeIndex] && (
63
+ <div className="grid grid-cols-2 gap-x-3 gap-y-0.5">
64
+ <span className="dash-caption text-fg-64">O</span>
65
+ <span className="dash-caption">{data[activeIndex].open}</span>
66
+ <span className="dash-caption text-fg-64">H</span>
67
+ <span className="dash-caption">{data[activeIndex].high}</span>
68
+ <span className="dash-caption text-fg-64">L</span>
69
+ <span className="dash-caption">{data[activeIndex].low}</span>
70
+ <span className="dash-caption text-fg-64">C</span>
71
+ <span className="dash-caption">{data[activeIndex].close}</span>
72
+ </div>
73
+ )}
74
+ </DashTooltip>
75
+ </div>
76
+ )
77
+ }
78
+
79
+ export default Candlestick