atomsystem 0.4.1 → 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.
@@ -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
+ }