This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

web/components: extract RepoCrumbs and RefChip from TreeHeader

Signed-off-by: dawn <dawn@tangled.org>

author
dawn
date (Aug 1, 2026, 2:21 AM +0300) commit e0fb899c parent e8b1a8e1 change-id pspmrpoz
+179 -39
+36
web/src/lib/components/repo/RefChip.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta, type StoryContext } from "@storybook/addon-svelte-csf"; 3 + import { expect } from "storybook/test"; 4 + import RefChip from "./RefChip.svelte"; 5 + 6 + type PlayContext = Pick<StoryContext<Record<string, unknown>>, "canvas">; 7 + 8 + const linksToTree = async ({ canvas }: PlayContext) => { 9 + await expect(canvas.getByText("at")).toBeVisible(); 10 + await expect(canvas.getByRole("link", { name: "main" })).toHaveAttribute( 11 + "href", 12 + "/dawn/tangled/tree/main" 13 + ); 14 + }; 15 + 16 + const encodesRef = async ({ canvas }: PlayContext) => { 17 + await expect(canvas.getByRole("link", { name: "feature/split-diffs" })).toHaveAttribute( 18 + "href", 19 + "/dawn/tangled/tree/feature%2Fsplit-diffs" 20 + ); 21 + }; 22 + 23 + const { Story } = defineMeta({ 24 + title: "Repo/RefChip", 25 + component: RefChip, 26 + tags: ["autodocs"], 27 + args: { 28 + ownerHandle: "dawn", 29 + repoName: "tangled", 30 + ref: "main" 31 + } 32 + }); 33 + </script> 34 + 35 + <Story name="Branch" play={linksToTree} /> 36 + <Story name="Branch with slash" args={{ ref: "feature/split-diffs" }} play={encodesRef} />
+23
web/src/lib/components/repo/RefChip.svelte
··· 1 + <script lang="ts"> 2 + import { resolve } from "$app/paths"; 3 + 4 + interface Props { 5 + ownerHandle: string; 6 + repoName: string; 7 + ref: string; 8 + class?: string; 9 + } 10 + 11 + let { ownerHandle, repoName, ref, class: className = "" }: Props = $props(); 12 + 13 + const treeHref = $derived( 14 + resolve(`/${ownerHandle}/${repoName}/tree/${encodeURIComponent(ref)}` as "/") 15 + ); 16 + </script> 17 + 18 + <div 19 + class="flex w-fit items-center justify-center gap-2 rounded-sm border border-border-default bg-background-inset p-2 whitespace-nowrap {className}" 20 + > 21 + <span class="text-foreground-muted">at</span> 22 + <a href={treeHref} class="text-foreground-default no-underline hover:underline">{ref}</a> 23 + </div>
+59
web/src/lib/components/repo/RepoCrumbs.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta, type StoryContext } from "@storybook/addon-svelte-csf"; 3 + import { expect } from "storybook/test"; 4 + import RepoCrumbs from "./RepoCrumbs.svelte"; 5 + 6 + type PlayContext = Pick<StoryContext<Record<string, unknown>>, "canvas">; 7 + 8 + const nestedPath = async ({ canvas }: PlayContext) => { 9 + await expect(canvas.getByRole("link", { name: "tangled" })).toHaveAttribute( 10 + "href", 11 + "/dawn/tangled/tree/main" 12 + ); 13 + await expect(canvas.getByRole("link", { name: "web" })).toHaveAttribute( 14 + "href", 15 + "/dawn/tangled/tree/main/web" 16 + ); 17 + await expect(canvas.getByRole("link", { name: "src" })).toHaveAttribute( 18 + "href", 19 + "/dawn/tangled/tree/main/web/src" 20 + ); 21 + const current = canvas.getByText("lib"); 22 + await expect(current).toHaveAttribute("aria-current", "page"); 23 + await expect(canvas.queryByRole("link", { name: "lib" })).toBeNull(); 24 + }; 25 + 26 + const rootIsCurrent = async ({ canvas }: PlayContext) => { 27 + const current = canvas.getByText("tangled"); 28 + await expect(current).toHaveAttribute("aria-current", "page"); 29 + await expect(canvas.queryByRole("link")).toBeNull(); 30 + }; 31 + 32 + const encodesSegments = async ({ canvas }: PlayContext) => { 33 + await expect(canvas.getByRole("link", { name: "what#next?" })).toHaveAttribute( 34 + "href", 35 + "/dawn/tangled/tree/main/what%23next%3F" 36 + ); 37 + await expect(canvas.getByText("100%.md")).toHaveAttribute("aria-current", "page"); 38 + }; 39 + 40 + const { Story } = defineMeta({ 41 + title: "Repo/RepoCrumbs", 42 + component: RepoCrumbs, 43 + tags: ["autodocs"], 44 + args: { 45 + ownerHandle: "dawn", 46 + repoName: "tangled", 47 + ref: "main", 48 + path: "web/src/lib" 49 + } 50 + }); 51 + </script> 52 + 53 + <Story name="Nested path" play={nestedPath} /> 54 + <Story name="Repository root" args={{ path: "" }} play={rootIsCurrent} /> 55 + <Story 56 + name="Segments needing encoding" 57 + args={{ path: "what#next?/100%.md" }} 58 + play={encodesSegments} 59 + />
+44
web/src/lib/components/repo/RepoCrumbs.svelte
··· 1 + <script lang="ts"> 2 + import { resolve } from "$app/paths"; 3 + import { encodePathSegments } from "./urls"; 4 + 5 + interface Props { 6 + ownerHandle: string; 7 + repoName: string; 8 + ref: string; 9 + // repo-relative path, empty at the tree root 10 + path: string; 11 + } 12 + 13 + let { ownerHandle, repoName, ref, path }: Props = $props(); 14 + 15 + const treeBase = $derived(`/${ownerHandle}/${repoName}/tree/${encodeURIComponent(ref)}`); 16 + 17 + const crumbs = $derived.by(() => { 18 + const parts = path.split("/").filter(Boolean); 19 + return [ 20 + { name: repoName, href: treeBase, last: parts.length === 0 }, 21 + ...parts.map((name, index) => ({ 22 + name, 23 + href: `${treeBase}/${encodePathSegments(parts.slice(0, index + 1).join("/"))}`, 24 + last: index === parts.length - 1 25 + })) 26 + ]; 27 + }); 28 + </script> 29 + 30 + <div 31 + class="flex w-fit items-center gap-1 overflow-x-auto rounded-sm border border-border-default bg-background-inset px-3 py-2 whitespace-nowrap" 32 + > 33 + {#each crumbs as crumb (crumb.href)} 34 + {#if crumb.last} 35 + <span class="text-foreground-default" aria-current="page">{crumb.name}</span> 36 + {:else} 37 + <a 38 + href={resolve(crumb.href as "/")} 39 + class="text-foreground-muted no-underline hover:underline">{crumb.name}</a 40 + > 41 + <span class="text-foreground-muted" aria-hidden="true">/</span> 42 + {/if} 43 + {/each} 44 + </div>
+4 -39
web/src/lib/components/repo/TreeHeader.svelte
··· 1 1 <script lang="ts"> 2 - import { resolve } from "$app/paths"; 2 + import RefChip from "./RefChip.svelte"; 3 + import RepoCrumbs from "./RepoCrumbs.svelte"; 3 4 import type { TreeEntrySummary } from "./types"; 4 5 5 6 interface Props { ··· 12 13 13 14 let { ownerHandle, repoName, ref, path, entries }: Props = $props(); 14 15 15 - const base = $derived(`/${ownerHandle}/${repoName}`); 16 - const encodedRef = $derived(encodeURIComponent(ref)); 17 - 18 - const crumbs = $derived.by(() => { 19 - const parts = path.split("/").filter(Boolean); 20 - return [ 21 - { name: repoName, href: `${base}/tree/${encodedRef}`, last: parts.length === 0 }, 22 - ...parts.map((name, index) => ({ 23 - name, 24 - href: `${base}/tree/${encodedRef}/${parts.slice(0, index + 1).join("/")}`, 25 - last: index === parts.length - 1 26 - })) 27 - ]; 28 - }); 29 - 30 16 const folders = $derived( 31 17 entries.filter((entry) => entry.kind === "directory" || entry.kind === "submodule").length 32 18 ); ··· 36 22 37 23 <div class="mb-3 flex items-center justify-between gap-2 text-sm"> 38 24 <div class="flex min-w-0 items-center gap-3"> 39 - <div 40 - class="flex w-fit items-center gap-1 overflow-x-auto rounded-sm border border-border-default bg-background-inset px-3 py-2 whitespace-nowrap" 41 - > 42 - {#each crumbs as crumb (crumb.href)} 43 - {#if crumb.last} 44 - <span class="text-foreground-default" aria-current="page">{crumb.name}</span> 45 - {:else} 46 - <a 47 - href={resolve(crumb.href as "/")} 48 - class="text-foreground-muted no-underline hover:underline">{crumb.name}</a 49 - > 50 - <span class="text-foreground-muted" aria-hidden="true">/</span> 51 - {/if} 52 - {/each} 53 - </div> 25 + <RepoCrumbs {ownerHandle} {repoName} {ref} {path} /> 54 26 55 27 <div class="flex items-center gap-2 text-foreground-muted"> 56 28 {#if folders > 0} ··· 65 37 </div> 66 38 </div> 67 39 68 - <div 69 - class="hidden w-fit items-center gap-1 rounded-sm border border-border-default bg-background-inset p-2 whitespace-nowrap text-foreground-muted md:flex" 70 - > 71 - at <a 72 - href={resolve(`${base}/tree/${encodedRef}` as "/")} 73 - class="text-foreground-default no-underline hover:underline">{ref}</a 74 - > 75 - </div> 40 + <RefChip {ownerHandle} {repoName} {ref} class="hidden md:flex" /> 76 41 </div>
+13
web/src/lib/components/repo/urls.ts
··· 1 + import { resolve } from "$app/paths"; 2 + 3 + // rest params arrive decoded, re-encode per segment or `#`/`?`/`%` corrupt 4 + // the url 5 + export const encodePathSegments = (path: string): string => 6 + path.split("/").map(encodeURIComponent).join("/"); 7 + 8 + export const baseName = (path: string): string => path.split("/").pop() ?? path; 9 + 10 + export const rawBlobHref = (ownerHandle: string, repoName: string, ref: string, path: string) => 11 + resolve( 12 + `/${ownerHandle}/${repoName}/raw/${encodeURIComponent(ref)}/${encodePathSegments(path)}` as "/" 13 + );