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
package/atom.design.md
CHANGED
|
@@ -95,6 +95,23 @@ CSS-in-JS의 런타임 파싱 비용, 자바스크립트 번들 팽창을 방지
|
|
|
95
95
|
|
|
96
96
|
---
|
|
97
97
|
|
|
98
|
+
### 07. 정갈한 AI 안티패턴 방어 (Anti-Pattern Prevention & Polish)
|
|
99
|
+
> **AI 슬롭(AI Slop)을 원천 차단하고 불필요한 장식을 덜어낸 정갈한 본질을 추구합니다.**
|
|
100
|
+
|
|
101
|
+
Impeccable 디자인 철학에 기반하여 LLM이 양산하는 클리셰적 디자인 왜곡을 엄격히 방어합니다. 과도한 네온 그라디언트, 촌스러운 단면 테두리, 불필요한 바운스 모션을 지양하고 담백하며 정돈된 모던 인터페이스를 완성합니다.
|
|
102
|
+
|
|
103
|
+
* **권장 실천 (DO)**:
|
|
104
|
+
- ✅ 상태 표시는 굵은 단면 테두리 대신 은은한 1px 보더, 상태 도트/아이콘, 부드러운 틴트 배경을 활용합니다.
|
|
105
|
+
- ✅ 모션은 튀는 바운스(bounce) 대신 자연스러운 감속 이징(ease-out, 200~300ms)과 미세한 페이드/스케일을 적용합니다.
|
|
106
|
+
- ✅ 코드 커밋 전 `npm run lint:design` (impeccable detect)을 통해 디자인 결함을 무비용(0-Token)으로 정적 검증합니다.
|
|
107
|
+
* **지양 안티패턴 (DON'T)**:
|
|
108
|
+
- ❌ 카드나 콜아웃, 사이드바에 굵은 좌측 테두리(border-l-4 등 Side-tab tell)를 남발하지 않습니다.
|
|
109
|
+
- ❌ 시각적 피로감을 유발하는 무의미한 보라색 그라디언트나 과도한 드롭 섀도를 인라인 하드코딩하지 않습니다.
|
|
110
|
+
- ❌ 시각적 위계와 무관하게 모든 요소를 둥근 플로팅 카드로 감싸는 레이아웃 남용을 피합니다.
|
|
111
|
+
* **표준 참조**: `Impeccable Design Quality Standards & Zero-Slop Heuristics`
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
98
115
|
## 2. 디자인 토큰 사양 (Design Token Specifications)
|
|
99
116
|
|
|
100
117
|
### 🎨 시맨틱 컬러 토큰
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Code2,
|
|
6
|
+
Eye,
|
|
7
|
+
Copy,
|
|
8
|
+
Check,
|
|
9
|
+
Monitor,
|
|
10
|
+
Tablet,
|
|
11
|
+
Smartphone,
|
|
12
|
+
ExternalLink,
|
|
13
|
+
Tag,
|
|
14
|
+
} from 'lucide-react';
|
|
15
|
+
import { BlockItem } from '@/lib/data/blocks';
|
|
16
|
+
|
|
17
|
+
interface BlockViewerCardProps {
|
|
18
|
+
block: BlockItem;
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function BlockViewerCard({ block, children }: BlockViewerCardProps) {
|
|
23
|
+
const [activeTab, setActiveTab] = useState<'preview' | 'react' | 'html'>('preview');
|
|
24
|
+
const [viewport, setViewport] = useState<'desktop' | 'tablet' | 'mobile'>('desktop');
|
|
25
|
+
const [copied, setCopied] = useState(false);
|
|
26
|
+
|
|
27
|
+
const handleCopy = (code: string) => {
|
|
28
|
+
navigator.clipboard.writeText(code);
|
|
29
|
+
setCopied(true);
|
|
30
|
+
setTimeout(() => setCopied(false), 2000);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const getViewportMaxWidth = () => {
|
|
34
|
+
switch (viewport) {
|
|
35
|
+
case 'mobile':
|
|
36
|
+
return 'max-w-[390px]';
|
|
37
|
+
case 'tablet':
|
|
38
|
+
return 'max-w-[768px]';
|
|
39
|
+
default:
|
|
40
|
+
return 'max-w-full';
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
id={block.id}
|
|
47
|
+
className="scroll-mt-20 rounded-2xl bg-(--bg-primary) border border-(--border-subtle) shadow-xs overflow-hidden transition-all duration-200"
|
|
48
|
+
>
|
|
49
|
+
{/* Block Header Info */}
|
|
50
|
+
<div className="p-5 sm:p-6 border-b border-(--border-subtle)">
|
|
51
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-2">
|
|
52
|
+
<div className="flex items-center gap-2.5">
|
|
53
|
+
<span className="px-2.5 py-0.5 rounded-full text-[11px] font-bold bg-(--accent-primary)/10 text-(--accent-primary)">
|
|
54
|
+
{block.badge}
|
|
55
|
+
</span>
|
|
56
|
+
<span className="text-xs text-(--text-muted) font-medium">{block.englishTitle}</span>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* Tokens used tags */}
|
|
60
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
61
|
+
<span className="text-[10px] text-(--text-muted) flex items-center gap-1 mr-1">
|
|
62
|
+
<Tag className="w-3 h-3" />
|
|
63
|
+
토큰 바인딩:
|
|
64
|
+
</span>
|
|
65
|
+
{block.tokensUsed.map((token) => (
|
|
66
|
+
<span
|
|
67
|
+
key={token}
|
|
68
|
+
className="font-mono text-[10px] px-1.5 py-0.2 rounded bg-(--bg-secondary) text-(--text-secondary) border border-(--border-subtle)"
|
|
69
|
+
>
|
|
70
|
+
{token}
|
|
71
|
+
</span>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<h3 className="text-xl font-bold text-(--text-primary) tracking-tight mb-1.5">
|
|
77
|
+
{block.title}
|
|
78
|
+
</h3>
|
|
79
|
+
<p className="text-xs sm:text-sm text-(--text-secondary) leading-relaxed">
|
|
80
|
+
{block.description}
|
|
81
|
+
</p>
|
|
82
|
+
|
|
83
|
+
{/* Feature Highlights Pill */}
|
|
84
|
+
<div className="flex flex-wrap gap-2 mt-4">
|
|
85
|
+
{block.features.map((feat, idx) => (
|
|
86
|
+
<span
|
|
87
|
+
key={idx}
|
|
88
|
+
className="text-[11px] text-(--text-secondary) bg-(--bg-secondary)/70 px-2.5 py-1 rounded-lg border border-(--border-subtle)"
|
|
89
|
+
>
|
|
90
|
+
✓ {feat}
|
|
91
|
+
</span>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Control Bar: Viewport selector & Code tabs */}
|
|
97
|
+
<div className="px-4 py-2.5 bg-(--bg-secondary)/60 border-b border-(--border-subtle) flex items-center justify-between gap-3 overflow-x-auto">
|
|
98
|
+
{/* Left: View Mode Tabs */}
|
|
99
|
+
<div className="flex items-center gap-1 bg-(--bg-primary) p-0.5 rounded-lg border border-(--border-subtle) shrink-0">
|
|
100
|
+
<button
|
|
101
|
+
onClick={() => setActiveTab('preview')}
|
|
102
|
+
data-tab="preview"
|
|
103
|
+
className={`flex items-center gap-1.5 px-3 py-1 rounded-md text-xs font-semibold transition-colors ${
|
|
104
|
+
activeTab === 'preview'
|
|
105
|
+
? 'bg-(--accent-primary) text-white shadow-xs'
|
|
106
|
+
: 'text-(--text-secondary) hover:text-(--text-primary)'
|
|
107
|
+
}`}
|
|
108
|
+
>
|
|
109
|
+
<Eye className="w-3.5 h-3.5" />
|
|
110
|
+
<span>라이브 프리뷰</span>
|
|
111
|
+
</button>
|
|
112
|
+
<button
|
|
113
|
+
onClick={() => setActiveTab('react')}
|
|
114
|
+
data-tab="react"
|
|
115
|
+
className={`flex items-center gap-1.5 px-3 py-1 rounded-md text-xs font-semibold transition-colors ${
|
|
116
|
+
activeTab === 'react'
|
|
117
|
+
? 'bg-(--accent-primary) text-white shadow-xs'
|
|
118
|
+
: 'text-(--text-secondary) hover:text-(--text-primary)'
|
|
119
|
+
}`}
|
|
120
|
+
>
|
|
121
|
+
<Code2 className="w-3.5 h-3.5" />
|
|
122
|
+
<span>React (TSX)</span>
|
|
123
|
+
</button>
|
|
124
|
+
<button
|
|
125
|
+
onClick={() => setActiveTab('html')}
|
|
126
|
+
data-tab="html"
|
|
127
|
+
className={`flex items-center gap-1.5 px-3 py-1 rounded-md text-xs font-semibold transition-colors ${
|
|
128
|
+
activeTab === 'html'
|
|
129
|
+
? 'bg-(--accent-primary) text-white shadow-xs'
|
|
130
|
+
: 'text-(--text-secondary) hover:text-(--text-primary)'
|
|
131
|
+
}`}
|
|
132
|
+
>
|
|
133
|
+
<Code2 className="w-3.5 h-3.5" />
|
|
134
|
+
<span>HTML & CSS</span>
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Right: Viewport resizing (Only in preview mode) & Copy button */}
|
|
139
|
+
<div className="flex items-center gap-2">
|
|
140
|
+
{activeTab === 'preview' && (
|
|
141
|
+
<div className="hidden sm:flex items-center gap-0.5 bg-(--bg-primary) p-0.5 rounded-lg border border-(--border-subtle)">
|
|
142
|
+
<button
|
|
143
|
+
onClick={() => setViewport('desktop')}
|
|
144
|
+
title="데스크톱 뷰 (100%)"
|
|
145
|
+
className={`p-1.5 rounded-md transition-colors ${
|
|
146
|
+
viewport === 'desktop'
|
|
147
|
+
? 'bg-(--bg-secondary) text-(--text-primary)'
|
|
148
|
+
: 'text-(--text-muted) hover:text-(--text-primary)'
|
|
149
|
+
}`}
|
|
150
|
+
>
|
|
151
|
+
<Monitor className="w-3.5 h-3.5" />
|
|
152
|
+
</button>
|
|
153
|
+
<button
|
|
154
|
+
onClick={() => setViewport('tablet')}
|
|
155
|
+
title="태블릿 뷰 (768px)"
|
|
156
|
+
className={`p-1.5 rounded-md transition-colors ${
|
|
157
|
+
viewport === 'tablet'
|
|
158
|
+
? 'bg-(--bg-secondary) text-(--text-primary)'
|
|
159
|
+
: 'text-(--text-muted) hover:text-(--text-primary)'
|
|
160
|
+
}`}
|
|
161
|
+
>
|
|
162
|
+
<Tablet className="w-3.5 h-3.5" />
|
|
163
|
+
</button>
|
|
164
|
+
<button
|
|
165
|
+
onClick={() => setViewport('mobile')}
|
|
166
|
+
title="모바일 뷰 (390px)"
|
|
167
|
+
className={`p-1.5 rounded-md transition-colors ${
|
|
168
|
+
viewport === 'mobile'
|
|
169
|
+
? 'bg-(--bg-secondary) text-(--text-primary)'
|
|
170
|
+
: 'text-(--text-muted) hover:text-(--text-primary)'
|
|
171
|
+
}`}
|
|
172
|
+
>
|
|
173
|
+
<Smartphone className="w-3.5 h-3.5" />
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
<button
|
|
179
|
+
onClick={() =>
|
|
180
|
+
handleCopy(
|
|
181
|
+
activeTab === 'html' ? block.codeSnippets.htmlCss : block.codeSnippets.react
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium bg-(--bg-primary) hover:bg-(--border-subtle) text-(--text-primary) border border-(--border-subtle) transition-colors"
|
|
185
|
+
>
|
|
186
|
+
{copied ? (
|
|
187
|
+
<>
|
|
188
|
+
<Check className="w-3.5 h-3.5 text-emerald-500" />
|
|
189
|
+
<span className="text-emerald-500">복사 완료!</span>
|
|
190
|
+
</>
|
|
191
|
+
) : (
|
|
192
|
+
<>
|
|
193
|
+
<Copy className="w-3.5 h-3.5 text-(--text-muted)" />
|
|
194
|
+
<span>코드 복사</span>
|
|
195
|
+
</>
|
|
196
|
+
)}
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{/* Main Content Area */}
|
|
202
|
+
<div className="p-4 sm:p-8 bg-(--bg-secondary)/30 flex justify-center items-center min-h-[380px]">
|
|
203
|
+
{activeTab === 'preview' ? (
|
|
204
|
+
<div
|
|
205
|
+
className={`w-full transition-all duration-300 mx-auto ${getViewportMaxWidth()} ${
|
|
206
|
+
viewport !== 'desktop'
|
|
207
|
+
? 'border-2 border-dashed border-(--border-strong) rounded-2xl p-2 bg-(--bg-primary) shadow-sm'
|
|
208
|
+
: ''
|
|
209
|
+
}`}
|
|
210
|
+
>
|
|
211
|
+
{children}
|
|
212
|
+
</div>
|
|
213
|
+
) : (
|
|
214
|
+
<div className="w-full bg-(--bg-primary) rounded-xl border border-(--border-subtle) overflow-hidden">
|
|
215
|
+
<div className="px-4 py-2 border-b border-(--border-subtle) bg-(--bg-secondary)/50 text-[11px] font-mono text-(--text-muted) flex justify-between items-center">
|
|
216
|
+
<span>{activeTab === 'react' ? `${block.id}.tsx` : `${block.id}.html`}</span>
|
|
217
|
+
<span>{activeTab === 'react' ? 'TypeScript React' : 'Vanilla HTML / Tailwind'}</span>
|
|
218
|
+
</div>
|
|
219
|
+
<pre className="p-4 text-xs font-mono text-(--text-primary) overflow-x-auto leading-relaxed max-h-96">
|
|
220
|
+
<code>
|
|
221
|
+
{activeTab === 'react' ? block.codeSnippets.react : block.codeSnippets.htmlCss}
|
|
222
|
+
</code>
|
|
223
|
+
</pre>
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
LayoutGrid,
|
|
6
|
+
List as ListIcon,
|
|
7
|
+
Search,
|
|
8
|
+
Bookmark,
|
|
9
|
+
Star,
|
|
10
|
+
Eye,
|
|
11
|
+
ArrowUpRight,
|
|
12
|
+
Sparkles,
|
|
13
|
+
Layers,
|
|
14
|
+
Palette,
|
|
15
|
+
ShieldCheck,
|
|
16
|
+
Zap,
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
|
|
19
|
+
interface CardItem {
|
|
20
|
+
id: string;
|
|
21
|
+
category: 'Foundation' | 'Component' | 'Pattern';
|
|
22
|
+
title: string;
|
|
23
|
+
description: string;
|
|
24
|
+
tag: string;
|
|
25
|
+
icon: 'palette' | 'layers' | 'shield' | 'zap';
|
|
26
|
+
stats: { views: string; rating: number };
|
|
27
|
+
date: string;
|
|
28
|
+
isBookmarked?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const INITIAL_CARDS: CardItem[] = [
|
|
32
|
+
{
|
|
33
|
+
id: 'c1',
|
|
34
|
+
category: 'Foundation',
|
|
35
|
+
title: '시맨틱 색상 및 다크모드 매트릭스',
|
|
36
|
+
description: 'W3C DTCG 표준 3단계 계층 구조(Primitive → Semantic → Component)를 완벽히 준수하는 컬러 시스템.',
|
|
37
|
+
tag: 'W3C 표준',
|
|
38
|
+
icon: 'palette',
|
|
39
|
+
stats: { views: '14.2k', rating: 4.9 },
|
|
40
|
+
date: '2026.11',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'c2',
|
|
44
|
+
category: 'Component',
|
|
45
|
+
title: '고접근성 인터랙티브 모달 & 드로어',
|
|
46
|
+
description: 'ARIA-1.2 명세를 100% 만족하며 포커스 트랩과 ESC 키 닫기, 백드롭 블러를 지원하는 레이어 컴포넌트.',
|
|
47
|
+
tag: 'A11y AAA',
|
|
48
|
+
icon: 'layers',
|
|
49
|
+
stats: { views: '9.8k', rating: 4.8 },
|
|
50
|
+
date: '2026.10',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'c3',
|
|
54
|
+
category: 'Pattern',
|
|
55
|
+
title: 'B2B 핀테크 인증 및 결제 승인 플로우',
|
|
56
|
+
description: '생체 인증, 간편 비밀번호, 카드 CVC 실시간 유효성 검증과 가맹점 수수료율 자동 산출 UX.',
|
|
57
|
+
tag: 'Fintech UX',
|
|
58
|
+
icon: 'shield',
|
|
59
|
+
stats: { views: '18.5k', rating: 5.0 },
|
|
60
|
+
date: '2026.11',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'c4',
|
|
64
|
+
category: 'Component',
|
|
65
|
+
title: '실시간 가변 폰트 & 타이포 스케일러',
|
|
66
|
+
description: 'Pretendard Variable 기반의 유동적(Fluid) 폰트 크기 및 행간(Line-height) 자동 보정 시스템.',
|
|
67
|
+
tag: 'Typography',
|
|
68
|
+
icon: 'zap',
|
|
69
|
+
stats: { views: '7.1k', rating: 4.7 },
|
|
70
|
+
date: '2026.09',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'c5',
|
|
74
|
+
category: 'Pattern',
|
|
75
|
+
title: 'AI 에이전트 스트리밍 메시지 뷰포트',
|
|
76
|
+
description: '대규모 언어 모델의 토큰 단위 스트리밍을 60fps로 매끄럽게 자동 스크롤하는 차세대 피드.',
|
|
77
|
+
tag: 'AI System',
|
|
78
|
+
icon: 'layers',
|
|
79
|
+
stats: { views: '23.4k', rating: 4.9 },
|
|
80
|
+
date: '2026.11',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'c6',
|
|
84
|
+
category: 'Foundation',
|
|
85
|
+
title: '모션 이징 & 스프링 물리 타이밍 토큰',
|
|
86
|
+
description: '자연스러운 사용자 경험을 위한 GSAP 및 CSS transition 타이밍 커브 세트.',
|
|
87
|
+
tag: 'Micro Interaction',
|
|
88
|
+
icon: 'palette',
|
|
89
|
+
stats: { views: '11.0k', rating: 4.8 },
|
|
90
|
+
date: '2026.10',
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
export function CardListBlock() {
|
|
95
|
+
const [cards, setCards] = useState<CardItem[]>(INITIAL_CARDS);
|
|
96
|
+
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
|
97
|
+
const [selectedCategory, setSelectedCategory] = useState<string>('All');
|
|
98
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
99
|
+
|
|
100
|
+
const toggleBookmark = (id: string) => {
|
|
101
|
+
setCards((prev) =>
|
|
102
|
+
prev.map((c) => (c.id === id ? { ...c, isBookmarked: !c.isBookmarked } : c))
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const filteredCards = cards.filter((card) => {
|
|
107
|
+
const matchesCategory = selectedCategory === 'All' || card.category === selectedCategory;
|
|
108
|
+
const matchesSearch =
|
|
109
|
+
card.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
110
|
+
card.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
111
|
+
card.tag.toLowerCase().includes(searchQuery.toLowerCase());
|
|
112
|
+
return matchesCategory && matchesSearch;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const getIcon = (type: CardItem['icon']) => {
|
|
116
|
+
switch (type) {
|
|
117
|
+
case 'palette':
|
|
118
|
+
return <Palette className="w-4 h-4 text-purple-500" />;
|
|
119
|
+
case 'layers':
|
|
120
|
+
return <Layers className="w-4 h-4 text-blue-500" />;
|
|
121
|
+
case 'shield':
|
|
122
|
+
return <ShieldCheck className="w-4 h-4 text-emerald-500" />;
|
|
123
|
+
case 'zap':
|
|
124
|
+
return <Zap className="w-4 h-4 text-amber-500" />;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div className="w-full bg-(--bg-primary) rounded-2xl border border-(--border-subtle) shadow-sm p-5 sm:p-6 transition-colors">
|
|
130
|
+
{/* Top Filter and Search Bar */}
|
|
131
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-5 border-b border-(--border-subtle)">
|
|
132
|
+
{/* Category Tabs */}
|
|
133
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
134
|
+
{['All', 'Foundation', 'Component', 'Pattern'].map((cat) => (
|
|
135
|
+
<button
|
|
136
|
+
key={cat}
|
|
137
|
+
onClick={() => setSelectedCategory(cat)}
|
|
138
|
+
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
|
139
|
+
selectedCategory === cat
|
|
140
|
+
? 'bg-(--accent-primary) text-white shadow-xs'
|
|
141
|
+
: 'bg-(--bg-secondary) text-(--text-secondary) hover:text-(--text-primary)'
|
|
142
|
+
}`}
|
|
143
|
+
>
|
|
144
|
+
{cat === 'All' ? '전체' : cat}
|
|
145
|
+
</button>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Search & View Mode Switcher */}
|
|
150
|
+
<div className="flex items-center gap-2">
|
|
151
|
+
<div className="relative">
|
|
152
|
+
<Search className="w-3.5 h-3.5 absolute left-3 top-1/2 -translate-y-1/2 text-(--text-muted)" />
|
|
153
|
+
<input
|
|
154
|
+
type="text"
|
|
155
|
+
value={searchQuery}
|
|
156
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
157
|
+
placeholder="블록 및 토큰 검색..."
|
|
158
|
+
className="text-xs pl-8 pr-3 py-1.5 rounded-lg bg-(--bg-secondary) text-(--text-primary) border border-(--border-subtle) focus:outline-none focus:border-(--accent-primary) w-36 sm:w-48 transition-colors"
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div className="flex items-center p-0.5 rounded-lg bg-(--bg-secondary) border border-(--border-subtle)">
|
|
163
|
+
<button
|
|
164
|
+
onClick={() => setViewMode('grid')}
|
|
165
|
+
aria-label="그리드 보기"
|
|
166
|
+
className={`p-1.5 rounded-md transition-colors ${
|
|
167
|
+
viewMode === 'grid'
|
|
168
|
+
? 'bg-(--bg-primary) text-(--text-primary) shadow-xs'
|
|
169
|
+
: 'text-(--text-muted) hover:text-(--text-primary)'
|
|
170
|
+
}`}
|
|
171
|
+
>
|
|
172
|
+
<LayoutGrid className="w-4 h-4" />
|
|
173
|
+
</button>
|
|
174
|
+
<button
|
|
175
|
+
onClick={() => setViewMode('list')}
|
|
176
|
+
aria-label="리스트 보기"
|
|
177
|
+
className={`p-1.5 rounded-md transition-colors ${
|
|
178
|
+
viewMode === 'list'
|
|
179
|
+
? 'bg-(--bg-primary) text-(--text-primary) shadow-xs'
|
|
180
|
+
: 'text-(--text-muted) hover:text-(--text-primary)'
|
|
181
|
+
}`}
|
|
182
|
+
>
|
|
183
|
+
<ListIcon className="w-4 h-4" />
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* Cards Viewport */}
|
|
190
|
+
{filteredCards.length === 0 ? (
|
|
191
|
+
<div className="py-16 text-center text-xs text-(--text-muted)">
|
|
192
|
+
검색 조건에 맞는 블록 항목이 없습니다.
|
|
193
|
+
</div>
|
|
194
|
+
) : viewMode === 'grid' ? (
|
|
195
|
+
/* Grid Layout */
|
|
196
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pt-5">
|
|
197
|
+
{filteredCards.map((card) => (
|
|
198
|
+
<div
|
|
199
|
+
key={card.id}
|
|
200
|
+
className="group flex flex-col justify-between p-4 rounded-xl bg-(--bg-secondary)/50 hover:bg-(--bg-secondary) border border-(--border-subtle) hover:border-(--border-strong) transition-all duration-200 hover:-translate-y-0.5 shadow-xs"
|
|
201
|
+
>
|
|
202
|
+
<div>
|
|
203
|
+
<div className="flex items-center justify-between mb-3">
|
|
204
|
+
<div className="flex items-center gap-2">
|
|
205
|
+
<div className="p-2 rounded-lg bg-(--bg-primary) border border-(--border-subtle)">
|
|
206
|
+
{getIcon(card.icon)}
|
|
207
|
+
</div>
|
|
208
|
+
<span className="text-[11px] font-semibold px-2 py-0.5 rounded-full bg-(--bg-primary) text-(--text-secondary) border border-(--border-subtle)">
|
|
209
|
+
{card.tag}
|
|
210
|
+
</span>
|
|
211
|
+
</div>
|
|
212
|
+
<button
|
|
213
|
+
onClick={() => toggleBookmark(card.id)}
|
|
214
|
+
className="p-1.5 rounded-lg text-(--text-muted) hover:text-(--accent-primary) transition-colors"
|
|
215
|
+
>
|
|
216
|
+
<Bookmark
|
|
217
|
+
className={`w-4 h-4 ${card.isBookmarked ? 'fill-(--accent-primary) text-(--accent-primary)' : ''}`}
|
|
218
|
+
/>
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<h4 className="text-sm font-bold text-(--text-primary) group-hover:text-(--accent-primary) transition-colors line-clamp-1 mb-1.5">
|
|
223
|
+
{card.title}
|
|
224
|
+
</h4>
|
|
225
|
+
<p className="text-xs text-(--text-secondary) leading-relaxed line-clamp-2 mb-4">
|
|
226
|
+
{card.description}
|
|
227
|
+
</p>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div className="flex items-center justify-between pt-3 border-t border-(--border-subtle) text-[11px] text-(--text-muted)">
|
|
231
|
+
<div className="flex items-center gap-3">
|
|
232
|
+
<span className="flex items-center gap-1">
|
|
233
|
+
<Star className="w-3.5 h-3.5 fill-amber-400 text-amber-400" />
|
|
234
|
+
<span className="font-semibold text-(--text-primary)">{card.stats.rating}</span>
|
|
235
|
+
</span>
|
|
236
|
+
<span className="flex items-center gap-1">
|
|
237
|
+
<Eye className="w-3.5 h-3.5" />
|
|
238
|
+
<span>{card.stats.views}</span>
|
|
239
|
+
</span>
|
|
240
|
+
</div>
|
|
241
|
+
<div className="flex items-center gap-1 font-semibold text-(--accent-primary) group-hover:translate-x-0.5 transition-transform">
|
|
242
|
+
<span>보기</span>
|
|
243
|
+
<ArrowUpRight className="w-3 h-3" />
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
))}
|
|
248
|
+
</div>
|
|
249
|
+
) : (
|
|
250
|
+
/* List Layout */
|
|
251
|
+
<div className="space-y-3 pt-5">
|
|
252
|
+
{filteredCards.map((card) => (
|
|
253
|
+
<div
|
|
254
|
+
key={card.id}
|
|
255
|
+
className="group flex flex-col sm:flex-row sm:items-center justify-between p-4 rounded-xl bg-(--bg-secondary)/50 hover:bg-(--bg-secondary) border border-(--border-subtle) hover:border-(--border-strong) transition-all gap-4 shadow-xs"
|
|
256
|
+
>
|
|
257
|
+
<div className="flex items-start sm:items-center gap-3">
|
|
258
|
+
<div className="p-2.5 rounded-lg bg-(--bg-primary) border border-(--border-subtle) shrink-0 mt-0.5 sm:mt-0">
|
|
259
|
+
{getIcon(card.icon)}
|
|
260
|
+
</div>
|
|
261
|
+
<div>
|
|
262
|
+
<div className="flex items-center gap-2 mb-1">
|
|
263
|
+
<span className="text-[10px] font-semibold px-2 py-0.5 rounded-full bg-(--bg-primary) text-(--text-secondary) border border-(--border-subtle)">
|
|
264
|
+
{card.tag}
|
|
265
|
+
</span>
|
|
266
|
+
<span className="text-[10px] text-(--text-muted)">{card.date}</span>
|
|
267
|
+
</div>
|
|
268
|
+
<h4 className="text-sm font-bold text-(--text-primary) group-hover:text-(--accent-primary) transition-colors">
|
|
269
|
+
{card.title}
|
|
270
|
+
</h4>
|
|
271
|
+
<p className="text-xs text-(--text-secondary) mt-0.5 line-clamp-1 max-w-xl">
|
|
272
|
+
{card.description}
|
|
273
|
+
</p>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<div className="flex items-center justify-between sm:justify-end gap-4 shrink-0 pt-2 sm:pt-0 border-t sm:border-0 border-(--border-subtle)">
|
|
278
|
+
<div className="flex items-center gap-3 text-xs text-(--text-muted)">
|
|
279
|
+
<span className="flex items-center gap-1">
|
|
280
|
+
<Star className="w-3.5 h-3.5 fill-amber-400 text-amber-400" />
|
|
281
|
+
<span className="font-semibold text-(--text-primary)">{card.stats.rating}</span>
|
|
282
|
+
</span>
|
|
283
|
+
<span className="flex items-center gap-1">
|
|
284
|
+
<Eye className="w-3.5 h-3.5" />
|
|
285
|
+
<span>{card.stats.views}</span>
|
|
286
|
+
</span>
|
|
287
|
+
</div>
|
|
288
|
+
<div className="flex items-center gap-2">
|
|
289
|
+
<button
|
|
290
|
+
onClick={() => toggleBookmark(card.id)}
|
|
291
|
+
className="p-1.5 rounded-lg text-(--text-muted) hover:text-(--accent-primary) transition-colors"
|
|
292
|
+
>
|
|
293
|
+
<Bookmark
|
|
294
|
+
className={`w-4 h-4 ${card.isBookmarked ? 'fill-(--accent-primary) text-(--accent-primary)' : ''}`}
|
|
295
|
+
/>
|
|
296
|
+
</button>
|
|
297
|
+
<button className="px-3 py-1.5 rounded-lg text-xs font-semibold bg-(--bg-primary) hover:bg-(--accent-primary) hover:text-white text-(--text-primary) border border-(--border-subtle) transition-colors flex items-center gap-1">
|
|
298
|
+
<span>상세보기</span>
|
|
299
|
+
<ArrowUpRight className="w-3 h-3" />
|
|
300
|
+
</button>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
))}
|
|
305
|
+
</div>
|
|
306
|
+
)}
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
}
|