added menu
This commit is contained in:
28
src/lib/components/ui/navigation-menu/index.ts
Normal file
28
src/lib/components/ui/navigation-menu/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import Root from "./navigation-menu.svelte";
|
||||
import Content from "./navigation-menu-content.svelte";
|
||||
import Indicator from "./navigation-menu-indicator.svelte";
|
||||
import Item from "./navigation-menu-item.svelte";
|
||||
import Link from "./navigation-menu-link.svelte";
|
||||
import List from "./navigation-menu-list.svelte";
|
||||
import Trigger from "./navigation-menu-trigger.svelte";
|
||||
import Viewport from "./navigation-menu-viewport.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Indicator,
|
||||
Item,
|
||||
Link,
|
||||
List,
|
||||
Trigger,
|
||||
Viewport,
|
||||
//
|
||||
Root as NavigationMenuRoot,
|
||||
Content as NavigationMenuContent,
|
||||
Indicator as NavigationMenuIndicator,
|
||||
Item as NavigationMenuItem,
|
||||
Link as NavigationMenuLink,
|
||||
List as NavigationMenuList,
|
||||
Trigger as NavigationMenuTrigger,
|
||||
Viewport as NavigationMenuViewport,
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: NavigationMenuPrimitive.ContentProps = $props();
|
||||
</script>
|
||||
|
||||
<NavigationMenuPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="navigation-menu-content"
|
||||
class={cn(
|
||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 left-0 top-0 w-full md:absolute md:w-auto",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: NavigationMenuPrimitive.IndicatorProps = $props();
|
||||
</script>
|
||||
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
bind:ref
|
||||
data-slot="navigation-menu-indicator"
|
||||
class={cn(
|
||||
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<div class="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md"></div>
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: NavigationMenuPrimitive.ItemProps = $props();
|
||||
</script>
|
||||
|
||||
<NavigationMenuPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="navigation-menu-item"
|
||||
class={cn("relative", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: NavigationMenuPrimitive.LinkProps = $props();
|
||||
</script>
|
||||
|
||||
<NavigationMenuPrimitive.Link
|
||||
bind:ref
|
||||
data-slot="navigation-menu-link"
|
||||
class={cn(
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm outline-none transition-all focus-visible:outline-1 focus-visible:ring-[3px] [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: NavigationMenuPrimitive.ListProps = $props();
|
||||
</script>
|
||||
|
||||
<NavigationMenuPrimitive.List
|
||||
bind:ref
|
||||
data-slot="navigation-menu-list"
|
||||
class={cn("group flex flex-1 list-none items-center justify-center gap-1", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts" module>
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
export const navigationMenuTriggerStyle = tv({
|
||||
base: "bg-background hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 group inline-flex h-9 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium outline-none transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50",
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: NavigationMenuPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="navigation-menu-trigger"
|
||||
class={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
<ChevronDownIcon
|
||||
class="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: NavigationMenuPrimitive.ViewportProps = $props();
|
||||
</script>
|
||||
|
||||
<div class={cn("absolute left-0 top-full isolate z-50 flex justify-center")}>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
bind:ref
|
||||
data-slot="navigation-menu-viewport"
|
||||
class={cn(
|
||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--bits-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--bits-navigation-menu-viewport-width)]",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</div>
|
||||
32
src/lib/components/ui/navigation-menu/navigation-menu.svelte
Normal file
32
src/lib/components/ui/navigation-menu/navigation-menu.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import NavigationMenuViewport from "./navigation-menu-viewport.svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
viewport = true,
|
||||
children,
|
||||
...restProps
|
||||
}: NavigationMenuPrimitive.RootProps & {
|
||||
viewport?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<NavigationMenuPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="navigation-menu"
|
||||
data-viewport={viewport}
|
||||
class={cn(
|
||||
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
{#if viewport}
|
||||
<NavigationMenuViewport />
|
||||
{/if}
|
||||
</NavigationMenuPrimitive.Root>
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { Home, FlaskConical } from '@lucide/svelte';
|
||||
import { Home, FlaskConical, Cog } from '@lucide/svelte';
|
||||
import { PUBLIC_APP_NAME } from '$env/static/public';
|
||||
import * as NavigationMenu from '$lib/components/ui/navigation-menu/index.js';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
@@ -17,8 +18,31 @@
|
||||
<span class="font-bold">{PUBLIC_APP_NAME || 'Cog Socket App'}</span>
|
||||
</a>
|
||||
<div class="flex flex-1 items-center justify-end space-x-2">
|
||||
<nav class="flex items-center">
|
||||
<nav class="flex items-center gap-2">
|
||||
<!-- Top bar right side elements, e.g., user menu, settings -->
|
||||
<NavigationMenu.Root viewport={false}>
|
||||
<NavigationMenu.List>
|
||||
<NavigationMenu.Item>
|
||||
<NavigationMenu.Trigger>
|
||||
<Cog class="size-5" />
|
||||
<span class="sr-only">Settings</span>
|
||||
</NavigationMenu.Trigger>
|
||||
<NavigationMenu.Content class="right-0 left-auto">
|
||||
<ul class="grid w-[200px] gap-2 p-2">
|
||||
<li>
|
||||
<NavigationMenu.Link href="/settings/profile">Profile</NavigationMenu.Link>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationMenu.Link href="/settings">Settings</NavigationMenu.Link>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationMenu.Link href="/settings/subscription">Subscription</NavigationMenu.Link>
|
||||
</li>
|
||||
</ul>
|
||||
</NavigationMenu.Content>
|
||||
</NavigationMenu.Item>
|
||||
</NavigationMenu.List>
|
||||
</NavigationMenu.Root>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,6 +64,8 @@ async function handleSubmit() {
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Multiplayer</TableHead>
|
||||
<TableHead>Created At</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
@@ -80,6 +82,8 @@ async function handleSubmit() {
|
||||
<a href={`/experiment/${exp.id}`} class="text-primary underline hover:text-primary/80">{exp.name}</a>
|
||||
</TableCell>
|
||||
<TableCell>{exp.description}</TableCell>
|
||||
<TableCell>{exp.type}</TableCell>
|
||||
<TableCell>{exp.multiplayer ? 'Yes' : 'No'}</TableCell>
|
||||
<TableCell>{new Date(exp.createdAt).toLocaleString()}</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
|
||||
@@ -20,6 +20,7 @@ let uploading = false;
|
||||
let uploadError = '';
|
||||
let copied = false;
|
||||
let origin = '';
|
||||
let activeTab = 'info';
|
||||
|
||||
async function fetchFiles() {
|
||||
if (!experimentId) return;
|
||||
@@ -93,6 +94,35 @@ function buildFileTree(files: any[]): any {
|
||||
return root;
|
||||
}
|
||||
|
||||
function downloadFile(key: string) {
|
||||
// Download file from S3 via API
|
||||
const url = `/api/experiment/${experimentId}/files?key=${encodeURIComponent(key)}&download=1`;
|
||||
fetch(url)
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Failed to download');
|
||||
const disposition = res.headers.get('Content-Disposition');
|
||||
let filename = key.split('/').pop() || 'file';
|
||||
if (disposition) {
|
||||
const match = disposition.match(/filename="?([^";]+)"?/);
|
||||
if (match) filename = match[1];
|
||||
}
|
||||
return res.blob().then(blob => ({ blob, filename }));
|
||||
})
|
||||
.then(({ blob, filename }) => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(() => {
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
}, 100);
|
||||
})
|
||||
.catch(() => alert('Failed to download file.'));
|
||||
}
|
||||
|
||||
let expanded = new Set<string>();
|
||||
function toggleFolder(path: string) {
|
||||
if (expanded.has(path)) expanded.delete(path);
|
||||
@@ -157,7 +187,13 @@ async function handleSave() {
|
||||
|
||||
{#if experiment}
|
||||
<div class="flex flex-col min-h-screen bg-background py-8">
|
||||
<Card class="mb-8 w-full max-w-xl mx-auto">
|
||||
<div class="w-full max-w-xl mx-auto mb-8">
|
||||
<div class="flex border-b mb-4">
|
||||
<button class="px-4 py-2 -mb-px border-b-2 font-medium focus:outline-none transition-colors duration-200" class:!border-primary={activeTab === 'info'} class:border-transparent={activeTab !== 'info'} on:click={() => activeTab = 'info'}>Info</button>
|
||||
<button class="px-4 py-2 -mb-px border-b-2 font-medium focus:outline-none transition-colors duration-200" class:!border-primary={activeTab === 'files'} class:border-transparent={activeTab !== 'files'} on:click={() => activeTab = 'files'}>Files</button>
|
||||
</div>
|
||||
{#if activeTab === 'info'}
|
||||
<Card class="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Experiment Details</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -166,6 +202,14 @@ async function handleSave() {
|
||||
<Label>ID</Label>
|
||||
<div class="text-sm text-muted-foreground mb-2">{experiment.id}</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<Label>Type</Label>
|
||||
<div class="text-sm text-muted-foreground mb-2">{experiment.type}</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<Label>Multiplayer</Label>
|
||||
<div class="text-sm text-muted-foreground mb-2">{experiment.multiplayer ? 'Yes' : 'No'}</div>
|
||||
</div>
|
||||
<form on:submit|preventDefault={handleSave} class="space-y-4">
|
||||
<div>
|
||||
<Label for="name">Name</Label>
|
||||
@@ -187,39 +231,34 @@ async function handleSave() {
|
||||
{/if}
|
||||
<Button type="submit">Save Changes</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-8 w-full max-w-xl mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle>Public Link</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p class="text-sm text-muted-foreground mb-2">
|
||||
Share this link with your participants to run the experiment.
|
||||
</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Input id="public-link" type="text" readonly value="{`${origin}/public/run/${experiment.id}`}" />
|
||||
<div class="mt-8">
|
||||
<Label>Public Link</Label>
|
||||
<div class="flex items-center space-x-2 mt-2">
|
||||
<Input id="public-link" type="text" readonly value={`${origin}/public/run/${experiment.id}`} />
|
||||
<Button on:click={copyLink}>Copy</Button>
|
||||
</div>
|
||||
{#if copied}
|
||||
<p class="text-green-600 text-sm mt-2">Copied to clipboard!</p>
|
||||
{/if}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card class="w-full max-w-xl mx-auto">
|
||||
{/if}
|
||||
{#if activeTab === 'files'}
|
||||
<Card class="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Experiment Files</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="mt-8">
|
||||
<Label>Experiment Files</Label>
|
||||
<div
|
||||
class="border-2 border-dashed rounded-md p-4 mb-4 text-center cursor-pointer bg-muted hover:bg-accent transition"
|
||||
<div class="flex justify-end mb-4">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 rounded bg-primary text-primary-foreground hover:bg-primary/90 transition font-medium"
|
||||
on:click={() => document.getElementById('file-input')?.click()}
|
||||
>
|
||||
<div>Click to select files or a folder to upload (folder structure will be preserved)</div>
|
||||
Upload Files or Folder
|
||||
</button>
|
||||
<input id="file-input" type="file" multiple webkitdirectory class="hidden" on:change={handleFileInput} />
|
||||
</div>
|
||||
{#if uploading}
|
||||
@@ -235,6 +274,8 @@ async function handleSave() {
|
||||
{expanded}
|
||||
onToggle={toggleFolder}
|
||||
onDelete={deleteFileOrFolder}
|
||||
onDownload={downloadFile}
|
||||
isRoot={true}
|
||||
/>
|
||||
{#if files.length === 0}
|
||||
<li class="text-muted-foreground text-sm py-2">No files uploaded yet.</li>
|
||||
@@ -243,6 +284,8 @@ async function handleSave() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="flex justify-center items-center min-h-screen bg-background">
|
||||
|
||||
@@ -1,45 +1,91 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import FileIcon from '@lucide/svelte/icons/file';
|
||||
import FolderIcon from '@lucide/svelte/icons/folder';
|
||||
import FolderOpenIcon from '@lucide/svelte/icons/folder-open';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import DownloadIcon from '@lucide/svelte/icons/download';
|
||||
|
||||
export let tree: any;
|
||||
export let parentPath = '';
|
||||
export let expanded: Set<string>;
|
||||
export let onToggle: (path: string) => void;
|
||||
export let onDelete: (key: string, isFolder: boolean) => void;
|
||||
export let onDownload: (key: string) => void = () => {};
|
||||
export let isRoot: boolean = false;
|
||||
|
||||
function handleToggle(path: string) {
|
||||
onToggle(path);
|
||||
}
|
||||
function handleDelete(key: string, isFolder: boolean) {
|
||||
console.log('FileBrowser handleDelete called:', { key, isFolder });
|
||||
onDelete(key, isFolder);
|
||||
}
|
||||
|
||||
function handleDownload(key: string) {
|
||||
onDownload(key);
|
||||
}
|
||||
function handleFolderClick(event: Event, path: string) {
|
||||
event.stopPropagation();
|
||||
handleToggle(path);
|
||||
}
|
||||
|
||||
function handleFolderDelete(event: Event, path: string) {
|
||||
event.stopPropagation();
|
||||
console.log('FileBrowser handleFolderDelete called:', { path });
|
||||
handleDelete(path, true);
|
||||
}
|
||||
function formatSize(size: number) {
|
||||
if (!size && size !== 0) return '';
|
||||
if (size < 1024) return `${size} B`;
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
||||
if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(1)} MB`;
|
||||
return `${(size / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
||||
}
|
||||
function formatDate(date: string | Date) {
|
||||
if (!date) return '';
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
return d.toLocaleString();
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul class="ml-2">
|
||||
{#if isRoot}
|
||||
<li class="flex items-center gap-2 py-1 pl-2 text-xs font-semibold text-muted-foreground select-none border-b border-border mb-1">
|
||||
<span class="flex-1 min-w-0">Name</span>
|
||||
<span class="w-24 text-right">Size</span>
|
||||
<span class="w-40 text-right">Last Uploaded</span>
|
||||
<span class="w-16"></span>
|
||||
</li>
|
||||
{/if}
|
||||
{#each Object.entries(tree) as [name, node] (parentPath + '/' + name)}
|
||||
{#if (node as any).isFile}
|
||||
<li class="flex items-center justify-between py-1 pl-4">
|
||||
<span>{name} <span class="text-xs text-muted-foreground">({(node as any).size} bytes)</span></span>
|
||||
<Button size="sm" variant="destructive" on:click={() => handleDelete((node as any).key, false)}>Delete</Button>
|
||||
<li class="flex items-center gap-2 py-1 pl-2 hover:bg-accent rounded group text-sm">
|
||||
<span class="flex-1 min-w-0 flex items-center gap-1">
|
||||
<FileIcon class="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
<span class="truncate" title={name || ''}>{name || ''}</span>
|
||||
</span>
|
||||
<span class="w-24 text-xs text-muted-foreground text-right ml-2 whitespace-nowrap">{formatSize((node as any).size)}</span>
|
||||
<span class="w-40 text-xs text-muted-foreground text-right ml-2 whitespace-nowrap">{formatDate((node as any).lastModified)}</span>
|
||||
<span class="w-16 flex items-center justify-end gap-1">
|
||||
<button class="p-1 hover:text-primary" title="Download" on:click={() => handleDownload((node as any).key)}><DownloadIcon class="w-4 h-4" /></button>
|
||||
<button class="p-1 hover:text-destructive" title="Delete" on:click={() => handleDelete((node as any).key, false)}><TrashIcon class="w-4 h-4" /></button>
|
||||
</span>
|
||||
</li>
|
||||
{:else}
|
||||
<li>
|
||||
<div class="flex items-center cursor-pointer" on:click={(e) => handleFolderClick(e, parentPath ? `${parentPath}/${name}` : name)}>
|
||||
<span class="mr-1">{expanded.has(parentPath ? `${parentPath}/${name}` : name) ? '▼' : '▶'}</span>
|
||||
<span class="font-semibold">{name}</span>
|
||||
<Button size="sm" variant="destructive" class="ml-2" on:click={(e) => handleFolderDelete(e, (parentPath ? `${parentPath}/${name}` : name))}>Delete</Button>
|
||||
<div class="flex items-center gap-1 px-1 py-1 hover:bg-accent rounded cursor-pointer text-sm" >
|
||||
<span class="flex-1 min-w-0 flex items-center gap-1" on:click={(e) => handleFolderClick(e, parentPath ? `${parentPath}/${name}` : name)}>
|
||||
<span class="flex-shrink-0 w-5 h-5 flex items-center justify-center">
|
||||
{#if expanded.has(parentPath ? `${parentPath}/${name}` : name)}
|
||||
<FolderOpenIcon class="w-4 h-4 text-muted-foreground" />
|
||||
{:else}
|
||||
<FolderIcon class="w-4 h-4 text-muted-foreground" />
|
||||
{/if}
|
||||
</span>
|
||||
<span class="font-semibold truncate" title={name || ''}>{name || ''}</span>
|
||||
</span>
|
||||
<span class="w-24"></span>
|
||||
<span class="w-40"></span>
|
||||
<span class="w-16 flex items-center justify-end">
|
||||
<button class="p-1 hover:text-destructive" title="Delete folder" on:click={(e) => handleFolderDelete(e, (parentPath ? `${parentPath}/${name}` : name))}><TrashIcon class="w-4 h-4" /></button>
|
||||
</span>
|
||||
</div>
|
||||
{#if expanded.has(parentPath ? `${parentPath}/${name}` : name)}
|
||||
<svelte:self
|
||||
@@ -48,6 +94,8 @@
|
||||
{expanded}
|
||||
{onToggle}
|
||||
{onDelete}
|
||||
{onDownload}
|
||||
isRoot={false}
|
||||
/>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user