Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193<script module lang="ts"> import { tv } from "tailwind-variants";
export const fileRow = tv({ slots: { root: "my-3xs grid items-center gap-md rounded-md px-1.5 py-0.75", link: "hit-area-y-1 flex min-w-0 cursor-pointer items-center gap-xs text-foreground-default no-underline hover:underline", icon: "text-foreground-placeholder", message: "hidden min-w-0 md:col-span-6 md:block", time: "text-right text-foreground-subtle" }, variants: { withMessage: { true: { root: "grid-cols-12", link: "col-span-8 md:col-span-4", time: "col-span-4 md:col-span-2" }, false: { root: "grid-cols-3", link: "col-span-2", time: "col-span-1" } } }, defaultVariants: { withMessage: false } });</script>
<script lang="ts"> import { resolve } from "$app/paths"; import { MediaQuery } from "svelte/reactivity"; import ChevronDown from "$icon/chevron-down"; import File from "$icon/file"; import FileSymlink from "$icon/file-symlink"; import Folder from "$icon/folder"; import FolderInput from "$icon/folder-input"; import { splitMessage } from "#lib/api/repo.ts"; import { button } from "#lib/components/ui/Button.svelte"; import TimeAgo from "#lib/components/ui/TimeAgo.svelte"; import type { TreeEntrySummary } from "#lib/components/repo/types.ts"; import { encodePathSegments } from "#lib/components/repo/urls.ts";
interface Props { ownerHandle: string; repoName: string; ref: string; entries: TreeEntrySummary[]; path?: string; withMessage?: boolean; clamp?: boolean; }
let { ownerHandle, repoName, ref, entries, path = "", withMessage = false, clamp = false }: Props = $props();
const wide = new MediaQuery("min-width: 48rem"); let root = $state<HTMLDivElement>(); let fit = $state<number>(); const cut = $derived( clamp && wide.current && fit !== undefined && fit < entries.length ? fit : undefined );
$effect(() => { if (!clamp || !wide.current || !root) return; const el = root; const measure = () => { if (el.querySelector("details[open]")) return; const rows = el.querySelectorAll<HTMLElement>(".tree-entry"); if (rows.length === 0) return; const step = rows.length > 1 ? rows[1].offsetTop - rows[0].offsetTop : rows[0].offsetHeight; fit = Math.max(Math.floor(el.clientHeight / step), 1); }; const observer = new ResizeObserver(measure); observer.observe(el); return () => observer.disconnect(); });
const base = $derived(`/${ownerHandle}/${repoName}`); const encodedRef = $derived(encodeURIComponent(ref)); const row = $derived(fileRow({ withMessage }));
const linkFor = (entry: TreeEntrySummary) => { const target = path ? `${path}/${entry.name}` : entry.name; const kind = entry.kind === "directory" ? "tree" : "blob"; return `${base}/${kind}/${encodedRef}/${encodePathSegments(target)}`; };
// lucide puts fill="none" on the path itself, which beats anything inherited // from the svg, so the class has to reach the path, and only the last svg so // icon extensions that copy its classes onto an icon put before it keep their fills const iconWrapClassFor = (entry: TreeEntrySummary) => entry.kind === "directory" ? [row.icon(), "[&>svg:last-child_path]:fill-current"] : row.icon();
const iconFor = (entry: TreeEntrySummary) => { switch (entry.kind) { case "directory": return Folder; case "submodule": return FolderInput; case "symlink": return FileSymlink; default: return File; } };</script>
{#snippet entryRow(entry: TreeEntrySummary)} {@const Glyph = iconFor(entry)} {@const href = resolve(linkFor(entry) as "/")} <div class={["tree-entry", `tree-${entry.kind}`, row.root()]} data-kind={entry.kind}> <a {href} class={["tree-link", row.link()]}> <span class={iconWrapClassFor(entry)}> <Glyph class="tree-icon size-3.5 shrink-0" aria-hidden="true" /> </span> <span class="tree-name truncate">{entry.name}</span> </a>
{#if withMessage} <div class={row.message()}> {#if entry.lastCommitHash && entry.lastCommitMessage} <a href={resolve(`${base}/commit/${entry.lastCommitHash}` as "/")} class="block truncate text-foreground-muted no-underline hover:text-foreground-default" > {splitMessage(entry.lastCommitMessage)[0]} </a> {/if} </div> {/if}
<div class={row.time()}> {#if entry.lastCommitHash && entry.lastCommitWhen} <a href={resolve(`${base}/commit/${entry.lastCommitHash}` as "/")} class="text-foreground-muted no-underline hover:text-foreground-default" title={entry.lastCommitMessage} > <TimeAgo value={entry.lastCommitWhen} /> </a> {/if} </div> </div>{/snippet}
<!-- the divider belongs to whatever column the tree is sat in --><div bind:this={root} class={["file-tree min-w-0", clamp && "md:relative md:h-full md:overflow-hidden"]}> {#if cut === undefined} {#each entries as entry (entry.name)} {@render entryRow(entry)} {/each} {:else} <div class="[&:has(+details:not([open]))]:mask-[linear-gradient(to_bottom,black_calc(100%-4rem),transparent)]" > {#each entries.slice(0, cut) as entry (entry.name)} {@render entryRow(entry)} {/each} </div> <details class="group"> <summary class={button({ variant: "ghost", size: "small", class: "absolute inset-x-0 bottom-0 mx-auto flex w-fit list-none group-open:hidden [&::-webkit-details-marker]:hidden" })} > <ChevronDown class="size-3.5" aria-hidden="true" /> Show all </summary> {#each entries.slice(cut) as entry (entry.name)} {@render entryRow(entry)} {/each} </details> {/if}</div>