tabby-sftp-ui 0.2.1 → 0.2.2

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/dist/index.js CHANGED
@@ -632,24 +632,31 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
632
632
  return;
633
633
  }
634
634
  // Drag & drop from OS file manager (Explorer/Finder) into the remote pane
635
- const dtFiles = Array.from(ev.dataTransfer?.files ?? []);
636
- if (dtFiles.length) {
637
- const paths = dtFiles
638
- .map(f => f.path)
639
- .filter((p) => Boolean(p));
640
- if (paths.length) {
641
- try {
642
- for (const p of paths) {
643
- await this.uploadLocalPathToRemote(this.remotePath, p);
644
- }
645
- await this.refreshRemote();
646
- }
647
- catch (e) {
648
- console.error('[SFTP-UI] Upload from OS drop failed', e);
635
+ const osPaths = this.getDroppedOsPaths(ev);
636
+ if (osPaths.length) {
637
+ try {
638
+ for (const p of osPaths) {
639
+ await this.uploadLocalPathToRemote(this.remotePath, p);
649
640
  }
641
+ await this.refreshRemote();
642
+ }
643
+ catch (e) {
644
+ console.error('[SFTP-UI] Upload from OS drop failed', e);
650
645
  }
651
646
  return;
652
647
  }
648
+ // Fallback: use Tabby's native drag parser (supports directories and HTMLFileUpload)
649
+ try {
650
+ const dirUpload = await this.platform.startUploadFromDragEvent?.(ev, true);
651
+ if (dirUpload && this.sftpSession) {
652
+ await this.uploadDirectoryUploadToRemote(this.remotePath, dirUpload);
653
+ await this.refreshRemote();
654
+ return;
655
+ }
656
+ }
657
+ catch (e) {
658
+ console.error('[SFTP-UI] startUploadFromDragEvent failed', e);
659
+ }
653
660
  const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
654
661
  if (!raw) {
655
662
  return;
@@ -679,24 +686,31 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
679
686
  ev.preventDefault();
680
687
  this.localDropActive = false;
681
688
  // Drag & drop from OS file manager into the local pane (copy into current local folder)
682
- const dtFiles = Array.from(ev.dataTransfer?.files ?? []);
683
- if (dtFiles.length) {
684
- const paths = dtFiles
685
- .map(f => f.path)
686
- .filter((p) => Boolean(p));
687
- if (paths.length) {
688
- try {
689
- for (const p of paths) {
690
- await this.copyLocalPathIntoLocalDir(this.localPath, p);
691
- }
692
- await this.refreshLocal();
693
- }
694
- catch (e) {
695
- console.error('[SFTP-UI] Local copy from OS drop failed', e);
689
+ const osPaths = this.getDroppedOsPaths(ev);
690
+ if (osPaths.length) {
691
+ try {
692
+ for (const p of osPaths) {
693
+ await this.copyLocalPathIntoLocalDir(this.localPath, p);
696
694
  }
695
+ await this.refreshLocal();
696
+ }
697
+ catch (e) {
698
+ console.error('[SFTP-UI] Local copy from OS drop failed', e);
697
699
  }
698
700
  return;
699
701
  }
702
+ // Fallback: use Tabby's native drag parser, then write files to disk
703
+ try {
704
+ const dirUpload = await this.platform.startUploadFromDragEvent?.(ev, true);
705
+ if (dirUpload) {
706
+ await this.writeDirectoryUploadToLocal(this.localPath, dirUpload);
707
+ await this.refreshLocal();
708
+ return;
709
+ }
710
+ }
711
+ catch (e) {
712
+ console.error('[SFTP-UI] startUploadFromDragEvent (local) failed', e);
713
+ }
700
714
  const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
701
715
  if (!raw) {
702
716
  return;
@@ -770,6 +784,110 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
770
784
  }
771
785
  await fs_promises__WEBPACK_IMPORTED_MODULE_2__.copyFile(srcPath, destPath);
772
786
  }
787
+ async uploadDirectoryUploadToRemote(remoteDir, dirUpload) {
788
+ if (!this.sftpSession) {
789
+ return;
790
+ }
791
+ const childrens = dirUpload?.getChildrens?.() ?? [];
792
+ for (const item of childrens) {
793
+ // DirectoryUpload
794
+ if (typeof item?.getChildrens === 'function') {
795
+ const name = item.getName?.() || 'folder';
796
+ const nextRemote = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(remoteDir, name);
797
+ try {
798
+ await this.sftpSession.mkdir(nextRemote);
799
+ }
800
+ catch {
801
+ // ignore (might already exist)
802
+ }
803
+ await this.uploadDirectoryUploadToRemote(nextRemote, item);
804
+ continue;
805
+ }
806
+ // FileUpload (including HTMLFileUpload)
807
+ if (typeof item?.read === 'function' && typeof item?.getName === 'function') {
808
+ const fileUpload = item;
809
+ const name = fileUpload.getName();
810
+ const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(remoteDir, name);
811
+ this.trackTransfer(fileUpload, 'upload', targetRemotePath, name);
812
+ await this.sftpSession.upload(targetRemotePath, fileUpload);
813
+ }
814
+ }
815
+ }
816
+ async writeDirectoryUploadToLocal(localDir, dirUpload) {
817
+ const childrens = dirUpload?.getChildrens?.() ?? [];
818
+ for (const item of childrens) {
819
+ if (typeof item?.getChildrens === 'function') {
820
+ const name = item.getName?.() || 'folder';
821
+ const nextLocal = path__WEBPACK_IMPORTED_MODULE_0__.join(localDir, name);
822
+ await fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(nextLocal, { recursive: true });
823
+ await this.writeDirectoryUploadToLocal(nextLocal, item);
824
+ continue;
825
+ }
826
+ if (typeof item?.readAll === 'function' && typeof item?.getName === 'function') {
827
+ const name = item.getName();
828
+ const targetLocal = path__WEBPACK_IMPORTED_MODULE_0__.join(localDir, name);
829
+ const buf = await item.readAll();
830
+ await fs_promises__WEBPACK_IMPORTED_MODULE_2__.writeFile(targetLocal, Buffer.from(buf));
831
+ try {
832
+ item.close?.();
833
+ }
834
+ catch {
835
+ // ignore
836
+ }
837
+ }
838
+ }
839
+ }
840
+ getDroppedOsPaths(ev) {
841
+ const dt = ev.dataTransfer;
842
+ if (!dt) {
843
+ return [];
844
+ }
845
+ // 1) Electron-style File.path
846
+ const filePaths = Array.from(dt.files ?? [])
847
+ .map(f => f.path)
848
+ .filter((p) => Boolean(p));
849
+ if (filePaths.length) {
850
+ return filePaths;
851
+ }
852
+ // 2) Sometimes paths are exposed as URIs
853
+ const uriList = dt.getData('text/uri-list') || '';
854
+ const uris = uriList
855
+ .split(/\r?\n/g)
856
+ .map(x => x.trim())
857
+ .filter(x => x && !x.startsWith('#'))
858
+ .map(x => {
859
+ if (x.startsWith('file://')) {
860
+ try {
861
+ return decodeURIComponent(x.replace(/^file:\/\//, ''));
862
+ }
863
+ catch {
864
+ return x.replace(/^file:\/\//, '');
865
+ }
866
+ }
867
+ return x;
868
+ })
869
+ .filter(x => x && (x.includes(':\\') || x.startsWith('/') || x.startsWith('\\\\')));
870
+ if (uris.length) {
871
+ return uris;
872
+ }
873
+ // 3) Plain text sometimes contains a local path
874
+ const text = dt.getData('text/plain') || '';
875
+ const textLines = text.split(/\r?\n/g).map(x => x.trim()).filter(Boolean);
876
+ const textPaths = textLines
877
+ .map(x => {
878
+ if (x.startsWith('file://')) {
879
+ try {
880
+ return decodeURIComponent(x.replace(/^file:\/\//, ''));
881
+ }
882
+ catch {
883
+ return x.replace(/^file:\/\//, '');
884
+ }
885
+ }
886
+ return x;
887
+ })
888
+ .filter(x => x.includes(':\\') || x.startsWith('/') || x.startsWith('\\\\'));
889
+ return textPaths;
890
+ }
773
891
  getFilteredLocalEntries() {
774
892
  const entriesRef = this.localEntries;
775
893
  const filter = this.localFilter;
@@ -1667,6 +1785,10 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1667
1785
  }
1668
1786
  }
1669
1787
  onKeyDown(event) {
1788
+ const target = event.target;
1789
+ const isTypingTarget = Boolean(target) && (target?.tagName === 'INPUT' ||
1790
+ target?.tagName === 'TEXTAREA' ||
1791
+ target?.isContentEditable);
1670
1792
  if (event.key === 'Escape') {
1671
1793
  if (this.inputDialogVisible) {
1672
1794
  event.preventDefault();
@@ -1680,6 +1802,10 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1680
1802
  }
1681
1803
  }
1682
1804
  if (event.key === 'Delete' || event.key === 'Backspace') {
1805
+ // Don't intercept Delete/Backspace while typing in inputs
1806
+ if (isTypingTarget) {
1807
+ return;
1808
+ }
1683
1809
  event.preventDefault();
1684
1810
  if (this.selectedRemote.length) {
1685
1811
  this.remoteDelete();
@@ -1979,368 +2105,368 @@ __decorate([
1979
2105
  SftpManagerTabComponent = __decorate([
1980
2106
  (0,_angular_core__WEBPACK_IMPORTED_MODULE_5__.Component)({
1981
2107
  selector: 'tabby-sftp-manager-tab',
1982
- template: `
1983
- <div class="sftp-root" tabindex="0" (keydown)="onKeyDown($event)">
1984
- <div class="top-profiles" *ngIf="profile || recentProfiles.length">
1985
- <div class="current" *ngIf="profile">
1986
- <span class="label">Device:</span>
1987
- <span class="value">{{ getProfileLabel(profile) }}</span>
1988
- </div>
1989
- <div class="recent" *ngIf="recentProfiles.length">
1990
- <span class="label">Recent:</span>
1991
- <button
1992
- class="profile-chip"
1993
- *ngFor="let p of recentProfiles"
1994
- (click)="launchProfileFromSFTP(p)"
1995
- >
1996
- {{ getProfileLabel(p) }}
1997
- </button>
1998
- </div>
1999
- </div>
2000
- <div class="sftp-body">
2001
- <div class="pane">
2002
- <div class="pane-title">
2003
- <div class="pane-label">Local</div>
2004
- <div class="pane-path">
2005
- <input
2006
- [(ngModel)]="localPathInput"
2007
- (keyup.enter)="goToLocalPathInput()"
2008
- />
2009
- </div>
2010
- <div class="pane-actions">
2011
- <select class="path-preset" (change)="onLocalPresetChange($event.target.value)">
2012
- <option value="">Go to…</option>
2013
- <option *ngFor="let p of localPathPresets" [value]="p.id">
2014
- {{ p.label }}
2015
- </option>
2016
- </select>
2017
- <button
2018
- class="fav-toggle"
2019
- [class.active]="isCurrentFavorite()"
2020
- (click)="toggleCurrentFavorite()"
2021
- title="Toggle favorite for this path"
2022
- >
2023
-
2024
- </button>
2025
- <select class="path-favorite" (change)="onLocalFavoriteSelect($event.target.value)">
2026
- <option value="">Favorites…</option>
2027
- <option *ngFor="let f of localFavorites" [value]="f.id">
2028
- {{ f.label }}
2029
- </option>
2030
- </select>
2031
- <button (click)="localUp()" [disabled]="!canLocalUp()">Up</button>
2032
- <button (click)="goToLocalPathInput()">Go</button>
2033
- <button (click)="refreshLocal()">Refresh</button>
2034
- </div>
2035
- </div>
2036
- <div class="pane-filters">
2037
- <div class="breadcrumbs">
2038
- <ng-container *ngFor="let part of getLocalBreadcrumbs(); let i = index; let last = last">
2039
- <button
2040
- class="crumb-button"
2041
- (click)="navigateLocalBreadcrumb(i)"
2042
- (contextmenu)="onLocalBreadcrumbContextMenu(i, $event)"
2043
- >
2044
- {{ part.label }}
2045
- </button>
2046
- <span class="crumb-separator" *ngIf="!last">›</span>
2047
- </ng-container>
2048
- </div>
2049
- <input [(ngModel)]="localFilter" placeholder="Filter files..." />
2050
- <label class="show-hidden-toggle">
2051
- <input type="checkbox" [(ngModel)]="showHiddenLocal" />
2052
- <span>Show hidden</span>
2053
- </label>
2054
- </div>
2055
- <div class="pane-list"
2056
- (dragover)="onDragOver($event)"
2057
- (drop)="onDropOnLocal($event)"
2058
- >
2059
- <div class="entry header">
2060
- <span class="icon"></span>
2061
- <span class="name sortable" (click)="setLocalSort('name')">Name</span>
2062
- <span class="size sortable" (click)="setLocalSort('size')">Size</span>
2063
- <span class="date sortable" (click)="setLocalSort('modified')">Modified</span>
2064
- </div>
2065
- <div
2066
- class="entry"
2067
- *ngFor="let e of getFilteredLocalEntries()"
2068
- (click)="selectLocal(e, $event)"
2069
- (dblclick)="openLocal(e)"
2070
- (mousedown)="onLocalMouseDown(e, $event)"
2071
- (contextmenu)="onLocalContextMenu(e, $event)"
2072
- (dragover)="onLocalEntryDragOver(e, $event)"
2073
- (drop)="onLocalEntryDrop(e, $event)"
2074
- [class.drop-target]="localDropActive"
2075
- [class.selected]="isLocalSelected(e)"
2076
- [draggable]="true"
2077
- (dragstart)="onDragStartLocal($event, e)"
2078
- >
2079
- <span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
2080
- <span class="name">{{ e.name }}</span>
2081
- <span class="size">{{ getLocalSizeDisplay(e) }}</span>
2082
- <span class="date">{{ e.mtimeMs ? (e.mtimeMs | date:'yyyy-MM-dd HH:mm') : '' }}</span>
2083
- </div>
2084
- </div>
2085
- <div class="pane-actions-bar">
2086
- <div class="selection" *ngIf="selectedLocal.length">
2087
- Selected: {{ selectedLocal.length === 1 ? selectedLocal[0].name : (selectedLocal.length + ' items') }}
2088
- </div>
2089
- <div class="action-inputs">
2090
- <input [(ngModel)]="localActionName" placeholder="Name / new name" />
2091
- <input [(ngModel)]="localActionPerms" placeholder="Perms (e.g. 755)" />
2092
- </div>
2093
- <div class="action-buttons">
2094
- <button (click)="localRename()" [disabled]="selectedLocal.length !== 1">Rename</button>
2095
- <button (click)="refreshLocal()">Refresh</button>
2096
- <button (click)="localDelete()" [disabled]="!selectedLocal.length">Delete</button>
2097
- <button (click)="localNewFolder()">New Folder</button>
2098
- <button (click)="localEditPermissions()" [disabled]="selectedLocal.length !== 1 || !localActionPerms">Edit Permissions</button>
2099
- <button (click)="localShowSize()" [disabled]="selectedLocal.length !== 1 || !selectedLocal[0].isDirectory">Show Size</button>
2100
- </div>
2101
- </div>
2102
- </div>
2103
-
2104
- <div class="pane">
2105
- <div class="pane-title">
2106
- <div class="pane-label">
2107
- Remote
2108
- <span *ngIf="connected && profile?.options?.host" class="pane-sub">
2109
- — {{ profile.options.host }}
2110
- </span>
2111
- </div>
2112
- <div class="pane-path">
2113
- <input
2114
- [(ngModel)]="remotePathInput"
2115
- (keyup.enter)="goToRemotePathInput()"
2116
- [disabled]="!connected"
2117
- />
2118
- </div>
2119
- <div class="pane-actions">
2120
- <button (click)="remoteUp()" [disabled]="!connected || remotePath === '/'">Up</button>
2121
- <button (click)="goToRemotePathInput()" [disabled]="!connected">Go</button>
2122
- <button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
2123
- </div>
2124
- </div>
2125
- <div class="pane-filters">
2126
- <div class="breadcrumbs" *ngIf="connected">
2127
- <ng-container *ngFor="let part of getRemoteBreadcrumbs(); let i = index; let last = last">
2128
- <button
2129
- class="crumb-button"
2130
- (click)="navigateRemoteBreadcrumb(i)"
2131
- >
2132
- {{ part.label }}
2133
- </button>
2134
- <span class="crumb-separator" *ngIf="!last">›</span>
2135
- </ng-container>
2136
- </div>
2137
- <input [(ngModel)]="remoteFilter" placeholder="Filter files..." />
2138
- <label class="show-hidden-toggle">
2139
- <input type="checkbox" [(ngModel)]="showHiddenRemote" />
2140
- <span>Show hidden</span>
2141
- </label>
2142
- </div>
2143
- <div class="pane-list"
2144
- (dragover)="onDragOver($event)"
2145
- (drop)="onDropOnRemote($event)"
2146
- >
2147
- <div class="entry dim" *ngIf="!connected">
2148
- <span class="name">Not connected</span>
2149
- </div>
2150
- <div class="entry header" *ngIf="connected">
2151
- <span class="icon"></span>
2152
- <span class="name sortable" (click)="setRemoteSort('name')">Name</span>
2153
- <span class="size sortable" (click)="setRemoteSort('size')">Size</span>
2154
- <span class="date sortable" (click)="setRemoteSort('modified')">Modified</span>
2155
- </div>
2156
- <div
2157
- class="entry"
2158
- *ngIf="connected && remotePath !== '/'"
2159
- (dblclick)="remoteUp()"
2160
- >
2161
- <span class="icon">⬆</span>
2162
- <span class="name">Go up</span>
2163
- <span class="size"></span>
2164
- <span class="date"></span>
2165
- </div>
2166
- <div
2167
- class="entry"
2168
- *ngFor="let e of getFilteredRemoteEntries()"
2169
- (click)="selectRemote(e, $event)"
2170
- (dblclick)="openRemote(e)"
2171
- (mousedown)="onRemoteMouseDown(e, $event)"
2172
- (contextmenu)="onRemoteContextMenu(e, $event)"
2173
- (dragover)="onRemoteEntryDragOver(e, $event)"
2174
- (drop)="onRemoteEntryDrop(e, $event)"
2175
- [class.drop-target]="remoteDropActive"
2176
- [class.selected]="isRemoteSelected(e)"
2177
- [draggable]="connected"
2178
- (dragstart)="onDragStartRemote($event, e)"
2179
- >
2180
- <span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
2181
- <span class="name">{{ e.name }}</span>
2182
- <span class="size">{{ getRemoteSizeDisplay(e) }}</span>
2183
- <span class="date">{{ e.modified | date:'yyyy-MM-dd HH:mm' }}</span>
2184
- </div>
2185
- </div>
2186
- <div class="pane-actions-bar">
2187
- <div class="selection" *ngIf="selectedRemote.length">
2188
- Selected: {{ selectedRemote.length === 1 ? selectedRemote[0].name : (selectedRemote.length + ' items') }}
2189
- </div>
2190
- <div class="action-inputs">
2191
- <input [(ngModel)]="remoteActionName" placeholder="Name / new name" />
2192
- <input [(ngModel)]="remoteActionPerms" placeholder="Perms (e.g. 755)" />
2193
- </div>
2194
- <div class="action-buttons">
2195
- <button (click)="remoteRename()" [disabled]="selectedRemote.length !== 1">Rename</button>
2196
- <button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
2197
- <button (click)="remoteDelete()" [disabled]="!selectedRemote.length">Delete</button>
2198
- <button (click)="remoteNewFolder()" [disabled]="!connected">New Folder</button>
2199
- <button (click)="remoteEditPermissions()" [disabled]="selectedRemote.length !== 1 || !remoteActionPerms">Edit Permissions</button>
2200
- <button (click)="remoteShowSize()" [disabled]="selectedRemote.length !== 1 || !selectedRemote[0].isDirectory">Show Size</button>
2201
- <button (click)="remoteDownload()" [disabled]="!selectedRemote.length">Download</button>
2202
- </div>
2203
- </div>
2204
- </div>
2205
- </div>
2206
- <div class="sftp-transfers" *ngIf="transfers.length">
2207
- <div class="transfer" *ngFor="let t of transfers">
2208
- <div class="transfer-main">
2209
- <div class="transfer-title">
2210
- <span class="direction">{{ t.direction === 'upload' ? 'Upload' : 'Download' }}</span>
2211
- <span class="name">{{ t.name }}</span>
2212
- </div>
2213
- <div class="transfer-path">
2214
- <span class="label">Remote:</span>
2215
- <span class="value">{{ t.remotePath }}</span>
2216
- </div>
2217
- <div class="transfer-path">
2218
- <span class="label">Local:</span>
2219
- <span class="value">{{ t.localPath }}</span>
2220
- </div>
2221
- <div class="bar">
2222
- <div class="fill" [style.width.%]="getTransferProgress(t.transfer)"></div>
2223
- </div>
2224
- </div>
2225
- <div class="transfer-stats">
2226
- <div class="percent">{{ getTransferProgress(t.transfer) | number:'1.0-0' }}%</div>
2227
- <div class="speed">{{ formatSpeed(t.transfer.getSpeed()) }}</div>
2228
- <button class="btn-cancel" (click)="cancelTransfer(t)" [disabled]="t.transfer.isComplete() || t.transfer.isCancelled()">Cancel</button>
2229
- </div>
2230
- </div>
2231
- </div>
2232
-
2233
- <div class="delete-overlay" *ngIf="deleteConfirmVisible">
2234
- <div class="delete-dialog">
2235
- <div class="delete-text">{{ deleteConfirmText }}</div>
2236
- <div class="delete-buttons">
2237
- <button class="danger" (click)="confirmDelete()">Delete</button>
2238
- <button (click)="cancelDelete()">Cancel</button>
2239
- </div>
2240
- </div>
2241
- </div>
2242
-
2243
- <div class="delete-overlay" *ngIf="inputDialogVisible">
2244
- <div class="delete-dialog" (click)="$event.stopPropagation()">
2245
- <div class="delete-text">{{ inputDialogTitle }}</div>
2246
- <input
2247
- class="dialog-input"
2248
- [(ngModel)]="inputDialogValue"
2249
- [placeholder]="inputDialogPlaceholder"
2250
- (keyup.enter)="confirmInputDialog()"
2251
- />
2252
- <div class="delete-buttons">
2253
- <button class="danger" (click)="confirmInputDialog()" [disabled]="!inputDialogValue.trim()">OK</button>
2254
- <button (click)="cancelInputDialog()">Cancel</button>
2255
- </div>
2256
- </div>
2257
- </div>
2258
-
2259
- <div
2260
- class="local-menu"
2261
- *ngIf="localMenuVisible"
2262
- [style.left.px]="localMenuX"
2263
- [style.top.px]="localMenuY"
2264
- (click)="$event.stopPropagation()"
2265
- >
2266
- <div class="local-menu-item" *ngFor="let item of localMenuItems" (click)="onLocalMenuItemClick(item)">
2267
- {{ item.label }}
2268
- </div>
2269
- </div>
2270
- </div>
2108
+ template: `
2109
+ <div class="sftp-root" tabindex="0" (keydown)="onKeyDown($event)">
2110
+ <div class="top-profiles" *ngIf="profile || recentProfiles.length">
2111
+ <div class="current" *ngIf="profile">
2112
+ <span class="label">Device:</span>
2113
+ <span class="value">{{ getProfileLabel(profile) }}</span>
2114
+ </div>
2115
+ <div class="recent" *ngIf="recentProfiles.length">
2116
+ <span class="label">Recent:</span>
2117
+ <button
2118
+ class="profile-chip"
2119
+ *ngFor="let p of recentProfiles"
2120
+ (click)="launchProfileFromSFTP(p)"
2121
+ >
2122
+ {{ getProfileLabel(p) }}
2123
+ </button>
2124
+ </div>
2125
+ </div>
2126
+ <div class="sftp-body">
2127
+ <div class="pane">
2128
+ <div class="pane-title">
2129
+ <div class="pane-label">Local</div>
2130
+ <div class="pane-path">
2131
+ <input
2132
+ [(ngModel)]="localPathInput"
2133
+ (keyup.enter)="goToLocalPathInput()"
2134
+ />
2135
+ </div>
2136
+ <div class="pane-actions">
2137
+ <select class="path-preset" (change)="onLocalPresetChange($event.target.value)">
2138
+ <option value="">Go to…</option>
2139
+ <option *ngFor="let p of localPathPresets" [value]="p.id">
2140
+ {{ p.label }}
2141
+ </option>
2142
+ </select>
2143
+ <button
2144
+ class="fav-toggle"
2145
+ [class.active]="isCurrentFavorite()"
2146
+ (click)="toggleCurrentFavorite()"
2147
+ title="Toggle favorite for this path"
2148
+ >
2149
+
2150
+ </button>
2151
+ <select class="path-favorite" (change)="onLocalFavoriteSelect($event.target.value)">
2152
+ <option value="">Favorites…</option>
2153
+ <option *ngFor="let f of localFavorites" [value]="f.id">
2154
+ {{ f.label }}
2155
+ </option>
2156
+ </select>
2157
+ <button (click)="localUp()" [disabled]="!canLocalUp()">Up</button>
2158
+ <button (click)="goToLocalPathInput()">Go</button>
2159
+ <button (click)="refreshLocal()">Refresh</button>
2160
+ </div>
2161
+ </div>
2162
+ <div class="pane-filters">
2163
+ <div class="breadcrumbs">
2164
+ <ng-container *ngFor="let part of getLocalBreadcrumbs(); let i = index; let last = last">
2165
+ <button
2166
+ class="crumb-button"
2167
+ (click)="navigateLocalBreadcrumb(i)"
2168
+ (contextmenu)="onLocalBreadcrumbContextMenu(i, $event)"
2169
+ >
2170
+ {{ part.label }}
2171
+ </button>
2172
+ <span class="crumb-separator" *ngIf="!last">›</span>
2173
+ </ng-container>
2174
+ </div>
2175
+ <input [(ngModel)]="localFilter" placeholder="Filter files..." />
2176
+ <label class="show-hidden-toggle">
2177
+ <input type="checkbox" [(ngModel)]="showHiddenLocal" />
2178
+ <span>Show hidden</span>
2179
+ </label>
2180
+ </div>
2181
+ <div class="pane-list"
2182
+ (dragover)="onDragOver($event)"
2183
+ (drop)="onDropOnLocal($event)"
2184
+ >
2185
+ <div class="entry header">
2186
+ <span class="icon"></span>
2187
+ <span class="name sortable" (click)="setLocalSort('name')">Name</span>
2188
+ <span class="size sortable" (click)="setLocalSort('size')">Size</span>
2189
+ <span class="date sortable" (click)="setLocalSort('modified')">Modified</span>
2190
+ </div>
2191
+ <div
2192
+ class="entry"
2193
+ *ngFor="let e of getFilteredLocalEntries()"
2194
+ (click)="selectLocal(e, $event)"
2195
+ (dblclick)="openLocal(e)"
2196
+ (mousedown)="onLocalMouseDown(e, $event)"
2197
+ (contextmenu)="onLocalContextMenu(e, $event)"
2198
+ (dragover)="onLocalEntryDragOver(e, $event)"
2199
+ (drop)="onLocalEntryDrop(e, $event)"
2200
+ [class.drop-target]="localDropActive"
2201
+ [class.selected]="isLocalSelected(e)"
2202
+ [draggable]="true"
2203
+ (dragstart)="onDragStartLocal($event, e)"
2204
+ >
2205
+ <span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
2206
+ <span class="name">{{ e.name }}</span>
2207
+ <span class="size">{{ getLocalSizeDisplay(e) }}</span>
2208
+ <span class="date">{{ e.mtimeMs ? (e.mtimeMs | date:'yyyy-MM-dd HH:mm') : '' }}</span>
2209
+ </div>
2210
+ </div>
2211
+ <div class="pane-actions-bar">
2212
+ <div class="selection" *ngIf="selectedLocal.length">
2213
+ Selected: {{ selectedLocal.length === 1 ? selectedLocal[0].name : (selectedLocal.length + ' items') }}
2214
+ </div>
2215
+ <div class="action-inputs">
2216
+ <input [(ngModel)]="localActionName" placeholder="Name / new name" />
2217
+ <input [(ngModel)]="localActionPerms" placeholder="Perms (e.g. 755)" />
2218
+ </div>
2219
+ <div class="action-buttons">
2220
+ <button (click)="localRename()" [disabled]="selectedLocal.length !== 1">Rename</button>
2221
+ <button (click)="refreshLocal()">Refresh</button>
2222
+ <button (click)="localDelete()" [disabled]="!selectedLocal.length">Delete</button>
2223
+ <button (click)="localNewFolder()">New Folder</button>
2224
+ <button (click)="localEditPermissions()" [disabled]="selectedLocal.length !== 1 || !localActionPerms">Edit Permissions</button>
2225
+ <button (click)="localShowSize()" [disabled]="selectedLocal.length !== 1 || !selectedLocal[0].isDirectory">Show Size</button>
2226
+ </div>
2227
+ </div>
2228
+ </div>
2229
+
2230
+ <div class="pane">
2231
+ <div class="pane-title">
2232
+ <div class="pane-label">
2233
+ Remote
2234
+ <span *ngIf="connected && profile?.options?.host" class="pane-sub">
2235
+ — {{ profile.options.host }}
2236
+ </span>
2237
+ </div>
2238
+ <div class="pane-path">
2239
+ <input
2240
+ [(ngModel)]="remotePathInput"
2241
+ (keyup.enter)="goToRemotePathInput()"
2242
+ [disabled]="!connected"
2243
+ />
2244
+ </div>
2245
+ <div class="pane-actions">
2246
+ <button (click)="remoteUp()" [disabled]="!connected || remotePath === '/'">Up</button>
2247
+ <button (click)="goToRemotePathInput()" [disabled]="!connected">Go</button>
2248
+ <button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
2249
+ </div>
2250
+ </div>
2251
+ <div class="pane-filters">
2252
+ <div class="breadcrumbs" *ngIf="connected">
2253
+ <ng-container *ngFor="let part of getRemoteBreadcrumbs(); let i = index; let last = last">
2254
+ <button
2255
+ class="crumb-button"
2256
+ (click)="navigateRemoteBreadcrumb(i)"
2257
+ >
2258
+ {{ part.label }}
2259
+ </button>
2260
+ <span class="crumb-separator" *ngIf="!last">›</span>
2261
+ </ng-container>
2262
+ </div>
2263
+ <input [(ngModel)]="remoteFilter" placeholder="Filter files..." />
2264
+ <label class="show-hidden-toggle">
2265
+ <input type="checkbox" [(ngModel)]="showHiddenRemote" />
2266
+ <span>Show hidden</span>
2267
+ </label>
2268
+ </div>
2269
+ <div class="pane-list"
2270
+ (dragover)="onDragOver($event)"
2271
+ (drop)="onDropOnRemote($event)"
2272
+ >
2273
+ <div class="entry dim" *ngIf="!connected">
2274
+ <span class="name">Not connected</span>
2275
+ </div>
2276
+ <div class="entry header" *ngIf="connected">
2277
+ <span class="icon"></span>
2278
+ <span class="name sortable" (click)="setRemoteSort('name')">Name</span>
2279
+ <span class="size sortable" (click)="setRemoteSort('size')">Size</span>
2280
+ <span class="date sortable" (click)="setRemoteSort('modified')">Modified</span>
2281
+ </div>
2282
+ <div
2283
+ class="entry"
2284
+ *ngIf="connected && remotePath !== '/'"
2285
+ (dblclick)="remoteUp()"
2286
+ >
2287
+ <span class="icon">⬆</span>
2288
+ <span class="name">Go up</span>
2289
+ <span class="size"></span>
2290
+ <span class="date"></span>
2291
+ </div>
2292
+ <div
2293
+ class="entry"
2294
+ *ngFor="let e of getFilteredRemoteEntries()"
2295
+ (click)="selectRemote(e, $event)"
2296
+ (dblclick)="openRemote(e)"
2297
+ (mousedown)="onRemoteMouseDown(e, $event)"
2298
+ (contextmenu)="onRemoteContextMenu(e, $event)"
2299
+ (dragover)="onRemoteEntryDragOver(e, $event)"
2300
+ (drop)="onRemoteEntryDrop(e, $event)"
2301
+ [class.drop-target]="remoteDropActive"
2302
+ [class.selected]="isRemoteSelected(e)"
2303
+ [draggable]="connected"
2304
+ (dragstart)="onDragStartRemote($event, e)"
2305
+ >
2306
+ <span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
2307
+ <span class="name">{{ e.name }}</span>
2308
+ <span class="size">{{ getRemoteSizeDisplay(e) }}</span>
2309
+ <span class="date">{{ e.modified | date:'yyyy-MM-dd HH:mm' }}</span>
2310
+ </div>
2311
+ </div>
2312
+ <div class="pane-actions-bar">
2313
+ <div class="selection" *ngIf="selectedRemote.length">
2314
+ Selected: {{ selectedRemote.length === 1 ? selectedRemote[0].name : (selectedRemote.length + ' items') }}
2315
+ </div>
2316
+ <div class="action-inputs">
2317
+ <input [(ngModel)]="remoteActionName" placeholder="Name / new name" />
2318
+ <input [(ngModel)]="remoteActionPerms" placeholder="Perms (e.g. 755)" />
2319
+ </div>
2320
+ <div class="action-buttons">
2321
+ <button (click)="remoteRename()" [disabled]="selectedRemote.length !== 1">Rename</button>
2322
+ <button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
2323
+ <button (click)="remoteDelete()" [disabled]="!selectedRemote.length">Delete</button>
2324
+ <button (click)="remoteNewFolder()" [disabled]="!connected">New Folder</button>
2325
+ <button (click)="remoteEditPermissions()" [disabled]="selectedRemote.length !== 1 || !remoteActionPerms">Edit Permissions</button>
2326
+ <button (click)="remoteShowSize()" [disabled]="selectedRemote.length !== 1 || !selectedRemote[0].isDirectory">Show Size</button>
2327
+ <button (click)="remoteDownload()" [disabled]="!selectedRemote.length">Download</button>
2328
+ </div>
2329
+ </div>
2330
+ </div>
2331
+ </div>
2332
+ <div class="sftp-transfers" *ngIf="transfers.length">
2333
+ <div class="transfer" *ngFor="let t of transfers">
2334
+ <div class="transfer-main">
2335
+ <div class="transfer-title">
2336
+ <span class="direction">{{ t.direction === 'upload' ? 'Upload' : 'Download' }}</span>
2337
+ <span class="name">{{ t.name }}</span>
2338
+ </div>
2339
+ <div class="transfer-path">
2340
+ <span class="label">Remote:</span>
2341
+ <span class="value">{{ t.remotePath }}</span>
2342
+ </div>
2343
+ <div class="transfer-path">
2344
+ <span class="label">Local:</span>
2345
+ <span class="value">{{ t.localPath }}</span>
2346
+ </div>
2347
+ <div class="bar">
2348
+ <div class="fill" [style.width.%]="getTransferProgress(t.transfer)"></div>
2349
+ </div>
2350
+ </div>
2351
+ <div class="transfer-stats">
2352
+ <div class="percent">{{ getTransferProgress(t.transfer) | number:'1.0-0' }}%</div>
2353
+ <div class="speed">{{ formatSpeed(t.transfer.getSpeed()) }}</div>
2354
+ <button class="btn-cancel" (click)="cancelTransfer(t)" [disabled]="t.transfer.isComplete() || t.transfer.isCancelled()">Cancel</button>
2355
+ </div>
2356
+ </div>
2357
+ </div>
2358
+
2359
+ <div class="delete-overlay" *ngIf="deleteConfirmVisible">
2360
+ <div class="delete-dialog">
2361
+ <div class="delete-text">{{ deleteConfirmText }}</div>
2362
+ <div class="delete-buttons">
2363
+ <button class="danger" (click)="confirmDelete()">Delete</button>
2364
+ <button (click)="cancelDelete()">Cancel</button>
2365
+ </div>
2366
+ </div>
2367
+ </div>
2368
+
2369
+ <div class="delete-overlay" *ngIf="inputDialogVisible">
2370
+ <div class="delete-dialog" (click)="$event.stopPropagation()">
2371
+ <div class="delete-text">{{ inputDialogTitle }}</div>
2372
+ <input
2373
+ class="dialog-input"
2374
+ [(ngModel)]="inputDialogValue"
2375
+ [placeholder]="inputDialogPlaceholder"
2376
+ (keyup.enter)="confirmInputDialog()"
2377
+ />
2378
+ <div class="delete-buttons">
2379
+ <button class="danger" (click)="confirmInputDialog()" [disabled]="!inputDialogValue.trim()">OK</button>
2380
+ <button (click)="cancelInputDialog()">Cancel</button>
2381
+ </div>
2382
+ </div>
2383
+ </div>
2384
+
2385
+ <div
2386
+ class="local-menu"
2387
+ *ngIf="localMenuVisible"
2388
+ [style.left.px]="localMenuX"
2389
+ [style.top.px]="localMenuY"
2390
+ (click)="$event.stopPropagation()"
2391
+ >
2392
+ <div class="local-menu-item" *ngFor="let item of localMenuItems" (click)="onLocalMenuItemClick(item)">
2393
+ {{ item.label }}
2394
+ </div>
2395
+ </div>
2396
+ </div>
2271
2397
  `,
2272
- styles: [`
2273
- .sftp-root { display: flex; flex-direction: column; height: 100%; padding: 10px; gap: 10px; position: relative; }
2274
- button { padding: 6px 10px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; }
2275
- button:disabled { opacity: 0.5; cursor: default; }
2276
- .top-profiles { display: flex; justify-content: space-between; align-items: center; padding: 4px 8px 8px; gap: 12px; font-size: 11px; opacity: 0.9; }
2277
- .top-profiles .current .label,
2278
- .top-profiles .recent .label { text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; margin-right: 4px; }
2279
- .top-profiles .value { font-weight: 600; }
2280
- .top-profiles .profile-chip { padding: 2px 8px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.04); color: inherit; cursor: pointer; font-size: 11px; }
2281
- .top-profiles .profile-chip:hover { background: rgba(255,255,255,0.12); }
2282
- .sftp-body { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; flex: 1; min-height: 0; }
2283
- .pane { display: flex; flex-direction: column; border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; overflow: hidden; min-height: 0; }
2284
- .pane-title { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; padding: 8px 10px; background: rgba(255,255,255,0.04); border-bottom: 1px solid rgba(255,255,255,0.08); }
2285
- .pane-label { font-weight: 600; display: flex; align-items: baseline; gap: 6px; }
2286
- .pane-sub { font-weight: 400; font-size: 11px; opacity: 0.75; }
2287
- .pane-path { opacity: 0.8; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2288
- .pane-path input { width: 100%; padding: 4px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-family: inherit; font-size: 12px; }
2289
- .pane-actions { display: flex; gap: 8px; align-items: center; }
2290
- .pane-actions .path-preset,
2291
- .pane-actions .path-favorite { max-width: 150px; padding: 3px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.22); background: rgba(20,20,20,0.95); color: inherit; font-size: 11px; }
2292
- .pane-actions .path-preset option { background: #151515; color: #f5f5f5; }
2293
- .pane-actions .path-favorite option { background: #151515; color: #f5f5f5; }
2294
- .pane-actions .fav-toggle { padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.05); font-size: 11px; line-height: 1; }
2295
- .pane-actions .fav-toggle.active { background: rgba(255,215,0,0.2); border-color: rgba(255,215,0,0.6); color: #ffd700; }
2296
- .pane-filters { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-bottom: 1px solid rgba(255,255,255,0.06); background: rgba(0,0,0,0.12); }
2297
- .pane-filters input { flex: 1; padding: 4px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 12px; }
2298
- .show-hidden-toggle { display: flex; align-items: center; gap: 4px; font-size: 11px; opacity: 0.8; white-space: nowrap; }
2299
- .show-hidden-toggle input[type="checkbox"] { margin: 0; }
2300
- .breadcrumbs { display: flex; flex-wrap: wrap; gap: 4px; font-size: 11px; opacity: 0.9; align-items: center; }
2301
- .crumb-button { padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.04); color: inherit; cursor: pointer; font-size: 11px; }
2302
- .crumb-button:hover { background: rgba(255,255,255,0.10); }
2303
- .crumb-separator { opacity: 0.6; }
2304
- .pane-list { flex: 1; overflow: auto; padding: 4px; }
2305
- .entry { display: grid; grid-template-columns: 24px minmax(0, 1.5fr) 80px 140px; gap: 8px; padding: 6px 8px; border-radius: 8px; user-select: none; align-items: center; }
2306
- .entry:hover { background: rgba(255,255,255,0.06); }
2307
- .entry.drop-target { outline: 1px dashed rgba(255,255,255,0.35); background: rgba(80, 160, 255, 0.10); }
2308
- .entry.dim { opacity: 0.7; }
2309
- .icon { text-align: center; opacity: 0.85; }
2310
- .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2311
- .size { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; text-align: right; opacity: 0.8; }
2312
- .date { font-size: 11px; opacity: 0.75; text-align: right; white-space: nowrap; }
2313
- .entry.header { font-weight: 600; opacity: 0.9; background: rgba(255,255,255,0.02); }
2314
- .sortable { cursor: pointer; }
2315
- .entry.selected { background: rgba(80,160,255,0.18); }
2316
- .pane-actions-bar { display: flex; flex-direction: column; gap: 4px; padding: 6px 8px; border-top: 1px solid rgba(255,255,255,0.06); background: rgba(0,0,0,0.18); }
2317
- .pane-actions-bar .selection { font-size: 11px; opacity: 0.85; }
2318
- .pane-actions-bar .action-inputs { display: flex; gap: 6px; }
2319
- .pane-actions-bar .action-inputs input { flex: 1; padding: 3px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 11px; }
2320
- .pane-actions-bar .action-buttons { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
2321
- .sftp-transfers { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; max-height: 120px; overflow-y: auto; }
2322
- .transfer { display: grid; grid-template-columns: 1fr auto; gap: 8px; padding: 6px 8px; border-radius: 8px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); font-size: 11px; }
2323
- .transfer-title { display: flex; gap: 6px; align-items: baseline; margin-bottom: 2px; }
2324
- .transfer-title .direction { text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.7; font-weight: 600; font-size: 10px; }
2325
- .transfer-title .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2326
- .transfer-path { display: flex; gap: 4px; opacity: 0.75; }
2327
- .transfer-path .label { min-width: 48px; }
2328
- .transfer-path .value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
2329
- .bar { position: relative; height: 4px; border-radius: 999px; background: rgba(255,255,255,0.07); margin-top: 4px; overflow: hidden; }
2330
- .bar .fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: inherit; background: linear-gradient(90deg, #4dabff, #78ffce); transition: width 0.15s linear; }
2331
- .transfer-stats { display: flex; flex-direction: column; justify-content: center; align-items: flex-end; gap: 4px; opacity: 0.8; }
2332
- .transfer-stats .percent { font-weight: 600; }
2333
- .transfer-stats .speed { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
2334
- .btn-cancel { padding: 2px 6px; font-size: 10px; border-radius: 999px; }
2335
- .delete-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; z-index: 20; }
2336
- .delete-dialog { min-width: 260px; max-width: 360px; padding: 14px 16px; border-radius: 10px; background: rgba(20,20,20,0.96); border: 1px solid rgba(255,255,255,0.15); box-shadow: 0 18px 45px rgba(0,0,0,0.75); display: flex; flex-direction: column; gap: 10px; }
2337
- .delete-text { font-size: 13px; }
2338
- .dialog-input { width: 100%; padding: 8px 10px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 13px; }
2339
- .delete-buttons { display: flex; justify-content: flex-end; gap: 8px; }
2340
- .delete-buttons .danger { background: rgba(255,80,80,0.85); border-color: rgba(255,120,120,0.85); }
2341
- .local-menu { position: absolute; min-width: 180px; max-width: 260px; max-height: 260px; overflow-y: auto; padding: 4px 0; border-radius: 10px; background: rgba(18,18,22,0.98); border: 1px solid rgba(255,255,255,0.16); box-shadow: 0 18px 45px rgba(0,0,0,0.8); z-index: 30; backdrop-filter: blur(12px); }
2342
- .local-menu-item { padding: 6px 12px; font-size: 12px; cursor: pointer; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
2343
- .local-menu-item:hover { background: linear-gradient(90deg, rgba(120,200,255,0.24), rgba(120,255,206,0.15)); }
2398
+ styles: [`
2399
+ .sftp-root { display: flex; flex-direction: column; height: 100%; padding: 10px; gap: 10px; position: relative; }
2400
+ button { padding: 6px 10px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; }
2401
+ button:disabled { opacity: 0.5; cursor: default; }
2402
+ .top-profiles { display: flex; justify-content: space-between; align-items: center; padding: 4px 8px 8px; gap: 12px; font-size: 11px; opacity: 0.9; }
2403
+ .top-profiles .current .label,
2404
+ .top-profiles .recent .label { text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; margin-right: 4px; }
2405
+ .top-profiles .value { font-weight: 600; }
2406
+ .top-profiles .profile-chip { padding: 2px 8px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.04); color: inherit; cursor: pointer; font-size: 11px; }
2407
+ .top-profiles .profile-chip:hover { background: rgba(255,255,255,0.12); }
2408
+ .sftp-body { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; flex: 1; min-height: 0; }
2409
+ .pane { display: flex; flex-direction: column; border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; overflow: hidden; min-height: 0; }
2410
+ .pane-title { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; padding: 8px 10px; background: rgba(255,255,255,0.04); border-bottom: 1px solid rgba(255,255,255,0.08); }
2411
+ .pane-label { font-weight: 600; display: flex; align-items: baseline; gap: 6px; }
2412
+ .pane-sub { font-weight: 400; font-size: 11px; opacity: 0.75; }
2413
+ .pane-path { opacity: 0.8; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2414
+ .pane-path input { width: 100%; padding: 4px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-family: inherit; font-size: 12px; }
2415
+ .pane-actions { display: flex; gap: 8px; align-items: center; }
2416
+ .pane-actions .path-preset,
2417
+ .pane-actions .path-favorite { max-width: 150px; padding: 3px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.22); background: rgba(20,20,20,0.95); color: inherit; font-size: 11px; }
2418
+ .pane-actions .path-preset option { background: #151515; color: #f5f5f5; }
2419
+ .pane-actions .path-favorite option { background: #151515; color: #f5f5f5; }
2420
+ .pane-actions .fav-toggle { padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.05); font-size: 11px; line-height: 1; }
2421
+ .pane-actions .fav-toggle.active { background: rgba(255,215,0,0.2); border-color: rgba(255,215,0,0.6); color: #ffd700; }
2422
+ .pane-filters { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-bottom: 1px solid rgba(255,255,255,0.06); background: rgba(0,0,0,0.12); }
2423
+ .pane-filters input { flex: 1; padding: 4px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 12px; }
2424
+ .show-hidden-toggle { display: flex; align-items: center; gap: 4px; font-size: 11px; opacity: 0.8; white-space: nowrap; }
2425
+ .show-hidden-toggle input[type="checkbox"] { margin: 0; }
2426
+ .breadcrumbs { display: flex; flex-wrap: wrap; gap: 4px; font-size: 11px; opacity: 0.9; align-items: center; }
2427
+ .crumb-button { padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.04); color: inherit; cursor: pointer; font-size: 11px; }
2428
+ .crumb-button:hover { background: rgba(255,255,255,0.10); }
2429
+ .crumb-separator { opacity: 0.6; }
2430
+ .pane-list { flex: 1; overflow: auto; padding: 4px; }
2431
+ .entry { display: grid; grid-template-columns: 24px minmax(0, 1.5fr) 80px 140px; gap: 8px; padding: 6px 8px; border-radius: 8px; user-select: none; align-items: center; }
2432
+ .entry:hover { background: rgba(255,255,255,0.06); }
2433
+ .entry.drop-target { outline: 1px dashed rgba(255,255,255,0.35); background: rgba(80, 160, 255, 0.10); }
2434
+ .entry.dim { opacity: 0.7; }
2435
+ .icon { text-align: center; opacity: 0.85; }
2436
+ .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2437
+ .size { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; text-align: right; opacity: 0.8; }
2438
+ .date { font-size: 11px; opacity: 0.75; text-align: right; white-space: nowrap; }
2439
+ .entry.header { font-weight: 600; opacity: 0.9; background: rgba(255,255,255,0.02); }
2440
+ .sortable { cursor: pointer; }
2441
+ .entry.selected { background: rgba(80,160,255,0.18); }
2442
+ .pane-actions-bar { display: flex; flex-direction: column; gap: 4px; padding: 6px 8px; border-top: 1px solid rgba(255,255,255,0.06); background: rgba(0,0,0,0.18); }
2443
+ .pane-actions-bar .selection { font-size: 11px; opacity: 0.85; }
2444
+ .pane-actions-bar .action-inputs { display: flex; gap: 6px; }
2445
+ .pane-actions-bar .action-inputs input { flex: 1; padding: 3px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 11px; }
2446
+ .pane-actions-bar .action-buttons { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
2447
+ .sftp-transfers { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; max-height: 120px; overflow-y: auto; }
2448
+ .transfer { display: grid; grid-template-columns: 1fr auto; gap: 8px; padding: 6px 8px; border-radius: 8px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); font-size: 11px; }
2449
+ .transfer-title { display: flex; gap: 6px; align-items: baseline; margin-bottom: 2px; }
2450
+ .transfer-title .direction { text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.7; font-weight: 600; font-size: 10px; }
2451
+ .transfer-title .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2452
+ .transfer-path { display: flex; gap: 4px; opacity: 0.75; }
2453
+ .transfer-path .label { min-width: 48px; }
2454
+ .transfer-path .value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
2455
+ .bar { position: relative; height: 4px; border-radius: 999px; background: rgba(255,255,255,0.07); margin-top: 4px; overflow: hidden; }
2456
+ .bar .fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: inherit; background: linear-gradient(90deg, #4dabff, #78ffce); transition: width 0.15s linear; }
2457
+ .transfer-stats { display: flex; flex-direction: column; justify-content: center; align-items: flex-end; gap: 4px; opacity: 0.8; }
2458
+ .transfer-stats .percent { font-weight: 600; }
2459
+ .transfer-stats .speed { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
2460
+ .btn-cancel { padding: 2px 6px; font-size: 10px; border-radius: 999px; }
2461
+ .delete-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; z-index: 20; }
2462
+ .delete-dialog { min-width: 260px; max-width: 360px; padding: 14px 16px; border-radius: 10px; background: rgba(20,20,20,0.96); border: 1px solid rgba(255,255,255,0.15); box-shadow: 0 18px 45px rgba(0,0,0,0.75); display: flex; flex-direction: column; gap: 10px; }
2463
+ .delete-text { font-size: 13px; }
2464
+ .dialog-input { width: 100%; padding: 8px 10px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 13px; }
2465
+ .delete-buttons { display: flex; justify-content: flex-end; gap: 8px; }
2466
+ .delete-buttons .danger { background: rgba(255,80,80,0.85); border-color: rgba(255,120,120,0.85); }
2467
+ .local-menu { position: absolute; min-width: 180px; max-width: 260px; max-height: 260px; overflow-y: auto; padding: 4px 0; border-radius: 10px; background: rgba(18,18,22,0.98); border: 1px solid rgba(255,255,255,0.16); box-shadow: 0 18px 45px rgba(0,0,0,0.8); z-index: 30; backdrop-filter: blur(12px); }
2468
+ .local-menu-item { padding: 6px 12px; font-size: 12px; cursor: pointer; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
2469
+ .local-menu-item:hover { background: linear-gradient(90deg, rgba(120,200,255,0.24), rgba(120,255,206,0.15)); }
2344
2470
  `],
2345
2471
  }),
2346
2472
  __metadata("design:paramtypes", [_angular_core__WEBPACK_IMPORTED_MODULE_5__.Injector,