atomsystem 0.4.0 → 0.4.3
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/atom.design.md +17 -0
- package/components/blocks/BlockViewerCard.tsx +229 -0
- package/components/blocks/CardListBlock.tsx +309 -0
- package/components/blocks/FileExplorerBlock.tsx +535 -0
- package/components/blocks/InteractiveChatBlock.tsx +233 -0
- package/components/blocks/MetricStatsBlock.tsx +178 -0
- package/components/blocks/PricingPlanBlock.tsx +231 -0
- package/components/blocks/RealtimeChartBlock.tsx +361 -0
- package/components/blocks/SliderShowcaseBlock.tsx +336 -0
- package/components/blocks/index.ts +8 -0
- package/components/ui/Tree.tsx +121 -0
- package/components/ui/index.ts +1 -0
- package/css/atom-ui.css +134 -7
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +90 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +90 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
- package/presets/default.design.md +17 -0
- package/tokens/token.json +1 -1
- package/tokens/tokens.json +1 -1
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Folder,
|
|
6
|
+
FolderOpen,
|
|
7
|
+
FileCode,
|
|
8
|
+
FileText,
|
|
9
|
+
FileJson,
|
|
10
|
+
Image as ImageIcon,
|
|
11
|
+
ChevronRight,
|
|
12
|
+
ChevronDown,
|
|
13
|
+
Search,
|
|
14
|
+
LayoutGrid,
|
|
15
|
+
List as ListIcon,
|
|
16
|
+
Download,
|
|
17
|
+
Copy,
|
|
18
|
+
Check,
|
|
19
|
+
Plus,
|
|
20
|
+
Trash2,
|
|
21
|
+
HardDrive,
|
|
22
|
+
File,
|
|
23
|
+
Eye,
|
|
24
|
+
ExternalLink,
|
|
25
|
+
} from 'lucide-react';
|
|
26
|
+
|
|
27
|
+
interface FileNode {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
type: 'folder' | 'file';
|
|
31
|
+
extension?: 'tsx' | 'ts' | 'json' | 'css' | 'svg' | 'md';
|
|
32
|
+
size?: string;
|
|
33
|
+
updatedAt: string;
|
|
34
|
+
content?: string;
|
|
35
|
+
children?: FileNode[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const FILE_TREE: FileNode[] = [
|
|
39
|
+
{
|
|
40
|
+
id: 'f-src',
|
|
41
|
+
name: 'src',
|
|
42
|
+
type: 'folder',
|
|
43
|
+
updatedAt: '2026.11.10',
|
|
44
|
+
children: [
|
|
45
|
+
{
|
|
46
|
+
id: 'f-components',
|
|
47
|
+
name: 'components',
|
|
48
|
+
type: 'folder',
|
|
49
|
+
updatedAt: '2026.11.11',
|
|
50
|
+
children: [
|
|
51
|
+
{
|
|
52
|
+
id: 'f-btn',
|
|
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
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'f-tokens',
|
|
82
|
+
name: 'tokens',
|
|
83
|
+
type: 'folder',
|
|
84
|
+
updatedAt: '2026.11.11',
|
|
85
|
+
children: [
|
|
86
|
+
{
|
|
87
|
+
id: 'f-tokens-json',
|
|
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
|
+
],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'f-public',
|
|
110
|
+
name: 'public',
|
|
111
|
+
type: 'folder',
|
|
112
|
+
updatedAt: '2026.11.05',
|
|
113
|
+
children: [
|
|
114
|
+
{
|
|
115
|
+
id: 'f-logo',
|
|
116
|
+
name: 'atom-logo.svg',
|
|
117
|
+
type: 'file',
|
|
118
|
+
extension: 'svg',
|
|
119
|
+
size: '5.2 KB',
|
|
120
|
+
updatedAt: '2026.11.05 10:00',
|
|
121
|
+
content: `<svg viewBox="0 0 100 100" fill="none">\n <circle cx="50" cy="50" r="40" stroke="#3182f6" strokeWidth="8"/>\n</svg>`,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
},
|
|
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
|
+
];
|
|
144
|
+
|
|
145
|
+
export function FileExplorerBlock() {
|
|
146
|
+
const [openFolders, setOpenFolders] = useState<Record<string, boolean>>({
|
|
147
|
+
'f-src': true,
|
|
148
|
+
'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
|
+
});
|
|
160
|
+
|
|
161
|
+
const [selectedFile, setSelectedFile] = useState<FileNode>(
|
|
162
|
+
FILE_TREE[0].children![0].children![0]
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const [viewMode, setViewMode] = useState<'grid' | 'table'>('grid');
|
|
166
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
167
|
+
const [copied, setCopied] = useState(false);
|
|
168
|
+
|
|
169
|
+
const toggleFolder = (folderId: string) => {
|
|
170
|
+
setOpenFolders((prev) => ({ ...prev, [folderId]: !prev[folderId] }));
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const getFileIcon = (ext?: string) => {
|
|
174
|
+
switch (ext) {
|
|
175
|
+
case 'tsx':
|
|
176
|
+
case 'ts':
|
|
177
|
+
return <FileCode className="w-4 h-4 text-blue-500" />;
|
|
178
|
+
case 'json':
|
|
179
|
+
return <FileJson className="w-4 h-4 text-amber-500" />;
|
|
180
|
+
case 'svg':
|
|
181
|
+
return <ImageIcon className="w-4 h-4 text-emerald-500" />;
|
|
182
|
+
case 'md':
|
|
183
|
+
return <FileText className="w-4 h-4 text-purple-500" />;
|
|
184
|
+
default:
|
|
185
|
+
return <File className="w-4 h-4 text-(--text-muted)" />;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const copyContent = (text?: string) => {
|
|
190
|
+
if (!text) return;
|
|
191
|
+
navigator.clipboard.writeText(text);
|
|
192
|
+
setCopied(true);
|
|
193
|
+
setTimeout(() => setCopied(false), 2000);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const currentFiles = (currentFolder.children || []).filter((item) =>
|
|
197
|
+
item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div className="w-full bg-(--bg-primary) rounded-2xl border border-(--border-subtle) shadow-sm overflow-hidden flex flex-col h-[520px] transition-colors">
|
|
202
|
+
{/* Top Header & Breadcrumb & Action Toolbar */}
|
|
203
|
+
<div className="px-4 py-3 border-b border-(--border-subtle) bg-(--bg-secondary)/40 flex flex-wrap items-center justify-between gap-3">
|
|
204
|
+
{/* Breadcrumb Path */}
|
|
205
|
+
<div className="flex items-center gap-1.5 text-xs font-medium text-(--text-secondary)">
|
|
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>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
{/* Search, View Mode & New File */}
|
|
217
|
+
<div className="flex items-center gap-2">
|
|
218
|
+
<div className="relative">
|
|
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) => {
|
|
419
|
+
const isSelected = selectedFile?.id === file.id;
|
|
420
|
+
return (
|
|
421
|
+
<tr
|
|
422
|
+
key={file.id}
|
|
423
|
+
onClick={() => setSelectedFile(file)}
|
|
424
|
+
className={`cursor-pointer transition-colors ${
|
|
425
|
+
isSelected
|
|
426
|
+
? 'bg-(--accent-primary)/10 font-semibold text-(--accent-primary)'
|
|
427
|
+
: 'hover:bg-(--bg-secondary)/50 text-(--text-primary)'
|
|
428
|
+
}`}
|
|
429
|
+
>
|
|
430
|
+
<td className="p-2.5 pl-3 flex items-center gap-2">
|
|
431
|
+
{getFileIcon(file.extension)}
|
|
432
|
+
<span>{file.name}</span>
|
|
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>
|
|
440
|
+
);
|
|
441
|
+
})}
|
|
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
|
+
</div>
|
|
496
|
+
|
|
497
|
+
{/* File Preview Snippet */}
|
|
498
|
+
<div>
|
|
499
|
+
<span className="text-[10px] font-bold text-(--text-muted) mb-1 block">
|
|
500
|
+
코드/데이터 미리보기
|
|
501
|
+
</span>
|
|
502
|
+
<div className="rounded-lg bg-(--bg-primary) border border-(--border-subtle) p-2 text-[10px] font-mono text-(--text-secondary) overflow-x-auto max-h-36 leading-relaxed">
|
|
503
|
+
<pre>
|
|
504
|
+
<code>{selectedFile.content || '// 파일 내용이 없습니다.'}</code>
|
|
505
|
+
</pre>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
) : (
|
|
510
|
+
<div className="py-10 text-center text-xs text-(--text-muted)">
|
|
511
|
+
선택된 파일이 없습니다.
|
|
512
|
+
</div>
|
|
513
|
+
)}
|
|
514
|
+
</div>
|
|
515
|
+
|
|
516
|
+
<div className="pt-3 border-t border-(--border-subtle) flex gap-2">
|
|
517
|
+
<button
|
|
518
|
+
onClick={() => copyContent(selectedFile?.content)}
|
|
519
|
+
className="flex-1 py-1.5 rounded-lg bg-(--accent-primary) hover:bg-(--accent-hover) text-white font-semibold text-xs flex items-center justify-center gap-1.5 shadow-xs transition-colors"
|
|
520
|
+
>
|
|
521
|
+
<Download className="w-3.5 h-3.5" />
|
|
522
|
+
<span>다운로드</span>
|
|
523
|
+
</button>
|
|
524
|
+
<button
|
|
525
|
+
className="p-1.5 rounded-lg bg-(--bg-primary) hover:bg-rose-500/10 hover:text-rose-500 text-(--text-muted) border border-(--border-subtle) transition-colors"
|
|
526
|
+
title="삭제"
|
|
527
|
+
>
|
|
528
|
+
<Trash2 className="w-3.5 h-3.5" />
|
|
529
|
+
</button>
|
|
530
|
+
</div>
|
|
531
|
+
</aside>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
);
|
|
535
|
+
}
|