atomsystem 0.4.1 → 0.5.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/README.md +112 -52
- package/atom.design.md +17 -0
- package/components/blocks/BlockDetailPageContent.tsx +337 -0
- package/components/blocks/BlockViewerCard.tsx +229 -0
- package/components/blocks/BlocksSidebar.tsx +93 -0
- package/components/blocks/CardListBlock.tsx +214 -0
- package/components/blocks/FileExplorerBlock.tsx +203 -0
- package/components/blocks/InteractiveChatBlock.tsx +217 -0
- package/components/blocks/MetricStatsBlock.tsx +150 -0
- package/components/blocks/PricingPlanBlock.tsx +175 -0
- package/components/blocks/RealtimeChartBlock.tsx +242 -0
- package/components/blocks/SliderShowcaseBlock.tsx +136 -0
- package/components/blocks/index.ts +8 -0
- 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/Tree.tsx +121 -0
- package/components/ui/index.ts +6 -0
- package/css/atom-ui.css +41 -716
- 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 +265 -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 +265 -3
- package/dist/index.js.map +1 -1
- package/package.json +7 -1
- package/presets/agents.guide.md +49 -0
- package/presets/blocks.md +101 -0
- package/presets/components.md +142 -0
- package/presets/default.design.md +17 -0
- package/tokens/token.json +63 -1
- package/tokens/tokens.json +63 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { Card, Button, Badge } from '@/components/ui';
|
|
5
|
+
import {
|
|
6
|
+
Check,
|
|
7
|
+
X,
|
|
8
|
+
Sparkles,
|
|
9
|
+
} from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
interface PricingTier {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
badge?: string;
|
|
15
|
+
isPopular?: boolean;
|
|
16
|
+
description: string;
|
|
17
|
+
monthlyPrice: number;
|
|
18
|
+
annualPrice: number;
|
|
19
|
+
features: { text: string; included: boolean }[];
|
|
20
|
+
ctaText: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const TIERS: PricingTier[] = [
|
|
24
|
+
{
|
|
25
|
+
id: 'starter',
|
|
26
|
+
name: 'Starter',
|
|
27
|
+
description: '개인 개발자 및 오픈소스 프로젝트를 위한 플랜.',
|
|
28
|
+
monthlyPrice: 0,
|
|
29
|
+
annualPrice: 0,
|
|
30
|
+
ctaText: '무료로 시작하기',
|
|
31
|
+
features: [
|
|
32
|
+
{ text: '최대 3개 프로젝트 연동', included: true },
|
|
33
|
+
{ text: 'W3C DTCG 표준 토큰 규격', included: true },
|
|
34
|
+
{ text: 'Tailwind v4 CSS 즉시 컴파일', included: true },
|
|
35
|
+
{ text: '실시간 커스텀 테마 에디터', included: false },
|
|
36
|
+
{ text: '우선 기술 지원 SLA', included: false },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'pro',
|
|
41
|
+
name: 'Pro',
|
|
42
|
+
isPopular: true,
|
|
43
|
+
badge: '추천 플랜',
|
|
44
|
+
description: '디자인 시스템 확장을 준비하는 프로덕트 팀.',
|
|
45
|
+
monthlyPrice: 29000,
|
|
46
|
+
annualPrice: 22000,
|
|
47
|
+
ctaText: '14일 무료 체험 시작',
|
|
48
|
+
features: [
|
|
49
|
+
{ text: '무제한 프로젝트 & 토큰 세트', included: true },
|
|
50
|
+
{ text: 'W3C DTCG 표준 토큰 규격', included: true },
|
|
51
|
+
{ text: 'Tailwind v4 CSS 즉시 컴파일', included: true },
|
|
52
|
+
{ text: '실시간 커스텀 테마 에디터', included: true },
|
|
53
|
+
{ text: 'Git 저장소 실시간 동기화', included: true },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'enterprise',
|
|
58
|
+
name: 'Enterprise',
|
|
59
|
+
description: '엄격한 보안과 거버넌스가 필요한 엔터프라이즈.',
|
|
60
|
+
monthlyPrice: 99000,
|
|
61
|
+
annualPrice: 79000,
|
|
62
|
+
ctaText: '영업팀 문의',
|
|
63
|
+
features: [
|
|
64
|
+
{ text: '무제한 프로젝트 & 전용 빌드 코어', included: true },
|
|
65
|
+
{ text: 'W3C DTCG 표준 토큰 규격', included: true },
|
|
66
|
+
{ text: 'Tailwind v4 CSS 즉시 컴파일', included: true },
|
|
67
|
+
{ text: '실시간 커스텀 테마 에디터', included: true },
|
|
68
|
+
{ text: 'SSO, 감사 로그 & 99.99% SLA', included: true },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
export function PricingPlanBlock() {
|
|
74
|
+
const [billingCycle, setBillingCycle] = useState<'monthly' | 'annual'>('annual');
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Card className="p-5 sm:p-6 transition-colors">
|
|
78
|
+
{/* 1. Header & Billing Toggle (Using Button & Badge Component) */}
|
|
79
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
|
|
80
|
+
<div>
|
|
81
|
+
<h3 className="text-base font-semibold text-(--text-primary)">
|
|
82
|
+
요금제 플랜 선택
|
|
83
|
+
</h3>
|
|
84
|
+
<p className="text-xs text-(--text-secondary) mt-0.5">
|
|
85
|
+
팀의 규모와 워크플로우에 맞는 플랜을 선택하세요.
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Billing Switch composed with Button components */}
|
|
90
|
+
<div className="flex items-center gap-1 p-0.5 rounded-(--radius-sm) bg-(--bg-secondary) border border-(--border-subtle) text-xs self-start sm:self-auto">
|
|
91
|
+
<Button
|
|
92
|
+
size="sm"
|
|
93
|
+
variant={billingCycle === 'monthly' ? 'primary' : 'ghost'}
|
|
94
|
+
onClick={() => setBillingCycle('monthly')}
|
|
95
|
+
className="text-xs py-1 px-3 h-auto"
|
|
96
|
+
>
|
|
97
|
+
월간 결제
|
|
98
|
+
</Button>
|
|
99
|
+
<Button
|
|
100
|
+
size="sm"
|
|
101
|
+
variant={billingCycle === 'annual' ? 'primary' : 'ghost'}
|
|
102
|
+
onClick={() => setBillingCycle('annual')}
|
|
103
|
+
className="text-xs py-1 px-3 h-auto flex items-center gap-1"
|
|
104
|
+
>
|
|
105
|
+
<span>연간 결제</span>
|
|
106
|
+
<Badge variant="success" className="text-xs px-1.5 py-0 font-bold">
|
|
107
|
+
-20%
|
|
108
|
+
</Badge>
|
|
109
|
+
</Button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* 2. Three Column Pricing Cards composed with Card & Button */}
|
|
114
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
115
|
+
{TIERS.map((tier) => {
|
|
116
|
+
const price = billingCycle === 'annual' ? tier.annualPrice : tier.monthlyPrice;
|
|
117
|
+
return (
|
|
118
|
+
<Card
|
|
119
|
+
key={tier.id}
|
|
120
|
+
className={`p-5 flex flex-col justify-between transition-colors bg-(--bg-primary) ${
|
|
121
|
+
tier.isPopular
|
|
122
|
+
? 'border-(--text-primary)'
|
|
123
|
+
: 'border-(--border-subtle)'
|
|
124
|
+
}`}
|
|
125
|
+
>
|
|
126
|
+
<div>
|
|
127
|
+
<div className="flex items-center justify-between mb-2">
|
|
128
|
+
<h4 className="font-semibold text-sm text-(--text-primary)">{tier.name}</h4>
|
|
129
|
+
{tier.badge && (
|
|
130
|
+
<Badge variant="primary" className="text-xs">
|
|
131
|
+
{tier.badge}
|
|
132
|
+
</Badge>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
<p className="text-xs text-(--text-secondary) mb-4 leading-relaxed">
|
|
136
|
+
{tier.description}
|
|
137
|
+
</p>
|
|
138
|
+
|
|
139
|
+
<div className="flex items-baseline gap-1 mb-5">
|
|
140
|
+
<span className="text-2xl font-bold tracking-tight text-(--text-primary)">
|
|
141
|
+
₩{price.toLocaleString()}
|
|
142
|
+
</span>
|
|
143
|
+
<span className="text-xs text-(--text-secondary)">/월</span>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div className="space-y-2.5 pt-4 border-t border-(--border-subtle) text-xs">
|
|
147
|
+
{tier.features.map((feat, idx) => (
|
|
148
|
+
<div key={idx} className="flex items-center gap-2">
|
|
149
|
+
{feat.included ? (
|
|
150
|
+
<Check className="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400 shrink-0" />
|
|
151
|
+
) : (
|
|
152
|
+
<X className="w-3.5 h-3.5 text-(--text-muted) shrink-0 opacity-40" />
|
|
153
|
+
)}
|
|
154
|
+
<span className={feat.included ? 'text-(--text-primary)' : 'text-(--text-muted) line-through opacity-60'}>
|
|
155
|
+
{feat.text}
|
|
156
|
+
</span>
|
|
157
|
+
</div>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<Button
|
|
163
|
+
variant={tier.isPopular ? 'primary' : 'secondary'}
|
|
164
|
+
size="md"
|
|
165
|
+
className="w-full mt-6 text-xs"
|
|
166
|
+
>
|
|
167
|
+
{tier.ctaText}
|
|
168
|
+
</Button>
|
|
169
|
+
</Card>
|
|
170
|
+
);
|
|
171
|
+
})}
|
|
172
|
+
</div>
|
|
173
|
+
</Card>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
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';
|
|
12
|
+
|
|
13
|
+
interface ChartPoint {
|
|
14
|
+
time: string;
|
|
15
|
+
price: number;
|
|
16
|
+
high: number;
|
|
17
|
+
low: number;
|
|
18
|
+
volume: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const INITIAL_POINTS: ChartPoint[] = [
|
|
22
|
+
{ time: '09:00', price: 82400, high: 82600, low: 82100, volume: 1420 },
|
|
23
|
+
{ time: '09:05', price: 82900, high: 83100, low: 82500, volume: 2100 },
|
|
24
|
+
{ time: '09:10', price: 82700, high: 83000, low: 82600, volume: 1850 },
|
|
25
|
+
{ time: '09:15', price: 83400, high: 83600, low: 82700, volume: 3200 },
|
|
26
|
+
{ time: '09:20', price: 83900, high: 84200, low: 83300, volume: 4100 },
|
|
27
|
+
{ time: '09:25', price: 83600, high: 84000, low: 83400, volume: 2600 },
|
|
28
|
+
{ time: '09:30', price: 84200, high: 84500, low: 83600, volume: 4800 },
|
|
29
|
+
{ time: '09:35', price: 84800, high: 85100, low: 84100, volume: 5300 },
|
|
30
|
+
{ time: '09:40', price: 84600, high: 84900, low: 84400, volume: 3100 },
|
|
31
|
+
{ time: '09:45', price: 85300, high: 85600, low: 84600, volume: 6200 },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export function RealtimeChartBlock() {
|
|
35
|
+
const [data, setData] = useState<ChartPoint[]>(INITIAL_POINTS);
|
|
36
|
+
const [isPlaying, setIsPlaying] = useState(true);
|
|
37
|
+
const [chartType, setChartType] = useState<'line' | 'candle'>('line');
|
|
38
|
+
const [timeframe, setTimeframe] = useState<'1m' | '5m' | '1h' | '1d'>('1m');
|
|
39
|
+
const [hoveredPoint, setHoveredPoint] = useState<ChartPoint | null>(null);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!isPlaying) return;
|
|
43
|
+
const interval = setInterval(() => {
|
|
44
|
+
setData((prev) => {
|
|
45
|
+
const last = prev[prev.length - 1];
|
|
46
|
+
const delta = (Math.random() - 0.48) * 350;
|
|
47
|
+
const newPrice = Math.round(last.price + delta);
|
|
48
|
+
const newHigh = Math.max(newPrice, last.high);
|
|
49
|
+
const newLow = Math.min(newPrice, last.low);
|
|
50
|
+
const newVolume = Math.round(2000 + Math.random() * 4000);
|
|
51
|
+
|
|
52
|
+
const now = new Date();
|
|
53
|
+
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
54
|
+
|
|
55
|
+
return [
|
|
56
|
+
...prev.slice(1),
|
|
57
|
+
{ time: timeStr, price: newPrice, high: newHigh, low: newLow, volume: newVolume },
|
|
58
|
+
];
|
|
59
|
+
});
|
|
60
|
+
}, 1200);
|
|
61
|
+
|
|
62
|
+
return () => clearInterval(interval);
|
|
63
|
+
}, [isPlaying]);
|
|
64
|
+
|
|
65
|
+
const currentPoint = hoveredPoint || data[data.length - 1];
|
|
66
|
+
const firstPoint = data[0];
|
|
67
|
+
const priceDiff = currentPoint.price - firstPoint.price;
|
|
68
|
+
const pctDiff = ((priceDiff / firstPoint.price) * 100).toFixed(2);
|
|
69
|
+
const isPositive = priceDiff >= 0;
|
|
70
|
+
|
|
71
|
+
const prices = data.map((d) => d.price);
|
|
72
|
+
const minPrice = Math.min(...prices) * 0.998;
|
|
73
|
+
const maxPrice = Math.max(...prices) * 1.002;
|
|
74
|
+
const range = maxPrice - minPrice || 1;
|
|
75
|
+
|
|
76
|
+
const svgWidth = 600;
|
|
77
|
+
const svgHeight = 220;
|
|
78
|
+
const paddingX = 20;
|
|
79
|
+
const paddingY = 20;
|
|
80
|
+
|
|
81
|
+
const getX = (idx: number) => paddingX + (idx / (data.length - 1)) * (svgWidth - paddingX * 2);
|
|
82
|
+
const getY = (price: number) => svgHeight - paddingY - ((price - minPrice) / range) * (svgHeight - paddingY * 2);
|
|
83
|
+
|
|
84
|
+
const pointsPath = data.map((d, i) => `${i === 0 ? 'M' : 'L'} ${getX(i)} ${getY(d.price)}`).join(' ');
|
|
85
|
+
|
|
86
|
+
return (
|
|
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">
|
|
90
|
+
<div>
|
|
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}%
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="text-xl font-bold tracking-tight text-(--text-primary)">
|
|
98
|
+
₩{currentPoint.price.toLocaleString()}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
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)">
|
|
105
|
+
{(['1m', '5m', '1h', '1d'] as const).map((tf) => (
|
|
106
|
+
<Button
|
|
107
|
+
key={tf}
|
|
108
|
+
variant={timeframe === tf ? 'primary' : 'ghost'}
|
|
109
|
+
size="sm"
|
|
110
|
+
onClick={() => setTimeframe(tf)}
|
|
111
|
+
className="px-2 py-0.5 text-xs h-auto"
|
|
112
|
+
>
|
|
113
|
+
{tf}
|
|
114
|
+
</Button>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
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"
|
|
123
|
+
onClick={() => setChartType('line')}
|
|
124
|
+
className="px-2 py-0.5 text-xs h-auto"
|
|
125
|
+
>
|
|
126
|
+
Line
|
|
127
|
+
</Button>
|
|
128
|
+
<Button
|
|
129
|
+
variant={chartType === 'candle' ? 'primary' : 'ghost'}
|
|
130
|
+
size="sm"
|
|
131
|
+
onClick={() => setChartType('candle')}
|
|
132
|
+
className="px-2 py-0.5 text-xs h-auto"
|
|
133
|
+
>
|
|
134
|
+
Candle
|
|
135
|
+
</Button>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Play/Pause Button */}
|
|
139
|
+
<Button
|
|
140
|
+
variant="outline"
|
|
141
|
+
size="sm"
|
|
142
|
+
onClick={() => setIsPlaying(!isPlaying)}
|
|
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"
|
|
146
|
+
>
|
|
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>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
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
|
+
/>
|
|
178
|
+
|
|
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
|
+
/>
|
|
188
|
+
|
|
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)}
|
|
201
|
+
/>
|
|
202
|
+
))}
|
|
203
|
+
</svg>
|
|
204
|
+
|
|
205
|
+
{hoveredPoint && (
|
|
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>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
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>
|
|
228
|
+
</div>
|
|
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>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</Card>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { Card, Button, Slider } from '@/components/ui';
|
|
5
|
+
import {
|
|
6
|
+
Users,
|
|
7
|
+
HardDrive,
|
|
8
|
+
Cpu,
|
|
9
|
+
FileSpreadsheet,
|
|
10
|
+
} from 'lucide-react';
|
|
11
|
+
|
|
12
|
+
export function SliderShowcaseBlock() {
|
|
13
|
+
const [teamMembers, setTeamMembers] = useState<number>(15);
|
|
14
|
+
const [storageGb, setStorageGb] = useState<number>(200);
|
|
15
|
+
const [computeUnits, setComputeUnits] = useState<number>(4);
|
|
16
|
+
|
|
17
|
+
const memberCost = teamMembers * 12000;
|
|
18
|
+
const storageCost = storageGb * 150;
|
|
19
|
+
const computeCost = computeUnits * 45000;
|
|
20
|
+
const totalCost = memberCost + storageCost + computeCost;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Card className="overflow-hidden transition-colors">
|
|
24
|
+
<div className="p-5 sm:p-6 grid grid-cols-1 lg:grid-cols-12 gap-6">
|
|
25
|
+
{/* Sliders Input Panel composed with UI Slider */}
|
|
26
|
+
<div className="lg:col-span-7 space-y-5">
|
|
27
|
+
{/* Slider 1: Team Members */}
|
|
28
|
+
<div className="space-y-2">
|
|
29
|
+
<div className="flex justify-between items-center text-xs">
|
|
30
|
+
<span className="font-medium text-(--text-primary) flex items-center gap-1.5">
|
|
31
|
+
<Users className="w-3.5 h-3.5 text-(--accent-primary)" />
|
|
32
|
+
<span>팀 멤버 수</span>
|
|
33
|
+
</span>
|
|
34
|
+
<span className="font-mono font-semibold text-xs px-2 py-0.5 rounded-(--radius-sm) border border-(--border-subtle) bg-(--bg-secondary)">
|
|
35
|
+
{teamMembers} 명
|
|
36
|
+
</span>
|
|
37
|
+
</div>
|
|
38
|
+
<Slider
|
|
39
|
+
min={1}
|
|
40
|
+
max={100}
|
|
41
|
+
value={teamMembers}
|
|
42
|
+
onChange={(val) => setTeamMembers(val)}
|
|
43
|
+
/>
|
|
44
|
+
<div className="flex justify-between text-[11px] text-(--text-secondary)">
|
|
45
|
+
<span>1명</span>
|
|
46
|
+
<span>100명</span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{/* Slider 2: Storage */}
|
|
51
|
+
<div className="space-y-2">
|
|
52
|
+
<div className="flex justify-between items-center text-xs">
|
|
53
|
+
<span className="font-medium text-(--text-primary) flex items-center gap-1.5">
|
|
54
|
+
<HardDrive className="w-3.5 h-3.5 text-(--accent-primary)" />
|
|
55
|
+
<span>클라우드 토큰 저장소</span>
|
|
56
|
+
</span>
|
|
57
|
+
<span className="font-mono font-semibold text-xs px-2 py-0.5 rounded-(--radius-sm) border border-(--border-subtle) bg-(--bg-secondary)">
|
|
58
|
+
{storageGb} GB
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
<Slider
|
|
62
|
+
min={10}
|
|
63
|
+
max={1000}
|
|
64
|
+
step={10}
|
|
65
|
+
value={storageGb}
|
|
66
|
+
onChange={(val) => setStorageGb(val)}
|
|
67
|
+
/>
|
|
68
|
+
<div className="flex justify-between text-[11px] text-(--text-secondary)">
|
|
69
|
+
<span>10 GB</span>
|
|
70
|
+
<span>1 TB</span>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Slider 3: Compute */}
|
|
75
|
+
<div className="space-y-2">
|
|
76
|
+
<div className="flex justify-between items-center text-xs">
|
|
77
|
+
<span className="font-medium text-(--text-primary) flex items-center gap-1.5">
|
|
78
|
+
<Cpu className="w-3.5 h-3.5 text-(--accent-primary)" />
|
|
79
|
+
<span>빌드 & 컴파일 엔진 코어</span>
|
|
80
|
+
</span>
|
|
81
|
+
<span className="font-mono font-semibold text-xs px-2 py-0.5 rounded-(--radius-sm) border border-(--border-subtle) bg-(--bg-secondary)">
|
|
82
|
+
{computeUnits} Core
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
<Slider
|
|
86
|
+
min={1}
|
|
87
|
+
max={16}
|
|
88
|
+
value={computeUnits}
|
|
89
|
+
onChange={(val) => setComputeUnits(val)}
|
|
90
|
+
/>
|
|
91
|
+
<div className="flex justify-between text-[11px] text-(--text-secondary)">
|
|
92
|
+
<span>1 Core</span>
|
|
93
|
+
<span>16 Core</span>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{/* Calculation Summary Card composed with Card & Button */}
|
|
99
|
+
<Card className="lg:col-span-5 p-5 bg-(--bg-secondary)/30 flex flex-col justify-between space-y-4">
|
|
100
|
+
<div className="space-y-2.5 text-xs">
|
|
101
|
+
<div className="font-semibold text-(--text-primary)">비용 산출 내역</div>
|
|
102
|
+
<div className="flex justify-between text-(--text-secondary)">
|
|
103
|
+
<span>팀 좌석 ({teamMembers}명)</span>
|
|
104
|
+
<span className="font-medium text-(--text-primary)">₩{memberCost.toLocaleString()}</span>
|
|
105
|
+
</div>
|
|
106
|
+
<div className="flex justify-between text-(--text-secondary)">
|
|
107
|
+
<span>스토리지 ({storageGb}GB)</span>
|
|
108
|
+
<span className="font-medium text-(--text-primary)">₩{storageCost.toLocaleString()}</span>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="flex justify-between text-(--text-secondary)">
|
|
111
|
+
<span>빌드 코어 ({computeUnits}코어)</span>
|
|
112
|
+
<span className="font-medium text-(--text-primary)">₩{computeCost.toLocaleString()}</span>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div className="pt-3 border-t border-(--border-subtle)">
|
|
117
|
+
<div className="text-[11px] text-(--text-secondary)">월간 예상 견적</div>
|
|
118
|
+
<div className="flex items-baseline gap-1 mt-0.5">
|
|
119
|
+
<span className="text-2xl font-bold tracking-tight text-(--text-primary)">
|
|
120
|
+
₩{totalCost.toLocaleString()}
|
|
121
|
+
</span>
|
|
122
|
+
<span className="text-xs text-(--text-secondary)"> /월</span>
|
|
123
|
+
</div>
|
|
124
|
+
<Button
|
|
125
|
+
variant="primary"
|
|
126
|
+
size="md"
|
|
127
|
+
className="w-full mt-3 text-xs"
|
|
128
|
+
>
|
|
129
|
+
견적서 다운로드
|
|
130
|
+
</Button>
|
|
131
|
+
</div>
|
|
132
|
+
</Card>
|
|
133
|
+
</div>
|
|
134
|
+
</Card>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './RealtimeChartBlock';
|
|
2
|
+
export * from './InteractiveChatBlock';
|
|
3
|
+
export * from './CardListBlock';
|
|
4
|
+
export * from './SliderShowcaseBlock';
|
|
5
|
+
export * from './PricingPlanBlock';
|
|
6
|
+
export * from './MetricStatsBlock';
|
|
7
|
+
export * from './FileExplorerBlock';
|
|
8
|
+
export * from './BlockViewerCard';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React, { ElementType } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ContainerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
|
|
5
|
+
gutters?: boolean;
|
|
6
|
+
center?: boolean;
|
|
7
|
+
as?: ElementType;
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const SIZE_MAP: Record<NonNullable<ContainerProps['size']>, string> = {
|
|
12
|
+
sm: 'max-w-(--atom-container-sm,640px)',
|
|
13
|
+
md: 'max-w-(--atom-container-md,768px)',
|
|
14
|
+
lg: 'max-w-(--atom-container-lg,1024px)',
|
|
15
|
+
xl: 'max-w-(--atom-container-xl,1280px)',
|
|
16
|
+
'2xl': 'max-w-(--atom-container-2xl,1536px)',
|
|
17
|
+
full: 'max-w-full',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const Container: React.FC<ContainerProps> = ({
|
|
21
|
+
size = 'xl',
|
|
22
|
+
gutters = true,
|
|
23
|
+
center = true,
|
|
24
|
+
as: Component = 'div',
|
|
25
|
+
className = '',
|
|
26
|
+
children,
|
|
27
|
+
...props
|
|
28
|
+
}) => {
|
|
29
|
+
const sizeClass = SIZE_MAP[size] || SIZE_MAP.xl;
|
|
30
|
+
const gutterClass = gutters
|
|
31
|
+
? 'px-[var(--atom-layout-gutter,16px)] sm:px-[var(--atom-layout-gutter-lg,24px)]'
|
|
32
|
+
: '';
|
|
33
|
+
const centerClass = center ? 'mx-auto' : '';
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Component
|
|
37
|
+
className={`w-full ${sizeClass} ${gutterClass} ${centerClass} ${className}`.trim()}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</Component>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
Container.displayName = 'Container';
|