Typing while the list is focused builds a filter query: a bar appears under the column headers (search icon, query, live match count, Esc hint, clear button) and the listing narrows to substring matches — '..' always stays. Backspace edits the query, Escape clears it, cursor parks on the first match, arrows/Enter keep working (the list never loses focus), and Space keeps its select meaning until a query exists. Filter resets on directory change and survives refresh; operations act only on visible (filtered) selection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
249 lines
9.7 KiB
HTML
249 lines
9.7 KiB
HTML
<!-- session strip -->
|
|
<div class="relative flex items-center gap-1.5 border-b border-border px-2 py-1">
|
|
<button
|
|
(click)="menuOpen.set(!menuOpen())"
|
|
class="flex min-w-0 items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium hover:bg-accent"
|
|
title="Choose what this panel shows"
|
|
>
|
|
@if (panel().connecting()) {
|
|
<ng-icon name="mynaSpinner" class="shrink-0 animate-spin text-primary" size="0.9rem" />
|
|
} @else {
|
|
<ng-icon
|
|
[name]="isRemote() ? 'mynaServers' : 'mynaHardDrive'"
|
|
[class]="isRemote() ? 'shrink-0 text-primary' : 'shrink-0 text-muted-foreground'"
|
|
size="0.9rem"
|
|
/>
|
|
}
|
|
<span class="truncate">{{ panel().title() }}</span>
|
|
<ng-icon name="mynaChevronDown" class="shrink-0 text-muted-foreground" size="0.8rem" />
|
|
</button>
|
|
@if (isRemote()) {
|
|
<button
|
|
(click)="disconnect()"
|
|
title="Disconnect (back to local)"
|
|
class="grid size-6 shrink-0 place-items-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
<ng-icon name="mynaLogout" size="0.85rem" />
|
|
</button>
|
|
}
|
|
|
|
<div class="flex-1"></div>
|
|
|
|
<!-- panel actions: act on THIS panel. Frequent/destructive verbs grow text
|
|
labels when the panel is wide enough (container query). -->
|
|
<button
|
|
(click)="copyToOther()"
|
|
[title]="side() === 'left' ? 'Copy to right panel (F5)' : 'Copy to left panel (F5)'"
|
|
class="flex h-6 shrink-0 items-center rounded-md px-1 text-muted-foreground hover:bg-accent hover:text-primary"
|
|
>
|
|
<ng-icon [name]="copyIcon()" size="0.95rem" />
|
|
<span class="ml-1 hidden text-[11px] @[30rem]:inline">Copy</span>
|
|
</button>
|
|
<button
|
|
(click)="run('edit')"
|
|
title="Edit file (F4)"
|
|
class="flex h-6 shrink-0 items-center rounded-md px-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
<ng-icon name="mynaFileText" size="0.9rem" />
|
|
<span class="ml-1 hidden text-[11px] @[30rem]:inline">Edit</span>
|
|
</button>
|
|
<button
|
|
(click)="run('rename')"
|
|
title="Rename (F2)"
|
|
class="flex h-6 shrink-0 items-center rounded-md px-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
<ng-icon name="mynaEdit" size="0.9rem" />
|
|
<span class="ml-1 hidden text-[11px] @[30rem]:inline">Rename</span>
|
|
</button>
|
|
<button
|
|
(click)="run('mkdir')"
|
|
title="New folder (F7)"
|
|
class="grid size-6 shrink-0 place-items-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
<ng-icon name="mynaFolderPlus" size="0.9rem" />
|
|
</button>
|
|
<button
|
|
(click)="run('createFile')"
|
|
title="New file"
|
|
class="grid size-6 shrink-0 place-items-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
<ng-icon name="mynaFilePlus" size="0.9rem" />
|
|
</button>
|
|
<button
|
|
(click)="run('chmod')"
|
|
title="Permissions"
|
|
class="grid size-6 shrink-0 place-items-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
<ng-icon name="mynaLock" size="0.9rem" />
|
|
</button>
|
|
<button
|
|
(click)="run('delete')"
|
|
title="Delete (F8 / Del)"
|
|
class="flex h-6 shrink-0 items-center rounded-md px-1 text-muted-foreground hover:bg-accent hover:text-destructive"
|
|
>
|
|
<ng-icon name="mynaTrash" size="0.9rem" />
|
|
<span class="ml-1 hidden text-[11px] @[30rem]:inline">Delete</span>
|
|
</button>
|
|
|
|
@if (menuOpen()) {
|
|
<div class="fixed inset-0 z-20" (click)="menuOpen.set(false)"></div>
|
|
<div
|
|
class="absolute top-full left-2 z-30 mt-1 max-h-80 w-64 overflow-y-auto rounded-lg border border-border bg-card py-1 text-card-foreground shadow-xl"
|
|
>
|
|
<button
|
|
(click)="pickLocal()"
|
|
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs hover:bg-accent"
|
|
>
|
|
<ng-icon name="mynaHardDrive" size="0.9rem" class="shrink-0 text-muted-foreground" />
|
|
<span class="flex-1">Local</span>
|
|
@if (!isRemote()) {
|
|
<ng-icon name="mynaCheck" size="0.85rem" class="shrink-0 text-primary" />
|
|
}
|
|
</button>
|
|
|
|
@if (connections.grouped().root.length || connections.grouped().folders.length) {
|
|
<div class="my-1 border-t border-border"></div>
|
|
}
|
|
@for (connection of connections.grouped().root; track connection.id) {
|
|
<button
|
|
(click)="pickConnection(connection)"
|
|
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs hover:bg-accent"
|
|
>
|
|
<ng-icon name="mynaServers" size="0.9rem" class="shrink-0 text-muted-foreground" />
|
|
<span class="flex-1 truncate">{{ connection.name }}</span>
|
|
</button>
|
|
}
|
|
@for (group of connections.grouped().folders; track group.folder.id) {
|
|
<div class="px-3 pt-1.5 pb-0.5 text-[10px] font-medium tracking-wide text-muted-foreground uppercase">
|
|
{{ group.folder.name }}
|
|
</div>
|
|
@for (connection of group.connections; track connection.id) {
|
|
<button
|
|
(click)="pickConnection(connection)"
|
|
class="flex w-full items-center gap-2 py-1.5 pr-3 pl-5 text-left text-xs hover:bg-accent"
|
|
>
|
|
<ng-icon name="mynaServers" size="0.9rem" class="shrink-0 text-muted-foreground" />
|
|
<span class="flex-1 truncate">{{ connection.name }}</span>
|
|
</button>
|
|
}
|
|
}
|
|
|
|
<div class="my-1 border-t border-border"></div>
|
|
<button
|
|
(click)="openManager()"
|
|
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs hover:bg-accent"
|
|
>
|
|
<ng-icon name="mynaCog" size="0.9rem" class="shrink-0 text-muted-foreground" />
|
|
Manage connections…
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- path bar -->
|
|
<div class="flex items-center gap-1 border-b border-border px-2 py-1.5">
|
|
<button
|
|
(click)="panel().up()"
|
|
title="Parent directory (Backspace)"
|
|
class="grid size-(--path-ctl-h) shrink-0 place-items-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
<ng-icon name="mynaArrowUp" size="1rem" />
|
|
</button>
|
|
<input
|
|
[value]="pathDraft()"
|
|
(input)="pathDraft.set($any($event.target).value)"
|
|
(keydown.enter)="commitPath()"
|
|
(keydown.escape)="resetPath()"
|
|
(keydown)="$event.stopPropagation()"
|
|
spellcheck="false"
|
|
class="h-(--path-ctl-h) min-w-0 flex-1 rounded-md border border-input bg-transparent px-2 font-mono text-(length:--path-font) outline-none focus:ring-2 focus:ring-ring"
|
|
/>
|
|
<button
|
|
(click)="panel().refresh()"
|
|
title="Refresh"
|
|
class="grid size-(--path-ctl-h) shrink-0 place-items-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
@if (panel().loading()) {
|
|
<ng-icon name="mynaSpinner" size="1rem" class="animate-spin" />
|
|
} @else {
|
|
<ng-icon name="mynaRefresh" size="1rem" />
|
|
}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- column headers -->
|
|
<div
|
|
class="grid grid-cols-[minmax(0,1fr)_5.5rem_8.5rem] border-b border-border text-[11px] font-medium text-muted-foreground"
|
|
>
|
|
<button class="px-2 py-1 text-left hover:text-foreground" (click)="panel().toggleSort('name')">
|
|
Name {{ sortIndicator('name') }}
|
|
</button>
|
|
<button class="px-2 py-1 text-right hover:text-foreground" (click)="panel().toggleSort('size')">
|
|
Size {{ sortIndicator('size') }}
|
|
</button>
|
|
<button class="px-2 py-1 text-left hover:text-foreground" (click)="panel().toggleSort('mtime')">
|
|
Modified {{ sortIndicator('mtime') }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- type-to-filter bar (appears while a query is active) -->
|
|
@if (panel().filter(); as query) {
|
|
<div class="flex items-center gap-1.5 border-b border-border bg-accent/50 px-2 py-1">
|
|
<ng-icon name="mynaSearch" size="0.85rem" class="shrink-0 text-muted-foreground" />
|
|
<span class="min-w-0 flex-1 truncate font-mono text-xs">{{ query }}</span>
|
|
<span class="shrink-0 text-[11px] text-muted-foreground">
|
|
{{ filterMatches() }} match{{ filterMatches() === 1 ? '' : 'es' }} · Esc
|
|
</span>
|
|
<button
|
|
(click)="clearFilter()"
|
|
title="Clear filter (Esc)"
|
|
class="grid size-5 shrink-0 place-items-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
<ng-icon name="mynaX" size="0.8rem" />
|
|
</button>
|
|
</div>
|
|
}
|
|
|
|
<!-- file list -->
|
|
<div
|
|
#list
|
|
tabindex="0"
|
|
(keydown)="onKeydown($event)"
|
|
(mousedown)="activate()"
|
|
class="flex-1 overflow-y-auto outline-none"
|
|
>
|
|
@for (entry of panel().sorted(); track entry.name; let i = $index) {
|
|
<div
|
|
[attr.data-index]="i"
|
|
(click)="rowClick($event, i)"
|
|
(dblclick)="openAt(i)"
|
|
class="grid h-(--row-h) grid-cols-[minmax(0,1fr)_5.5rem_8.5rem] items-center text-(length:--list-font)"
|
|
[class.bg-accent]="i === panel().cursor() && !isSelected(entry)"
|
|
[class.bg-primary/15]="isSelected(entry)"
|
|
[class.ring-1]="i === panel().cursor() && isActive()"
|
|
[class.ring-inset]="i === panel().cursor() && isActive()"
|
|
[class.ring-ring]="i === panel().cursor() && isActive()"
|
|
>
|
|
<div class="flex min-w-0 items-center gap-1.5 px-2">
|
|
<ng-icon
|
|
[name]="iconFor(entry)"
|
|
size="0.95rem"
|
|
[class]="entry.type === 'dir' ? 'shrink-0 text-primary' : 'shrink-0 text-muted-foreground'"
|
|
/>
|
|
<span class="truncate" [class.italic]="entry.type === 'symlink'">{{ entry.name }}</span>
|
|
</div>
|
|
<span class="px-2 text-right font-mono text-(length:--list-meta-font) tabular-nums text-muted-foreground">
|
|
{{ entry.type === 'dir' ? '—' : formatSize(entry.size) }}
|
|
</span>
|
|
<span class="px-2 font-mono text-(length:--list-meta-font) tabular-nums text-muted-foreground">
|
|
{{ formatDate(entry.mtime) }}
|
|
</span>
|
|
</div>
|
|
} @empty {
|
|
<div class="p-6 text-center text-xs text-muted-foreground">Empty directory</div>
|
|
}
|
|
@if (panel().filter() && filterMatches() === 0) {
|
|
<div class="p-6 text-center text-xs text-muted-foreground">No matches</div>
|
|
}
|
|
</div>
|