desktop: components/apps/file-explorer — dual-pane file browser with navigation, icons, selection, status bar

This commit is contained in:
Butterfly Dev 2026-04-07 03:35:30 +00:00
parent 283e53d1dc
commit 0048eabb60
3 changed files with 216 additions and 0 deletions

View File

@ -0,0 +1,39 @@
<div class="file-explorer">
<!-- Toolbar -->
<div class="fe-toolbar">
<button class="fe-btn" (click)="goUp()" title="Go up">
<svg width="14" height="14" viewBox="0 0 14 14"><path d="M2 7l5-5 5 5M7 2v10" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
</button>
<button class="fe-btn" (click)="goHome()" title="Home">🏠</button>
<div class="fe-path">
<span class="path-text">{{ currentPath() }}</span>
</div>
</div>
<!-- File list -->
<div class="fe-content">
@for (file of files(); track file.name) {
<div
class="fe-item"
[class.selected]="selectedFile() === file.name"
(click)="onSelect(file)"
(dblclick)="onDoubleClick(file)"
>
<span class="fe-icon">{{ file.type === 'folder' ? '📁' : '📄' }}</span>
<span class="fe-name">{{ file.name }}</span>
@if (file.size) {
<span class="fe-size">{{ file.size }}</span>
}
<span class="fe-date">{{ file.modified }}</span>
</div>
}
</div>
<!-- Status bar -->
<div class="fe-statusbar">
<span>{{ files().length }} items</span>
@if (selectedFile()) {
<span>{{ selectedFile() }}</span>
}
</div>
</div>

View File

@ -0,0 +1,124 @@
:host {
display: block;
width: 100%;
height: 100%;
}
.file-explorer {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: #1e1e1e;
}
.fe-toolbar {
display: flex;
align-items: center;
height: 36px;
padding: 0 8px;
background: #2d2d2d;
border-bottom: 1px solid #404040;
gap: 4px;
}
.fe-btn {
width: 30px;
height: 28px;
border: none;
background: transparent;
color: #ccc;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background: rgba(255, 255, 255, 0.08);
}
}
.fe-path {
flex: 1;
height: 26px;
margin-left: 8px;
display: flex;
align-items: center;
background: #383838;
border: 1px solid #555;
border-radius: 4px;
padding: 0 10px;
}
.path-text {
font-size: 12px;
color: #ccc;
}
.fe-content {
flex: 1;
overflow-y: auto;
padding: 4px;
}
.fe-item {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 12px;
border-radius: 4px;
cursor: default;
font-size: 13px;
color: #ddd;
&:hover {
background: rgba(255, 255, 255, 0.04);
}
&.selected {
background: rgba(0, 120, 212, 0.25);
}
}
.fe-icon {
font-size: 16px;
flex-shrink: 0;
}
.fe-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fe-size {
font-size: 11px;
color: #888;
width: 70px;
text-align: right;
flex-shrink: 0;
}
.fe-date {
font-size: 11px;
color: #888;
width: 90px;
text-align: right;
flex-shrink: 0;
}
.fe-statusbar {
display: flex;
justify-content: space-between;
height: 24px;
padding: 0 12px;
background: #2d2d2d;
border-top: 1px solid #404040;
font-size: 11px;
color: #888;
align-items: center;
}

View File

@ -0,0 +1,53 @@
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
interface FileItem {
name: string;
type: 'folder' | 'file';
size?: string;
modified?: string;
}
@Component({
selector: 'app-file-explorer',
standalone: true,
imports: [CommonModule],
templateUrl: './file-explorer.component.html',
styleUrl: './file-explorer.component.scss',
})
export class FileExplorerComponent {
readonly currentPath = signal('/home/butterfly');
readonly selectedFile = signal<string | null>(null);
readonly files = signal<FileItem[]>([
{ name: 'Documents', type: 'folder', modified: '2026-04-07' },
{ name: 'Downloads', type: 'folder', modified: '2026-04-07' },
{ name: 'Pictures', type: 'folder', modified: '2026-04-06' },
{ name: 'Desktop', type: 'folder', modified: '2026-04-07' },
{ name: 'readme.txt', type: 'file', size: '1.2 KB', modified: '2026-04-07' },
{ name: 'notes.md', type: 'file', size: '3.4 KB', modified: '2026-04-06' },
{ name: 'config.json', type: 'file', size: '512 B', modified: '2026-04-05' },
]);
onSelect(file: FileItem): void {
this.selectedFile.set(file.name);
}
onDoubleClick(file: FileItem): void {
if (file.type === 'folder') {
this.currentPath.set(this.currentPath() + '/' + file.name);
}
}
goUp(): void {
const path = this.currentPath();
const idx = path.lastIndexOf('/');
if (idx > 0) {
this.currentPath.set(path.substring(0, idx));
}
}
goHome(): void {
this.currentPath.set('/home/butterfly');
}
}