sticky folders

This commit is contained in:
vcoppe committed 2026-10-17 16:24:19 +02:00
1 parent 8fa88680b3
commit 593b43e433
5 files changed
+48 -4

No files matched your search

@@ -10,6 +10,9 @@
id: string | number;
class?: ClassValue;
trigger: Snippet;
/** Classes and style of the trigger row (when the trigger holds the content), to make it stick. */
triggerClass?: ClassValue;
triggerStyle?: string;
/** Gets whether the node is open, for contents that are only worth building then. */
content: Snippet<[boolean]>;
} = $props();
@@ -32,7 +35,7 @@
<Collapsible.Root bind:open={open.current} class={props.class}>
{#if slotInsideTrigger}
<Collapsible.Trigger class="w-full">
<Collapsible.Trigger class={['w-full', props.triggerClass]} style={props.triggerStyle}>
<Button
variant="ghost"
size="icon"
@@ -2,6 +2,7 @@
import { CollapsibleTree, CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
import ExplorerTree from './ExplorerTree.svelte';
import { stickyRowStyle } from './sticky';
import { i18n } from '$lib/i18n/i18n.svelte';
import { engine } from '$lib/engine';
import { folderName } from '$lib/engine/workspace';
@@ -24,7 +25,11 @@
{#key folder}
<!-- the folder itself, which is open at first -->
<CollapsibleTree side="left" defaultState="open">
<CollapsibleTreeNode id="folder">
<CollapsibleTreeNode
id="folder"
triggerClass="sticky flex items-center bg-background"
triggerStyle={stickyRowStyle(0)}
>
{#snippet trigger()}
<span class="grow truncate text-left" title={folder}>
{folderName(folder)}
@@ -3,6 +3,7 @@
import { Button } from '$lib/components/ui/button';
import { FileText } from '@lucide/svelte';
import ExplorerTree from './ExplorerTree.svelte';
import { stickyRowStyle } from './sticky';
import { openPath } from '$lib/actions/file-actions';
import { engine, type FolderEntry } from '$lib/engine';
import { selectedFileIds } from '$lib/engine/selection-helpers';
@@ -10,9 +11,12 @@
let {
path,
depth = 1,
}: {
/** The folder whose content is listed, the open folder if it is not given. */
path?: string;
/** How many folders deep the folders listed are (the open folder is 0). */
depth?: number;
} = $props();
const { workspace, selection, folderVersion } = engine;
@@ -45,7 +49,11 @@
{#each entries as entry (entry.path)}
{#if entry.isDir}
<CollapsibleTreeNode id={entry.name}>
<CollapsibleTreeNode
id={entry.name}
triggerClass="sticky flex items-center bg-background"
triggerStyle={stickyRowStyle(depth)}
>
{#snippet trigger()}
<span class="grow truncate text-left">{entry.name}</span>
{/snippet}
@@ -53,7 +61,7 @@
<!-- the content of a folder is only read when it is opened -->
{#if opened}
<div class="ml-4">
<ExplorerTree path={entry.path} />
<ExplorerTree path={entry.path} depth={depth + 1} />
</div>
{/if}
{/snippet}
@@ -0,0 +1,17 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { rowHeight, stickyRowStyle } from './sticky';
describe('stickyRowStyle', () => {
it('puts each row under the ones of the folders it is in', () => {
assert.match(stickyRowStyle(0), /top: 0px/);
assert.match(stickyRowStyle(2), new RegExp(`top: ${2 * rowHeight}px`));
});
it('puts the rows of the folders above over the rows below', () => {
const zIndex = (depth: number) => Number(/z-index: (\d+)/.exec(stickyRowStyle(depth))![1]);
assert.ok(zIndex(0) > zIndex(1));
assert.ok(zIndex(1) > zIndex(5));
assert.ok(zIndex(500) >= 1);
});
});
@@ -0,0 +1,11 @@
/** The height of a row of the explorer. */
export const rowHeight = 28;
/**
* The style of the row of a folder that is `depth` folders deep (0 for the open folder), to stick
* under the rows of the folders it is in while any of its content can be seen: the rows of the
* folders above are above it too.
*/
export function stickyRowStyle(depth: number): string {
return `top: ${depth * rowHeight}px; height: ${rowHeight}px; z-index: ${Math.max(100 - depth, 1)};`;
}