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.
- 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
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
Monitor,
|
|
10
10
|
Tablet,
|
|
11
11
|
Smartphone,
|
|
12
|
-
|
|
13
|
-
Tag,
|
|
12
|
+
Terminal,
|
|
14
13
|
} from 'lucide-react';
|
|
15
14
|
import { BlockItem } from '@/lib/data/blocks';
|
|
15
|
+
import { CodeHighlight } from '@/components/common/CodeHighlight';
|
|
16
16
|
|
|
17
17
|
interface BlockViewerCardProps {
|
|
18
18
|
block: BlockItem;
|
|
@@ -22,12 +22,21 @@ interface BlockViewerCardProps {
|
|
|
22
22
|
export function BlockViewerCard({ block, children }: BlockViewerCardProps) {
|
|
23
23
|
const [activeTab, setActiveTab] = useState<'preview' | 'react' | 'html'>('preview');
|
|
24
24
|
const [viewport, setViewport] = useState<'desktop' | 'tablet' | 'mobile'>('desktop');
|
|
25
|
-
const [
|
|
25
|
+
const [copiedCode, setCopiedCode] = useState(false);
|
|
26
|
+
const [copiedCli, setCopiedCli] = useState(false);
|
|
26
27
|
|
|
27
|
-
const
|
|
28
|
+
const cliCommand = `npx atomsystem add ${block.id}`;
|
|
29
|
+
|
|
30
|
+
const handleCopyCode = (code: string) => {
|
|
28
31
|
navigator.clipboard.writeText(code);
|
|
29
|
-
|
|
30
|
-
setTimeout(() =>
|
|
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);
|
|
31
40
|
};
|
|
32
41
|
|
|
33
42
|
const getViewportMaxWidth = () => {
|
|
@@ -44,130 +53,54 @@ export function BlockViewerCard({ block, children }: BlockViewerCardProps) {
|
|
|
44
53
|
return (
|
|
45
54
|
<div
|
|
46
55
|
id={block.id}
|
|
47
|
-
className="scroll-mt-20 rounded-
|
|
56
|
+
className="scroll-mt-20 rounded-lg bg-(--bg-primary) border border-(--border-subtle) overflow-hidden"
|
|
48
57
|
>
|
|
49
|
-
{/* Block
|
|
50
|
-
<div className="
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
))}
|
|
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>
|
|
93
68
|
</div>
|
|
94
|
-
</div>
|
|
95
69
|
|
|
96
|
-
|
|
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 */}
|
|
70
|
+
{/* Right Toolbar */}
|
|
139
71
|
<div className="flex items-center gap-2">
|
|
72
|
+
{/* Viewport Resizer */}
|
|
140
73
|
{activeTab === 'preview' && (
|
|
141
|
-
<div className="hidden sm:flex items-center gap-0.5 bg-(--bg-primary) p-0.5 rounded
|
|
74
|
+
<div className="hidden sm:flex items-center gap-0.5 bg-(--bg-primary) p-0.5 rounded border border-(--border-subtle)">
|
|
142
75
|
<button
|
|
143
76
|
onClick={() => setViewport('desktop')}
|
|
144
|
-
title="
|
|
145
|
-
className={`p-1.5 rounded
|
|
77
|
+
title="Desktop 100%"
|
|
78
|
+
className={`p-1.5 rounded transition-colors ${
|
|
146
79
|
viewport === 'desktop'
|
|
147
80
|
? 'bg-(--bg-secondary) text-(--text-primary)'
|
|
148
|
-
: 'text-(--text-
|
|
81
|
+
: 'text-(--text-secondary) hover:text-(--text-primary)'
|
|
149
82
|
}`}
|
|
150
83
|
>
|
|
151
84
|
<Monitor className="w-3.5 h-3.5" />
|
|
152
85
|
</button>
|
|
153
86
|
<button
|
|
154
87
|
onClick={() => setViewport('tablet')}
|
|
155
|
-
title="
|
|
156
|
-
className={`p-1.5 rounded
|
|
88
|
+
title="Tablet 768px"
|
|
89
|
+
className={`p-1.5 rounded transition-colors ${
|
|
157
90
|
viewport === 'tablet'
|
|
158
91
|
? 'bg-(--bg-secondary) text-(--text-primary)'
|
|
159
|
-
: 'text-(--text-
|
|
92
|
+
: 'text-(--text-secondary) hover:text-(--text-primary)'
|
|
160
93
|
}`}
|
|
161
94
|
>
|
|
162
95
|
<Tablet className="w-3.5 h-3.5" />
|
|
163
96
|
</button>
|
|
164
97
|
<button
|
|
165
98
|
onClick={() => setViewport('mobile')}
|
|
166
|
-
title="
|
|
167
|
-
className={`p-1.5 rounded
|
|
99
|
+
title="Mobile 390px"
|
|
100
|
+
className={`p-1.5 rounded transition-colors ${
|
|
168
101
|
viewport === 'mobile'
|
|
169
102
|
? 'bg-(--bg-secondary) text-(--text-primary)'
|
|
170
|
-
: 'text-(--text-
|
|
103
|
+
: 'text-(--text-secondary) hover:text-(--text-primary)'
|
|
171
104
|
}`}
|
|
172
105
|
>
|
|
173
106
|
<Smartphone className="w-3.5 h-3.5" />
|
|
@@ -175,55 +108,122 @@ export function BlockViewerCard({ block, children }: BlockViewerCardProps) {
|
|
|
175
108
|
</div>
|
|
176
109
|
)}
|
|
177
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 */}
|
|
178
162
|
<button
|
|
179
163
|
onClick={() =>
|
|
180
|
-
|
|
164
|
+
handleCopyCode(
|
|
181
165
|
activeTab === 'html' ? block.codeSnippets.htmlCss : block.codeSnippets.react
|
|
182
166
|
)
|
|
183
167
|
}
|
|
184
|
-
className="flex items-center gap-1 px-3 py-1
|
|
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"
|
|
185
169
|
>
|
|
186
|
-
{
|
|
170
|
+
{copiedCode ? (
|
|
187
171
|
<>
|
|
188
|
-
<Check className="w-3.5 h-3.5 text-emerald-
|
|
189
|
-
<span className="text-emerald-
|
|
172
|
+
<Check className="w-3.5 h-3.5 text-emerald-600" />
|
|
173
|
+
<span className="text-emerald-600 font-semibold">Copied</span>
|
|
190
174
|
</>
|
|
191
175
|
) : (
|
|
192
176
|
<>
|
|
193
|
-
<Copy className="w-3.5 h-3.5 text-(--text-
|
|
194
|
-
<span
|
|
177
|
+
<Copy className="w-3.5 h-3.5 text-(--text-secondary)" />
|
|
178
|
+
<span>Copy Code</span>
|
|
195
179
|
</>
|
|
196
180
|
)}
|
|
197
181
|
</button>
|
|
198
182
|
</div>
|
|
199
183
|
</div>
|
|
200
184
|
|
|
201
|
-
{/* Main
|
|
202
|
-
<div className="p-4 sm:p-
|
|
185
|
+
{/* 2. Main Viewport */}
|
|
186
|
+
<div className="p-4 sm:p-6 bg-(--bg-secondary)/30 flex justify-center items-center">
|
|
203
187
|
{activeTab === 'preview' ? (
|
|
204
188
|
<div
|
|
205
189
|
className={`w-full transition-all duration-300 mx-auto ${getViewportMaxWidth()} ${
|
|
206
190
|
viewport !== 'desktop'
|
|
207
|
-
? 'border
|
|
191
|
+
? 'border border-(--border-strong) rounded-lg p-2 bg-(--bg-primary) shadow-sm'
|
|
208
192
|
: ''
|
|
209
193
|
}`}
|
|
210
194
|
>
|
|
211
195
|
{children}
|
|
212
196
|
</div>
|
|
213
197
|
) : (
|
|
214
|
-
<div className="w-full bg-(--bg-primary) rounded-
|
|
215
|
-
<div className="px-4 py-2 border-b border-(--border-subtle) bg-(--bg-secondary)/50 text-
|
|
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">
|
|
216
200
|
<span>{activeTab === 'react' ? `${block.id}.tsx` : `${block.id}.html`}</span>
|
|
217
|
-
<span>{activeTab === 'react' ? '
|
|
201
|
+
<span>{activeTab === 'react' ? 'React + Tailwind TSX' : 'HTML / Tailwind'}</span>
|
|
218
202
|
</div>
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
+
/>
|
|
224
208
|
</div>
|
|
225
209
|
)}
|
|
226
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
227
|
</div>
|
|
228
228
|
);
|
|
229
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
|
+
}
|