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.
@@ -1,336 +1,136 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useState } from 'react';
4
+ import { Card, Button, Slider } from '@/components/ui';
4
5
  import {
5
6
  Users,
6
7
  HardDrive,
7
8
  Cpu,
8
- ChevronLeft,
9
- ChevronRight,
10
- CheckCircle2,
11
- Sparkles,
12
- SlidersHorizontal,
13
- Layers,
9
+ FileSpreadsheet,
14
10
  } from 'lucide-react';
15
11
 
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
12
  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);
13
+ const [teamMembers, setTeamMembers] = useState<number>(15);
14
+ const [storageGb, setStorageGb] = useState<number>(200);
58
15
  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
16
 
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
- };
17
+ const memberCost = teamMembers * 12000;
18
+ const storageCost = storageGb * 150;
19
+ const computeCost = computeUnits * 45000;
20
+ const totalCost = memberCost + storageCost + computeCost;
77
21
 
78
22
  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>
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>
146
37
  </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>
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>
173
47
  </div>
48
+ </div>
174
49
 
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>
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>
200
71
  </div>
201
72
  </div>
202
73
 
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>
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>
245
84
  </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>
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>
268
94
  </div>
269
95
  </div>
270
96
  </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
97
 
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>
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>
291
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>
292
115
 
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>
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>
330
123
  </div>
124
+ <Button
125
+ variant="primary"
126
+ size="md"
127
+ className="w-full mt-3 text-xs"
128
+ >
129
+ 견적서 다운로드
130
+ </Button>
331
131
  </div>
332
- </div>
333
- )}
334
- </div>
132
+ </Card>
133
+ </div>
134
+ </Card>
335
135
  );
336
136
  }
@@ -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';