added menu

This commit is contained in:
Shaheed Azaad
2025-07-15 15:23:54 +02:00
parent e3c27b304f
commit 55401fd37b
13 changed files with 432 additions and 100 deletions

View 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,
};

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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}
/>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import '../app.css'; 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 { PUBLIC_APP_NAME } from '$env/static/public';
import * as NavigationMenu from '$lib/components/ui/navigation-menu/index.js';
let { children } = $props(); let { children } = $props();
@@ -17,8 +18,31 @@
<span class="font-bold">{PUBLIC_APP_NAME || 'Cog Socket App'}</span> <span class="font-bold">{PUBLIC_APP_NAME || 'Cog Socket App'}</span>
</a> </a>
<div class="flex flex-1 items-center justify-end space-x-2"> <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 --> <!-- 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> </nav>
</div> </div>
</div> </div>

View File

@@ -64,6 +64,8 @@ async function handleSubmit() {
<TableRow> <TableRow>
<TableHead>Name</TableHead> <TableHead>Name</TableHead>
<TableHead>Description</TableHead> <TableHead>Description</TableHead>
<TableHead>Type</TableHead>
<TableHead>Multiplayer</TableHead>
<TableHead>Created At</TableHead> <TableHead>Created At</TableHead>
<TableHead>Actions</TableHead> <TableHead>Actions</TableHead>
</TableRow> </TableRow>
@@ -80,6 +82,8 @@ async function handleSubmit() {
<a href={`/experiment/${exp.id}`} class="text-primary underline hover:text-primary/80">{exp.name}</a> <a href={`/experiment/${exp.id}`} class="text-primary underline hover:text-primary/80">{exp.name}</a>
</TableCell> </TableCell>
<TableCell>{exp.description}</TableCell> <TableCell>{exp.description}</TableCell>
<TableCell>{exp.type}</TableCell>
<TableCell>{exp.multiplayer ? 'Yes' : 'No'}</TableCell>
<TableCell>{new Date(exp.createdAt).toLocaleString()}</TableCell> <TableCell>{new Date(exp.createdAt).toLocaleString()}</TableCell>
<TableCell> <TableCell>
<Button <Button

View File

@@ -20,6 +20,7 @@ let uploading = false;
let uploadError = ''; let uploadError = '';
let copied = false; let copied = false;
let origin = ''; let origin = '';
let activeTab = 'info';
async function fetchFiles() { async function fetchFiles() {
if (!experimentId) return; if (!experimentId) return;
@@ -93,6 +94,35 @@ function buildFileTree(files: any[]): any {
return root; 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>(); let expanded = new Set<string>();
function toggleFolder(path: string) { function toggleFolder(path: string) {
if (expanded.has(path)) expanded.delete(path); if (expanded.has(path)) expanded.delete(path);
@@ -157,92 +187,105 @@ async function handleSave() {
{#if experiment} {#if experiment}
<div class="flex flex-col min-h-screen bg-background py-8"> <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">
<CardHeader> <div class="flex border-b mb-4">
<CardTitle>Experiment Details</CardTitle> <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>
</CardHeader> <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>
<CardContent> </div>
<div class="mb-4"> {#if activeTab === 'info'}
<Label>ID</Label> <Card class="w-full">
<div class="text-sm text-muted-foreground mb-2">{experiment.id}</div> <CardHeader>
</div> <CardTitle>Experiment Details</CardTitle>
<form on:submit|preventDefault={handleSave} class="space-y-4"> </CardHeader>
<div> <CardContent>
<Label for="name">Name</Label> <div class="mb-4">
<Input id="name" type="text" bind:value={name} required /> <Label>ID</Label>
</div> <div class="text-sm text-muted-foreground mb-2">{experiment.id}</div>
<div> </div>
<Label for="description">Description</Label> <div class="mb-4">
<Input id="description" type="text" bind:value={description} /> <Label>Type</Label>
</div> <div class="text-sm text-muted-foreground mb-2">{experiment.type}</div>
<div> </div>
<Label>Created At</Label> <div class="mb-4">
<div class="text-sm text-muted-foreground mb-2">{new Date(experiment.createdAt).toLocaleString()}</div> <Label>Multiplayer</Label>
</div> <div class="text-sm text-muted-foreground mb-2">{experiment.multiplayer ? 'Yes' : 'No'}</div>
{#if error} </div>
<div class="text-red-500 text-sm">{error}</div> <form on:submit|preventDefault={handleSave} class="space-y-4">
{/if} <div>
{#if success} <Label for="name">Name</Label>
<div class="text-green-600 text-sm">{success}</div> <Input id="name" type="text" bind:value={name} required />
{/if} </div>
<Button type="submit">Save Changes</Button> <div>
</form> <Label for="description">Description</Label>
</CardContent> <Input id="description" type="text" bind:value={description} />
</Card> </div>
<div>
<Card class="mb-8 w-full max-w-xl mx-auto"> <Label>Created At</Label>
<CardHeader> <div class="text-sm text-muted-foreground mb-2">{new Date(experiment.createdAt).toLocaleString()}</div>
<CardTitle>Public Link</CardTitle> </div>
</CardHeader> {#if error}
<CardContent> <div class="text-red-500 text-sm">{error}</div>
<p class="text-sm text-muted-foreground mb-2"> {/if}
Share this link with your participants to run the experiment. {#if success}
</p> <div class="text-green-600 text-sm">{success}</div>
<div class="flex items-center space-x-2"> {/if}
<Input id="public-link" type="text" readonly value="{`${origin}/public/run/${experiment.id}`}" /> <Button type="submit">Save Changes</Button>
<Button on:click={copyLink}>Copy</Button> </form>
</div> <div class="mt-8">
{#if copied} <Label>Public Link</Label>
<p class="text-green-600 text-sm mt-2">Copied to clipboard!</p> <div class="flex items-center space-x-2 mt-2">
{/if} <Input id="public-link" type="text" readonly value={`${origin}/public/run/${experiment.id}`} />
</CardContent> <Button on:click={copyLink}>Copy</Button>
</Card> </div>
{#if copied}
<Card class="w-full max-w-xl mx-auto"> <p class="text-green-600 text-sm mt-2">Copied to clipboard!</p>
<CardHeader> {/if}
<CardTitle>Experiment Files</CardTitle> </div>
</CardHeader> </CardContent>
<CardContent> </Card>
<div class="mt-8"> {/if}
<Label>Experiment Files</Label> {#if activeTab === 'files'}
<div <Card class="w-full">
class="border-2 border-dashed rounded-md p-4 mb-4 text-center cursor-pointer bg-muted hover:bg-accent transition" <CardHeader>
on:click={() => document.getElementById('file-input')?.click()} <CardTitle>Experiment Files</CardTitle>
> </CardHeader>
<div>Click to select files or a folder to upload (folder structure will be preserved)</div> <CardContent>
<input id="file-input" type="file" multiple webkitdirectory class="hidden" on:change={handleFileInput} /> <div class="mt-8">
</div> <div class="flex justify-end mb-4">
{#if uploading} <button
<div class="text-blue-600 text-sm mb-2">Uploading...</div> type="button"
{/if} class="px-4 py-2 rounded bg-primary text-primary-foreground hover:bg-primary/90 transition font-medium"
{#if uploadError} on:click={() => document.getElementById('file-input')?.click()}
<div class="text-red-500 text-sm mb-2">{uploadError}</div> >
{/if} Upload Files or Folder
<ul class="divide-y divide-border"> </button>
<FileBrowser <input id="file-input" type="file" multiple webkitdirectory class="hidden" on:change={handleFileInput} />
tree={buildFileTree(files)} </div>
parentPath="" {#if uploading}
{expanded} <div class="text-blue-600 text-sm mb-2">Uploading...</div>
onToggle={toggleFolder} {/if}
onDelete={deleteFileOrFolder} {#if uploadError}
/> <div class="text-red-500 text-sm mb-2">{uploadError}</div>
{#if files.length === 0} {/if}
<li class="text-muted-foreground text-sm py-2">No files uploaded yet.</li> <ul class="divide-y divide-border">
{/if} <FileBrowser
</ul> tree={buildFileTree(files)}
</div> parentPath=""
</CardContent> {expanded}
</Card> 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>
{/if}
</ul>
</div>
</CardContent>
</Card>
{/if}
</div>
</div> </div>
{:else if error} {:else if error}
<div class="flex justify-center items-center min-h-screen bg-background"> <div class="flex justify-center items-center min-h-screen bg-background">

View File

@@ -1,45 +1,91 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button'; 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 tree: any;
export let parentPath = ''; export let parentPath = '';
export let expanded: Set<string>; export let expanded: Set<string>;
export let onToggle: (path: string) => void; export let onToggle: (path: string) => void;
export let onDelete: (key: string, isFolder: boolean) => void; export let onDelete: (key: string, isFolder: boolean) => void;
export let onDownload: (key: string) => void = () => {};
export let isRoot: boolean = false;
function handleToggle(path: string) { function handleToggle(path: string) {
onToggle(path); onToggle(path);
} }
function handleDelete(key: string, isFolder: boolean) { function handleDelete(key: string, isFolder: boolean) {
console.log('FileBrowser handleDelete called:', { key, isFolder });
onDelete(key, isFolder); onDelete(key, isFolder);
} }
function handleDownload(key: string) {
onDownload(key);
}
function handleFolderClick(event: Event, path: string) { function handleFolderClick(event: Event, path: string) {
event.stopPropagation(); event.stopPropagation();
handleToggle(path); handleToggle(path);
} }
function handleFolderDelete(event: Event, path: string) { function handleFolderDelete(event: Event, path: string) {
event.stopPropagation(); event.stopPropagation();
console.log('FileBrowser handleFolderDelete called:', { path });
handleDelete(path, true); 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> </script>
<ul class="ml-2"> <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)} {#each Object.entries(tree) as [name, node] (parentPath + '/' + name)}
{#if (node as any).isFile} {#if (node as any).isFile}
<li class="flex items-center justify-between py-1 pl-4"> <li class="flex items-center gap-2 py-1 pl-2 hover:bg-accent rounded group text-sm">
<span>{name} <span class="text-xs text-muted-foreground">({(node as any).size} bytes)</span></span> <span class="flex-1 min-w-0 flex items-center gap-1">
<Button size="sm" variant="destructive" on:click={() => handleDelete((node as any).key, false)}>Delete</Button> <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> </li>
{:else} {:else}
<li> <li>
<div class="flex items-center cursor-pointer" on:click={(e) => handleFolderClick(e, parentPath ? `${parentPath}/${name}` : name)}> <div class="flex items-center gap-1 px-1 py-1 hover:bg-accent rounded cursor-pointer text-sm" >
<span class="mr-1">{expanded.has(parentPath ? `${parentPath}/${name}` : name) ? '▼' : '▶'}</span> <span class="flex-1 min-w-0 flex items-center gap-1" on:click={(e) => handleFolderClick(e, parentPath ? `${parentPath}/${name}` : name)}>
<span class="font-semibold">{name}</span> <span class="flex-shrink-0 w-5 h-5 flex items-center justify-center">
<Button size="sm" variant="destructive" class="ml-2" on:click={(e) => handleFolderDelete(e, (parentPath ? `${parentPath}/${name}` : name))}>Delete</Button> {#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> </div>
{#if expanded.has(parentPath ? `${parentPath}/${name}` : name)} {#if expanded.has(parentPath ? `${parentPath}/${name}` : name)}
<svelte:self <svelte:self
@@ -48,6 +94,8 @@
{expanded} {expanded}
{onToggle} {onToggle}
{onDelete} {onDelete}
{onDownload}
isRoot={false}
/> />
{/if} {/if}
</li> </li>