atomsystem 0.4.1 → 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.
Files changed (38) hide show
  1. package/README.md +112 -52
  2. package/atom.design.md +17 -0
  3. package/components/blocks/BlockDetailPageContent.tsx +337 -0
  4. package/components/blocks/BlockViewerCard.tsx +229 -0
  5. package/components/blocks/BlocksSidebar.tsx +93 -0
  6. package/components/blocks/CardListBlock.tsx +214 -0
  7. package/components/blocks/FileExplorerBlock.tsx +203 -0
  8. package/components/blocks/InteractiveChatBlock.tsx +217 -0
  9. package/components/blocks/MetricStatsBlock.tsx +150 -0
  10. package/components/blocks/PricingPlanBlock.tsx +175 -0
  11. package/components/blocks/RealtimeChartBlock.tsx +242 -0
  12. package/components/blocks/SliderShowcaseBlock.tsx +136 -0
  13. package/components/blocks/index.ts +8 -0
  14. package/components/ui/Container.tsx +45 -0
  15. package/components/ui/Layout.tsx +220 -0
  16. package/components/ui/Switch.tsx +39 -0
  17. package/components/ui/Table.tsx +88 -0
  18. package/components/ui/Tabs.tsx +58 -0
  19. package/components/ui/Tree.tsx +121 -0
  20. package/components/ui/index.ts +6 -0
  21. package/css/atom-ui.css +41 -716
  22. package/dist/cli.cjs +633 -41
  23. package/dist/cli.cjs.map +1 -1
  24. package/dist/cli.js +630 -39
  25. package/dist/cli.js.map +1 -1
  26. package/dist/index.cjs +265 -2
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +13 -4
  29. package/dist/index.d.ts +13 -4
  30. package/dist/index.js +265 -3
  31. package/dist/index.js.map +1 -1
  32. package/package.json +7 -1
  33. package/presets/agents.guide.md +49 -0
  34. package/presets/blocks.md +101 -0
  35. package/presets/components.md +142 -0
  36. package/presets/default.design.md +17 -0
  37. package/tokens/token.json +63 -1
  38. package/tokens/tokens.json +63 -1
@@ -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
+ Terminal,
13
+ } from 'lucide-react';
14
+ import { BlockItem } from '@/lib/data/blocks';
15
+ import { CodeHighlight } from '@/components/common/CodeHighlight';
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 [copiedCode, setCopiedCode] = useState(false);
26
+ const [copiedCli, setCopiedCli] = useState(false);
27
+
28
+ const cliCommand = `npx atomsystem add ${block.id}`;
29
+
30
+ const handleCopyCode = (code: string) => {
31
+ navigator.clipboard.writeText(code);
32
+ setCopiedCode(true);
33
+ setTimeout(() => setCopiedCode(false), 2000);
34
+ };
35
+
36
+ const handleCopyCli = () => {
37
+ navigator.clipboard.writeText(cliCommand);
38
+ setCopiedCli(true);
39
+ setTimeout(() => setCopiedCli(false), 2000);
40
+ };
41
+
42
+ const getViewportMaxWidth = () => {
43
+ switch (viewport) {
44
+ case 'mobile':
45
+ return 'max-w-[390px]';
46
+ case 'tablet':
47
+ return 'max-w-[768px]';
48
+ default:
49
+ return 'max-w-full';
50
+ }
51
+ };
52
+
53
+ return (
54
+ <div
55
+ id={block.id}
56
+ className="scroll-mt-20 rounded-lg bg-(--bg-primary) border border-(--border-subtle) overflow-hidden"
57
+ >
58
+ {/* 1. Shadcn Block Toolbar */}
59
+ <div className="px-5 py-3 border-b border-(--border-subtle) bg-(--bg-secondary)/40 flex flex-wrap items-center justify-between gap-3">
60
+ {/* Left: ID & Title */}
61
+ <div className="flex items-center gap-3">
62
+ <span className="font-mono text-xs font-semibold text-(--text-primary) px-2 py-0.5 rounded border border-(--border-subtle) bg-(--bg-primary)">
63
+ {block.id}
64
+ </span>
65
+ <h2 className="text-sm font-semibold text-(--text-primary)">
66
+ {block.title}
67
+ </h2>
68
+ </div>
69
+
70
+ {/* Right Toolbar */}
71
+ <div className="flex items-center gap-2">
72
+ {/* Viewport Resizer */}
73
+ {activeTab === 'preview' && (
74
+ <div className="hidden sm:flex items-center gap-0.5 bg-(--bg-primary) p-0.5 rounded border border-(--border-subtle)">
75
+ <button
76
+ onClick={() => setViewport('desktop')}
77
+ title="Desktop 100%"
78
+ className={`p-1.5 rounded transition-colors ${
79
+ viewport === 'desktop'
80
+ ? 'bg-(--bg-secondary) text-(--text-primary)'
81
+ : 'text-(--text-secondary) hover:text-(--text-primary)'
82
+ }`}
83
+ >
84
+ <Monitor className="w-3.5 h-3.5" />
85
+ </button>
86
+ <button
87
+ onClick={() => setViewport('tablet')}
88
+ title="Tablet 768px"
89
+ className={`p-1.5 rounded transition-colors ${
90
+ viewport === 'tablet'
91
+ ? 'bg-(--bg-secondary) text-(--text-primary)'
92
+ : 'text-(--text-secondary) hover:text-(--text-primary)'
93
+ }`}
94
+ >
95
+ <Tablet className="w-3.5 h-3.5" />
96
+ </button>
97
+ <button
98
+ onClick={() => setViewport('mobile')}
99
+ title="Mobile 390px"
100
+ className={`p-1.5 rounded transition-colors ${
101
+ viewport === 'mobile'
102
+ ? 'bg-(--bg-secondary) text-(--text-primary)'
103
+ : 'text-(--text-secondary) hover:text-(--text-primary)'
104
+ }`}
105
+ >
106
+ <Smartphone className="w-3.5 h-3.5" />
107
+ </button>
108
+ </div>
109
+ )}
110
+
111
+ {/* Preview / Code Tabs */}
112
+ <div className="flex items-center gap-1 bg-(--bg-primary) p-0.5 rounded border border-(--border-subtle)">
113
+ <button
114
+ onClick={() => setActiveTab('preview')}
115
+ data-tab="preview"
116
+ className={`flex items-center gap-1 px-2.5 py-1 rounded text-xs font-medium transition-colors ${
117
+ activeTab === 'preview'
118
+ ? 'bg-(--bg-secondary) text-(--text-primary) font-semibold'
119
+ : 'text-(--text-secondary) hover:text-(--text-primary)'
120
+ }`}
121
+ >
122
+ <Eye className="w-3.5 h-3.5" />
123
+ <span>Preview</span>
124
+ </button>
125
+ <button
126
+ onClick={() => setActiveTab('react')}
127
+ data-tab="react"
128
+ className={`flex items-center gap-1 px-2.5 py-1 rounded text-xs font-medium transition-colors ${
129
+ activeTab === 'react'
130
+ ? 'bg-(--bg-secondary) text-(--text-primary) font-semibold'
131
+ : 'text-(--text-secondary) hover:text-(--text-primary)'
132
+ }`}
133
+ >
134
+ <Code2 className="w-3.5 h-3.5" />
135
+ <span>React</span>
136
+ </button>
137
+ <button
138
+ onClick={() => setActiveTab('html')}
139
+ data-tab="html"
140
+ className={`flex items-center gap-1 px-2.5 py-1 rounded text-xs font-medium transition-colors ${
141
+ activeTab === 'html'
142
+ ? 'bg-(--bg-secondary) text-(--text-primary) font-semibold'
143
+ : 'text-(--text-secondary) hover:text-(--text-primary)'
144
+ }`}
145
+ >
146
+ <Code2 className="w-3.5 h-3.5" />
147
+ <span>HTML</span>
148
+ </button>
149
+ </div>
150
+
151
+ {/* CLI Add Button */}
152
+ <button
153
+ onClick={handleCopyCli}
154
+ title={cliCommand}
155
+ className="hidden lg:flex items-center gap-1 px-2.5 py-1 rounded text-xs font-mono bg-(--bg-primary) hover:bg-(--bg-secondary) text-(--text-secondary) border border-(--border-subtle) transition-colors"
156
+ >
157
+ <Terminal className="w-3 h-3 text-(--text-primary)" />
158
+ <span>{copiedCli ? 'Copied' : cliCommand}</span>
159
+ </button>
160
+
161
+ {/* Copy Code Button */}
162
+ <button
163
+ onClick={() =>
164
+ handleCopyCode(
165
+ activeTab === 'html' ? block.codeSnippets.htmlCss : block.codeSnippets.react
166
+ )
167
+ }
168
+ className="flex items-center gap-1 px-3 py-1 rounded text-xs font-medium bg-(--bg-primary) hover:bg-(--bg-secondary) text-(--text-primary) border border-(--border-subtle) transition-colors"
169
+ >
170
+ {copiedCode ? (
171
+ <>
172
+ <Check className="w-3.5 h-3.5 text-emerald-600" />
173
+ <span className="text-emerald-600 font-semibold">Copied</span>
174
+ </>
175
+ ) : (
176
+ <>
177
+ <Copy className="w-3.5 h-3.5 text-(--text-secondary)" />
178
+ <span>Copy Code</span>
179
+ </>
180
+ )}
181
+ </button>
182
+ </div>
183
+ </div>
184
+
185
+ {/* 2. Main Viewport */}
186
+ <div className="p-4 sm:p-6 bg-(--bg-secondary)/30 flex justify-center items-center">
187
+ {activeTab === 'preview' ? (
188
+ <div
189
+ className={`w-full transition-all duration-300 mx-auto ${getViewportMaxWidth()} ${
190
+ viewport !== 'desktop'
191
+ ? 'border border-(--border-strong) rounded-lg p-2 bg-(--bg-primary) shadow-sm'
192
+ : ''
193
+ }`}
194
+ >
195
+ {children}
196
+ </div>
197
+ ) : (
198
+ <div className="w-full bg-(--bg-primary) rounded-md border border-(--border-subtle) overflow-hidden">
199
+ <div className="px-4 py-2 border-b border-(--border-subtle) bg-(--bg-secondary)/50 text-xs font-mono text-(--text-secondary) flex justify-between items-center">
200
+ <span>{activeTab === 'react' ? `${block.id}.tsx` : `${block.id}.html`}</span>
201
+ <span>{activeTab === 'react' ? 'React + Tailwind TSX' : 'HTML / Tailwind'}</span>
202
+ </div>
203
+ <CodeHighlight
204
+ code={activeTab === 'react' ? block.codeSnippets.react : block.codeSnippets.htmlCss}
205
+ language={activeTab === 'react' ? 'tsx' : 'html'}
206
+ className="p-4 max-h-[480px]"
207
+ />
208
+ </div>
209
+ )}
210
+ </div>
211
+
212
+ {/* 3. Block Meta Footer */}
213
+ <div className="px-5 py-2.5 bg-(--bg-primary) border-t border-(--border-subtle) flex flex-wrap items-center justify-between gap-2 text-xs text-(--text-secondary)">
214
+ <p className="line-clamp-1 max-w-xl">{block.description}</p>
215
+ <div className="flex items-center gap-1.5 shrink-0">
216
+ <span className="text-(--text-muted)">Tokens:</span>
217
+ {block.tokensUsed.map((token) => (
218
+ <span
219
+ key={token}
220
+ className="font-mono text-xs px-1.5 py-0.5 rounded bg-(--bg-secondary) text-(--text-primary) border border-(--border-subtle)"
221
+ >
222
+ {token}
223
+ </span>
224
+ ))}
225
+ </div>
226
+ </div>
227
+ </div>
228
+ );
229
+ }
@@ -0,0 +1,93 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { useThemeEditor } from '@/components/editor/ThemeEditorProvider';
6
+ import { BLOCKS_COLLECTION, BLOCK_CATEGORIES, BlockItem } from '@/lib/data/blocks';
7
+ import { Search, Layers, ChevronRight } from 'lucide-react';
8
+
9
+ interface BlocksSidebarProps {
10
+ activeId: string;
11
+ }
12
+
13
+ export function BlocksSidebar({ activeId }: BlocksSidebarProps) {
14
+ const { theme } = useThemeEditor();
15
+ const brandColor = theme?.color?.primary || 'var(--accent-primary)';
16
+ const [searchQuery, setSearchQuery] = useState('');
17
+
18
+ const filteredBlocks = BLOCKS_COLLECTION.filter((block) => {
19
+ if (!searchQuery.trim()) return true;
20
+ const q = searchQuery.toLowerCase();
21
+ return (
22
+ block.title.toLowerCase().includes(q) ||
23
+ block.englishTitle.toLowerCase().includes(q) ||
24
+ block.id.toLowerCase().includes(q)
25
+ );
26
+ });
27
+
28
+ return (
29
+ <aside className="w-64 border-r border-(--border-subtle) bg-(--bg-primary) p-4 flex flex-col shrink-0 h-full overflow-y-auto">
30
+ {/* 1. Header & Search */}
31
+ <div className="mb-4 space-y-2">
32
+ <div className="flex items-center justify-between">
33
+ <Link
34
+ href="/blocks"
35
+ className="flex items-center gap-1.5 text-xs font-semibold text-(--text-primary) hover:text-(--accent-primary) transition-colors"
36
+ >
37
+ <Layers className="w-3.5 h-3.5" />
38
+ <span>디자인 블록 카탈로그</span>
39
+ </Link>
40
+ <span className="font-mono text-[10px] px-1.5 py-0.5 rounded-(--radius-sm) bg-(--bg-secondary) text-(--text-secondary) border border-(--border-subtle)">
41
+ {BLOCKS_COLLECTION.length}
42
+ </span>
43
+ </div>
44
+
45
+ <div className="relative">
46
+ <Search className="w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-(--text-secondary)" />
47
+ <input
48
+ type="text"
49
+ value={searchQuery}
50
+ onChange={(e) => setSearchQuery(e.target.value)}
51
+ placeholder="블록 검색..."
52
+ className="w-full text-xs pl-7 pr-2.5 py-1.5 rounded-(--radius-sm) bg-(--bg-secondary) text-(--text-primary) border border-(--border-subtle) focus:outline-none focus:border-(--border-strong) transition-colors"
53
+ />
54
+ </div>
55
+ </div>
56
+
57
+ {/* 2. Block List by Category */}
58
+ <div className="space-y-4 text-xs">
59
+ {BLOCK_CATEGORIES.filter((cat) => cat.id !== 'all').map((cat) => {
60
+ const categoryBlocks = filteredBlocks.filter((b) => b.category === cat.id);
61
+ if (categoryBlocks.length === 0) return null;
62
+
63
+ return (
64
+ <div key={cat.id} className="space-y-1">
65
+ <div className="text-[11px] font-mono font-medium text-(--text-secondary) uppercase tracking-wider px-2 py-0.5">
66
+ {cat.name}
67
+ </div>
68
+ <div className="space-y-0.5">
69
+ {categoryBlocks.map((block) => {
70
+ const isActive = activeId === block.id;
71
+ return (
72
+ <Link
73
+ key={block.id}
74
+ href={`/blocks/${block.id}`}
75
+ className={`w-full flex items-center justify-between px-2.5 py-1.5 rounded-(--radius-sm) text-xs transition-colors ${
76
+ isActive
77
+ ? 'bg-(--text-primary) text-(--bg-primary) font-medium'
78
+ : 'text-(--text-secondary) hover:text-(--text-primary) hover:bg-(--bg-secondary)'
79
+ }`}
80
+ >
81
+ <span className="truncate">{block.title}</span>
82
+ <ChevronRight className={`w-3 h-3 shrink-0 ${isActive ? 'text-(--bg-primary)' : 'opacity-30'}`} />
83
+ </Link>
84
+ );
85
+ })}
86
+ </div>
87
+ </div>
88
+ );
89
+ })}
90
+ </div>
91
+ </aside>
92
+ );
93
+ }
@@ -0,0 +1,214 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import { Card, Button, Badge } from '@/components/ui';
5
+ import {
6
+ LayoutGrid,
7
+ List as ListIcon,
8
+ Search,
9
+ Bookmark,
10
+ ArrowUpRight,
11
+ Palette,
12
+ Layers,
13
+ Shield,
14
+ Zap,
15
+ } from 'lucide-react';
16
+
17
+ interface CardItem {
18
+ id: string;
19
+ category: 'Foundation' | 'Component' | 'Pattern';
20
+ title: string;
21
+ description: string;
22
+ tag: string;
23
+ price: string;
24
+ icon: 'palette' | 'layers' | 'shield' | 'zap';
25
+ }
26
+
27
+ const INITIAL_CARDS: CardItem[] = [
28
+ {
29
+ id: 'c1',
30
+ category: 'Foundation',
31
+ title: '시맨틱 색상 & 다크모드 매트릭스',
32
+ description: 'W3C DTCG 표준 3단계 계층 구조를 준수하는 토큰 기반 컬러 시스템.',
33
+ tag: 'W3C 표준',
34
+ price: 'Free',
35
+ icon: 'palette',
36
+ },
37
+ {
38
+ id: 'c2',
39
+ category: 'Component',
40
+ title: '고접근성 인터랙티브 모달 & 드로어',
41
+ description: '포커스 트랩과 키보드 내비게이션을 보장하는 시맨틱 레이어 컴포넌트.',
42
+ tag: 'A11y AAA',
43
+ price: '$19',
44
+ icon: 'layers',
45
+ },
46
+ {
47
+ id: 'c3',
48
+ category: 'Pattern',
49
+ title: 'B2B 핀테크 인증 & 결제 승인 플로우',
50
+ description: '실시간 유효성 검증과 가맹점 수수료율 자동 산출 반응형 위젯.',
51
+ tag: 'Fintech UX',
52
+ price: '$39',
53
+ icon: 'shield',
54
+ },
55
+ {
56
+ id: 'c4',
57
+ category: 'Component',
58
+ title: '가변 폰트 & 타이포 스케일러',
59
+ description: 'Fluid 폰트 크기 및 행간을 토큰으로 제어하는 타이포그래피 블록.',
60
+ tag: 'Typography',
61
+ price: 'Free',
62
+ icon: 'zap',
63
+ },
64
+ ];
65
+
66
+ export function CardListBlock() {
67
+ const [cards, setCards] = useState<CardItem[]>(INITIAL_CARDS);
68
+ const [selectedCategory, setSelectedCategory] = useState<string>('All');
69
+ const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
70
+ const [searchQuery, setSearchQuery] = useState('');
71
+
72
+ const filteredCards = cards.filter((c) => {
73
+ const matchesCat = selectedCategory === 'All' || c.category === selectedCategory;
74
+ const matchesSearch =
75
+ c.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
76
+ c.description.toLowerCase().includes(searchQuery.toLowerCase());
77
+ return matchesCat && matchesSearch;
78
+ });
79
+
80
+ const getIcon = (type: CardItem['icon']) => {
81
+ switch (type) {
82
+ case 'palette':
83
+ return <Palette className="w-4 h-4 text-purple-500" />;
84
+ case 'layers':
85
+ return <Layers className="w-4 h-4 text-blue-500" />;
86
+ case 'shield':
87
+ return <Shield className="w-4 h-4 text-emerald-500" />;
88
+ case 'zap':
89
+ return <Zap className="w-4 h-4 text-amber-500" />;
90
+ }
91
+ };
92
+
93
+ return (
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">
99
+ {['All', 'Foundation', 'Component', 'Pattern'].map((cat) => (
100
+ <Button
101
+ key={cat}
102
+ variant={selectedCategory === cat ? 'primary' : 'ghost'}
103
+ size="sm"
104
+ onClick={() => setSelectedCategory(cat)}
105
+ className="text-xs py-1 px-2.5 whitespace-nowrap"
106
+ >
107
+ {cat === 'All' ? '전체' : cat}
108
+ </Button>
109
+ ))}
110
+ </div>
111
+
112
+ {/* View mode & Search */}
113
+ <div className="flex items-center gap-2">
114
+ <div className="relative">
115
+ <Search className="w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-(--text-secondary)" />
116
+ <input
117
+ type="text"
118
+ value={searchQuery}
119
+ onChange={(e) => setSearchQuery(e.target.value)}
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"
122
+ />
123
+ </div>
124
+
125
+ <div className="flex rounded-(--radius-sm) border border-(--border-subtle) p-0.5 bg-(--bg-secondary)">
126
+ <button
127
+ onClick={() => setViewMode('grid')}
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)'
131
+ }`}
132
+ >
133
+ <LayoutGrid className="w-3.5 h-3.5" />
134
+ </button>
135
+ <button
136
+ onClick={() => setViewMode('list')}
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)'
140
+ }`}
141
+ >
142
+ <ListIcon className="w-3.5 h-3.5" />
143
+ </button>
144
+ </div>
145
+ </div>
146
+ </div>
147
+
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
+ >
158
+ <div>
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>
166
+ </div>
167
+ <h4 className="font-semibold text-xs text-(--text-primary) line-clamp-1 mb-1">
168
+ {c.title}
169
+ </h4>
170
+ <p className="text-xs text-(--text-secondary) line-clamp-2 leading-relaxed">
171
+ {c.description}
172
+ </p>
173
+ </div>
174
+
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>
179
+ <ArrowUpRight className="w-3 h-3" />
180
+ </button>
181
+ </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>
213
+ );
214
+ }