Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
Svelte
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125<script module lang="ts"> import { tv } from "tailwind-variants";
const treeRow = tv({ slots: { root: "pt-1 pr-4 pb-8 pl-1.5", row: "group my-0.5 inline-flex w-full items-center gap-2 rounded-sm px-1.5 py-0.75 text-foreground-default transition-colors hover:bg-background-subtle", icon: "size-4 shrink-0 text-foreground-placeholder transition-colors group-hover:text-foreground-default", name: "truncate select-none", children: "ml-3.5 border-l border-border-subtle pl-1.5" }, variants: { kind: { dir: { row: "cursor-pointer list-none", name: "text-foreground-muted group-hover:text-inherit" }, file: { row: "flex items-center gap-2" } } } });
interface TreeDir { type: "dir"; name: string; path: string; children: TreeNode[]; }
interface TreeFile { type: "file"; name: string; path: string; }
type TreeNode = TreeDir | TreeFile;
const buildTree = (paths: string[]): TreeNode[] => { const roots: TreeNode[] = []; const dirs: Record<string, TreeDir> = Object.create(null); for (const path of paths) { const parts = path.split("/"); let siblings = roots; let prefix = ""; for (const dir of parts.slice(0, -1)) { prefix = prefix ? `${prefix}/${dir}` : dir; let node = dirs[prefix]; if (!node) { node = { type: "dir", name: dir, path: prefix, children: [] }; dirs[prefix] = node; siblings.push(node); } siblings = node.children; } siblings.push({ type: "file", name: parts.at(-1) ?? path, path }); } return roots; };</script>
<script lang="ts"> import File from "$icon/file"; import { fileAnchor } from "$lib/components/repo/fileAnchor.svelte"; import ChevronDown from "$icon/chevron-down"; import ChevronRight from "$icon/chevron-right";
interface Props { /** full slash-separated paths, linked as the diff's own file anchors */ files: { name: string }[]; /** when the diff owns its own scroller, jumping is a call, not an anchor */ onSelect?: (path: string) => void; }
let { files, onSelect }: Props = $props();
const tree = $derived(buildTree(files.map((file) => file.name)));
let closed = $state<Record<string, boolean>>({});
const dir = treeRow({ kind: "dir" }); const file = treeRow({ kind: "file" });</script>
{#snippet nodes(list: TreeNode[])} {#each list as node (`${node.type}:${node.path}`)} {#if node.type === "dir"} {@const open = !closed[node.path]} <details {open} ontoggle={(e) => (closed[node.path] = !e.currentTarget.open)}> <summary class={dir.row()}> {#if open} <ChevronDown class={dir.icon()} aria-hidden="true" /> {:else} <ChevronRight class={dir.icon()} aria-hidden="true" /> {/if} <span class={dir.name()}>{node.name}</span> </summary> <div class={dir.children()}> {@render nodes(node.children)} </div> </details> {:else} <!-- an in-page anchor the scroller honours, so there is no route to resolve --> <!-- eslint-disable svelte/no-navigation-without-resolve --> <a href={fileAnchor(node.path)} class={file.row()} onclick={onSelect ? (event) => { event.preventDefault(); onSelect(node.path); } : undefined} > <File class={file.icon()} aria-hidden="true" /> <span class={file.name()}>{node.name}</span> </a> <!-- eslint-enable svelte/no-navigation-without-resolve --> {/if} {/each}{/snippet}
<div class={dir.root()}> {@render nodes(tree)}</div>