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.
- 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
|
@@ -1,27 +1,18 @@
|
|
|
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
|
Folder,
|
|
6
7
|
FolderOpen,
|
|
7
8
|
FileCode,
|
|
8
9
|
FileText,
|
|
9
10
|
FileJson,
|
|
10
|
-
Image as ImageIcon,
|
|
11
11
|
ChevronRight,
|
|
12
12
|
ChevronDown,
|
|
13
|
-
Search,
|
|
14
|
-
LayoutGrid,
|
|
15
|
-
List as ListIcon,
|
|
16
|
-
Download,
|
|
17
13
|
Copy,
|
|
18
14
|
Check,
|
|
19
|
-
Plus,
|
|
20
|
-
Trash2,
|
|
21
|
-
HardDrive,
|
|
22
15
|
File,
|
|
23
|
-
Eye,
|
|
24
|
-
ExternalLink,
|
|
25
16
|
} from 'lucide-react';
|
|
26
17
|
|
|
27
18
|
interface FileNode {
|
|
@@ -37,133 +28,68 @@ interface FileNode {
|
|
|
37
28
|
|
|
38
29
|
const FILE_TREE: FileNode[] = [
|
|
39
30
|
{
|
|
40
|
-
id: 'f-
|
|
41
|
-
name: '
|
|
31
|
+
id: 'f-tokens',
|
|
32
|
+
name: 'tokens',
|
|
42
33
|
type: 'folder',
|
|
43
|
-
updatedAt: '2026.11.
|
|
34
|
+
updatedAt: '2026.11.11',
|
|
44
35
|
children: [
|
|
45
36
|
{
|
|
46
|
-
id: 'f-
|
|
47
|
-
name: '
|
|
48
|
-
type: '
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
name: 'Button.tsx',
|
|
54
|
-
type: 'file',
|
|
55
|
-
extension: 'tsx',
|
|
56
|
-
size: '3.4 KB',
|
|
57
|
-
updatedAt: '2026.11.10 14:20',
|
|
58
|
-
content: `import React from 'react';\n\nexport interface ButtonProps {\n variant?: 'primary' | 'secondary';\n children: React.ReactNode;\n}\n\nexport const Button = ({ variant = 'primary', children }: ButtonProps) => {\n return <button className={\`atom-btn-\${variant}\`}>{children}</button>;\n};`,
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
id: 'f-card',
|
|
62
|
-
name: 'Card.tsx',
|
|
63
|
-
type: 'file',
|
|
64
|
-
extension: 'tsx',
|
|
65
|
-
size: '2.1 KB',
|
|
66
|
-
updatedAt: '2026.11.09 11:15',
|
|
67
|
-
content: `export const Card = ({ title, children }) => (\n <div className="atom-card">\n <h3>{title}</h3>\n {children}\n </div>\n);`,
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
id: 'f-input',
|
|
71
|
-
name: 'Input.tsx',
|
|
72
|
-
type: 'file',
|
|
73
|
-
extension: 'tsx',
|
|
74
|
-
size: '1.8 KB',
|
|
75
|
-
updatedAt: '2026.11.08 17:40',
|
|
76
|
-
content: `export const Input = (props) => <input className="atom-input" {...props} />;`,
|
|
77
|
-
},
|
|
78
|
-
],
|
|
37
|
+
id: 'f-color',
|
|
38
|
+
name: 'color.json',
|
|
39
|
+
type: 'file',
|
|
40
|
+
extension: 'json',
|
|
41
|
+
size: '1.8 KB',
|
|
42
|
+
updatedAt: '2026.11.11 10:20',
|
|
43
|
+
content: `{\n "color": {\n "primary": { "value": "var(--accent-primary)" },\n "neutral": { "value": "var(--bg-primary)" }\n }\n}`,
|
|
79
44
|
},
|
|
80
45
|
{
|
|
81
|
-
id: 'f-
|
|
82
|
-
name: '
|
|
83
|
-
type: '
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
name: 'tokens.json',
|
|
89
|
-
type: 'file',
|
|
90
|
-
extension: 'json',
|
|
91
|
-
size: '18.6 KB',
|
|
92
|
-
updatedAt: '2026.11.11 09:30',
|
|
93
|
-
content: `{\n "color": {\n "primary": { "value": "#3182f6", "type": "color" },\n "bg": { "value": "#ffffff", "type": "color" }\n }\n}`,
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
id: 'f-token-types',
|
|
97
|
-
name: 'token.types.ts',
|
|
98
|
-
type: 'file',
|
|
99
|
-
extension: 'ts',
|
|
100
|
-
size: '4.2 KB',
|
|
101
|
-
updatedAt: '2026.11.10 16:55',
|
|
102
|
-
content: `export type ColorToken = 'primary' | 'secondary' | 'accent';`,
|
|
103
|
-
},
|
|
104
|
-
],
|
|
46
|
+
id: 'f-space',
|
|
47
|
+
name: 'spacing.json',
|
|
48
|
+
type: 'file',
|
|
49
|
+
extension: 'json',
|
|
50
|
+
size: '1.2 KB',
|
|
51
|
+
updatedAt: '2026.11.11 09:15',
|
|
52
|
+
content: `{\n "spacing": {\n "1": { "value": "4px" },\n "2": { "value": "8px" },\n "4": { "value": "16px" }\n }\n}`,
|
|
105
53
|
},
|
|
106
54
|
],
|
|
107
55
|
},
|
|
108
56
|
{
|
|
109
|
-
id: 'f-
|
|
110
|
-
name: '
|
|
57
|
+
id: 'f-components',
|
|
58
|
+
name: 'components',
|
|
111
59
|
type: 'folder',
|
|
112
|
-
updatedAt: '2026.11.
|
|
60
|
+
updatedAt: '2026.11.11',
|
|
113
61
|
children: [
|
|
114
62
|
{
|
|
115
|
-
id: 'f-
|
|
116
|
-
name: '
|
|
63
|
+
id: 'f-btn',
|
|
64
|
+
name: 'Button.tsx',
|
|
117
65
|
type: 'file',
|
|
118
|
-
extension: '
|
|
119
|
-
size: '
|
|
120
|
-
updatedAt: '2026.11.
|
|
121
|
-
content:
|
|
66
|
+
extension: 'tsx',
|
|
67
|
+
size: '3.4 KB',
|
|
68
|
+
updatedAt: '2026.11.11 09:30',
|
|
69
|
+
content: `export function Button({ children, variant = 'primary' }) {\n return (\n <button className="px-4 py-2 rounded-(--radius) bg-(--accent-primary) text-white font-medium">\n {children}\n </button>\n );\n}`,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'f-card',
|
|
73
|
+
name: 'Card.tsx',
|
|
74
|
+
type: 'file',
|
|
75
|
+
extension: 'tsx',
|
|
76
|
+
size: '4.2 KB',
|
|
77
|
+
updatedAt: '2026.11.10 16:20',
|
|
78
|
+
content: `export function Card({ title, children }) {\n return (\n <div className="p-4 rounded-(--radius) border border-(--border-subtle) bg-(--bg-primary)">\n <h3>{title}</h3>\n {children}\n </div>\n );\n}`,
|
|
122
79
|
},
|
|
123
80
|
],
|
|
124
81
|
},
|
|
125
|
-
{
|
|
126
|
-
id: 'f-readme',
|
|
127
|
-
name: 'README.md',
|
|
128
|
-
type: 'file',
|
|
129
|
-
extension: 'md',
|
|
130
|
-
size: '6.8 KB',
|
|
131
|
-
updatedAt: '2026.11.10 18:00',
|
|
132
|
-
content: `# AtomDesignToken\n\nFramework-Agnostic W3C DTCG Token Architecture with 0KB Runtime.`,
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
id: 'f-package',
|
|
136
|
-
name: 'package.json',
|
|
137
|
-
type: 'file',
|
|
138
|
-
extension: 'json',
|
|
139
|
-
size: '2.5 KB',
|
|
140
|
-
updatedAt: '2026.11.11 08:45',
|
|
141
|
-
content: `{\n "name": "atomsystem",\n "version": "0.4.0",\n "type": "module"\n}`,
|
|
142
|
-
},
|
|
143
82
|
];
|
|
144
83
|
|
|
145
84
|
export function FileExplorerBlock() {
|
|
146
85
|
const [openFolders, setOpenFolders] = useState<Record<string, boolean>>({
|
|
147
|
-
'f-
|
|
86
|
+
'f-tokens': true,
|
|
148
87
|
'f-components': true,
|
|
149
|
-
'f-tokens': false,
|
|
150
|
-
'f-public': false,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const [currentFolder, setCurrentFolder] = useState<FileNode>({
|
|
154
|
-
id: 'f-components',
|
|
155
|
-
name: 'components',
|
|
156
|
-
type: 'folder',
|
|
157
|
-
updatedAt: '2026.11.11',
|
|
158
|
-
children: FILE_TREE[0].children![0].children,
|
|
159
88
|
});
|
|
160
89
|
|
|
161
90
|
const [selectedFile, setSelectedFile] = useState<FileNode>(
|
|
162
|
-
FILE_TREE[0].children![0]
|
|
91
|
+
FILE_TREE[0].children![0]
|
|
163
92
|
);
|
|
164
|
-
|
|
165
|
-
const [viewMode, setViewMode] = useState<'grid' | 'table'>('grid');
|
|
166
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
167
93
|
const [copied, setCopied] = useState(false);
|
|
168
94
|
|
|
169
95
|
const toggleFolder = (folderId: string) => {
|
|
@@ -174,362 +100,104 @@ export function FileExplorerBlock() {
|
|
|
174
100
|
switch (ext) {
|
|
175
101
|
case 'tsx':
|
|
176
102
|
case 'ts':
|
|
177
|
-
return <FileCode className="w-
|
|
103
|
+
return <FileCode className="w-3.5 h-3.5 text-blue-500" />;
|
|
178
104
|
case 'json':
|
|
179
|
-
return <FileJson className="w-
|
|
180
|
-
case 'svg':
|
|
181
|
-
return <ImageIcon className="w-4 h-4 text-emerald-500" />;
|
|
105
|
+
return <FileJson className="w-3.5 h-3.5 text-amber-500" />;
|
|
182
106
|
case 'md':
|
|
183
|
-
return <FileText className="w-
|
|
107
|
+
return <FileText className="w-3.5 h-3.5 text-purple-500" />;
|
|
184
108
|
default:
|
|
185
|
-
return <File className="w-
|
|
109
|
+
return <File className="w-3.5 h-3.5 text-(--text-secondary)" />;
|
|
186
110
|
}
|
|
187
111
|
};
|
|
188
112
|
|
|
189
|
-
const
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
113
|
+
const handleCopy = () => {
|
|
114
|
+
if (selectedFile.content) {
|
|
115
|
+
navigator.clipboard.writeText(selectedFile.content);
|
|
116
|
+
setCopied(true);
|
|
117
|
+
setTimeout(() => setCopied(false), 2000);
|
|
118
|
+
}
|
|
194
119
|
};
|
|
195
120
|
|
|
196
|
-
const currentFiles = (currentFolder.children || []).filter((item) =>
|
|
197
|
-
item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
198
|
-
);
|
|
199
|
-
|
|
200
121
|
return (
|
|
201
|
-
<
|
|
202
|
-
{/*
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
<HardDrive className="w-4 h-4 text-(--accent-primary)" />
|
|
207
|
-
<span>project-root</span>
|
|
208
|
-
<ChevronRight className="w-3.5 h-3.5 text-(--text-muted)" />
|
|
209
|
-
<span>src</span>
|
|
210
|
-
<ChevronRight className="w-3.5 h-3.5 text-(--text-muted)" />
|
|
211
|
-
<span className="font-bold text-(--text-primary) px-1.5 py-0.5 rounded bg-(--bg-secondary)">
|
|
212
|
-
{currentFolder.name}
|
|
213
|
-
</span>
|
|
122
|
+
<Card className="flex flex-col md:flex-row h-[380px] overflow-hidden transition-colors">
|
|
123
|
+
{/* 1. Left Tree Nav */}
|
|
124
|
+
<aside className="w-full md:w-56 border-r border-(--border-subtle) bg-(--bg-secondary)/20 p-3 overflow-y-auto shrink-0 space-y-1 text-xs">
|
|
125
|
+
<div className="text-[11px] font-mono text-(--text-secondary) uppercase tracking-wider px-2 py-1 mb-1">
|
|
126
|
+
Project Files
|
|
214
127
|
</div>
|
|
128
|
+
{FILE_TREE.map((node) => {
|
|
129
|
+
const isOpen = openFolders[node.id];
|
|
130
|
+
return (
|
|
131
|
+
<div key={node.id} className="space-y-0.5">
|
|
132
|
+
<button
|
|
133
|
+
onClick={() => toggleFolder(node.id)}
|
|
134
|
+
className="w-full flex items-center gap-1.5 px-2 py-1 rounded-(--radius-sm) text-(--text-secondary) hover:text-(--text-primary) hover:bg-(--bg-secondary) text-left transition-colors"
|
|
135
|
+
>
|
|
136
|
+
{isOpen ? (
|
|
137
|
+
<ChevronDown className="w-3 h-3 text-(--text-secondary)" />
|
|
138
|
+
) : (
|
|
139
|
+
<ChevronRight className="w-3 h-3 text-(--text-secondary)" />
|
|
140
|
+
)}
|
|
141
|
+
{isOpen ? (
|
|
142
|
+
<FolderOpen className="w-3.5 h-3.5 text-(--accent-primary)" />
|
|
143
|
+
) : (
|
|
144
|
+
<Folder className="w-3.5 h-3.5 text-(--text-secondary)" />
|
|
145
|
+
)}
|
|
146
|
+
<span className="font-medium truncate">{node.name}</span>
|
|
147
|
+
</button>
|
|
215
148
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
<Search className="w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-(--text-muted)" />
|
|
220
|
-
<input
|
|
221
|
-
type="text"
|
|
222
|
-
value={searchQuery}
|
|
223
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
224
|
-
placeholder="파일 검색..."
|
|
225
|
-
className="text-xs pl-8 pr-3 py-1 rounded-lg bg-(--bg-primary) text-(--text-primary) border border-(--border-subtle) focus:outline-none focus:border-(--accent-primary) w-32 sm:w-44 transition-colors"
|
|
226
|
-
/>
|
|
227
|
-
</div>
|
|
228
|
-
|
|
229
|
-
<div className="flex items-center p-0.5 rounded-lg bg-(--bg-primary) border border-(--border-subtle)">
|
|
230
|
-
<button
|
|
231
|
-
onClick={() => setViewMode('grid')}
|
|
232
|
-
aria-label="그리드 뷰"
|
|
233
|
-
className={`p-1 rounded ${
|
|
234
|
-
viewMode === 'grid'
|
|
235
|
-
? 'bg-(--bg-secondary) text-(--text-primary)'
|
|
236
|
-
: 'text-(--text-muted) hover:text-(--text-primary)'
|
|
237
|
-
}`}
|
|
238
|
-
>
|
|
239
|
-
<LayoutGrid className="w-3.5 h-3.5" />
|
|
240
|
-
</button>
|
|
241
|
-
<button
|
|
242
|
-
onClick={() => setViewMode('table')}
|
|
243
|
-
aria-label="테이블 뷰"
|
|
244
|
-
className={`p-1 rounded ${
|
|
245
|
-
viewMode === 'table'
|
|
246
|
-
? 'bg-(--bg-secondary) text-(--text-primary)'
|
|
247
|
-
: 'text-(--text-muted) hover:text-(--text-primary)'
|
|
248
|
-
}`}
|
|
249
|
-
>
|
|
250
|
-
<ListIcon className="w-3.5 h-3.5" />
|
|
251
|
-
</button>
|
|
252
|
-
</div>
|
|
253
|
-
|
|
254
|
-
<button className="hidden sm:flex items-center gap-1 px-2.5 py-1 rounded-lg text-xs font-semibold bg-(--accent-primary) text-white shadow-xs hover:bg-(--accent-hover) transition-colors">
|
|
255
|
-
<Plus className="w-3 h-3" />
|
|
256
|
-
<span>추가</span>
|
|
257
|
-
</button>
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
|
|
261
|
-
{/* Main 3-Column Body: Tree Sidebar | File List Viewport | File Inspector */}
|
|
262
|
-
<div className="flex-1 flex overflow-hidden">
|
|
263
|
-
{/* 1. Left Tree Sidebar */}
|
|
264
|
-
<aside className="w-48 sm:w-56 border-r border-(--border-subtle) bg-(--bg-secondary)/20 p-3 overflow-y-auto shrink-0 select-none">
|
|
265
|
-
<div className="text-[10px] font-bold text-(--text-muted) uppercase tracking-wider mb-2 px-1">
|
|
266
|
-
EXPLORER TREE
|
|
267
|
-
</div>
|
|
268
|
-
|
|
269
|
-
<div className="space-y-0.5 text-xs">
|
|
270
|
-
{FILE_TREE.map((node) => {
|
|
271
|
-
if (node.type === 'folder') {
|
|
272
|
-
const isOpen = openFolders[node.id];
|
|
273
|
-
return (
|
|
274
|
-
<div key={node.id}>
|
|
275
|
-
<button
|
|
276
|
-
onClick={() => toggleFolder(node.id)}
|
|
277
|
-
className="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-(--text-secondary) hover:text-(--text-primary) hover:bg-(--bg-secondary) transition-colors text-left"
|
|
278
|
-
>
|
|
279
|
-
{isOpen ? (
|
|
280
|
-
<ChevronDown className="w-3.5 h-3.5 text-(--text-muted)" />
|
|
281
|
-
) : (
|
|
282
|
-
<ChevronRight className="w-3.5 h-3.5 text-(--text-muted)" />
|
|
283
|
-
)}
|
|
284
|
-
{isOpen ? (
|
|
285
|
-
<FolderOpen className="w-4 h-4 text-amber-500 fill-amber-500/20" />
|
|
286
|
-
) : (
|
|
287
|
-
<Folder className="w-4 h-4 text-amber-500 fill-amber-500/20" />
|
|
288
|
-
)}
|
|
289
|
-
<span className="font-semibold truncate">{node.name}</span>
|
|
290
|
-
</button>
|
|
291
|
-
|
|
292
|
-
{/* Subfolders & Subfiles */}
|
|
293
|
-
{isOpen && node.children && (
|
|
294
|
-
<div className="pl-4 space-y-0.5 border-l border-(--border-subtle) ml-3.5 my-0.5">
|
|
295
|
-
{node.children.map((sub) => {
|
|
296
|
-
if (sub.type === 'folder') {
|
|
297
|
-
const isSubOpen = openFolders[sub.id];
|
|
298
|
-
const isCurrent = currentFolder.id === sub.id;
|
|
299
|
-
return (
|
|
300
|
-
<button
|
|
301
|
-
key={sub.id}
|
|
302
|
-
onClick={() => {
|
|
303
|
-
toggleFolder(sub.id);
|
|
304
|
-
setCurrentFolder(sub);
|
|
305
|
-
}}
|
|
306
|
-
className={`w-full flex items-center gap-1.5 px-2 py-1 rounded-md text-left transition-colors ${
|
|
307
|
-
isCurrent
|
|
308
|
-
? 'bg-(--accent-primary)/10 text-(--accent-primary) font-bold'
|
|
309
|
-
: 'text-(--text-secondary) hover:text-(--text-primary) hover:bg-(--bg-secondary)'
|
|
310
|
-
}`}
|
|
311
|
-
>
|
|
312
|
-
{isSubOpen ? (
|
|
313
|
-
<FolderOpen className="w-3.5 h-3.5 text-amber-500" />
|
|
314
|
-
) : (
|
|
315
|
-
<Folder className="w-3.5 h-3.5 text-amber-500" />
|
|
316
|
-
)}
|
|
317
|
-
<span className="truncate">{sub.name}</span>
|
|
318
|
-
</button>
|
|
319
|
-
);
|
|
320
|
-
} else {
|
|
321
|
-
const isSelected = selectedFile?.id === sub.id;
|
|
322
|
-
return (
|
|
323
|
-
<button
|
|
324
|
-
key={sub.id}
|
|
325
|
-
onClick={() => setSelectedFile(sub)}
|
|
326
|
-
className={`w-full flex items-center gap-1.5 px-2 py-1 rounded-md text-left transition-colors ${
|
|
327
|
-
isSelected
|
|
328
|
-
? 'bg-(--accent-primary)/10 text-(--accent-primary) font-semibold'
|
|
329
|
-
: 'text-(--text-secondary) hover:text-(--text-primary) hover:bg-(--bg-secondary)'
|
|
330
|
-
}`}
|
|
331
|
-
>
|
|
332
|
-
{getFileIcon(sub.extension)}
|
|
333
|
-
<span className="truncate">{sub.name}</span>
|
|
334
|
-
</button>
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
})}
|
|
338
|
-
</div>
|
|
339
|
-
)}
|
|
340
|
-
</div>
|
|
341
|
-
);
|
|
342
|
-
} else {
|
|
343
|
-
return (
|
|
344
|
-
<button
|
|
345
|
-
key={node.id}
|
|
346
|
-
onClick={() => setSelectedFile(node)}
|
|
347
|
-
className="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-(--text-secondary) hover:text-(--text-primary) hover:bg-(--bg-secondary) text-left"
|
|
348
|
-
>
|
|
349
|
-
{getFileIcon(node.extension)}
|
|
350
|
-
<span className="truncate">{node.name}</span>
|
|
351
|
-
</button>
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
})}
|
|
355
|
-
</div>
|
|
356
|
-
</aside>
|
|
357
|
-
|
|
358
|
-
{/* 2. Middle Files Viewport */}
|
|
359
|
-
<main className="flex-1 p-4 overflow-y-auto bg-(--bg-primary)">
|
|
360
|
-
<div className="flex items-center justify-between text-xs text-(--text-muted) mb-3 pb-2 border-b border-(--border-subtle)">
|
|
361
|
-
<span>{currentFiles.length}개 파일 항목</span>
|
|
362
|
-
<span>최종 갱신: {currentFolder.updatedAt}</span>
|
|
363
|
-
</div>
|
|
364
|
-
|
|
365
|
-
{currentFiles.length === 0 ? (
|
|
366
|
-
<div className="py-16 text-center text-xs text-(--text-muted)">
|
|
367
|
-
검색된 파일이 없습니다.
|
|
368
|
-
</div>
|
|
369
|
-
) : viewMode === 'grid' ? (
|
|
370
|
-
/* Grid Mode */
|
|
371
|
-
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
|
372
|
-
{currentFiles.map((file) => {
|
|
373
|
-
const isSelected = selectedFile?.id === file.id;
|
|
374
|
-
return (
|
|
375
|
-
<div
|
|
376
|
-
key={file.id}
|
|
377
|
-
onClick={() => setSelectedFile(file)}
|
|
378
|
-
className={`group p-3.5 rounded-xl border transition-all cursor-pointer flex flex-col justify-between ${
|
|
379
|
-
isSelected
|
|
380
|
-
? 'border-(--accent-primary) bg-(--accent-primary)/5 shadow-xs ring-2 ring-(--accent-primary)/10'
|
|
381
|
-
: 'border-(--border-subtle) hover:border-(--border-strong) bg-(--bg-secondary)/30 hover:bg-(--bg-secondary)'
|
|
382
|
-
}`}
|
|
383
|
-
>
|
|
384
|
-
<div className="flex items-center justify-between mb-3">
|
|
385
|
-
<div className="p-2 rounded-lg bg-(--bg-primary) border border-(--border-subtle)">
|
|
386
|
-
{getFileIcon(file.extension)}
|
|
387
|
-
</div>
|
|
388
|
-
<span className="text-[10px] font-mono text-(--text-muted) uppercase">
|
|
389
|
-
{file.extension}
|
|
390
|
-
</span>
|
|
391
|
-
</div>
|
|
392
|
-
<div>
|
|
393
|
-
<h4 className="text-xs font-bold text-(--text-primary) truncate mb-1">
|
|
394
|
-
{file.name}
|
|
395
|
-
</h4>
|
|
396
|
-
<div className="flex items-center justify-between text-[10px] text-(--text-muted)">
|
|
397
|
-
<span>{file.size}</span>
|
|
398
|
-
<span>{file.updatedAt.split(' ')[0]}</span>
|
|
399
|
-
</div>
|
|
400
|
-
</div>
|
|
401
|
-
</div>
|
|
402
|
-
);
|
|
403
|
-
})}
|
|
404
|
-
</div>
|
|
405
|
-
) : (
|
|
406
|
-
/* Table Mode */
|
|
407
|
-
<div className="border border-(--border-subtle) rounded-xl overflow-hidden text-xs">
|
|
408
|
-
<table className="w-full text-left border-collapse">
|
|
409
|
-
<thead className="bg-(--bg-secondary)/60 text-(--text-muted) text-[11px] font-semibold border-b border-(--border-subtle)">
|
|
410
|
-
<tr>
|
|
411
|
-
<th className="p-2.5 pl-3">이름</th>
|
|
412
|
-
<th className="p-2.5">유형</th>
|
|
413
|
-
<th className="p-2.5">크기</th>
|
|
414
|
-
<th className="p-2.5">수정 일시</th>
|
|
415
|
-
</tr>
|
|
416
|
-
</thead>
|
|
417
|
-
<tbody className="divide-y divide-(--border-subtle)">
|
|
418
|
-
{currentFiles.map((file) => {
|
|
149
|
+
{isOpen && node.children && (
|
|
150
|
+
<div className="pl-4 space-y-0.5 border-l border-(--border-subtle) ml-3">
|
|
151
|
+
{node.children.map((file) => {
|
|
419
152
|
const isSelected = selectedFile?.id === file.id;
|
|
420
153
|
return (
|
|
421
|
-
<
|
|
154
|
+
<button
|
|
422
155
|
key={file.id}
|
|
423
156
|
onClick={() => setSelectedFile(file)}
|
|
424
|
-
className={`
|
|
157
|
+
className={`w-full flex items-center gap-1.5 px-2 py-1 rounded-(--radius-sm) text-left transition-colors ${
|
|
425
158
|
isSelected
|
|
426
|
-
? 'bg-(--accent-primary)/10
|
|
427
|
-
: '
|
|
159
|
+
? 'bg-(--accent-primary)/10 text-(--accent-primary) font-semibold'
|
|
160
|
+
: 'text-(--text-secondary) hover:text-(--text-primary) hover:bg-(--bg-secondary)'
|
|
428
161
|
}`}
|
|
429
162
|
>
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
</td>
|
|
434
|
-
<td className="p-2.5 text-(--text-muted) uppercase font-mono text-[10px]">
|
|
435
|
-
{file.extension}
|
|
436
|
-
</td>
|
|
437
|
-
<td className="p-2.5 text-(--text-secondary)">{file.size}</td>
|
|
438
|
-
<td className="p-2.5 text-(--text-muted)">{file.updatedAt}</td>
|
|
439
|
-
</tr>
|
|
163
|
+
{getFileIcon(file.extension)}
|
|
164
|
+
<span className="truncate">{file.name}</span>
|
|
165
|
+
</button>
|
|
440
166
|
);
|
|
441
167
|
})}
|
|
442
|
-
</tbody>
|
|
443
|
-
</table>
|
|
444
|
-
</div>
|
|
445
|
-
)}
|
|
446
|
-
</main>
|
|
447
|
-
|
|
448
|
-
{/* 3. Right File Preview & Inspector Panel */}
|
|
449
|
-
<aside className="w-60 sm:w-72 border-l border-(--border-subtle) bg-(--bg-secondary)/30 p-4 flex flex-col justify-between overflow-y-auto shrink-0">
|
|
450
|
-
<div>
|
|
451
|
-
<div className="flex items-center justify-between mb-3 pb-2 border-b border-(--border-subtle)">
|
|
452
|
-
<span className="text-[11px] font-bold text-(--text-muted) uppercase tracking-wider flex items-center gap-1">
|
|
453
|
-
<Eye className="w-3.5 h-3.5" />
|
|
454
|
-
파일 인스펙터
|
|
455
|
-
</span>
|
|
456
|
-
<button
|
|
457
|
-
onClick={() => copyContent(selectedFile?.content)}
|
|
458
|
-
className="text-[10px] text-(--accent-primary) font-semibold flex items-center gap-1 hover:underline"
|
|
459
|
-
>
|
|
460
|
-
{copied ? <Check className="w-3 h-3 text-emerald-500" /> : <Copy className="w-3 h-3" />}
|
|
461
|
-
<span>{copied ? '복사됨' : '내용 복사'}</span>
|
|
462
|
-
</button>
|
|
463
|
-
</div>
|
|
464
|
-
|
|
465
|
-
{selectedFile ? (
|
|
466
|
-
<div className="space-y-3">
|
|
467
|
-
<div className="flex items-center gap-2.5 p-2 rounded-xl bg-(--bg-primary) border border-(--border-subtle)">
|
|
468
|
-
<div className="p-2 rounded-lg bg-(--bg-secondary)">
|
|
469
|
-
{getFileIcon(selectedFile.extension)}
|
|
470
|
-
</div>
|
|
471
|
-
<div className="truncate">
|
|
472
|
-
<h5 className="text-xs font-bold text-(--text-primary) truncate">
|
|
473
|
-
{selectedFile.name}
|
|
474
|
-
</h5>
|
|
475
|
-
<span className="text-[10px] text-(--text-muted)">{selectedFile.size}</span>
|
|
476
|
-
</div>
|
|
477
|
-
</div>
|
|
478
|
-
|
|
479
|
-
{/* Details Meta */}
|
|
480
|
-
<div className="text-[11px] space-y-1.5 p-2.5 rounded-lg bg-(--bg-primary)/60 border border-(--border-subtle)">
|
|
481
|
-
<div className="flex justify-between text-(--text-secondary)">
|
|
482
|
-
<span>확장자</span>
|
|
483
|
-
<span className="font-mono uppercase font-bold text-(--text-primary)">
|
|
484
|
-
{selectedFile.extension}
|
|
485
|
-
</span>
|
|
486
|
-
</div>
|
|
487
|
-
<div className="flex justify-between text-(--text-secondary)">
|
|
488
|
-
<span>수정 일시</span>
|
|
489
|
-
<span className="text-(--text-primary)">{selectedFile.updatedAt}</span>
|
|
490
|
-
</div>
|
|
491
|
-
<div className="flex justify-between text-(--text-secondary)">
|
|
492
|
-
<span>보안 검증</span>
|
|
493
|
-
<span className="text-emerald-500 font-semibold">100% Verified</span>
|
|
494
|
-
</div>
|
|
495
168
|
</div>
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
선택된 파일이 없습니다.
|
|
512
|
-
</div>
|
|
513
|
-
)}
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
})}
|
|
173
|
+
</aside>
|
|
174
|
+
|
|
175
|
+
{/* 2. File Preview & Code Area composed with Button & Card */}
|
|
176
|
+
<main className="flex-1 flex flex-col bg-(--bg-primary) overflow-hidden">
|
|
177
|
+
<div className="px-4 py-2.5 border-b border-(--border-subtle) bg-(--bg-secondary)/30 flex items-center justify-between text-xs">
|
|
178
|
+
<div className="flex items-center gap-2">
|
|
179
|
+
{getFileIcon(selectedFile.extension)}
|
|
180
|
+
<span className="font-mono font-medium text-(--text-primary)">{selectedFile.name}</span>
|
|
181
|
+
<Badge variant="neutral" className="text-xs">
|
|
182
|
+
{selectedFile.size}
|
|
183
|
+
</Badge>
|
|
514
184
|
</div>
|
|
515
185
|
|
|
516
|
-
<
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
</
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
</div>
|
|
533
|
-
</div>
|
|
186
|
+
<Button
|
|
187
|
+
variant="outline"
|
|
188
|
+
size="sm"
|
|
189
|
+
onClick={handleCopy}
|
|
190
|
+
className="text-xs py-1 px-2.5 flex items-center gap-1"
|
|
191
|
+
>
|
|
192
|
+
{copied ? <Check className="w-3 h-3 text-emerald-600" /> : <Copy className="w-3 h-3" />}
|
|
193
|
+
<span>{copied ? '복사됨' : '코드 복사'}</span>
|
|
194
|
+
</Button>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div className="flex-1 p-4 overflow-auto font-mono text-xs leading-relaxed bg-(--bg-secondary)/10">
|
|
198
|
+
<pre className="text-(--text-primary)">{selectedFile.content}</pre>
|
|
199
|
+
</div>
|
|
200
|
+
</main>
|
|
201
|
+
</Card>
|
|
534
202
|
);
|
|
535
203
|
}
|