atomsystem 0.4.0 → 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 +134 -7
- 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,336 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Users,
|
|
6
|
+
HardDrive,
|
|
7
|
+
Cpu,
|
|
8
|
+
ChevronLeft,
|
|
9
|
+
ChevronRight,
|
|
10
|
+
CheckCircle2,
|
|
11
|
+
Sparkles,
|
|
12
|
+
SlidersHorizontal,
|
|
13
|
+
Layers,
|
|
14
|
+
} from 'lucide-react';
|
|
15
|
+
|
|
16
|
+
interface ShowcaseSlide {
|
|
17
|
+
id: number;
|
|
18
|
+
tag: string;
|
|
19
|
+
title: string;
|
|
20
|
+
subtitle: string;
|
|
21
|
+
gradient: string;
|
|
22
|
+
stats: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SLIDES: ShowcaseSlide[] = [
|
|
26
|
+
{
|
|
27
|
+
id: 1,
|
|
28
|
+
tag: 'W3C DTCG Engine',
|
|
29
|
+
title: '디자인 토큰 컴파일러',
|
|
30
|
+
subtitle: 'JSON 명세로부터 Tailwind, CSS Variables, SCSS 코드를 0KB 런타임으로 즉시 변환합니다.',
|
|
31
|
+
gradient: 'from-blue-600/20 via-indigo-600/10 to-transparent',
|
|
32
|
+
stats: '빌드 속도 12ms',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 2,
|
|
36
|
+
tag: 'Universal A11y',
|
|
37
|
+
title: 'WCAG 2.1 AAA 고대비 컬러',
|
|
38
|
+
subtitle: '명도 대비 7:1을 항상 충족하는 색상 스케일 매핑을 자동 보증합니다.',
|
|
39
|
+
gradient: 'from-purple-600/20 via-pink-600/10 to-transparent',
|
|
40
|
+
stats: '접근성 통과율 100%',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 3,
|
|
44
|
+
tag: 'Zero-Runtime',
|
|
45
|
+
title: '크로스 프레임워크 호환성',
|
|
46
|
+
subtitle: 'React, Vue, Svelte, Angular, 그리고 순수 바닐라 HTML까지 단 하나의 코드로 동작합니다.',
|
|
47
|
+
gradient: 'from-emerald-600/20 via-teal-600/10 to-transparent',
|
|
48
|
+
stats: '런타임 오버헤드 0KB',
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
export function SliderShowcaseBlock() {
|
|
53
|
+
const [activeTab, setActiveTab] = useState<'calculator' | 'carousel'>('calculator');
|
|
54
|
+
|
|
55
|
+
// Calculator State
|
|
56
|
+
const [teamMembers, setTeamMembers] = useState<number>(18);
|
|
57
|
+
const [storageGb, setStorageGb] = useState<number>(350);
|
|
58
|
+
const [computeUnits, setComputeUnits] = useState<number>(4);
|
|
59
|
+
const [isAnnualBilling, setIsAnnualBilling] = useState<boolean>(true);
|
|
60
|
+
|
|
61
|
+
// Carousel State
|
|
62
|
+
const [currentSlide, setCurrentSlide] = useState<number>(0);
|
|
63
|
+
|
|
64
|
+
// Dynamic Calculation
|
|
65
|
+
const basePrice = teamMembers * 9 + Math.round(storageGb * 0.12) + computeUnits * 35;
|
|
66
|
+
const discountMultiplier = isAnnualBilling ? 0.8 : 1.0;
|
|
67
|
+
const finalPrice = Math.round(basePrice * discountMultiplier);
|
|
68
|
+
const annualSavings = Math.round(basePrice * 12 * 0.2);
|
|
69
|
+
|
|
70
|
+
const prevSlide = () => {
|
|
71
|
+
setCurrentSlide((prev) => (prev === 0 ? SLIDES.length - 1 : prev - 1));
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const nextSlide = () => {
|
|
75
|
+
setCurrentSlide((prev) => (prev === SLIDES.length - 1 ? 0 : prev + 1));
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="w-full bg-(--bg-primary) rounded-2xl border border-(--border-subtle) shadow-sm p-5 sm:p-6 transition-colors">
|
|
80
|
+
{/* Header and Switcher */}
|
|
81
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-5 border-b border-(--border-subtle)">
|
|
82
|
+
<div>
|
|
83
|
+
<span className="text-[11px] font-semibold text-(--accent-primary) uppercase tracking-wider">
|
|
84
|
+
인터랙티브 슬라이더 모듈
|
|
85
|
+
</span>
|
|
86
|
+
<h3 className="text-lg font-bold text-(--text-primary) mt-0.5">
|
|
87
|
+
{activeTab === 'calculator' ? '실시간 사용량 & 비용 견적 시뮬레이터' : '피처 하이라이트 인터랙티브 캐러셀'}
|
|
88
|
+
</h3>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="flex bg-(--bg-secondary) p-1 rounded-xl border border-(--border-subtle) text-xs">
|
|
92
|
+
<button
|
|
93
|
+
onClick={() => setActiveTab('calculator')}
|
|
94
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg font-semibold transition-all ${
|
|
95
|
+
activeTab === 'calculator'
|
|
96
|
+
? 'bg-(--bg-primary) text-(--text-primary) shadow-xs'
|
|
97
|
+
: 'text-(--text-secondary) hover:text-(--text-primary)'
|
|
98
|
+
}`}
|
|
99
|
+
>
|
|
100
|
+
<SlidersHorizontal className="w-3.5 h-3.5" />
|
|
101
|
+
<span>비용 슬라이더</span>
|
|
102
|
+
</button>
|
|
103
|
+
<button
|
|
104
|
+
onClick={() => setActiveTab('carousel')}
|
|
105
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg font-semibold transition-all ${
|
|
106
|
+
activeTab === 'carousel'
|
|
107
|
+
? 'bg-(--bg-primary) text-(--text-primary) shadow-xs'
|
|
108
|
+
: 'text-(--text-secondary) hover:text-(--text-primary)'
|
|
109
|
+
}`}
|
|
110
|
+
>
|
|
111
|
+
<Layers className="w-3.5 h-3.5" />
|
|
112
|
+
<span>쇼케이스 캐러셀</span>
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{activeTab === 'calculator' ? (
|
|
118
|
+
/* Tab 1: Interactive Cost Calculator Slider */
|
|
119
|
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 pt-6 items-center">
|
|
120
|
+
{/* Left: Sliders Controls (7 Cols) */}
|
|
121
|
+
<div className="lg:col-span-7 space-y-6">
|
|
122
|
+
{/* Slider 1: Team Members */}
|
|
123
|
+
<div className="space-y-2">
|
|
124
|
+
<div className="flex justify-between items-center text-xs">
|
|
125
|
+
<span className="font-semibold text-(--text-primary) flex items-center gap-1.5">
|
|
126
|
+
<Users className="w-4 h-4 text-(--accent-primary)" />
|
|
127
|
+
활성 팀원 수 (Team Members)
|
|
128
|
+
</span>
|
|
129
|
+
<span className="font-mono font-bold text-sm text-(--accent-primary) bg-(--accent-primary)/10 px-2 py-0.5 rounded-md">
|
|
130
|
+
{teamMembers} 명
|
|
131
|
+
</span>
|
|
132
|
+
</div>
|
|
133
|
+
<input
|
|
134
|
+
type="range"
|
|
135
|
+
min="1"
|
|
136
|
+
max="100"
|
|
137
|
+
value={teamMembers}
|
|
138
|
+
onChange={(e) => setTeamMembers(Number(e.target.value))}
|
|
139
|
+
className="w-full h-2 bg-(--bg-secondary) rounded-lg appearance-none cursor-pointer accent-(--accent-primary)"
|
|
140
|
+
/>
|
|
141
|
+
<div className="flex justify-between text-[10px] text-(--text-muted)">
|
|
142
|
+
<span>1명 (Solo)</span>
|
|
143
|
+
<span>50명 (Team)</span>
|
|
144
|
+
<span>100명 (Enterprise)</span>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{/* Slider 2: Storage */}
|
|
149
|
+
<div className="space-y-2">
|
|
150
|
+
<div className="flex justify-between items-center text-xs">
|
|
151
|
+
<span className="font-semibold text-(--text-primary) flex items-center gap-1.5">
|
|
152
|
+
<HardDrive className="w-4 h-4 text-emerald-500" />
|
|
153
|
+
클라우드 스토리지 용량 (Storage)
|
|
154
|
+
</span>
|
|
155
|
+
<span className="font-mono font-bold text-sm text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded-md">
|
|
156
|
+
{storageGb} GB
|
|
157
|
+
</span>
|
|
158
|
+
</div>
|
|
159
|
+
<input
|
|
160
|
+
type="range"
|
|
161
|
+
min="10"
|
|
162
|
+
max="1000"
|
|
163
|
+
step="10"
|
|
164
|
+
value={storageGb}
|
|
165
|
+
onChange={(e) => setStorageGb(Number(e.target.value))}
|
|
166
|
+
className="w-full h-2 bg-(--bg-secondary) rounded-lg appearance-none cursor-pointer accent-emerald-500"
|
|
167
|
+
/>
|
|
168
|
+
<div className="flex justify-between text-[10px] text-(--text-muted)">
|
|
169
|
+
<span>10 GB</span>
|
|
170
|
+
<span>500 GB</span>
|
|
171
|
+
<span>1,000 GB (1TB)</span>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{/* Slider 3: Compute */}
|
|
176
|
+
<div className="space-y-2">
|
|
177
|
+
<div className="flex justify-between items-center text-xs">
|
|
178
|
+
<span className="font-semibold text-(--text-primary) flex items-center gap-1.5">
|
|
179
|
+
<Cpu className="w-4 h-4 text-purple-500" />
|
|
180
|
+
병렬 빌드 & 연산 인스턴스 (Compute Units)
|
|
181
|
+
</span>
|
|
182
|
+
<span className="font-mono font-bold text-sm text-purple-600 dark:text-purple-400 bg-purple-500/10 px-2 py-0.5 rounded-md">
|
|
183
|
+
{computeUnits} CU
|
|
184
|
+
</span>
|
|
185
|
+
</div>
|
|
186
|
+
<input
|
|
187
|
+
type="range"
|
|
188
|
+
min="1"
|
|
189
|
+
max="16"
|
|
190
|
+
step="1"
|
|
191
|
+
value={computeUnits}
|
|
192
|
+
onChange={(e) => setComputeUnits(Number(e.target.value))}
|
|
193
|
+
className="w-full h-2 bg-(--bg-secondary) rounded-lg appearance-none cursor-pointer accent-purple-500"
|
|
194
|
+
/>
|
|
195
|
+
<div className="flex justify-between text-[10px] text-(--text-muted)">
|
|
196
|
+
<span>1 CU</span>
|
|
197
|
+
<span>8 CU</span>
|
|
198
|
+
<span>16 CU (Ultra)</span>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{/* Right: Price Receipt Card (5 Cols) */}
|
|
204
|
+
<div className="lg:col-span-5 p-5 rounded-2xl bg-(--bg-secondary) border border-(--border-strong) flex flex-col justify-between space-y-4">
|
|
205
|
+
<div>
|
|
206
|
+
<div className="flex items-center justify-between mb-3">
|
|
207
|
+
<span className="text-xs font-semibold text-(--text-secondary)">결제 주기 선택</span>
|
|
208
|
+
<div className="flex items-center gap-2">
|
|
209
|
+
<span className={`text-[11px] ${!isAnnualBilling ? 'font-bold text-(--text-primary)' : 'text-(--text-muted)'}`}>월간</span>
|
|
210
|
+
<button
|
|
211
|
+
onClick={() => setIsAnnualBilling(!isAnnualBilling)}
|
|
212
|
+
aria-label="결제 주기 토글"
|
|
213
|
+
className="w-10 h-5 rounded-full bg-(--accent-primary) p-0.5 flex items-center transition-colors"
|
|
214
|
+
>
|
|
215
|
+
<div
|
|
216
|
+
className={`w-4 h-4 rounded-full bg-white transition-transform ${
|
|
217
|
+
isAnnualBilling ? 'translate-x-5' : 'translate-x-0'
|
|
218
|
+
}`}
|
|
219
|
+
/>
|
|
220
|
+
</button>
|
|
221
|
+
<span className={`text-[11px] ${isAnnualBilling ? 'font-bold text-(--accent-primary)' : 'text-(--text-muted)'}`}>연간 (-20%)</span>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<div className="space-y-2 py-3 border-y border-(--border-subtle) text-xs">
|
|
226
|
+
<div className="flex justify-between text-(--text-secondary)">
|
|
227
|
+
<span>팀원 라이선스 ({teamMembers}명)</span>
|
|
228
|
+
<span className="font-medium text-(--text-primary)">₩{(teamMembers * 12000).toLocaleString()}</span>
|
|
229
|
+
</div>
|
|
230
|
+
<div className="flex justify-between text-(--text-secondary)">
|
|
231
|
+
<span>스토리지 ({storageGb}GB)</span>
|
|
232
|
+
<span className="font-medium text-(--text-primary)">₩{(storageGb * 150).toLocaleString()}</span>
|
|
233
|
+
</div>
|
|
234
|
+
<div className="flex justify-between text-(--text-secondary)">
|
|
235
|
+
<span>연산 인스턴스 ({computeUnits}CU)</span>
|
|
236
|
+
<span className="font-medium text-(--text-primary)">₩{(computeUnits * 45000).toLocaleString()}</span>
|
|
237
|
+
</div>
|
|
238
|
+
{isAnnualBilling && (
|
|
239
|
+
<div className="flex justify-between text-emerald-600 dark:text-emerald-400 font-semibold">
|
|
240
|
+
<span>연간 결제 프로모션 할인</span>
|
|
241
|
+
<span>-20% 적용</span>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div>
|
|
248
|
+
<div className="flex items-baseline justify-between mb-3">
|
|
249
|
+
<span className="text-xs text-(--text-muted)">월 예상 결제 금액</span>
|
|
250
|
+
<div className="text-right">
|
|
251
|
+
<span className="text-2xl sm:text-3xl font-black text-(--text-primary)">
|
|
252
|
+
\${finalPrice}
|
|
253
|
+
</span>
|
|
254
|
+
<span className="text-xs text-(--text-muted)"> /월</span>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
{isAnnualBilling && (
|
|
259
|
+
<div className="text-[11px] text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 px-3 py-1.5 rounded-lg mb-3 flex items-center justify-between">
|
|
260
|
+
<span>연간 결제 시 연간 절감액</span>
|
|
261
|
+
<span className="font-bold">\${annualSavings} 절약</span>
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
<button className="w-full py-2.5 rounded-xl bg-(--accent-primary) hover:bg-(--accent-hover) text-white font-semibold text-xs shadow-xs transition-colors">
|
|
266
|
+
이 견적으로 즉시 시작하기
|
|
267
|
+
</button>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
) : (
|
|
272
|
+
/* Tab 2: Carousel Showcase */
|
|
273
|
+
<div className="pt-6">
|
|
274
|
+
<div className="relative overflow-hidden rounded-2xl border border-(--border-subtle) bg-(--bg-secondary)/40 p-6 sm:p-10 min-h-[280px] flex flex-col justify-between">
|
|
275
|
+
{/* Background gradient decorative */}
|
|
276
|
+
<div
|
|
277
|
+
className={`absolute inset-0 bg-gradient-to-br ${SLIDES[currentSlide].gradient} pointer-events-none transition-all duration-500`}
|
|
278
|
+
/>
|
|
279
|
+
|
|
280
|
+
<div className="relative z-10 max-w-xl">
|
|
281
|
+
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold bg-(--accent-primary)/15 text-(--accent-primary) mb-3">
|
|
282
|
+
<Sparkles className="w-3.5 h-3.5" />
|
|
283
|
+
{SLIDES[currentSlide].tag}
|
|
284
|
+
</span>
|
|
285
|
+
<h3 className="text-xl sm:text-2xl font-extrabold text-(--text-primary) tracking-tight mb-2">
|
|
286
|
+
{SLIDES[currentSlide].title}
|
|
287
|
+
</h3>
|
|
288
|
+
<p className="text-xs sm:text-sm text-(--text-secondary) leading-relaxed">
|
|
289
|
+
{SLIDES[currentSlide].subtitle}
|
|
290
|
+
</p>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<div className="relative z-10 flex flex-col sm:flex-row sm:items-center justify-between gap-4 pt-6 border-t border-(--border-subtle) mt-6">
|
|
294
|
+
<div className="flex items-center gap-2 text-xs font-semibold text-(--text-primary)">
|
|
295
|
+
<CheckCircle2 className="w-4 h-4 text-emerald-500" />
|
|
296
|
+
<span>검증 메트릭: {SLIDES[currentSlide].stats}</span>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
{/* Navigation Controls */}
|
|
300
|
+
<div className="flex items-center gap-2">
|
|
301
|
+
{/* Dots */}
|
|
302
|
+
<div className="flex items-center gap-1.5 mr-2">
|
|
303
|
+
{SLIDES.map((_, i) => (
|
|
304
|
+
<button
|
|
305
|
+
key={i}
|
|
306
|
+
onClick={() => setCurrentSlide(i)}
|
|
307
|
+
aria-label={`슬라이드 ${i + 1}로 이동`}
|
|
308
|
+
className={`h-1.5 rounded-full transition-all ${
|
|
309
|
+
currentSlide === i ? 'w-6 bg-(--accent-primary)' : 'w-2 bg-(--border-strong)'
|
|
310
|
+
}`}
|
|
311
|
+
/>
|
|
312
|
+
))}
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<button
|
|
316
|
+
onClick={prevSlide}
|
|
317
|
+
aria-label="이전 슬라이드"
|
|
318
|
+
className="p-2 rounded-lg bg-(--bg-primary) hover:bg-(--border-subtle) text-(--text-primary) border border-(--border-subtle) transition-colors"
|
|
319
|
+
>
|
|
320
|
+
<ChevronLeft className="w-4 h-4" />
|
|
321
|
+
</button>
|
|
322
|
+
<button
|
|
323
|
+
onClick={nextSlide}
|
|
324
|
+
aria-label="다음 슬라이드"
|
|
325
|
+
className="p-2 rounded-lg bg-(--bg-primary) hover:bg-(--border-subtle) text-(--text-primary) border border-(--border-subtle) transition-colors"
|
|
326
|
+
>
|
|
327
|
+
<ChevronRight className="w-4 h-4" />
|
|
328
|
+
</button>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
@@ -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,121 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface TreeNode {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
icon?: React.ReactNode;
|
|
7
|
+
children?: TreeNode[];
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TreeProps {
|
|
12
|
+
data: TreeNode[];
|
|
13
|
+
selectedId?: string;
|
|
14
|
+
onSelect?: (node: TreeNode) => void;
|
|
15
|
+
defaultExpandedIds?: string[];
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Tree: React.FC<TreeProps> = ({
|
|
20
|
+
data,
|
|
21
|
+
selectedId,
|
|
22
|
+
onSelect,
|
|
23
|
+
defaultExpandedIds = [],
|
|
24
|
+
className = '',
|
|
25
|
+
}) => {
|
|
26
|
+
const [expandedIds, setExpandedIds] = useState<string[]>(defaultExpandedIds);
|
|
27
|
+
const [internalSelectedId, setInternalSelectedId] = useState<string | undefined>(selectedId);
|
|
28
|
+
|
|
29
|
+
const activeSelectedId = selectedId !== undefined ? selectedId : internalSelectedId;
|
|
30
|
+
|
|
31
|
+
const toggleExpand = (id: string, e: React.MouseEvent) => {
|
|
32
|
+
e.stopPropagation();
|
|
33
|
+
setExpandedIds((prev) =>
|
|
34
|
+
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleSelect = (node: TreeNode) => {
|
|
39
|
+
if (node.disabled) return;
|
|
40
|
+
setInternalSelectedId(node.id);
|
|
41
|
+
if (onSelect) {
|
|
42
|
+
onSelect(node);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const renderNodes = (nodes: TreeNode[]) => {
|
|
47
|
+
return (
|
|
48
|
+
<ul className="atom-tree-list" role="group">
|
|
49
|
+
{nodes.map((node) => {
|
|
50
|
+
const hasChildren = Boolean(node.children && node.children.length > 0);
|
|
51
|
+
const isExpanded = expandedIds.includes(node.id);
|
|
52
|
+
const isSelected = activeSelectedId === node.id;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<li
|
|
56
|
+
key={node.id}
|
|
57
|
+
className="atom-tree-item"
|
|
58
|
+
role="treeitem"
|
|
59
|
+
aria-expanded={hasChildren ? isExpanded : undefined}
|
|
60
|
+
aria-selected={isSelected}
|
|
61
|
+
aria-disabled={node.disabled}
|
|
62
|
+
>
|
|
63
|
+
<div
|
|
64
|
+
className={`atom-tree-node ${isSelected ? 'is-selected' : ''} ${
|
|
65
|
+
node.disabled ? 'is-disabled' : ''
|
|
66
|
+
}`}
|
|
67
|
+
onClick={() => {
|
|
68
|
+
if (hasChildren && !isExpanded) {
|
|
69
|
+
setExpandedIds((prev) => [...prev, node.id]);
|
|
70
|
+
}
|
|
71
|
+
handleSelect(node);
|
|
72
|
+
}}
|
|
73
|
+
tabIndex={node.disabled ? -1 : 0}
|
|
74
|
+
onKeyDown={(e) => {
|
|
75
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
if (hasChildren) {
|
|
78
|
+
setExpandedIds((prev) =>
|
|
79
|
+
prev.includes(node.id)
|
|
80
|
+
? prev.filter((item) => item !== node.id)
|
|
81
|
+
: [...prev, node.id]
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
handleSelect(node);
|
|
85
|
+
}
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{hasChildren ? (
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
className={`atom-tree-toggle ${isExpanded ? 'is-expanded' : ''}`}
|
|
92
|
+
onClick={(e) => toggleExpand(node.id, e)}
|
|
93
|
+
aria-label={isExpanded ? '접기' : '펼치기'}
|
|
94
|
+
tabIndex={-1}
|
|
95
|
+
>
|
|
96
|
+
▶
|
|
97
|
+
</button>
|
|
98
|
+
) : (
|
|
99
|
+
<span className="atom-tree-toggle-spacer" aria-hidden="true" />
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
{node.icon && <span className="atom-tree-icon">{node.icon}</span>}
|
|
103
|
+
<span className="atom-tree-label">{node.label}</span>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{hasChildren && isExpanded && (
|
|
107
|
+
<div className="atom-tree-children">{renderNodes(node.children!)}</div>
|
|
108
|
+
)}
|
|
109
|
+
</li>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
</ul>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className={`atom-tree ${className}`} role="tree">
|
|
118
|
+
{renderNodes(data)}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
package/components/ui/index.ts
CHANGED
package/css/atom-ui.css
CHANGED
|
@@ -191,19 +191,46 @@
|
|
|
191
191
|
border-color: transparent;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
/* Typography
|
|
195
|
-
.atom-title-1 {
|
|
196
|
-
font-size: 1.
|
|
194
|
+
/* Typography & Heading Scale */
|
|
195
|
+
.atom-h1, .atom-title-1 {
|
|
196
|
+
font-size: 1.625rem;
|
|
197
197
|
font-weight: 700;
|
|
198
198
|
line-height: 1.25;
|
|
199
|
+
letter-spacing: -0.025em;
|
|
199
200
|
color: var(--atom-text-primary);
|
|
200
201
|
}
|
|
201
|
-
.atom-title-2 {
|
|
202
|
+
.atom-h2, .atom-title-2 {
|
|
202
203
|
font-size: 1.25rem;
|
|
203
204
|
font-weight: 600;
|
|
205
|
+
line-height: 1.3;
|
|
206
|
+
letter-spacing: -0.02em;
|
|
207
|
+
color: var(--atom-text-primary);
|
|
208
|
+
}
|
|
209
|
+
.atom-h3, .atom-title-3 {
|
|
210
|
+
font-size: 1.0625rem;
|
|
211
|
+
font-weight: 600;
|
|
204
212
|
line-height: 1.35;
|
|
213
|
+
letter-spacing: -0.015em;
|
|
214
|
+
color: var(--atom-text-primary);
|
|
215
|
+
}
|
|
216
|
+
.atom-h4 {
|
|
217
|
+
font-size: 0.9375rem;
|
|
218
|
+
font-weight: 600;
|
|
219
|
+
line-height: 1.4;
|
|
205
220
|
color: var(--atom-text-primary);
|
|
206
221
|
}
|
|
222
|
+
.atom-h5 {
|
|
223
|
+
font-size: 0.875rem;
|
|
224
|
+
font-weight: 600;
|
|
225
|
+
line-height: 1.45;
|
|
226
|
+
color: var(--atom-text-primary);
|
|
227
|
+
}
|
|
228
|
+
.atom-h6 {
|
|
229
|
+
font-size: 0.75rem;
|
|
230
|
+
font-weight: 600;
|
|
231
|
+
line-height: 1.5;
|
|
232
|
+
color: var(--atom-text-secondary);
|
|
233
|
+
}
|
|
207
234
|
.atom-body-1 {
|
|
208
235
|
font-size: 1rem;
|
|
209
236
|
line-height: 1.5;
|
|
@@ -439,13 +466,16 @@
|
|
|
439
466
|
color: var(--atom-text-primary);
|
|
440
467
|
}
|
|
441
468
|
.atom-toast-success {
|
|
442
|
-
border-
|
|
469
|
+
border-color: rgba(16, 185, 129, 0.35);
|
|
470
|
+
box-shadow: 0 4px 20px rgba(16, 185, 129, 0.08);
|
|
443
471
|
}
|
|
444
472
|
.atom-toast-error {
|
|
445
|
-
border-
|
|
473
|
+
border-color: rgba(244, 63, 94, 0.35);
|
|
474
|
+
box-shadow: 0 4px 20px rgba(244, 63, 94, 0.08);
|
|
446
475
|
}
|
|
447
476
|
.atom-toast-info {
|
|
448
|
-
border-
|
|
477
|
+
border-color: rgba(49, 130, 246, 0.35);
|
|
478
|
+
box-shadow: 0 4px 20px rgba(49, 130, 246, 0.08);
|
|
449
479
|
}
|
|
450
480
|
|
|
451
481
|
/* Pagination */
|
|
@@ -697,3 +727,100 @@
|
|
|
697
727
|
font-size: 0.75rem;
|
|
698
728
|
color: var(--atom-text-muted);
|
|
699
729
|
}
|
|
730
|
+
|
|
731
|
+
/* Tree Component */
|
|
732
|
+
.atom-tree {
|
|
733
|
+
list-style: none;
|
|
734
|
+
padding: 0;
|
|
735
|
+
margin: 0;
|
|
736
|
+
font-size: 0.8125rem;
|
|
737
|
+
color: var(--atom-text-primary);
|
|
738
|
+
user-select: none;
|
|
739
|
+
}
|
|
740
|
+
.atom-tree-list {
|
|
741
|
+
list-style: none;
|
|
742
|
+
padding: 0;
|
|
743
|
+
margin: 0;
|
|
744
|
+
}
|
|
745
|
+
.atom-tree-item {
|
|
746
|
+
position: relative;
|
|
747
|
+
}
|
|
748
|
+
.atom-tree-node {
|
|
749
|
+
display: flex;
|
|
750
|
+
align-items: center;
|
|
751
|
+
gap: 0.375rem;
|
|
752
|
+
width: 100%;
|
|
753
|
+
padding: 0.35rem 0.5rem;
|
|
754
|
+
border-radius: var(--atom-radius-sm);
|
|
755
|
+
border: 1px solid transparent;
|
|
756
|
+
background: transparent;
|
|
757
|
+
color: inherit;
|
|
758
|
+
font-size: inherit;
|
|
759
|
+
text-align: left;
|
|
760
|
+
cursor: pointer;
|
|
761
|
+
transition: background-color 0.15s ease, color 0.15s ease;
|
|
762
|
+
}
|
|
763
|
+
.atom-tree-node:hover {
|
|
764
|
+
background-color: var(--atom-bg-secondary);
|
|
765
|
+
}
|
|
766
|
+
.atom-tree-node:focus-visible {
|
|
767
|
+
outline: 2px solid var(--atom-primary);
|
|
768
|
+
outline-offset: -1px;
|
|
769
|
+
}
|
|
770
|
+
.atom-tree-node.is-selected {
|
|
771
|
+
background-color: var(--atom-primary-light);
|
|
772
|
+
color: var(--atom-primary);
|
|
773
|
+
font-weight: 600;
|
|
774
|
+
}
|
|
775
|
+
.atom-tree-node.is-disabled {
|
|
776
|
+
opacity: 0.5;
|
|
777
|
+
cursor: not-allowed;
|
|
778
|
+
pointer-events: none;
|
|
779
|
+
}
|
|
780
|
+
.atom-tree-toggle {
|
|
781
|
+
display: inline-flex;
|
|
782
|
+
align-items: center;
|
|
783
|
+
justify-content: center;
|
|
784
|
+
width: 1rem;
|
|
785
|
+
height: 1rem;
|
|
786
|
+
padding: 0;
|
|
787
|
+
border: none;
|
|
788
|
+
background: transparent;
|
|
789
|
+
color: var(--atom-text-muted);
|
|
790
|
+
cursor: pointer;
|
|
791
|
+
font-size: 0.75rem;
|
|
792
|
+
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), color 0.15s ease;
|
|
793
|
+
shrink: 0;
|
|
794
|
+
}
|
|
795
|
+
.atom-tree-toggle:hover {
|
|
796
|
+
color: var(--atom-text-primary);
|
|
797
|
+
}
|
|
798
|
+
.atom-tree-toggle.is-expanded {
|
|
799
|
+
transform: rotate(90deg);
|
|
800
|
+
}
|
|
801
|
+
.atom-tree-toggle-spacer {
|
|
802
|
+
width: 1rem;
|
|
803
|
+
height: 1rem;
|
|
804
|
+
shrink: 0;
|
|
805
|
+
}
|
|
806
|
+
.atom-tree-icon {
|
|
807
|
+
display: inline-flex;
|
|
808
|
+
align-items: center;
|
|
809
|
+
justify-content: center;
|
|
810
|
+
width: 1rem;
|
|
811
|
+
height: 1rem;
|
|
812
|
+
color: var(--atom-text-muted);
|
|
813
|
+
shrink: 0;
|
|
814
|
+
}
|
|
815
|
+
.atom-tree-label {
|
|
816
|
+
flex: 1;
|
|
817
|
+
min-width: 0;
|
|
818
|
+
overflow: hidden;
|
|
819
|
+
text-overflow: ellipsis;
|
|
820
|
+
white-space: nowrap;
|
|
821
|
+
}
|
|
822
|
+
.atom-tree-children {
|
|
823
|
+
padding-left: 1.25rem;
|
|
824
|
+
border-left: 1px dashed var(--atom-border-subtle);
|
|
825
|
+
margin-left: 0.5rem;
|
|
826
|
+
}
|
package/dist/cli.cjs
CHANGED
|
@@ -82,7 +82,7 @@ function showVersion() {
|
|
|
82
82
|
const pkg = JSON.parse(fs__default.default.readFileSync(pkgPath, "utf-8"));
|
|
83
83
|
console.log(`atomsystem v${pkg.version}`);
|
|
84
84
|
} else {
|
|
85
|
-
console.log("atomsystem v0.4.
|
|
85
|
+
console.log("atomsystem v0.4.3");
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
switch (command) {
|