feat: Sprint 4 — SFTP dual-pane with per-type icons, breadcrumb, drop-zone, create/delete, toast

This commit is contained in:
swanadiva
2026-07-07 13:35:01 +07:00
parent c5ac07e27f
commit 941a67b6ea
8 changed files with 364 additions and 10 deletions
+128
View File
@@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SFTP · Hostkeeper V2</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/output.css">
<link rel="stylesheet" href="/static/css/custom.css">
<script src="/static/js/htmx.min.js"></script>
<script src="/static/js/alpine.min.js" defer></script>
</head>
<body class="bg-surface text-on-surface antialiased dot-grid min-h-screen">
<div class="flex min-h-screen">
{{template "nav" .}}
<div class="flex-1 flex flex-col overflow-hidden">
<header class="h-14 border-b border-outline-variant/30 flex items-center px-6 bg-white/70 backdrop-blur-md">
<h1 class="text-lg font-semibold text-on-surface">{{.Title}}</h1>
</header>
<main id="main-content" class="flex-1 overflow-auto p-6">
<div id="sftp-page" class="max-w-7xl mx-auto"
x-data="{ toast: '', showToast: false, showFolderModal: false, folderPane: 'local' }"
x-init="$watch('showToast', v => v && setTimeout(() => showToast = false, 3500))">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Local pane -->
<div class="rounded-xl bg-white/70 backdrop-blur-md border border-outline-variant/30 overflow-hidden">
<div class="flex items-center gap-2 px-4 py-3 border-b border-outline-variant/20 bg-white/50">
<svg class="w-4 h-4 text-on-surface-variant" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
<span class="text-sm font-medium text-on-surface">Local Workstation</span>
<div class="flex-1"></div>
<button @click="folderPane = 'local'; showFolderModal = true"
class="p-1 rounded-lg hover:bg-surface-container-low text-on-surface-variant hover:text-on-surface transition-colors" title="New Folder">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3-3v6m-5-8H5a2 2 0 00-2 2v8a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2v2"/></svg>
</button>
<input type="search" placeholder="Filter files..." hx-get="/sftp/pane?pane=local&q=..." hx-trigger="keyup delay:200ms" hx-target="#local-pane"
class="w-40 px-2 py-1 rounded-lg border border-outline-variant/30 bg-white text-xs text-on-surface placeholder-on-surface-variant focus:outline-none focus:ring-2 focus:ring-primary/30">
</div>
<div class="flex items-center gap-1 px-4 py-2 border-b border-outline-variant/10 text-xs text-on-surface-variant font-mono bg-surface-container-low/30">
{{range .LocalBreadcrumb}}
<a href="#" hx-get="/sftp/pane?pane=local&dir={{.Path}}" hx-target="#local-pane"
class="hover:text-primary transition-colors">{{.Name}}</a>
<span class="text-on-surface-variant/40">/</span>
{{end}}
</div>
<div id="local-pane">
{{template "sftp_pane" dict "Pane" "local" "Files" .LocalFiles "Breadcrumb" .LocalBreadcrumb}}
</div>
</div>
<!-- Remote pane -->
<div class="rounded-xl bg-white/70 backdrop-blur-md border border-outline-variant/30 overflow-hidden">
<div class="flex items-center gap-2 px-4 py-3 border-b border-outline-variant/20 bg-white/50">
<svg class="w-4 h-4 text-on-surface-variant" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/></svg>
<span class="text-sm font-medium text-on-surface">Remote Cluster Node</span>
<div class="flex-1"></div>
<button @click="folderPane = 'remote'; showFolderModal = true"
class="p-1 rounded-lg hover:bg-surface-container-low text-on-surface-variant hover:text-on-surface transition-colors" title="New Folder">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3-3v6m-5-8H5a2 2 0 00-2 2v8a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2v2"/></svg>
</button>
<input type="search" placeholder="Filter files..." hx-get="/sftp/pane?pane=remote&q=..." hx-trigger="keyup delay:200ms" hx-target="#remote-pane"
class="w-40 px-2 py-1 rounded-lg border border-outline-variant/30 bg-white text-xs text-on-surface placeholder-on-surface-variant focus:outline-none focus:ring-2 focus:ring-primary/30">
</div>
<div class="flex items-center gap-1 px-4 py-2 border-b border-outline-variant/10 text-xs text-on-surface-variant font-mono bg-surface-container-low/30">
{{range .RemoteBreadcrumb}}
<a href="#" hx-get="/sftp/pane?pane=remote&dir={{.Path}}" hx-target="#remote-pane"
class="hover:text-primary transition-colors">{{.Name}}</a>
<span class="text-on-surface-variant/40">/</span>
{{end}}
</div>
<div id="remote-pane">
{{template "sftp_pane" dict "Pane" "remote" "Files" .RemoteFiles "Breadcrumb" .RemoteBreadcrumb}}
</div>
</div>
</div>
<!-- Drop zone -->
<div class="mt-6 rounded-xl border-2 border-dashed border-outline-variant/30 p-8 flex flex-col items-center justify-center text-on-surface-variant/50 hover:border-primary/30 hover:text-primary/50 transition-colors cursor-pointer">
<svg class="w-10 h-10 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/></svg>
<p class="text-sm font-medium">Drop files here to upload</p>
<p class="text-xs mt-1">or click to browse</p>
</div>
<!-- Toast -->
<div x-show="showToast" x-cloak
x-text="toast"
class="fixed bottom-6 right-6 bg-[#191c1e] text-white px-4 py-3 rounded-xl text-sm shadow-lg z-50 animate-slide-in"
style="max-width: 360px;">
</div>
<!-- Folder create modal -->
<div id="folder-modal" class="fixed inset-0 z-50" x-data="{ open: false }" x-show="open" x-cloak
@open-folder-modal.window="open = true; folderPane = $event.detail.pane" @keydown.escape.window="open = false">
<div class="absolute inset-0 bg-black/30 backdrop-blur-sm" @click="open = false"></div>
<div class="absolute inset-0 flex items-center justify-center p-4" x-show="open" x-transition>
<div class="bg-white rounded-xl shadow-xl w-full max-w-sm p-6" @click.outside="open = false">
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-semibold text-on-surface">New Folder</h2>
<button @click="open = false" class="p-1 rounded-lg hover:bg-surface-container-low text-on-surface-variant">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<form hx-post="/sftp/mkdir" hx-target="#local-pane" hx-swap="outerHTML"
@submit="open = false"
class="space-y-4">
<input type="hidden" name="pane" :value="folderPane">
<div>
<label class="block text-sm font-medium text-on-surface mb-1">Folder Name</label>
<input type="text" name="name" required placeholder="e.g. new-project"
class="w-full px-3 py-2 rounded-lg border border-outline-variant/50 bg-white text-sm text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30">
</div>
<div class="flex justify-end gap-3 pt-2">
<button type="button" @click="open = false"
class="px-4 py-2 rounded-lg text-sm font-medium text-on-surface-variant hover:bg-surface-container-low transition-colors">Cancel</button>
<button type="submit"
class="px-4 py-2 rounded-lg text-sm font-medium bg-primary text-white hover:bg-primary/90 transition-colors">Create</button>
</div>
</form>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
</body>
</html>