atomsystem 0.4.3 → 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/components/blocks/BlockDetailPageContent.tsx +337 -0
- package/components/blocks/BlockViewerCard.tsx +123 -123
- package/components/blocks/BlocksSidebar.tsx +93 -0
- package/components/blocks/CardListBlock.tsx +101 -196
- package/components/blocks/FileExplorerBlock.tsx +114 -446
- package/components/blocks/InteractiveChatBlock.tsx +107 -123
- package/components/blocks/MetricStatsBlock.tsx +78 -106
- package/components/blocks/PricingPlanBlock.tsx +88 -144
- package/components/blocks/RealtimeChartBlock.tsx +131 -250
- package/components/blocks/SliderShowcaseBlock.tsx +105 -305
- package/components/ui/Container.tsx +45 -0
- package/components/ui/Layout.tsx +220 -0
- package/components/ui/Switch.tsx +39 -0
- package/components/ui/Table.tsx +88 -0
- package/components/ui/Tabs.tsx +58 -0
- package/components/ui/index.ts +5 -0
- package/css/atom-ui.css +41 -816
- package/dist/cli.cjs +633 -41
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +630 -39
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +176 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -4
- package/dist/index.d.ts +13 -4
- package/dist/index.js +176 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/presets/agents.guide.md +49 -0
- package/presets/blocks.md +101 -0
- package/presets/components.md +142 -0
- package/tokens/token.json +63 -1
- package/tokens/tokens.json +63 -1
|
@@ -1,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
|
-
|
|
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 [
|
|
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
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
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
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<div>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
<
|
|
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
|
-
{/*
|
|
204
|
-
<div className="
|
|
205
|
-
<div>
|
|
206
|
-
<
|
|
207
|
-
<
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
<
|
|
286
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
</
|
|
333
|
-
|
|
334
|
-
</
|
|
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';
|