Spaces:
Build error
Build error
| <script lang="ts"> | |
| import { getContext } from 'svelte'; | |
| import { DropdownMenu } from 'bits-ui'; | |
| import { flyAndScale } from '$lib/utils/transitions'; | |
| import { formatFileSize } from '$lib/utils'; | |
| import type { FileEntry } from '$lib/apis/terminal'; | |
| import Folder from '../../icons/Folder.svelte'; | |
| import EllipsisHorizontal from '../../icons/EllipsisHorizontal.svelte'; | |
| import GarbageBin from '../../icons/GarbageBin.svelte'; | |
| const i18n = getContext('i18n'); | |
| export let entry: FileEntry; | |
| export let currentPath: string; | |
| export let terminalUrl: string; | |
| export let terminalKey: string; | |
| export let onOpen: (entry: FileEntry) => void = () => {}; | |
| export let onDownload: (path: string) => void = () => {}; | |
| export let onDelete: (path: string, name: string) => void = () => {}; | |
| export let onMove: (source: string, destFolder: string) => void = () => {}; | |
| let dragOverFolder = false; | |
| </script> | |
| <li class="group"> | |
| <div | |
| class="w-full flex items-center hover:bg-gray-50 dark:hover:bg-gray-800 transition | |
| {dragOverFolder | |
| ? 'bg-blue-50 dark:bg-blue-900/30 ring-1 ring-blue-400 dark:ring-blue-500 ring-inset' | |
| : ''}" | |
| role={entry.type === 'directory' ? 'button' : undefined} | |
| on:dragover={(e) => { | |
| if (entry.type !== 'directory') return; | |
| if (!e.dataTransfer?.types.includes('application/x-terminal-file-move')) return; | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| dragOverFolder = true; | |
| }} | |
| on:dragleave={(e) => { | |
| if (entry.type !== 'directory') return; | |
| e.stopPropagation(); | |
| dragOverFolder = false; | |
| }} | |
| on:drop={(e) => { | |
| if (entry.type !== 'directory') return; | |
| const raw = e.dataTransfer?.getData('application/x-terminal-file-move'); | |
| if (!raw) return; | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| dragOverFolder = false; | |
| try { | |
| const data = JSON.parse(raw); | |
| if (data.path) { | |
| const destFolder = `${currentPath}${entry.name}/`; | |
| // Don't allow dropping a folder onto itself | |
| if (data.path + '/' === destFolder || data.path === destFolder) return; | |
| onMove(data.path, destFolder); | |
| } | |
| } catch {} | |
| }} | |
| > | |
| <button | |
| class="flex-1 flex items-center gap-2 px-3 py-1.5 text-left min-w-0" | |
| draggable={true} | |
| on:dragstart={(e) => { | |
| const filePath = `${currentPath}${entry.name}`; | |
| // Internal move data | |
| e.dataTransfer?.setData( | |
| 'application/x-terminal-file-move', | |
| JSON.stringify({ path: filePath, name: entry.name }) | |
| ); | |
| // Keep existing chat-attachment drag for files | |
| if (entry.type === 'file') { | |
| e.dataTransfer?.setData( | |
| 'application/x-terminal-file', | |
| JSON.stringify({ | |
| path: filePath, | |
| name: entry.name, | |
| url: terminalUrl, | |
| key: terminalKey | |
| }) | |
| ); | |
| } | |
| }} | |
| on:click={() => onOpen(entry)} | |
| > | |
| {#if entry.type === 'directory'} | |
| <Folder className="size-4 shrink-0 text-blue-400 dark:text-blue-300" /> | |
| {:else} | |
| <svg | |
| xmlns="http://www.w3.org/2000/svg" | |
| viewBox="0 0 24 24" | |
| fill="none" | |
| stroke="currentColor" | |
| stroke-width="1.5" | |
| class="size-4 shrink-0 text-gray-400" | |
| > | |
| <path | |
| stroke-linecap="round" | |
| stroke-linejoin="round" | |
| d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" | |
| /> | |
| </svg> | |
| {/if} | |
| <span class="flex-1 text-xs text-gray-800 dark:text-gray-200 truncate"> | |
| {entry.name} | |
| </span> | |
| {#if entry.type === 'file' && entry.size !== undefined} | |
| <span class="text-xs text-gray-400 shrink-0">{formatFileSize(entry.size)}</span> | |
| {/if} | |
| </button> | |
| <DropdownMenu.Root> | |
| <DropdownMenu.Trigger | |
| class="shrink-0 p-0.5 mr-1 rounded-lg transition | |
| text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-400 | |
| hover:bg-gray-100 dark:hover:bg-gray-800" | |
| on:click={(e) => e.stopPropagation()} | |
| aria-label={$i18n.t('More')} | |
| > | |
| <EllipsisHorizontal className="size-3.5" /> | |
| </DropdownMenu.Trigger> | |
| <DropdownMenu.Content | |
| strategy="fixed" | |
| class="w-full max-w-[150px] rounded-2xl p-1 z-[9999999] bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-100 dark:border-gray-800" | |
| sideOffset={4} | |
| side="bottom" | |
| align="end" | |
| transition={flyAndScale} | |
| > | |
| {#if entry.type !== 'directory'} | |
| <DropdownMenu.Item | |
| type="button" | |
| class="select-none flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2 text-sm" | |
| on:click={(e) => { | |
| e.stopPropagation(); | |
| onDownload(`${currentPath}${entry.name}`); | |
| }} | |
| > | |
| <svg | |
| xmlns="http://www.w3.org/2000/svg" | |
| viewBox="0 0 20 20" | |
| fill="currentColor" | |
| class="size-4" | |
| > | |
| <path | |
| d="M10.75 2.75a.75.75 0 0 0-1.5 0v8.614L6.295 8.235a.75.75 0 1 0-1.09 1.03l4.25 4.5a.75.75 0 0 0 1.09 0l4.25-4.5a.75.75 0 0 0-1.09-1.03l-2.955 3.129V2.75Z" | |
| /> | |
| <path | |
| d="M3.5 12.75a.75.75 0 0 0-1.5 0v2.5A2.75 2.75 0 0 0 4.75 18h10.5A2.75 2.75 0 0 0 18 15.25v-2.5a.75.75 0 0 0-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5Z" | |
| /> | |
| </svg> | |
| <div class="flex items-center">{$i18n.t('Download')}</div> | |
| </DropdownMenu.Item> | |
| {/if} | |
| <DropdownMenu.Item | |
| type="button" | |
| class="select-none flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2 text-sm" | |
| on:click={(e) => { | |
| e.stopPropagation(); | |
| onDelete(`${currentPath}${entry.name}`, entry.name); | |
| }} | |
| > | |
| <GarbageBin className="size-4" /> | |
| <div class="flex items-center">{$i18n.t('Delete')}</div> | |
| </DropdownMenu.Item> | |
| </DropdownMenu.Content> | |
| </DropdownMenu.Root> | |
| </div> | |
| </li> | |