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.
@@ -9,10 +9,10 @@ import {
9
9
  Monitor,
10
10
  Tablet,
11
11
  Smartphone,
12
- ExternalLink,
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 [copied, setCopied] = useState(false);
25
+ const [copiedCode, setCopiedCode] = useState(false);
26
+ const [copiedCli, setCopiedCli] = useState(false);
26
27
 
27
- const handleCopy = (code: string) => {
28
+ const cliCommand = `npx atomsystem add ${block.id}`;
29
+
30
+ const handleCopyCode = (code: string) => {
28
31
  navigator.clipboard.writeText(code);
29
- setCopied(true);
30
- setTimeout(() => setCopied(false), 2000);
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-2xl bg-(--bg-primary) border border-(--border-subtle) shadow-xs overflow-hidden transition-all duration-200"
56
+ className="scroll-mt-20 rounded-lg bg-(--bg-primary) border border-(--border-subtle) overflow-hidden"
48
57
  >
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
- ))}
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
- {/* 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 */}
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-lg border border-(--border-subtle)">
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="데스크톱 뷰 (100%)"
145
- className={`p-1.5 rounded-md transition-colors ${
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-muted) hover:text-(--text-primary)'
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="태블릿 뷰 (768px)"
156
- className={`p-1.5 rounded-md transition-colors ${
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-muted) hover:text-(--text-primary)'
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="모바일 뷰 (390px)"
167
- className={`p-1.5 rounded-md transition-colors ${
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-muted) hover:text-(--text-primary)'
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
- handleCopy(
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.5 rounded-lg text-xs font-medium bg-(--bg-primary) hover:bg-(--border-subtle) text-(--text-primary) border border-(--border-subtle) transition-colors"
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
- {copied ? (
170
+ {copiedCode ? (
187
171
  <>
188
- <Check className="w-3.5 h-3.5 text-emerald-500" />
189
- <span className="text-emerald-500">복사 완료!</span>
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-muted)" />
194
- <span>코드 복사</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 Content Area */}
202
- <div className="p-4 sm:p-8 bg-(--bg-secondary)/30 flex justify-center items-center min-h-[380px]">
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-2 border-dashed border-(--border-strong) rounded-2xl p-2 bg-(--bg-primary) shadow-sm'
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-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">
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' ? 'TypeScript React' : 'Vanilla HTML / Tailwind'}</span>
201
+ <span>{activeTab === 'react' ? 'React + Tailwind TSX' : 'HTML / Tailwind'}</span>
218
202
  </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>
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
+ }