Recursive rendering

We can’t see inside our folders. Let’s fix that. Create a folder snippet, using the …

{#snippet folder(f: typeof file.$inferSelect)}
	<details>
		<summary>
			<span class="name">{f.name}</span>
		</summary>

		<div class="contents">
			{@render list(await getFiles(f.id))}
		</div>
	</details>
{/snippet}

…and render it when the type matches our artificial directory type:

{#snippet list(files: (typeof file.$inferSelect)[])}
	<div role="list">
		{#each files as f (f.id)}
			<div role="listitem">
+				{#if f.type === 'application/x-directory'}
+					{@render folder(f)}
+				{:else}
					<div class="file">
						<FileIcon />

						<a href={resolve('/file/[id]', { id: f.id })} class="name">{f.name}</a>

						<span class="date">{timeago(f.created_at, new Date())}</span>
						<span class="size">{bytes(f.size)}</span>
					</div>
+				{/if}
			</div>
		{/each}
	</div>
{/snippet}

This works but it looks terrible! Update the .file rule to match summary as well.

We need some folder icons. Import these from Lucide…

import FolderOpenIcon from '@lucide/svelte/icons/folder-open';
import FolderClosedIcon from '@lucide/svelte/icons/folder-closed';

…and put them in the <summary>:

<span class="if-open"><FolderOpenIcon /></span>
<span class="if-closed"><FolderClosedIcon /></span>

Add styles:

details {
	&[open] > summary > .if-closed {
		display: none;
	}

	&:not([open]) > summary > .if-open {
		display: none;
	}
}

Without indentation, it’s hard to understand the structure. We can fix that with CSS variables:

+{#snippet list(files: (typeof file.$inferSelect)[], depth = 0)}
	<div role="list">
		{#each files as f (f.id)}
+			<div role="listitem" style:--depth={depth}>
				{#if f.type === 'application/x-directory'}
+					{@render folder(f, depth)}
				{:else}
					<div class="file">
						<FileIcon />

						<a href={resolve('/file/[id]', { id: f.id })} class="name">{f.name}</a>

						<span class="date">{timeago(f.created_at, new Date())}</span>
						<span class="size">{bytes(f.size)}</span>
					</div>
				{/if}
			</div>
		{/each}
	</div>
{/snippet}

+{#snippet folder(f: typeof file.$inferSelect, depth: number)}
	<details>
		<summary>
			<span class="if-open"><FolderOpenIcon /></span>
			<span class="if-closed"><FolderClosedIcon /></span>

			<span class="name">{f.name}</span>
		</summary>

		<div class="contents">
+			{@render list(await getFiles(f.id), depth + 1)}
		</div>
	</details>
{/snippet}

Update the padding on the summary, .file rule to use the variable:

padding: 0 1.2rem 0 calc(1.2rem + 0.5rem * var(--depth));