atomsystem 0.4.1 โ 0.4.3
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/atom.design.md +17 -0
- package/components/blocks/BlockViewerCard.tsx +229 -0
- package/components/blocks/CardListBlock.tsx +309 -0
- package/components/blocks/FileExplorerBlock.tsx +535 -0
- package/components/blocks/InteractiveChatBlock.tsx +233 -0
- package/components/blocks/MetricStatsBlock.tsx +178 -0
- package/components/blocks/PricingPlanBlock.tsx +231 -0
- package/components/blocks/RealtimeChartBlock.tsx +361 -0
- package/components/blocks/SliderShowcaseBlock.tsx +336 -0
- package/components/blocks/index.ts +8 -0
- package/components/ui/Tree.tsx +121 -0
- package/components/ui/index.ts +1 -0
- package/css/atom-ui.css +103 -3
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +90 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +90 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
- package/presets/default.design.md +17 -0
- package/tokens/token.json +1 -1
- package/tokens/tokens.json +1 -1
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import { Send, Bot, User, Sparkles, Paperclip, MoreVertical, CheckCheck } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface ChatMessage {
|
|
7
|
+
id: string;
|
|
8
|
+
sender: 'user' | 'assistant';
|
|
9
|
+
text: string;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const QUICK_PROMPTS = [
|
|
14
|
+
'๐จ primary ๋ธ๋๋ ์์ ๋ณ๊ฒฝ ๋ฐฉ๋ฒ',
|
|
15
|
+
'๐ 8pt ๊ทธ๋ฆฌ๋ ์คํ์ด์ฑ ํ ํฐ ์กฐํ',
|
|
16
|
+
'โก ๋คํฌ๋ชจ๋ ์๋ ์ ํ ๊ตฌํ ๊ฐ์ด๋',
|
|
17
|
+
'๐ 0KB ๋ฐํ์ CSS ๋ด๋ณด๋ด๊ธฐ'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function InteractiveChatBlock() {
|
|
21
|
+
const [messages, setMessages] = useState<ChatMessage[]>([
|
|
22
|
+
{
|
|
23
|
+
id: 'm1',
|
|
24
|
+
sender: 'assistant',
|
|
25
|
+
text: '์๋
ํ์ธ์! AtomDesignToken ๋ํํ ์ด์์คํดํธ์
๋๋ค. ๋ฌด์์ด๋ ๋ฌผ์ด๋ณด์ธ์. ๋์์ธ ์์คํ
๊ตฌ์ถ๊ณผ ํ ํฐ ํ์ฅ์ ์ ์ํ ๋์๋๋ฆฝ๋๋ค.',
|
|
26
|
+
timestamp: '์ค์ 10:24',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'm2',
|
|
30
|
+
sender: 'user',
|
|
31
|
+
text: '๋์์ธ ํ ํฐ์ Tailwind v4 ํ๋ก์ ํธ์ ์ด๋ป๊ฒ ์ฐ๊ฒฐํ๋์?',
|
|
32
|
+
timestamp: '์ค์ 10:25',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'm3',
|
|
36
|
+
sender: 'assistant',
|
|
37
|
+
text: '@import "tailwindcss"; ์ค์ ํ css/atom-ui.css๋ฅผ ์ต์๋จ์ importํ๋ฉด ๋ชจ๋ --atom-* CSS ๋ณ์๊ฐ @theme ๊ท์น๊ณผ ์๋ ๋๊ธฐํ๋์ด ์ฆ์ ํด๋์ค๋ก ํ์ฉํ ์ ์์ต๋๋ค!',
|
|
38
|
+
timestamp: '์ค์ 10:25',
|
|
39
|
+
},
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const [inputValue, setInputValue] = useState('');
|
|
43
|
+
const [isTyping, setIsTyping] = useState(false);
|
|
44
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
45
|
+
|
|
46
|
+
const scrollToBottom = () => {
|
|
47
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
scrollToBottom();
|
|
52
|
+
}, [messages, isTyping]);
|
|
53
|
+
|
|
54
|
+
const handleSendMessage = (textToSend?: string) => {
|
|
55
|
+
const text = textToSend || inputValue;
|
|
56
|
+
if (!text.trim()) return;
|
|
57
|
+
|
|
58
|
+
const now = new Date();
|
|
59
|
+
const timeStr = `${now.getHours() < 12 ? '์ค์ ' : '์คํ'} ${now.getHours() % 12 || 12}:${String(now.getMinutes()).padStart(2, '0')}`;
|
|
60
|
+
|
|
61
|
+
const newUserMsg: ChatMessage = {
|
|
62
|
+
id: `usr-${Date.now()}`,
|
|
63
|
+
sender: 'user',
|
|
64
|
+
text: text.trim(),
|
|
65
|
+
timestamp: timeStr,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
setMessages((prev) => [...prev, newUserMsg]);
|
|
69
|
+
if (!textToSend) setInputValue('');
|
|
70
|
+
setIsTyping(true);
|
|
71
|
+
|
|
72
|
+
// AI ์๋ ๋ต๋ณ ์๋ฎฌ๋ ์ด์
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
let replyText = '๋ฌธ์ํด์ฃผ์ ๋ด์ฉ์ ๋ํ ํ์ค ๊ฐ์ด๋๋ผ์ธ์ด ์ค๋น๋์์ต๋๋ค. AtomDesignToken์ W3C DTCG ํ ํฐ ๋ช
์ธ๋ฅผ ํตํด ๋ชจ๋ ํ๋ ์์ํฌ์์ 100% ์ผ๊ด๋ UI๋ฅผ ์ ์งํ ์ ์์ต๋๋ค.';
|
|
75
|
+
if (text.includes('์์') || text.includes('primary')) {
|
|
76
|
+
replyText = '๋ธ๋๋ ๊ธฐ๋ณธ ์์์ `--atom-primary: #3182f6;` ํ ํฐ์ผ๋ก ์ ์๋์ด ์์ผ๋ฉฐ, Theme Editor์์ ์ฌ๋ผ์ด๋๋ ํ๋ฆฌ์
์ ํตํด ์ค์๊ฐ์ผ๋ก ๋ณ๊ฒฝํ๊ณ ์ฆ์ ๋ด๋ณด๋ผ ์ ์์ต๋๋ค.';
|
|
77
|
+
} else if (text.includes('๊ทธ๋ฆฌ๋') || text.includes('์คํ์ด์ฑ')) {
|
|
78
|
+
replyText = '์คํ์ด์ฑ ํ ํฐ์ 4px ๊ธฐ๋ฐ์ผ๋ก `--spacing-1: 4px;`, `--spacing-2: 8px;`, `--spacing-4: 16px;` ์์ผ๋ก ๊ท ์ผํ๊ฒ ๋ชจ๋ํ๋์ด ์์ต๋๋ค.';
|
|
79
|
+
} else if (text.includes('๋คํฌ๋ชจ๋')) {
|
|
80
|
+
replyText = '์ต์์ html ํ๊ทธ์ `data-theme="dark"` ์์ฑ ๋๋ `.dark` ํด๋์ค๋ฅผ ํ ๊ธํ๋ฉด ๋ชจ๋ ์๋งจํฑ ์์ ํ ํฐ์ด ๋ถ๋๋ฌ์ด ํธ๋์ง์
๊ณผ ํจ๊ป ์ฆ์ ๋ฐ์ ๋ฉ๋๋ค.';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const newBotMsg: ChatMessage = {
|
|
84
|
+
id: `bot-${Date.now()}`,
|
|
85
|
+
sender: 'assistant',
|
|
86
|
+
text: replyText,
|
|
87
|
+
timestamp: timeStr,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
setMessages((prev) => [...prev, newBotMsg]);
|
|
91
|
+
setIsTyping(false);
|
|
92
|
+
}, 1100);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="w-full bg-(--bg-primary) rounded-2xl border border-(--border-subtle) shadow-sm overflow-hidden flex flex-col h-[520px] transition-colors">
|
|
97
|
+
{/* Chat Header */}
|
|
98
|
+
<div className="px-5 py-3.5 border-b border-(--border-subtle) bg-(--bg-secondary)/50 flex items-center justify-between">
|
|
99
|
+
<div className="flex items-center gap-3">
|
|
100
|
+
<div className="relative">
|
|
101
|
+
<div className="w-9 h-9 rounded-xl bg-(--accent-primary) flex items-center justify-center text-white shadow-xs">
|
|
102
|
+
<Bot className="w-5 h-5" />
|
|
103
|
+
</div>
|
|
104
|
+
<span className="absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full bg-emerald-500 border-2 border-(--bg-primary)" />
|
|
105
|
+
</div>
|
|
106
|
+
<div>
|
|
107
|
+
<div className="flex items-center gap-1.5">
|
|
108
|
+
<h3 className="text-sm font-bold text-(--text-primary)">Atom AI Agent</h3>
|
|
109
|
+
<span className="px-1.5 py-0.2 rounded text-[10px] font-semibold bg-(--accent-primary)/10 text-(--accent-primary)">
|
|
110
|
+
์จ๋ผ์ธ
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
<p className="text-[11px] text-(--text-muted)">ํ๊ท ์๋ต ์๋ 0.8์ด ยท ๋์์ธ ์์คํ
๋์ฐ๋ฏธ</p>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
<div className="flex items-center gap-1">
|
|
117
|
+
<button
|
|
118
|
+
onClick={() => setMessages(messages.slice(0, 1))}
|
|
119
|
+
className="text-xs text-(--text-muted) hover:text-(--text-primary) px-2 py-1 rounded transition-colors"
|
|
120
|
+
>
|
|
121
|
+
๋ํ ๋น์ฐ๊ธฐ
|
|
122
|
+
</button>
|
|
123
|
+
<button className="p-1.5 text-(--text-muted) hover:text-(--text-primary) rounded-lg transition-colors">
|
|
124
|
+
<MoreVertical className="w-4 h-4" />
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{/* Messages Scroll Feed */}
|
|
130
|
+
<div className="flex-1 p-4 sm:p-5 overflow-y-auto space-y-4">
|
|
131
|
+
{messages.map((m) => {
|
|
132
|
+
const isUser = m.sender === 'user';
|
|
133
|
+
return (
|
|
134
|
+
<div
|
|
135
|
+
key={m.id}
|
|
136
|
+
className={`flex items-end gap-2.5 ${isUser ? 'flex-row-reverse' : 'flex-row'}`}
|
|
137
|
+
>
|
|
138
|
+
{/* Avatar */}
|
|
139
|
+
<div
|
|
140
|
+
className={`w-7 h-7 rounded-lg flex items-center justify-center text-xs shrink-0 ${
|
|
141
|
+
isUser
|
|
142
|
+
? 'bg-(--bg-secondary) text-(--text-secondary) border border-(--border-subtle)'
|
|
143
|
+
: 'bg-(--accent-primary)/15 text-(--accent-primary)'
|
|
144
|
+
}`}
|
|
145
|
+
>
|
|
146
|
+
{isUser ? <User className="w-4 h-4" /> : <Sparkles className="w-4 h-4" />}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Message Bubble */}
|
|
150
|
+
<div className={`max-w-[78%] flex flex-col ${isUser ? 'items-end' : 'items-start'}`}>
|
|
151
|
+
<div
|
|
152
|
+
className={`px-4 py-2.5 rounded-2xl text-xs sm:text-sm leading-relaxed ${
|
|
153
|
+
isUser
|
|
154
|
+
? 'bg-(--accent-primary) text-white rounded-br-xs shadow-xs'
|
|
155
|
+
: 'bg-(--bg-secondary) text-(--text-primary) border border-(--border-subtle) rounded-bl-xs'
|
|
156
|
+
}`}
|
|
157
|
+
>
|
|
158
|
+
{m.text}
|
|
159
|
+
</div>
|
|
160
|
+
<div className="flex items-center gap-1 mt-1 text-[10px] text-(--text-muted)">
|
|
161
|
+
<span>{m.timestamp}</span>
|
|
162
|
+
{isUser && <CheckCheck className="w-3 h-3 text-(--accent-primary)" />}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
|
|
169
|
+
{/* Typing Pulse Indicator */}
|
|
170
|
+
{isTyping && (
|
|
171
|
+
<div className="flex items-center gap-2.5">
|
|
172
|
+
<div className="w-7 h-7 rounded-lg bg-(--accent-primary)/15 text-(--accent-primary) flex items-center justify-center shrink-0">
|
|
173
|
+
<Sparkles className="w-4 h-4" />
|
|
174
|
+
</div>
|
|
175
|
+
<div className="bg-(--bg-secondary) border border-(--border-subtle) px-3 py-2 rounded-2xl rounded-bl-xs flex items-center gap-1">
|
|
176
|
+
<span className="w-2 h-2 rounded-full bg-(--accent-primary) animate-bounce" style={{ animationDelay: '0ms' }} />
|
|
177
|
+
<span className="w-2 h-2 rounded-full bg-(--accent-primary) animate-bounce" style={{ animationDelay: '150ms' }} />
|
|
178
|
+
<span className="w-2 h-2 rounded-full bg-(--accent-primary) animate-bounce" style={{ animationDelay: '300ms' }} />
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
<div ref={messagesEndRef} />
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Quick Prompts Chips */}
|
|
186
|
+
<div className="px-4 py-2 bg-(--bg-primary) border-t border-(--border-subtle) flex items-center gap-1.5 overflow-x-auto scrollbar-none">
|
|
187
|
+
{QUICK_PROMPTS.map((prompt, idx) => (
|
|
188
|
+
<button
|
|
189
|
+
key={idx}
|
|
190
|
+
onClick={() => handleSendMessage(prompt)}
|
|
191
|
+
className="shrink-0 text-[11px] font-medium px-2.5 py-1 rounded-full bg-(--bg-secondary) hover:bg-(--border-subtle) text-(--text-secondary) hover:text-(--text-primary) border border-(--border-subtle) transition-colors"
|
|
192
|
+
>
|
|
193
|
+
{prompt}
|
|
194
|
+
</button>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
{/* Input Bar */}
|
|
199
|
+
<div className="p-3 sm:p-4 bg-(--bg-primary) border-t border-(--border-subtle)">
|
|
200
|
+
<form
|
|
201
|
+
onSubmit={(e) => {
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
handleSendMessage();
|
|
204
|
+
}}
|
|
205
|
+
className="flex items-center gap-2"
|
|
206
|
+
>
|
|
207
|
+
<button
|
|
208
|
+
type="button"
|
|
209
|
+
className="p-2 text-(--text-muted) hover:text-(--text-primary) hover:bg-(--bg-secondary) rounded-xl transition-colors shrink-0"
|
|
210
|
+
title="์ฒจ๋ถ ํ์ผ"
|
|
211
|
+
>
|
|
212
|
+
<Paperclip className="w-4 h-4" />
|
|
213
|
+
</button>
|
|
214
|
+
<input
|
|
215
|
+
type="text"
|
|
216
|
+
value={inputValue}
|
|
217
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
218
|
+
placeholder="AI ์์ด์ ํธ์๊ฒ ๋ฉ์์ง๋ฅผ ์
๋ ฅํ์ธ์ (Enter ์ ์ก)..."
|
|
219
|
+
className="flex-1 text-xs sm:text-sm px-3.5 py-2.5 rounded-xl bg-(--bg-secondary) text-(--text-primary) border border-(--border-subtle) focus:outline-none focus:border-(--accent-primary) placeholder:text-(--text-muted) transition-colors"
|
|
220
|
+
/>
|
|
221
|
+
<button
|
|
222
|
+
type="submit"
|
|
223
|
+
disabled={!inputValue.trim()}
|
|
224
|
+
className="p-2.5 rounded-xl bg-(--accent-primary) hover:bg-(--accent-hover) disabled:opacity-40 disabled:hover:bg-(--accent-primary) text-white font-medium shadow-xs transition-colors shrink-0"
|
|
225
|
+
title="์ ์ก"
|
|
226
|
+
>
|
|
227
|
+
<Send className="w-4 h-4" />
|
|
228
|
+
</button>
|
|
229
|
+
</form>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import {
|
|
5
|
+
TrendingUp,
|
|
6
|
+
TrendingDown,
|
|
7
|
+
DollarSign,
|
|
8
|
+
Users,
|
|
9
|
+
Target,
|
|
10
|
+
Server,
|
|
11
|
+
ArrowUpRight,
|
|
12
|
+
} from 'lucide-react';
|
|
13
|
+
|
|
14
|
+
interface MetricItem {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
subLabel: string;
|
|
18
|
+
value: string;
|
|
19
|
+
change: string;
|
|
20
|
+
isPositive: boolean;
|
|
21
|
+
icon: 'dollar' | 'users' | 'target' | 'server';
|
|
22
|
+
sparkline: number[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const METRICS: MetricItem[] = [
|
|
26
|
+
{
|
|
27
|
+
id: 'm-arr',
|
|
28
|
+
label: '์ฐ๊ฐ ๋ฐ๋ณต ๋งค์ถ (ARR)',
|
|
29
|
+
subLabel: '์ ์ฒด ์ ๋ฃ ๊ตฌ๋
ํฉ์ฐ',
|
|
30
|
+
value: 'โฉ142,850,000',
|
|
31
|
+
change: '+18.4%',
|
|
32
|
+
isPositive: true,
|
|
33
|
+
icon: 'dollar',
|
|
34
|
+
sparkline: [35, 42, 40, 55, 60, 58, 72, 85],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'm-mau',
|
|
38
|
+
label: '์๊ฐ ํ์ฑ ๊ฐ๋ฐ์ (MAU)',
|
|
39
|
+
subLabel: 'ํ ํฐ CLI ํธ์ถ ๊ณ์ ',
|
|
40
|
+
value: '49,210 ๋ช
',
|
|
41
|
+
change: '+12.6%',
|
|
42
|
+
isPositive: true,
|
|
43
|
+
icon: 'users',
|
|
44
|
+
sparkline: [20, 25, 22, 38, 45, 48, 55, 64],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'm-cvr',
|
|
48
|
+
label: '์ฒดํ ๊ฐ์
์ ํ์จ (CVR)',
|
|
49
|
+
subLabel: '์คํํฐ โ ํ๋ก ์ ํ',
|
|
50
|
+
value: '4.92 %',
|
|
51
|
+
change: '-0.3%',
|
|
52
|
+
isPositive: false,
|
|
53
|
+
icon: 'target',
|
|
54
|
+
sparkline: [60, 58, 62, 54, 52, 55, 48, 50],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'm-sla',
|
|
58
|
+
label: 'API ์๋ต ์ ๋ขฐ๋ & ๊ฐ๋๋ฅ ',
|
|
59
|
+
subLabel: '๊ธ๋ก๋ฒ ์ฃ์ง ๋คํธ์ํฌ',
|
|
60
|
+
value: '99.99 %',
|
|
61
|
+
change: '+0.01%',
|
|
62
|
+
isPositive: true,
|
|
63
|
+
icon: 'server',
|
|
64
|
+
sparkline: [98, 99, 99, 100, 99, 100, 100, 100],
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
export function MetricStatsBlock() {
|
|
69
|
+
const getIcon = (type: MetricItem['icon']) => {
|
|
70
|
+
switch (type) {
|
|
71
|
+
case 'dollar':
|
|
72
|
+
return <DollarSign className="w-4 h-4 text-emerald-500" />;
|
|
73
|
+
case 'users':
|
|
74
|
+
return <Users className="w-4 h-4 text-blue-500" />;
|
|
75
|
+
case 'target':
|
|
76
|
+
return <Target className="w-4 h-4 text-amber-500" />;
|
|
77
|
+
case 'server':
|
|
78
|
+
return <Server className="w-4 h-4 text-purple-500" />;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const renderSparkline = (points: number[], isPositive: boolean) => {
|
|
83
|
+
const min = Math.min(...points);
|
|
84
|
+
const max = Math.max(...points);
|
|
85
|
+
const range = max - min || 1;
|
|
86
|
+
const width = 80;
|
|
87
|
+
const height = 28;
|
|
88
|
+
|
|
89
|
+
const coords = points.map((p, idx) => {
|
|
90
|
+
const x = (idx / (points.length - 1)) * width;
|
|
91
|
+
const y = height - ((p - min) / range) * (height - 6) - 3;
|
|
92
|
+
return `${idx === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${y.toFixed(1)}`;
|
|
93
|
+
}).join(' ');
|
|
94
|
+
|
|
95
|
+
const strokeColor = isPositive ? 'var(--atom-success, #10b981)' : 'var(--atom-destructive, #f04452)';
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<svg width={width} height={height} className="overflow-visible">
|
|
99
|
+
<path
|
|
100
|
+
d={coords}
|
|
101
|
+
fill="none"
|
|
102
|
+
stroke={strokeColor}
|
|
103
|
+
strokeWidth="2"
|
|
104
|
+
strokeLinecap="round"
|
|
105
|
+
strokeLinejoin="round"
|
|
106
|
+
/>
|
|
107
|
+
</svg>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="w-full bg-(--bg-primary) rounded-2xl border border-(--border-subtle) shadow-sm p-5 sm:p-6 transition-colors">
|
|
113
|
+
{/* Top Header */}
|
|
114
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between pb-4 border-b border-(--border-subtle) mb-5">
|
|
115
|
+
<div>
|
|
116
|
+
<span className="text-[11px] font-semibold text-(--accent-primary) uppercase tracking-wider">
|
|
117
|
+
๊ฒฝ์ & ์ธํ๋ผ ํ
๋ ๋ฉํธ๋ฆฌ
|
|
118
|
+
</span>
|
|
119
|
+
<h3 className="text-lg font-bold text-(--text-primary) mt-0.5">
|
|
120
|
+
4๋ ํต์ฌ ๋น์ฆ๋์ค KPI ์งํ ๋์๋ณด๋
|
|
121
|
+
</h3>
|
|
122
|
+
</div>
|
|
123
|
+
<div className="text-[11px] text-(--text-muted) flex items-center gap-1.5 mt-2 sm:mt-0">
|
|
124
|
+
<span className="w-2 h-2 rounded-full bg-emerald-500" />
|
|
125
|
+
<span>์ค์๊ฐ ๋๊ธฐํ ์ํ: ์ ์ (Latency 14ms)</span>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{/* 4 Cards Grid */}
|
|
130
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
131
|
+
{METRICS.map((metric) => (
|
|
132
|
+
<div
|
|
133
|
+
key={metric.id}
|
|
134
|
+
className="p-4 rounded-xl bg-(--bg-secondary)/50 hover:bg-(--bg-secondary) border border-(--border-subtle) hover:border-(--border-strong) transition-all duration-200 shadow-xs flex flex-col justify-between"
|
|
135
|
+
>
|
|
136
|
+
<div>
|
|
137
|
+
{/* Header inside card */}
|
|
138
|
+
<div className="flex items-center justify-between mb-2">
|
|
139
|
+
<div className="p-2 rounded-lg bg-(--bg-primary) border border-(--border-subtle)">
|
|
140
|
+
{getIcon(metric.icon)}
|
|
141
|
+
</div>
|
|
142
|
+
<div
|
|
143
|
+
className={`inline-flex items-center gap-0.5 text-xs font-bold px-2 py-0.5 rounded-md ${
|
|
144
|
+
metric.isPositive
|
|
145
|
+
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
|
146
|
+
: 'bg-rose-500/10 text-rose-600 dark:text-rose-400'
|
|
147
|
+
}`}
|
|
148
|
+
>
|
|
149
|
+
{metric.isPositive ? (
|
|
150
|
+
<TrendingUp className="w-3 h-3" />
|
|
151
|
+
) : (
|
|
152
|
+
<TrendingDown className="w-3 h-3" />
|
|
153
|
+
)}
|
|
154
|
+
<span>{metric.change}</span>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{/* Title & Sub */}
|
|
159
|
+
<div className="text-xs font-semibold text-(--text-secondary)">{metric.label}</div>
|
|
160
|
+
<div className="text-[10px] text-(--text-muted) mb-2">{metric.subLabel}</div>
|
|
161
|
+
|
|
162
|
+
{/* Value */}
|
|
163
|
+
<div className="text-xl sm:text-2xl font-black text-(--text-primary) tracking-tight">
|
|
164
|
+
{metric.value}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{/* Sparkline & Footer */}
|
|
169
|
+
<div className="flex items-end justify-between pt-4 mt-2 border-t border-(--border-subtle)">
|
|
170
|
+
<span className="text-[10px] text-(--text-muted)">์ง๋ 30์ผ ์ถ์ด</span>
|
|
171
|
+
<div>{renderSparkline(metric.sparkline, metric.isPositive)}</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { Check, X, Sparkles, Zap, Shield, Building2 } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface PricingTier {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
badge?: string;
|
|
10
|
+
isPopular?: boolean;
|
|
11
|
+
description: string;
|
|
12
|
+
monthlyPrice: number;
|
|
13
|
+
annualPrice: number;
|
|
14
|
+
icon: 'zap' | 'sparkles' | 'building';
|
|
15
|
+
features: { text: string; included: boolean }[];
|
|
16
|
+
ctaText: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const TIERS: PricingTier[] = [
|
|
20
|
+
{
|
|
21
|
+
id: 'starter',
|
|
22
|
+
name: 'Starter',
|
|
23
|
+
description: '๊ฐ์ธ ๊ฐ๋ฐ์ ๋ฐ ์๊ท๋ชจ ์ฌ์ด๋ ํ๋ก์ ํธ๋ฅผ ์ํ ๋ฌด๋ฃ ์คํํฐ ํฉ',
|
|
24
|
+
monthlyPrice: 0,
|
|
25
|
+
annualPrice: 0,
|
|
26
|
+
icon: 'zap',
|
|
27
|
+
ctaText: '๋ฌด๋ฃ๋ก ์์ํ๊ธฐ',
|
|
28
|
+
features: [
|
|
29
|
+
{ text: '์ต๋ 3๊ฐ ํ๋ก์ ํธ ์ฐ๋', included: true },
|
|
30
|
+
{ text: '๊ธฐ๋ณธ W3C DTCG ํ ํฐ ๋ช
์ธ ์ ๊ณต', included: true },
|
|
31
|
+
{ text: 'Tailwind v4 CSS ์๋ ์ปดํ์ผ', included: true },
|
|
32
|
+
{ text: '์ปค์คํ
๋ธ๋๋ ํ
๋ง ์๋ํฐ', included: false },
|
|
33
|
+
{ text: '์ฐ์ ์์ ๊ธฐ์ ์ง์ SLA', included: false },
|
|
34
|
+
{ text: 'SSO ๋จ์ผ ๋ก๊ทธ์ธ ๋ฐ ๊ฐ์ฌ ๋ก๊ทธ', included: false },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'pro',
|
|
39
|
+
name: 'Pro',
|
|
40
|
+
isPopular: true,
|
|
41
|
+
badge: '๊ฐ์ฅ ์ธ๊ธฐ ์๋ ํ๋',
|
|
42
|
+
description: '์ฑ์ฅํ๋ ์คํํธ์
๋ฐ ๋์์ธ ์์คํ
ํ์ ์ํ ์ ๋ฌธ ๋๊ตฌ ๋ชจ์',
|
|
43
|
+
monthlyPrice: 29,
|
|
44
|
+
annualPrice: 22,
|
|
45
|
+
icon: 'sparkles',
|
|
46
|
+
ctaText: '14์ผ ๋ฌด๋ฃ ์ฒดํ ์์',
|
|
47
|
+
features: [
|
|
48
|
+
{ text: '๋ฌด์ ํ ํ๋ก์ ํธ & ํ ํฐ ์ธํธ', included: true },
|
|
49
|
+
{ text: '๊ณ ๊ธ ์ค์๊ฐ ์ฐจํธ & ๋ธ๋ก ์ ์ฒด ์ ๊ณต', included: true },
|
|
50
|
+
{ text: 'Tailwind v4 / CSS / React ์ํด๋ฆญ ๋ด๋ณด๋ด๊ธฐ', included: true },
|
|
51
|
+
{ text: '์ปค์คํ
๋ธ๋๋ ํ
๋ง ์๋ํฐ ๋ฐ ํ๋ฆฌ์
', included: true },
|
|
52
|
+
{ text: '์์
์ผ 24์๊ฐ ๋ด ๊ธฐ์ ์ง์ SLA', included: true },
|
|
53
|
+
{ text: 'SSO ๋จ์ผ ๋ก๊ทธ์ธ ๋ฐ ๊ฐ์ฌ ๋ก๊ทธ', included: false },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'enterprise',
|
|
58
|
+
name: 'Enterprise',
|
|
59
|
+
description: '์ํฐํ๋ผ์ด์ฆ ๋ณด์, ๊ท์ ์ค์, ๋๊ท๋ชจ ํ์ ์ํ ๋ง์ถคํ ์ธํ๋ผ',
|
|
60
|
+
monthlyPrice: 99,
|
|
61
|
+
annualPrice: 79,
|
|
62
|
+
icon: 'building',
|
|
63
|
+
ctaText: '์์
ํ ๋ฌธ์ํ๊ธฐ',
|
|
64
|
+
features: [
|
|
65
|
+
{ text: '๋ชจ๋ Pro ๊ธฐ๋ฅ ๋ฌด์ ํ ํฌํจ', included: true },
|
|
66
|
+
{ text: '์ ๋ด ์๋ฃจ์
์ํคํ
ํธ ์ง์', included: true },
|
|
67
|
+
{ text: '์จํ๋ ๋ฏธ์ค / VPC ํ๋ผ์ด๋น ๋ฐฐํฌ', included: true },
|
|
68
|
+
{ text: '99.99% ์์คํ
๊ฐ๋๋ฅ SLA ๋ณด์ฆ', included: true },
|
|
69
|
+
{ text: 'SSO (SAML, Okta) & ์์ธ ๊ฐ์ฌ ๋ก๊ทธ', included: true },
|
|
70
|
+
{ text: '๋ง์ถคํ ๊ณ์ฝ ๋ฐ ๋ณด์์ฑ ์ฌ์ฌ ์ง์', included: true },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export function PricingPlanBlock() {
|
|
76
|
+
const [isAnnual, setIsAnnual] = useState<boolean>(true);
|
|
77
|
+
const [selectedPlan, setSelectedPlan] = useState<string>('pro');
|
|
78
|
+
|
|
79
|
+
const getTierIcon = (icon: PricingTier['icon']) => {
|
|
80
|
+
switch (icon) {
|
|
81
|
+
case 'zap':
|
|
82
|
+
return <Zap className="w-5 h-5 text-blue-500" />;
|
|
83
|
+
case 'sparkles':
|
|
84
|
+
return <Sparkles className="w-5 h-5 text-amber-500" />;
|
|
85
|
+
case 'building':
|
|
86
|
+
return <Building2 className="w-5 h-5 text-purple-500" />;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="w-full bg-(--bg-primary) rounded-2xl border border-(--border-subtle) shadow-sm p-5 sm:p-8 transition-colors">
|
|
92
|
+
{/* Header & Billing Toggle */}
|
|
93
|
+
<div className="text-center max-w-2xl mx-auto mb-10">
|
|
94
|
+
<span className="text-xs font-semibold text-(--accent-primary) uppercase tracking-wider">
|
|
95
|
+
ํฌ๋ช
ํ๊ณ ์์ธก ๊ฐ๋ฅํ ์๊ธ์
|
|
96
|
+
</span>
|
|
97
|
+
<h3 className="text-2xl sm:text-3xl font-extrabold text-(--text-primary) tracking-tight mt-1 mb-3">
|
|
98
|
+
ํ์ ๊ท๋ชจ์ ๊ผญ ๋ง๋ ์ ์ฐํ ํ๋
|
|
99
|
+
</h3>
|
|
100
|
+
<p className="text-xs sm:text-sm text-(--text-secondary) leading-relaxed">
|
|
101
|
+
์จ๊ฒจ์ง ๋น์ฉ ์์ด ์ธ์ ๋ ํ๋์ ๋ณ๊ฒฝํ๊ฑฐ๋ ํด์งํ ์ ์์ต๋๋ค. ์ฐ๊ฐ ๊ฒฐ์ ์ 20%์ ์ถ๊ฐ ํ ์ธ์ด ์ ์ฉ๋ฉ๋๋ค.
|
|
102
|
+
</p>
|
|
103
|
+
|
|
104
|
+
{/* Toggle Switch */}
|
|
105
|
+
<div className="inline-flex items-center gap-3 p-1 rounded-full bg-(--bg-secondary) border border-(--border-subtle) mt-6">
|
|
106
|
+
<button
|
|
107
|
+
onClick={() => setIsAnnual(false)}
|
|
108
|
+
className={`px-4 py-1.5 rounded-full text-xs font-semibold transition-all ${
|
|
109
|
+
!isAnnual
|
|
110
|
+
? 'bg-(--bg-primary) text-(--text-primary) shadow-xs'
|
|
111
|
+
: 'text-(--text-secondary) hover:text-(--text-primary)'
|
|
112
|
+
}`}
|
|
113
|
+
>
|
|
114
|
+
์๊ฐ ๊ฒฐ์
|
|
115
|
+
</button>
|
|
116
|
+
<button
|
|
117
|
+
onClick={() => setIsAnnual(true)}
|
|
118
|
+
className={`flex items-center gap-1.5 px-4 py-1.5 rounded-full text-xs font-semibold transition-all ${
|
|
119
|
+
isAnnual
|
|
120
|
+
? 'bg-(--accent-primary) text-white shadow-xs'
|
|
121
|
+
: 'text-(--text-secondary) hover:text-(--text-primary)'
|
|
122
|
+
}`}
|
|
123
|
+
>
|
|
124
|
+
<span>์ฐ๊ฐ ๊ฒฐ์ </span>
|
|
125
|
+
<span
|
|
126
|
+
className={`text-[10px] px-1.5 py-0.2 rounded-full font-bold ${
|
|
127
|
+
isAnnual ? 'bg-white/20 text-white' : 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400'
|
|
128
|
+
}`}
|
|
129
|
+
>
|
|
130
|
+
20% OFF
|
|
131
|
+
</span>
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{/* 3-Column Pricing Grid */}
|
|
137
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 items-stretch">
|
|
138
|
+
{TIERS.map((tier) => {
|
|
139
|
+
const isSelected = selectedPlan === tier.id;
|
|
140
|
+
const price = isAnnual ? tier.annualPrice : tier.monthlyPrice;
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div
|
|
144
|
+
key={tier.id}
|
|
145
|
+
onClick={() => setSelectedPlan(tier.id)}
|
|
146
|
+
className={`relative flex flex-col justify-between rounded-2xl p-6 transition-all duration-200 cursor-pointer ${
|
|
147
|
+
tier.isPopular
|
|
148
|
+
? 'border-2 border-(--accent-primary) bg-(--bg-secondary)/40 shadow-md ring-4 ring-(--accent-primary)/10'
|
|
149
|
+
: 'border border-(--border-strong) bg-(--bg-primary) hover:border-(--accent-primary)/50 shadow-xs'
|
|
150
|
+
}`}
|
|
151
|
+
>
|
|
152
|
+
{/* Popular Badge */}
|
|
153
|
+
{tier.badge && (
|
|
154
|
+
<div className="absolute -top-3.5 left-1/2 -translate-x-1/2 px-3 py-1 rounded-full text-[11px] font-bold bg-(--accent-primary) text-white shadow-sm flex items-center gap-1">
|
|
155
|
+
<Sparkles className="w-3 h-3" />
|
|
156
|
+
<span>{tier.badge}</span>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
<div>
|
|
161
|
+
{/* Tier Title & Icon */}
|
|
162
|
+
<div className="flex items-center justify-between mb-4">
|
|
163
|
+
<div className="p-2.5 rounded-xl bg-(--bg-secondary) border border-(--border-subtle)">
|
|
164
|
+
{getTierIcon(tier.icon)}
|
|
165
|
+
</div>
|
|
166
|
+
{isSelected && (
|
|
167
|
+
<span className="text-[11px] font-bold px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
|
|
168
|
+
์ ํ๋จ
|
|
169
|
+
</span>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<h4 className="text-lg font-bold text-(--text-primary)">{tier.name}</h4>
|
|
174
|
+
<p className="text-xs text-(--text-secondary) mt-1 min-h-[32px] leading-relaxed">
|
|
175
|
+
{tier.description}
|
|
176
|
+
</p>
|
|
177
|
+
|
|
178
|
+
{/* Price Display */}
|
|
179
|
+
<div className="my-6 pb-6 border-b border-(--border-subtle)">
|
|
180
|
+
<div className="flex items-baseline gap-1">
|
|
181
|
+
<span className="text-3xl sm:text-4xl font-black text-(--text-primary)">
|
|
182
|
+
\${price}
|
|
183
|
+
</span>
|
|
184
|
+
<span className="text-xs text-(--text-muted)">/์ (๊ณ์ ๋น)</span>
|
|
185
|
+
</div>
|
|
186
|
+
<div className="text-[11px] text-(--text-muted) mt-1">
|
|
187
|
+
{isAnnual ? '์ฐ๊ฐ ์ผ๊ด ๊ฒฐ์ ๊ธฐ์ค' : '๋งค์ ์๋ ๊ฐฑ์ '}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{/* Feature Checklist */}
|
|
192
|
+
<div className="space-y-3 mb-6">
|
|
193
|
+
<div className="text-[11px] font-semibold text-(--text-muted) uppercase tracking-wider">
|
|
194
|
+
ํฌํจ๋ ํต์ฌ ๊ธฐ๋ฅ
|
|
195
|
+
</div>
|
|
196
|
+
{tier.features.map((feat, idx) => (
|
|
197
|
+
<div key={idx} className="flex items-center gap-2.5 text-xs">
|
|
198
|
+
{feat.included ? (
|
|
199
|
+
<div className="w-4 h-4 rounded-full bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 flex items-center justify-center shrink-0">
|
|
200
|
+
<Check className="w-2.5 h-2.5 stroke-[3]" />
|
|
201
|
+
</div>
|
|
202
|
+
) : (
|
|
203
|
+
<div className="w-4 h-4 rounded-full bg-(--bg-secondary) text-(--text-muted) flex items-center justify-center shrink-0">
|
|
204
|
+
<X className="w-2.5 h-2.5" />
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
<span className={feat.included ? 'text-(--text-primary)' : 'text-(--text-muted) line-through'}>
|
|
208
|
+
{feat.text}
|
|
209
|
+
</span>
|
|
210
|
+
</div>
|
|
211
|
+
))}
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* Action CTA */}
|
|
216
|
+
<button
|
|
217
|
+
className={`w-full py-2.5 rounded-xl font-bold text-xs tracking-wide transition-all shadow-xs ${
|
|
218
|
+
tier.isPopular
|
|
219
|
+
? 'bg-(--accent-primary) hover:bg-(--accent-hover) text-white'
|
|
220
|
+
: 'bg-(--bg-secondary) hover:bg-(--border-strong) text-(--text-primary) border border-(--border-subtle)'
|
|
221
|
+
}`}
|
|
222
|
+
>
|
|
223
|
+
{tier.ctaText}
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
})}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|