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.
@@ -1,18 +1,16 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useState } from 'react';
4
+ import { Card, Button, Badge } from '@/components/ui';
4
5
  import {
5
6
  LayoutGrid,
6
7
  List as ListIcon,
7
8
  Search,
8
9
  Bookmark,
9
- Star,
10
- Eye,
11
10
  ArrowUpRight,
12
- Sparkles,
13
- Layers,
14
11
  Palette,
15
- ShieldCheck,
12
+ Layers,
13
+ Shield,
16
14
  Zap,
17
15
  } from 'lucide-react';
18
16
 
@@ -22,94 +20,61 @@ interface CardItem {
22
20
  title: string;
23
21
  description: string;
24
22
  tag: string;
23
+ price: string;
25
24
  icon: 'palette' | 'layers' | 'shield' | 'zap';
26
- stats: { views: string; rating: number };
27
- date: string;
28
- isBookmarked?: boolean;
29
25
  }
30
26
 
31
27
  const INITIAL_CARDS: CardItem[] = [
32
28
  {
33
29
  id: 'c1',
34
30
  category: 'Foundation',
35
- title: '시맨틱 색상 및 다크모드 매트릭스',
36
- description: 'W3C DTCG 표준 3단계 계층 구조(Primitive → Semantic → Component)를 완벽히 준수하는 컬러 시스템.',
31
+ title: '시맨틱 색상 & 다크모드 매트릭스',
32
+ description: 'W3C DTCG 표준 3단계 계층 구조를 준수하는 토큰 기반 컬러 시스템.',
37
33
  tag: 'W3C 표준',
34
+ price: 'Free',
38
35
  icon: 'palette',
39
- stats: { views: '14.2k', rating: 4.9 },
40
- date: '2026.11',
41
36
  },
42
37
  {
43
38
  id: 'c2',
44
39
  category: 'Component',
45
40
  title: '고접근성 인터랙티브 모달 & 드로어',
46
- description: 'ARIA-1.2 명세를 100% 만족하며 포커스 트랩과 ESC 키 닫기, 백드롭 블러를 지원하는 레이어 컴포넌트.',
41
+ description: '포커스 트랩과 키보드 내비게이션을 보장하는 시맨틱 레이어 컴포넌트.',
47
42
  tag: 'A11y AAA',
43
+ price: '$19',
48
44
  icon: 'layers',
49
- stats: { views: '9.8k', rating: 4.8 },
50
- date: '2026.10',
51
45
  },
52
46
  {
53
47
  id: 'c3',
54
48
  category: 'Pattern',
55
- title: 'B2B 핀테크 인증 및 결제 승인 플로우',
56
- description: '생체 인증, 간편 비밀번호, 카드 CVC 실시간 유효성 검증과 가맹점 수수료율 자동 산출 UX.',
49
+ title: 'B2B 핀테크 인증 & 결제 승인 플로우',
50
+ description: '실시간 유효성 검증과 가맹점 수수료율 자동 산출 반응형 위젯.',
57
51
  tag: 'Fintech UX',
52
+ price: '$39',
58
53
  icon: 'shield',
59
- stats: { views: '18.5k', rating: 5.0 },
60
- date: '2026.11',
61
54
  },
62
55
  {
63
56
  id: 'c4',
64
57
  category: 'Component',
65
- title: '실시간 가변 폰트 & 타이포 스케일러',
66
- description: 'Pretendard Variable 기반의 유동적(Fluid) 폰트 크기 및 행간(Line-height) 자동 보정 시스템.',
58
+ title: '가변 폰트 & 타이포 스케일러',
59
+ description: 'Fluid 폰트 크기 및 행간을 토큰으로 제어하는 타이포그래피 블록.',
67
60
  tag: 'Typography',
61
+ price: 'Free',
68
62
  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
63
  },
92
64
  ];
93
65
 
94
66
  export function CardListBlock() {
95
67
  const [cards, setCards] = useState<CardItem[]>(INITIAL_CARDS);
96
- const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
97
68
  const [selectedCategory, setSelectedCategory] = useState<string>('All');
69
+ const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
98
70
  const [searchQuery, setSearchQuery] = useState('');
99
71
 
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;
72
+ const filteredCards = cards.filter((c) => {
73
+ const matchesCat = selectedCategory === 'All' || c.category === selectedCategory;
108
74
  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;
75
+ c.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
76
+ c.description.toLowerCase().includes(searchQuery.toLowerCase());
77
+ return matchesCat && matchesSearch;
113
78
  });
114
79
 
115
80
  const getIcon = (type: CardItem['icon']) => {
@@ -119,191 +84,131 @@ export function CardListBlock() {
119
84
  case 'layers':
120
85
  return <Layers className="w-4 h-4 text-blue-500" />;
121
86
  case 'shield':
122
- return <ShieldCheck className="w-4 h-4 text-emerald-500" />;
87
+ return <Shield className="w-4 h-4 text-emerald-500" />;
123
88
  case 'zap':
124
89
  return <Zap className="w-4 h-4 text-amber-500" />;
125
90
  }
126
91
  };
127
92
 
128
93
  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">
94
+ <Card className="overflow-hidden transition-colors">
95
+ {/* 1. Filter Toolbar composed with Button components */}
96
+ <div className="p-4 border-b border-(--border-subtle) bg-(--bg-secondary)/20 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
97
+ {/* Category Pills */}
98
+ <div className="flex items-center gap-1 overflow-x-auto scrollbar-none">
134
99
  {['All', 'Foundation', 'Component', 'Pattern'].map((cat) => (
135
- <button
100
+ <Button
136
101
  key={cat}
102
+ variant={selectedCategory === cat ? 'primary' : 'ghost'}
103
+ size="sm"
137
104
  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
- }`}
105
+ className="text-xs py-1 px-2.5 whitespace-nowrap"
143
106
  >
144
107
  {cat === 'All' ? '전체' : cat}
145
- </button>
108
+ </Button>
146
109
  ))}
147
110
  </div>
148
111
 
149
- {/* Search & View Mode Switcher */}
112
+ {/* View mode & Search */}
150
113
  <div className="flex items-center gap-2">
151
114
  <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)" />
115
+ <Search className="w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-(--text-secondary)" />
153
116
  <input
154
117
  type="text"
155
118
  value={searchQuery}
156
119
  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"
120
+ placeholder="카드 필터..."
121
+ className="text-xs pl-7 pr-2.5 py-1 rounded-(--radius-sm) bg-(--bg-secondary) text-(--text-primary) border border-(--border-subtle) focus:outline-none"
159
122
  />
160
123
  </div>
161
124
 
162
- <div className="flex items-center p-0.5 rounded-lg bg-(--bg-secondary) border border-(--border-subtle)">
125
+ <div className="flex rounded-(--radius-sm) border border-(--border-subtle) p-0.5 bg-(--bg-secondary)">
163
126
  <button
164
127
  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)'
128
+ aria-label="Grid"
129
+ className={`p-1 rounded-(--radius-sm) transition-colors ${
130
+ viewMode === 'grid' ? 'bg-(--bg-primary) text-(--text-primary) shadow-xs' : 'text-(--text-secondary)'
170
131
  }`}
171
132
  >
172
- <LayoutGrid className="w-4 h-4" />
133
+ <LayoutGrid className="w-3.5 h-3.5" />
173
134
  </button>
174
135
  <button
175
136
  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)'
137
+ aria-label="List"
138
+ className={`p-1 rounded-(--radius-sm) transition-colors ${
139
+ viewMode === 'list' ? 'bg-(--bg-primary) text-(--text-primary) shadow-xs' : 'text-(--text-secondary)'
181
140
  }`}
182
141
  >
183
- <ListIcon className="w-4 h-4" />
142
+ <ListIcon className="w-3.5 h-3.5" />
184
143
  </button>
185
144
  </div>
186
145
  </div>
187
146
  </div>
188
147
 
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>
148
+ {/* 2. Cards Viewport composed with UI Card & Badge */}
149
+ <div className="p-4 sm:p-5">
150
+ {viewMode === 'grid' ? (
151
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3.5">
152
+ {filteredCards.map((c) => (
153
+ <Card
154
+ key={c.id}
155
+ interactive
156
+ className="p-4 flex flex-col justify-between"
157
+ >
261
158
  <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>
159
+ <div className="flex items-center justify-between mb-2.5">
160
+ <div className="p-2 rounded-(--radius-sm) bg-(--bg-secondary) border border-(--border-subtle)">
161
+ {getIcon(c.icon)}
162
+ </div>
163
+ <Badge variant="neutral" className="text-[11px]">
164
+ {c.tag}
165
+ </Badge>
267
166
  </div>
268
- <h4 className="text-sm font-bold text-(--text-primary) group-hover:text-(--accent-primary) transition-colors">
269
- {card.title}
167
+ <h4 className="font-semibold text-xs text-(--text-primary) line-clamp-1 mb-1">
168
+ {c.title}
270
169
  </h4>
271
- <p className="text-xs text-(--text-secondary) mt-0.5 line-clamp-1 max-w-xl">
272
- {card.description}
170
+ <p className="text-xs text-(--text-secondary) line-clamp-2 leading-relaxed">
171
+ {c.description}
273
172
  </p>
274
173
  </div>
275
- </div>
276
174
 
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>
175
+ <div className="pt-3 border-t border-(--border-subtle) flex items-center justify-between mt-3 text-xs">
176
+ <span className="font-semibold text-(--text-primary)">{c.price}</span>
177
+ <button className="text-xs font-medium text-(--accent-primary) hover:underline flex items-center gap-0.5">
178
+ <span>선택</span>
299
179
  <ArrowUpRight className="w-3 h-3" />
300
180
  </button>
301
181
  </div>
302
- </div>
303
- </div>
304
- ))}
305
- </div>
306
- )}
307
- </div>
182
+ </Card>
183
+ ))}
184
+ </div>
185
+ ) : (
186
+ <div className="space-y-2">
187
+ {filteredCards.map((c) => (
188
+ <Card
189
+ key={c.id}
190
+ className="p-3 px-4 flex items-center justify-between transition-colors gap-3"
191
+ >
192
+ <div className="flex items-center gap-3">
193
+ <div className="p-1.5 rounded-(--radius-sm) bg-(--bg-secondary) border border-(--border-subtle)">
194
+ {getIcon(c.icon)}
195
+ </div>
196
+ <div>
197
+ <div className="font-semibold text-xs text-(--text-primary)">{c.title}</div>
198
+ <div className="text-[11px] text-(--text-secondary) line-clamp-1">{c.description}</div>
199
+ </div>
200
+ </div>
201
+ <div className="flex items-center gap-3 shrink-0 text-xs">
202
+ <span className="font-semibold text-(--text-primary)">{c.price}</span>
203
+ <Button variant="secondary" size="sm" className="text-xs py-1 px-2.5">
204
+ 상세보기
205
+ </Button>
206
+ </div>
207
+ </Card>
208
+ ))}
209
+ </div>
210
+ )}
211
+ </div>
212
+ </Card>
308
213
  );
309
214
  }