atomsystem 0.4.3 → 0.5.1
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/README.md +112 -52
- package/components/blocks/BlockDetailPageContent.tsx +337 -0
- package/components/blocks/BlockViewerCard.tsx +123 -123
- package/components/blocks/BlocksSidebar.tsx +93 -0
- package/components/blocks/CardListBlock.tsx +101 -196
- package/components/blocks/FileExplorerBlock.tsx +114 -446
- package/components/blocks/InteractiveChatBlock.tsx +107 -123
- package/components/blocks/MetricStatsBlock.tsx +78 -106
- package/components/blocks/PricingPlanBlock.tsx +88 -144
- package/components/blocks/RealtimeChartBlock.tsx +131 -250
- package/components/blocks/SliderShowcaseBlock.tsx +105 -305
- package/components/ui/Container.tsx +45 -0
- package/components/ui/Layout.tsx +220 -0
- package/components/ui/Switch.tsx +39 -0
- package/components/ui/Table.tsx +88 -0
- package/components/ui/Tabs.tsx +58 -0
- package/components/ui/index.ts +5 -0
- package/css/atom-ui.css +41 -816
- package/dist/cli.cjs +633 -41
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +630 -39
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +176 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -4
- package/dist/index.d.ts +13 -4
- package/dist/index.js +176 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/presets/agents.guide.md +49 -0
- package/presets/blocks.md +101 -0
- package/presets/components.md +142 -0
- package/tokens/token.json +63 -1
- package/tokens/tokens.json +63 -1
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useState, useEffect
|
|
4
|
-
import {
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { Card, Button, Badge } from '@/components/ui';
|
|
5
|
+
import {
|
|
6
|
+
Play,
|
|
7
|
+
Pause,
|
|
8
|
+
TrendingUp,
|
|
9
|
+
TrendingDown,
|
|
10
|
+
Activity,
|
|
11
|
+
} from 'lucide-react';
|
|
5
12
|
|
|
6
13
|
interface ChartPoint {
|
|
7
14
|
time: string;
|
|
@@ -30,11 +37,7 @@ export function RealtimeChartBlock() {
|
|
|
30
37
|
const [chartType, setChartType] = useState<'line' | 'candle'>('line');
|
|
31
38
|
const [timeframe, setTimeframe] = useState<'1m' | '5m' | '1h' | '1d'>('1m');
|
|
32
39
|
const [hoveredPoint, setHoveredPoint] = useState<ChartPoint | null>(null);
|
|
33
|
-
const [tickFlash, setTickFlash] = useState<'up' | 'down' | null>(null);
|
|
34
40
|
|
|
35
|
-
const gradientId = useId();
|
|
36
|
-
|
|
37
|
-
// 실시간 틱 데이터 시뮬레이션
|
|
38
41
|
useEffect(() => {
|
|
39
42
|
if (!isPlaying) return;
|
|
40
43
|
const interval = setInterval(() => {
|
|
@@ -46,21 +49,13 @@ export function RealtimeChartBlock() {
|
|
|
46
49
|
const newLow = Math.min(newPrice, last.low);
|
|
47
50
|
const newVolume = Math.round(2000 + Math.random() * 4000);
|
|
48
51
|
|
|
49
|
-
setTickFlash(delta >= 0 ? 'up' : 'down');
|
|
50
|
-
setTimeout(() => setTickFlash(null), 400);
|
|
51
|
-
|
|
52
52
|
const now = new Date();
|
|
53
53
|
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
price: newPrice,
|
|
58
|
-
|
|
59
|
-
low: newLow,
|
|
60
|
-
volume: newVolume,
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
return [...prev.slice(1), newPoint];
|
|
55
|
+
return [
|
|
56
|
+
...prev.slice(1),
|
|
57
|
+
{ time: timeStr, price: newPrice, high: newHigh, low: newLow, volume: newVolume },
|
|
58
|
+
];
|
|
64
59
|
});
|
|
65
60
|
}, 1200);
|
|
66
61
|
|
|
@@ -73,14 +68,13 @@ export function RealtimeChartBlock() {
|
|
|
73
68
|
const pctDiff = ((priceDiff / firstPoint.price) * 100).toFixed(2);
|
|
74
69
|
const isPositive = priceDiff >= 0;
|
|
75
70
|
|
|
76
|
-
// 차트 좌표 계산
|
|
77
71
|
const prices = data.map((d) => d.price);
|
|
78
72
|
const minPrice = Math.min(...prices) * 0.998;
|
|
79
73
|
const maxPrice = Math.max(...prices) * 1.002;
|
|
80
74
|
const range = maxPrice - minPrice || 1;
|
|
81
75
|
|
|
82
76
|
const svgWidth = 600;
|
|
83
|
-
const svgHeight =
|
|
77
|
+
const svgHeight = 220;
|
|
84
78
|
const paddingX = 20;
|
|
85
79
|
const paddingY = 20;
|
|
86
80
|
|
|
@@ -88,274 +82,161 @@ export function RealtimeChartBlock() {
|
|
|
88
82
|
const getY = (price: number) => svgHeight - paddingY - ((price - minPrice) / range) * (svgHeight - paddingY * 2);
|
|
89
83
|
|
|
90
84
|
const pointsPath = data.map((d, i) => `${i === 0 ? 'M' : 'L'} ${getX(i)} ${getY(d.price)}`).join(' ');
|
|
91
|
-
const areaPath = `${pointsPath} L ${getX(data.length - 1)} ${svgHeight} L ${getX(0)} ${svgHeight} Z`;
|
|
92
85
|
|
|
93
86
|
return (
|
|
94
|
-
<
|
|
95
|
-
{/*
|
|
96
|
-
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-
|
|
87
|
+
<Card className="overflow-hidden bg-(--bg-primary) border border-(--border-subtle) transition-colors">
|
|
88
|
+
{/* 1. Header: Minimalist Price & Controls */}
|
|
89
|
+
<div className="p-4 sm:p-5 border-b border-(--border-subtle) flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
97
90
|
<div>
|
|
98
|
-
<div className="flex items-center gap-2 mb-
|
|
99
|
-
<span className="
|
|
100
|
-
|
|
101
|
-
|
|
91
|
+
<div className="flex items-center gap-2 mb-0.5">
|
|
92
|
+
<span className="font-mono text-xs font-semibold text-(--text-secondary)">KRW-ATOM</span>
|
|
93
|
+
<span className={`text-xs font-mono font-medium ${isPositive ? 'text-emerald-600 dark:text-emerald-400' : 'text-rose-600 dark:text-rose-400'}`}>
|
|
94
|
+
{isPositive ? '+' : ''}{pctDiff}%
|
|
102
95
|
</span>
|
|
103
|
-
<span className="text-xs font-medium text-(--text-muted)">KRW-ATOM / 실시간 지수</span>
|
|
104
96
|
</div>
|
|
105
|
-
<div className="
|
|
106
|
-
|
|
107
|
-
className={`text-2xl sm:text-3xl font-extrabold tracking-tight transition-colors duration-300 ${
|
|
108
|
-
tickFlash === 'up'
|
|
109
|
-
? 'text-emerald-500'
|
|
110
|
-
: tickFlash === 'down'
|
|
111
|
-
? 'text-rose-500'
|
|
112
|
-
: 'text-(--text-primary)'
|
|
113
|
-
}`}
|
|
114
|
-
>
|
|
115
|
-
₩{currentPoint.price.toLocaleString()}
|
|
116
|
-
</h2>
|
|
117
|
-
<div
|
|
118
|
-
className={`inline-flex items-center gap-1 text-xs sm:text-sm font-semibold px-2 py-0.5 rounded-md ${
|
|
119
|
-
isPositive
|
|
120
|
-
? 'text-emerald-600 bg-emerald-500/10 dark:text-emerald-400'
|
|
121
|
-
: 'text-rose-600 bg-rose-500/10 dark:text-rose-400'
|
|
122
|
-
}`}
|
|
123
|
-
>
|
|
124
|
-
{isPositive ? <TrendingUp className="w-3.5 h-3.5" /> : <TrendingDown className="w-3.5 h-3.5" />}
|
|
125
|
-
<span>{isPositive ? '+' : ''}{priceDiff.toLocaleString()}원 ({isPositive ? '+' : ''}{pctDiff}%)</span>
|
|
126
|
-
</div>
|
|
97
|
+
<div className="text-xl font-bold tracking-tight text-(--text-primary)">
|
|
98
|
+
₩{currentPoint.price.toLocaleString()}
|
|
127
99
|
</div>
|
|
128
100
|
</div>
|
|
129
101
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<div className="flex bg-(--bg-secondary) p-0.5 rounded-lg border border-(--border-subtle) text-xs">
|
|
102
|
+
<div className="flex items-center gap-2">
|
|
103
|
+
{/* Timeframe Button Group */}
|
|
104
|
+
<div className="flex rounded-(--radius-sm) border border-(--border-subtle) p-0.5 text-xs bg-(--bg-secondary)">
|
|
134
105
|
{(['1m', '5m', '1h', '1d'] as const).map((tf) => (
|
|
135
|
-
<
|
|
106
|
+
<Button
|
|
136
107
|
key={tf}
|
|
108
|
+
variant={timeframe === tf ? 'primary' : 'ghost'}
|
|
109
|
+
size="sm"
|
|
137
110
|
onClick={() => setTimeframe(tf)}
|
|
138
|
-
className=
|
|
139
|
-
timeframe === tf
|
|
140
|
-
? 'bg-(--bg-primary) text-(--text-primary) shadow-xs font-semibold'
|
|
141
|
-
: 'text-(--text-secondary) hover:text-(--text-primary)'
|
|
142
|
-
}`}
|
|
111
|
+
className="px-2 py-0.5 text-xs h-auto"
|
|
143
112
|
>
|
|
144
113
|
{tf}
|
|
145
|
-
</
|
|
114
|
+
</Button>
|
|
146
115
|
))}
|
|
147
116
|
</div>
|
|
148
117
|
|
|
149
|
-
{/*
|
|
150
|
-
<div className="flex
|
|
151
|
-
<
|
|
118
|
+
{/* Type Button Group */}
|
|
119
|
+
<div className="flex rounded-(--radius-sm) border border-(--border-subtle) p-0.5 text-xs bg-(--bg-secondary)">
|
|
120
|
+
<Button
|
|
121
|
+
variant={chartType === 'line' ? 'primary' : 'ghost'}
|
|
122
|
+
size="sm"
|
|
152
123
|
onClick={() => setChartType('line')}
|
|
153
|
-
|
|
154
|
-
className={`px-2.5 py-1 rounded-md font-medium transition-colors ${
|
|
155
|
-
chartType === 'line'
|
|
156
|
-
? 'bg-(--bg-primary) text-(--text-primary) shadow-xs font-semibold'
|
|
157
|
-
: 'text-(--text-secondary) hover:text-(--text-primary)'
|
|
158
|
-
}`}
|
|
124
|
+
className="px-2 py-0.5 text-xs h-auto"
|
|
159
125
|
>
|
|
160
|
-
|
|
161
|
-
</
|
|
162
|
-
<
|
|
126
|
+
Line
|
|
127
|
+
</Button>
|
|
128
|
+
<Button
|
|
129
|
+
variant={chartType === 'candle' ? 'primary' : 'ghost'}
|
|
130
|
+
size="sm"
|
|
163
131
|
onClick={() => setChartType('candle')}
|
|
164
|
-
|
|
165
|
-
className={`px-2.5 py-1 rounded-md font-medium transition-colors ${
|
|
166
|
-
chartType === 'candle'
|
|
167
|
-
? 'bg-(--bg-primary) text-(--text-primary) shadow-xs font-semibold'
|
|
168
|
-
: 'text-(--text-secondary) hover:text-(--text-primary)'
|
|
169
|
-
}`}
|
|
132
|
+
className="px-2 py-0.5 text-xs h-auto"
|
|
170
133
|
>
|
|
171
|
-
|
|
172
|
-
</
|
|
134
|
+
Candle
|
|
135
|
+
</Button>
|
|
173
136
|
</div>
|
|
174
137
|
|
|
175
138
|
{/* Play/Pause Button */}
|
|
176
|
-
<
|
|
139
|
+
<Button
|
|
140
|
+
variant="outline"
|
|
141
|
+
size="sm"
|
|
177
142
|
onClick={() => setIsPlaying(!isPlaying)}
|
|
178
|
-
aria-label={isPlaying ? '
|
|
179
|
-
|
|
143
|
+
aria-label={isPlaying ? 'Pause stream' : 'Resume stream'}
|
|
144
|
+
title={isPlaying ? 'Pause stream' : 'Resume stream'}
|
|
145
|
+
className="px-2.5 py-1 text-xs h-auto flex items-center gap-1.5"
|
|
180
146
|
>
|
|
181
|
-
{isPlaying ?
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
<span>일시정지</span>
|
|
185
|
-
</>
|
|
186
|
-
) : (
|
|
187
|
-
<>
|
|
188
|
-
<Play className="w-3.5 h-3.5 text-emerald-500 fill-emerald-500" />
|
|
189
|
-
<span>재생</span>
|
|
190
|
-
</>
|
|
191
|
-
)}
|
|
192
|
-
</button>
|
|
193
|
-
</div>
|
|
194
|
-
</div>
|
|
195
|
-
|
|
196
|
-
{/* KPI Stats Pill Row */}
|
|
197
|
-
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 my-4">
|
|
198
|
-
<div className="p-2.5 rounded-xl bg-(--bg-secondary)/60 border border-(--border-subtle)">
|
|
199
|
-
<div className="text-[10px] text-(--text-muted) font-medium">최고가 (High)</div>
|
|
200
|
-
<div className="text-xs sm:text-sm font-bold text-rose-500">₩{Math.max(...data.map(d => d.high)).toLocaleString()}</div>
|
|
201
|
-
</div>
|
|
202
|
-
<div className="p-2.5 rounded-xl bg-(--bg-secondary)/60 border border-(--border-subtle)">
|
|
203
|
-
<div className="text-[10px] text-(--text-muted) font-medium">최저가 (Low)</div>
|
|
204
|
-
<div className="text-xs sm:text-sm font-bold text-blue-500">₩{Math.min(...data.map(d => d.low)).toLocaleString()}</div>
|
|
205
|
-
</div>
|
|
206
|
-
<div className="p-2.5 rounded-xl bg-(--bg-secondary)/60 border border-(--border-subtle)">
|
|
207
|
-
<div className="text-[10px] text-(--text-muted) font-medium">실시간 거래량</div>
|
|
208
|
-
<div className="text-xs sm:text-sm font-bold text-(--text-primary)">{currentPoint.volume.toLocaleString()} ATOM</div>
|
|
209
|
-
</div>
|
|
210
|
-
<div className="p-2.5 rounded-xl bg-(--bg-secondary)/60 border border-(--border-subtle)">
|
|
211
|
-
<div className="text-[10px] text-(--text-muted) font-medium">마지막 체결</div>
|
|
212
|
-
<div className="text-xs sm:text-sm font-bold text-(--text-primary) flex items-center gap-1">
|
|
213
|
-
<Clock className="w-3 h-3 text-(--text-muted)" />
|
|
214
|
-
{currentPoint.time}
|
|
215
|
-
</div>
|
|
147
|
+
{isPlaying ? <Pause className="w-3.5 h-3.5" /> : <Play className="w-3.5 h-3.5" />}
|
|
148
|
+
<span className="text-xs font-medium">{isPlaying ? '일시정지' : '재생'}</span>
|
|
149
|
+
</Button>
|
|
216
150
|
</div>
|
|
217
151
|
</div>
|
|
218
152
|
|
|
219
|
-
{/*
|
|
220
|
-
<div className="
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
153
|
+
{/* 2. Pure SVG Canvas (Zero-dependency, Token-based) */}
|
|
154
|
+
<div className="p-4 sm:p-5">
|
|
155
|
+
<div className="relative w-full h-56">
|
|
156
|
+
<svg
|
|
157
|
+
className="w-full h-full overflow-visible"
|
|
158
|
+
viewBox={`0 0 ${svgWidth} ${svgHeight}`}
|
|
159
|
+
preserveAspectRatio="none"
|
|
160
|
+
>
|
|
161
|
+
<defs>
|
|
162
|
+
<linearGradient id="chartGradientToken" x1="0" y1="0" x2="0" y2="1">
|
|
163
|
+
<stop offset="0%" stopColor="var(--accent-primary)" stopOpacity="0.22" />
|
|
164
|
+
<stop offset="100%" stopColor="var(--accent-primary)" stopOpacity="0.0" />
|
|
165
|
+
</linearGradient>
|
|
166
|
+
</defs>
|
|
167
|
+
|
|
168
|
+
{/* Subtle Grid Lines */}
|
|
169
|
+
<line x1={paddingX} y1={paddingY} x2={svgWidth - paddingX} y2={paddingY} stroke="var(--border-subtle)" strokeDasharray="4 4" />
|
|
170
|
+
<line x1={paddingX} y1={svgHeight / 2} x2={svgWidth - paddingX} y2={svgHeight / 2} stroke="var(--border-subtle)" strokeDasharray="4 4" />
|
|
171
|
+
<line x1={paddingX} y1={svgHeight - paddingY} x2={svgWidth - paddingX} y2={svgHeight - paddingY} stroke="var(--border-subtle)" strokeDasharray="4 4" />
|
|
172
|
+
|
|
173
|
+
{/* Area */}
|
|
174
|
+
<path
|
|
175
|
+
d={`${pointsPath} L ${getX(data.length - 1)} ${svgHeight - paddingY} L ${getX(0)} ${svgHeight - paddingY} Z`}
|
|
176
|
+
fill="url(#chartGradientToken)"
|
|
177
|
+
/>
|
|
233
178
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
179
|
+
{/* Path */}
|
|
180
|
+
<path
|
|
181
|
+
d={pointsPath}
|
|
182
|
+
fill="none"
|
|
183
|
+
stroke="var(--accent-primary)"
|
|
184
|
+
strokeWidth="2"
|
|
185
|
+
strokeLinecap="round"
|
|
186
|
+
strokeLinejoin="round"
|
|
187
|
+
/>
|
|
238
188
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
fill="
|
|
247
|
-
stroke="var(--accent-primary
|
|
248
|
-
strokeWidth="2
|
|
249
|
-
|
|
250
|
-
|
|
189
|
+
{/* Points */}
|
|
190
|
+
{data.map((d, i) => (
|
|
191
|
+
<circle
|
|
192
|
+
key={i}
|
|
193
|
+
cx={getX(i)}
|
|
194
|
+
cy={getY(d.price)}
|
|
195
|
+
r={hoveredPoint === d ? 5 : 3}
|
|
196
|
+
fill="var(--bg-primary)"
|
|
197
|
+
stroke="var(--accent-primary)"
|
|
198
|
+
strokeWidth="2"
|
|
199
|
+
className="cursor-pointer transition-all"
|
|
200
|
+
onMouseEnter={() => setHoveredPoint(d)}
|
|
251
201
|
/>
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
<circle
|
|
255
|
-
key={i}
|
|
256
|
-
cx={getX(i)}
|
|
257
|
-
cy={getY(d.price)}
|
|
258
|
-
r={i === data.length - 1 ? 4.5 : 2.5}
|
|
259
|
-
className="fill-(--accent-primary) transition-all duration-300"
|
|
260
|
-
/>
|
|
261
|
-
))}
|
|
262
|
-
</>
|
|
263
|
-
) : (
|
|
264
|
-
/* Candlestick view */
|
|
265
|
-
data.map((d, i) => {
|
|
266
|
-
const x = getX(i);
|
|
267
|
-
const isUp = d.price >= (data[i - 1]?.price || d.price);
|
|
268
|
-
const candleTop = getY(Math.max(d.price, data[i - 1]?.price || d.price));
|
|
269
|
-
const candleBottom = getY(Math.min(d.price, data[i - 1]?.price || d.price));
|
|
270
|
-
const candleHeight = Math.max(3, candleBottom - candleTop);
|
|
271
|
-
const colorClass = isUp ? 'fill-emerald-500 stroke-emerald-500' : 'fill-rose-500 stroke-rose-500';
|
|
272
|
-
|
|
273
|
-
return (
|
|
274
|
-
<g key={i}>
|
|
275
|
-
{/* High/Low wick */}
|
|
276
|
-
<line
|
|
277
|
-
x1={x}
|
|
278
|
-
y1={getY(d.high)}
|
|
279
|
-
x2={x}
|
|
280
|
-
y2={getY(d.low)}
|
|
281
|
-
strokeWidth="1.5"
|
|
282
|
-
className={colorClass}
|
|
283
|
-
/>
|
|
284
|
-
{/* Body */}
|
|
285
|
-
<rect
|
|
286
|
-
x={x - 8}
|
|
287
|
-
y={candleTop}
|
|
288
|
-
width={16}
|
|
289
|
-
height={candleHeight}
|
|
290
|
-
rx={2}
|
|
291
|
-
className={colorClass}
|
|
292
|
-
/>
|
|
293
|
-
</g>
|
|
294
|
-
);
|
|
295
|
-
})
|
|
296
|
-
)}
|
|
202
|
+
))}
|
|
203
|
+
</svg>
|
|
297
204
|
|
|
298
|
-
{/* Interactive Hover Crosshair */}
|
|
299
205
|
{hoveredPoint && (
|
|
300
|
-
<
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
/>
|
|
310
|
-
<circle
|
|
311
|
-
cx={getX(data.indexOf(hoveredPoint))}
|
|
312
|
-
cy={getY(hoveredPoint.price)}
|
|
313
|
-
r={6}
|
|
314
|
-
className="fill-(--accent-primary) stroke-white stroke-2"
|
|
315
|
-
/>
|
|
316
|
-
</g>
|
|
206
|
+
<div
|
|
207
|
+
className="absolute top-2 pointer-events-none transform -translate-x-1/2 bg-(--bg-primary) px-2.5 py-1 rounded-(--radius-sm) border border-(--border-strong) shadow-md text-xs"
|
|
208
|
+
style={{
|
|
209
|
+
left: `${(data.indexOf(hoveredPoint) / (data.length - 1)) * 100}%`,
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
<div className="font-semibold text-(--text-primary)">₩{hoveredPoint.price.toLocaleString()}</div>
|
|
213
|
+
<div className="text-[11px] text-(--text-secondary)">{hoveredPoint.time} · Vol: {hoveredPoint.volume}</div>
|
|
214
|
+
</div>
|
|
317
215
|
)}
|
|
318
|
-
|
|
319
|
-
{/* Hover hit areas */}
|
|
320
|
-
{data.map((d, i) => (
|
|
321
|
-
<rect
|
|
322
|
-
key={i}
|
|
323
|
-
x={getX(i) - 15}
|
|
324
|
-
y={0}
|
|
325
|
-
width={30}
|
|
326
|
-
height={svgHeight}
|
|
327
|
-
fill="transparent"
|
|
328
|
-
className="cursor-crosshair"
|
|
329
|
-
onMouseEnter={() => setHoveredPoint(d)}
|
|
330
|
-
/>
|
|
331
|
-
))}
|
|
332
|
-
</svg>
|
|
333
|
-
|
|
334
|
-
{/* Hover Tooltip Overlay */}
|
|
335
|
-
{hoveredPoint && (
|
|
336
|
-
<div
|
|
337
|
-
className="absolute top-2 pointer-events-none transform -translate-x-1/2 bg-(--bg-primary)/95 backdrop-blur-md px-3 py-1.5 rounded-lg border border-(--border-strong) shadow-lg text-xs"
|
|
338
|
-
style={{
|
|
339
|
-
left: `${(data.indexOf(hoveredPoint) / (data.length - 1)) * 100}%`,
|
|
340
|
-
}}
|
|
341
|
-
>
|
|
342
|
-
<div className="font-semibold text-(--text-primary)">₩{hoveredPoint.price.toLocaleString()}</div>
|
|
343
|
-
<div className="text-[10px] text-(--text-secondary)">{hoveredPoint.time} · 거래량 {hoveredPoint.volume}</div>
|
|
344
|
-
</div>
|
|
345
|
-
)}
|
|
216
|
+
</div>
|
|
346
217
|
</div>
|
|
347
218
|
|
|
348
|
-
{/*
|
|
349
|
-
<div className="
|
|
350
|
-
<div className="
|
|
351
|
-
<
|
|
352
|
-
<
|
|
219
|
+
{/* 3. Metrics Summary Strip: Clean Monochrome */}
|
|
220
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 border-t border-(--border-subtle) divide-x divide-(--border-subtle) text-xs">
|
|
221
|
+
<div className="p-3 px-4">
|
|
222
|
+
<div className="text-[11px] text-(--text-muted)">24h High</div>
|
|
223
|
+
<div className="font-mono text-xs text-(--text-primary) mt-0.5">₩{maxPrice.toFixed(0)}</div>
|
|
224
|
+
</div>
|
|
225
|
+
<div className="p-3 px-4">
|
|
226
|
+
<div className="text-[11px] text-(--text-muted)">24h Low</div>
|
|
227
|
+
<div className="font-mono text-xs text-(--text-primary) mt-0.5">₩{minPrice.toFixed(0)}</div>
|
|
353
228
|
</div>
|
|
354
|
-
<div className="
|
|
355
|
-
<
|
|
356
|
-
<
|
|
229
|
+
<div className="p-3 px-4">
|
|
230
|
+
<div className="text-[11px] text-(--text-muted)">24h Volume</div>
|
|
231
|
+
<div className="font-mono text-xs text-(--text-primary) mt-0.5">38,420 ATOM</div>
|
|
232
|
+
</div>
|
|
233
|
+
<div className="p-3 px-4">
|
|
234
|
+
<div className="text-[11px] text-(--text-muted)">Change</div>
|
|
235
|
+
<div className={`font-mono text-xs mt-0.5 ${isPositive ? 'text-emerald-600 dark:text-emerald-400' : 'text-rose-600 dark:text-rose-400'}`}>
|
|
236
|
+
{isPositive ? '+' : ''}{priceDiff.toLocaleString()} 원
|
|
237
|
+
</div>
|
|
357
238
|
</div>
|
|
358
239
|
</div>
|
|
359
|
-
</
|
|
240
|
+
</Card>
|
|
360
241
|
);
|
|
361
242
|
}
|