@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.
- package/package.json +4 -2
- package/src/chess/ChessPiece.jsx +191 -0
- package/src/chess/apparatus/AlternativeControlsMock.jsx +460 -0
- package/src/chess/apparatus/ChessAnalysisLayout.jsx +24 -0
- package/src/chess/apparatus/ChessBoard.jsx +210 -0
- package/src/chess/apparatus/ChessBoardFullscreen.jsx +49 -0
- package/src/chess/apparatus/ChessBoardWithControls.jsx +41 -0
- package/src/chess/apparatus/ChessBoardWithSidebar.jsx +122 -0
- package/src/chess/apparatus/ChessSidebar.jsx +131 -0
- package/src/chess/apparatus/GameArchiveTable.jsx +493 -0
- package/src/chess/apparatus/GameSelector.jsx +47 -0
- package/src/chess/apparatus/NotationPanel.jsx +70 -0
- package/src/chess/apparatus/PlaybackControls.jsx +52 -0
- package/src/chess/apparatus/VariationTree.jsx +137 -0
- package/src/chess/assets/chess-extra-set/set-1/bishop-1.svg +6 -0
- package/src/chess/assets/chess-extra-set/set-1/bishop.svg +6 -0
- package/src/chess/assets/chess-extra-set/set-1/king-1.svg +9 -0
- package/src/chess/assets/chess-extra-set/set-1/king.svg +7 -0
- package/src/chess/assets/chess-extra-set/set-1/knight-1.svg +8 -0
- package/src/chess/assets/chess-extra-set/set-1/knight.svg +7 -0
- package/src/chess/assets/chess-extra-set/set-1/pawn-1.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-1/pawn.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-1/queen-1.svg +11 -0
- package/src/chess/assets/chess-extra-set/set-1/queen.svg +6 -0
- package/src/chess/assets/chess-extra-set/set-1/rook-1.svg +7 -0
- package/src/chess/assets/chess-extra-set/set-1/rook.svg +11 -0
- package/src/chess/assets/chess-extra-set/set-2/bishop-1.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/bishop.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/king-1.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/king.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/knight-1.svg +5 -0
- package/src/chess/assets/chess-extra-set/set-2/knight.svg +5 -0
- package/src/chess/assets/chess-extra-set/set-2/pawn-1.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/pawn.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/queen-1.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/queen.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/rook-1.svg +5 -0
- package/src/chess/assets/chess-extra-set/set-2/rook.svg +5 -0
- package/src/chess/assets/chess-extra-set/set-3/bishop-1.svg +10 -0
- package/src/chess/assets/chess-extra-set/set-3/bishop.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-3/king-1.svg +12 -0
- package/src/chess/assets/chess-extra-set/set-3/king.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-3/knight-1.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-3/knight.svg +10 -0
- package/src/chess/assets/chess-extra-set/set-3/pawn-1.svg +10 -0
- package/src/chess/assets/chess-extra-set/set-3/pawn.svg +10 -0
- package/src/chess/assets/chess-extra-set/set-3/queen-1.svg +10 -0
- package/src/chess/assets/chess-extra-set/set-3/queen.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-3/rook-1.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-3/rook.svg +11 -0
- package/src/chess/assets/chess-vector-set/ABoard.svg +164 -0
- package/src/chess/assets/chess-vector-set/BBiskup.svg +12 -0
- package/src/chess/assets/chess-vector-set/BKing.svg +15 -0
- package/src/chess/assets/chess-vector-set/BKnight.svg +13 -0
- package/src/chess/assets/chess-vector-set/BPawn.svg +7 -0
- package/src/chess/assets/chess-vector-set/BQueen.svg +6 -0
- package/src/chess/assets/chess-vector-set/BRook.svg +12 -0
- package/src/chess/assets/chess-vector-set/WBiskup.svg +14 -0
- package/src/chess/assets/chess-vector-set/WKing.svg +13 -0
- package/src/chess/assets/chess-vector-set/WKnight.svg +10 -0
- package/src/chess/assets/chess-vector-set/WPawn.svg +8 -0
- package/src/chess/assets/chess-vector-set/WQueen.svg +17 -0
- package/src/chess/assets/chess-vector-set/WRook.svg +13 -0
- package/src/chess/context/ChessControlsContext.jsx +379 -0
- package/src/chess/dashboards/ChessHero.jsx +303 -0
- package/src/chess/index.js +32 -0
- package/src/chess/utils/parsePgnTree.js +97 -0
- package/src/dashboards/cards/DashAlertCard.jsx +44 -0
- package/src/dashboards/cards/DashChartCard.jsx +44 -0
- package/src/dashboards/cards/DashFeaturedCard.jsx +42 -0
- package/src/dashboards/cards/DashListCard.jsx +87 -0
- package/src/dashboards/cards/DashMetricCard.jsx +59 -0
- package/src/dashboards/cards/DashSlotCard.jsx +45 -0
- package/src/dashboards/cards/DashStackedBarCard.jsx +70 -0
- package/src/dashboards/cards/DashTableCard.jsx +31 -0
- package/src/dashboards/cards/_shared/CardHeader.jsx +15 -0
- package/src/dashboards/cards/_shared/LegendRow.jsx +25 -0
- package/src/dashboards/charts/Candlestick.jsx +79 -0
- package/src/dashboards/charts/DonutChart.jsx +87 -0
- package/src/dashboards/charts/Heatmap.jsx +79 -0
- package/src/dashboards/charts/Histogram.jsx +56 -0
- package/src/dashboards/charts/LineChart.jsx +157 -0
- package/src/dashboards/charts/ScatterPlot.jsx +93 -0
- package/src/dashboards/charts/Sparkline.jsx +63 -0
- package/src/dashboards/index.js +28 -0
- package/src/dashboards/layout/DashboardGrid.jsx +30 -0
- package/src/dashboards/layout/GridCard.jsx +44 -0
- package/src/dashboards/shared/DashTooltip.jsx +6 -0
- package/src/dashboards/shared/formatMetric.js +26 -0
- package/src/dashboards/shared/useChartTooltip.js +25 -0
- 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
|