<script lang="ts">
import type { TreeItemEntry } from "../../../api/types/workbench.js";
import { TreeItemCollapsibleState } from "../../../api/types/workbench.js";
import { commands } from "../../../stores/commands.svelte.js";
import AppIcon from "../../layout/AppIcon.svelte";
import TreeView from "./TreeView.svelte";
interface Props {
items: TreeItemEntry[];
depth?: number;
}
let { items, depth = 0 }: Props = $props();
// Track user overrides. Key = item.id, value = true (expanded) or false (collapsed).
// Items not in this map use their initial collapsible_state.
let overrides: Map<string, boolean> = $state(new Map());
function toggle(item: TreeItemEntry) {
if (item.collapsible_state === TreeItemCollapsibleState.None) return;
const next = new Map(overrides);
const currentlyExpanded = isExpanded(item);
next.set(item.id, !currentlyExpanded);
overrides = next;
}
function isExpanded(item: TreeItemEntry): boolean {
const override = overrides.get(item.id);
if (override !== undefined) return override;
return item.collapsible_state === TreeItemCollapsibleState.Expanded;
}
function activate(item: TreeItemEntry) {
if (item.command) {
void commands.execute(item.command);
}
}
</script>
<ul class="tree-list" style:--depth={depth}>
{#each items as item (item.id)}
{@const expanded = isExpanded(item)}
{@const hasChildren = item.collapsible_state !== TreeItemCollapsibleState.None}
<li class="tree-item">
<button
class="tree-item-row"
onclick={() => {
if (hasChildren) toggle(item);
else activate(item);
}}
title={item.tooltip ?? item.label}
>
{#if hasChildren}
<span class="tree-chevron" class:expanded>
<AppIcon name="chevron-right" size={12} />
</span>
{:else}
<span class="tree-chevron-spacer"></span>
{/if}
<span class="tree-label">{item.label}</span>
{#if item.description}
<span class="tree-description">{item.description}</span>
{/if}
</button>
{#if hasChildren && expanded && item.children.length > 0}
<TreeView items={item.children} depth={depth + 1} />
{/if}
</li>
{/each}
</ul>
<style>
.tree-list {
list-style: none;
margin: 0;
padding: 0;
}
.tree-item {
margin: 0;
padding: 0;
}
.tree-item-row {
width: 100%;
display: flex;
align-items: center;
gap: var(--wb-spacing-4);
height: var(--wb-control-size-22);
font-size: var(--wb-font-size-12);
color: var(--vscode-foreground);
cursor: pointer;
text-align: left;
padding-left: calc(
var(--wb-tree-indent-base) +
var(--depth) * var(--wb-tree-indent-step)
);
}
.tree-item-row:hover {
background: var(--vscode-list-hoverBackground);
color: var(--vscode-editor-foreground);
}
.tree-chevron {
display: flex;
align-items: center;
justify-content: center;
width: var(--wb-control-size-16);
flex-shrink: 0;
transition: transform 0.1s;
color: var(--vscode-disabledForeground);
}
.tree-chevron.expanded {
transform: rotate(90deg);
}
.tree-chevron-spacer {
width: var(--wb-control-size-16);
flex-shrink: 0;
}
.tree-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.tree-description {
color: var(--vscode-disabledForeground);
font-size: var(--wb-font-size-11);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 1;
min-width: 0;
}
</style>