This repository has no description
0

Configure Feed

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

web: pull view

Signed-off-by: Seongmin Lee <git@boltless.me>

author
Seongmin Lee
date (Aug 4, 2026, 4:33 PM +0300) commit 70809a99 parent 72effbb6 change-id xwxsotxt
+420 -5
+90
web/src/lib/components/repo/pulls/PullDiscussion.svelte
··· 1 + <script lang="ts"> 2 + import ChevronDown from "$icon/chevron-down"; 3 + import ArrowRight from "$icon/arrow-right"; 4 + import User from "$lib/components/ui/User.svelte"; 5 + import PullStatePill, { type PullStatePillVariants } from "./PullStatePill.svelte"; 6 + 7 + interface PullVersion { 8 + base: string; 9 + head: string; 10 + comments: unknown[]; 11 + } 12 + 13 + interface Props { 14 + state: PullStatePillVariants["state"]; 15 + versions: PullVersion[]; 16 + authorHandle: string; 17 + authorDid: string; 18 + } 19 + 20 + let { state, versions, authorHandle, authorDid }: Props = $props(); 21 + 22 + // the appview rings the whole history panel in the pull's state colour 23 + const accents = { 24 + open: "border-border-success", 25 + merged: "border-background-info-emphasis", 26 + closed: "border-border-strong", 27 + abandoned: "border-border-strong" 28 + }; 29 + const accent = $derived(accents[state ?? "open"]); 30 + 31 + const comments = $derived(versions.reduce((total, v) => total + v.comments.length, 0)); 32 + const plural = (n: number, noun: string) => `${n} ${noun}${n === 1 ? "" : "s"}`; 33 + const short = (hash: string) => hash.slice(0, 7); 34 + </script> 35 + 36 + <details open class="group/history flex flex-col"> 37 + <summary 38 + class="flex list-none items-center justify-between gap-2 rounded-t-sm border-t-2 px-1 py-2 {accent}" 39 + > 40 + <div class="flex items-center gap-2"> 41 + <span class="lg:hidden"><PullStatePill {state} /></span> 42 + <h2 class="typography-heading-4">History</h2> 43 + </div> 44 + <div class="flex items-center gap-2 typography-paragraph-small text-foreground-subtle"> 45 + <span>{plural(versions.length, "version")}</span> 46 + <span class="before:content-['·']"></span> 47 + <span>{plural(comments, "comment")}</span> 48 + <ChevronDown class="size-4 shrink-0 group-open/history:rotate-180" aria-hidden="true" /> 49 + </div> 50 + </summary> 51 + 52 + <div class="flex flex-col gap-4 overflow-y-auto pb-4 lg:max-h-[calc(100vh-3rem)]"> 53 + {#each versions as version, index (index)} 54 + {@const active = index === versions.length - 1} 55 + <div class="overflow-clip rounded-sm border border-border-default"> 56 + <header 57 + class="flex gap-2 px-3 py-2 {active ? 'bg-background-inset' : 'bg-background-default'}" 58 + > 59 + <div class="flex min-w-0 flex-1 flex-col gap-1"> 60 + <div 61 + class="flex flex-wrap items-center gap-1 typography-paragraph-small text-foreground-subtle" 62 + > 63 + <User handle={authorHandle} did={authorDid} size="small" /> 64 + submitted 65 + <span class="rounded-sm bg-background-muted px-1 typography-monospace-small" 66 + >#{index}</span 67 + > 68 + </div> 69 + <div 70 + class="flex items-center gap-1 typography-monospace-small text-foreground-muted" 71 + title="{version.base} → {version.head}" 72 + > 73 + {short(version.base)} 74 + <ArrowRight class="size-3 shrink-0" aria-hidden="true" /> 75 + {short(version.head)} 76 + </div> 77 + </div> 78 + </header> 79 + 80 + <!-- todo: this version's comments, then the merge check and the 81 + merge/close/resubmit actions on the latest one --> 82 + <div class="border-t border-border-default px-3 py-2 typography-paragraph-small"> 83 + {#if version.comments.length === 0} 84 + <span class="text-foreground-subtle italic">No comments</span> 85 + {/if} 86 + </div> 87 + </div> 88 + {/each} 89 + </div> 90 + </details>
+36
web/src/lib/components/repo/pulls/PullInfoBar.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import PullInfoBar from "./PullInfoBar.svelte"; 4 + 5 + const { Story } = defineMeta({ 6 + title: "Repo/Pulls/PullInfoBar", 7 + component: PullInfoBar, 8 + tags: ["autodocs"], 9 + argTypes: { 10 + state: { 11 + control: { type: "inline-radio" }, 12 + options: ["open", "merged", "closed", "abandoned"] 13 + }, 14 + authorHandle: { control: "text" }, 15 + authorDid: { control: "text" }, 16 + createdAt: { control: "text" }, 17 + targetBranch: { control: "text" }, 18 + sourceBranch: { control: "text" }, 19 + sourceRepo: { control: "text" } 20 + }, 21 + args: { 22 + state: "open", 23 + authorHandle: "joshka.net", 24 + authorDid: "did:plc:3p7ejjxnufohsygt5vbyxb2i", 25 + createdAt: "2025-06-01T10:30:00Z", 26 + ownerHandle: "tangled.org", 27 + repoName: "core", 28 + targetBranch: "master" 29 + } 30 + }); 31 + </script> 32 + 33 + <Story name="Open" /> 34 + <Story name="BranchSource" args={{ sourceBranch: "ref-based-pr" }} /> 35 + <Story name="ForkSource" args={{ sourceBranch: "ref-based-pr", sourceRepo: "joshka.net/core" }} /> 36 + <Story name="Merged" args={{ state: "merged" }} />
+79
web/src/lib/components/repo/pulls/PullInfoBar.svelte
··· 1 + <script lang="ts"> 2 + import { resolve } from "$app/paths"; 3 + import TimeAgo from "$lib/components/ui/TimeAgo.svelte"; 4 + import User from "$lib/components/ui/User.svelte"; 5 + import PullStatePill, { type PullStatePillVariants } from "./PullStatePill.svelte"; 6 + 7 + interface Props { 8 + state: PullStatePillVariants["state"]; 9 + authorHandle: string; 10 + authorDid: string; 11 + // absent on the mock loader for now, the separator hides with it 12 + createdAt?: string; 13 + ownerHandle: string; 14 + repoName: string; 15 + targetBranch: string; 16 + // missing on patch-based pulls, which have no branch to point at 17 + sourceBranch?: string; 18 + // set only when the source branch lives in a fork, as `owner/repo` or a did 19 + sourceRepo?: string; 20 + } 21 + 22 + let { 23 + state, 24 + authorHandle, 25 + authorDid, 26 + createdAt, 27 + ownerHandle, 28 + repoName, 29 + targetBranch, 30 + sourceBranch, 31 + sourceRepo 32 + }: Props = $props(); 33 + 34 + const treeHref = (repo: string, ref: string) => 35 + resolve(`/${repo}/tree/${encodeURIComponent(ref)}` as "/"); 36 + const target = $derived(`${ownerHandle}/${repoName}`); 37 + const chip = 38 + "inline-flex items-center gap-0.5 rounded-sm bg-background-inset px-2 typography-monospace-small"; 39 + </script> 40 + 41 + <div 42 + class="flex flex-wrap items-center gap-x-1 gap-y-1.5 typography-paragraph-small text-foreground-subtle" 43 + > 44 + <PullStatePill {state} /> 45 + 46 + <span class="ml-1 flex items-center gap-1"> 47 + opened by 48 + <User handle={authorHandle} did={authorDid} /> 49 + </span> 50 + 51 + {#if createdAt} 52 + <span class="before:mr-1 before:content-['·']"> 53 + <TimeAgo value={createdAt} /> 54 + </span> 55 + {/if} 56 + 57 + <span class="flex items-center gap-1 before:mr-1 before:content-['·']"> 58 + targeting 59 + <span class={chip}> 60 + <a href={treeHref(target, targetBranch)} class="no-underline hover:underline" 61 + >{targetBranch}</a 62 + > 63 + </span> 64 + </span> 65 + 66 + {#if sourceBranch} 67 + <span class="flex items-center gap-1"> 68 + from 69 + <span class={chip}> 70 + {#if sourceRepo} 71 + <a href={resolve(`/${sourceRepo}` as "/")} class="no-underline hover:underline">fork</a>: 72 + {/if} 73 + <a href={treeHref(sourceRepo ?? target, sourceBranch)} class="no-underline hover:underline" 74 + >{sourceBranch}</a 75 + > 76 + </span> 77 + </span> 78 + {/if} 79 + </div>
+24
web/src/lib/components/repo/pulls/PullStatePill.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import PullStatePill from "./PullStatePill.svelte"; 4 + 5 + const { Story } = defineMeta({ 6 + title: "Repo/Pulls/PullStatePill", 7 + component: PullStatePill, 8 + tags: ["autodocs"], 9 + argTypes: { 10 + state: { 11 + control: { type: "inline-radio" }, 12 + options: ["open", "merged", "closed", "abandoned"] 13 + } 14 + }, 15 + args: { 16 + state: "open" 17 + } 18 + }); 19 + </script> 20 + 21 + <Story name="Open" /> 22 + <Story name="Merged" args={{ state: "merged" }} /> 23 + <Story name="Closed" args={{ state: "closed" }} /> 24 + <Story name="Abandoned" args={{ state: "abandoned" }} />
+52
web/src/lib/components/repo/pulls/PullStatePill.svelte
··· 1 + <script module lang="ts"> 2 + import { tv, type VariantProps } from "tailwind-variants"; 3 + 4 + export const pullStatePill = tv({ 5 + base: "inline-flex items-center gap-1 rounded-sm px-2 py-1 typography-paragraph-small text-foreground-on-emphasis select-none", 6 + variants: { 7 + state: { 8 + open: "bg-background-success-emphasis", 9 + // todo: the design system has no purple token yet, indigo is the 10 + // closest thing to the appview's purple "merged" 11 + merged: "bg-background-info-emphasis", 12 + closed: "bg-background-emphasis", 13 + abandoned: "bg-background-emphasis" 14 + } 15 + }, 16 + defaultVariants: { 17 + state: "open" 18 + } 19 + }); 20 + 21 + export type PullStatePillVariants = VariantProps<typeof pullStatePill>; 22 + export type PullState = NonNullable<PullStatePillVariants["state"]>; 23 + </script> 24 + 25 + <script lang="ts"> 26 + import Ban from "$icon/ban"; 27 + import GitMerge from "$icon/git-merge"; 28 + import GitPullRequest from "$icon/git-pull-request"; 29 + import GitPullRequestClosed from "$icon/git-pull-request-closed"; 30 + 31 + interface Props { 32 + state: PullStatePillVariants["state"]; 33 + class?: string; 34 + } 35 + 36 + let { state = "open", class: className }: Props = $props(); 37 + 38 + const icons = { 39 + open: GitPullRequest, 40 + merged: GitMerge, 41 + closed: GitPullRequestClosed, 42 + abandoned: Ban 43 + }; 44 + const labels = { open: "Open", merged: "Merged", closed: "Closed", abandoned: "Abandoned" }; 45 + 46 + const Icon = $derived(icons[state ?? "open"]); 47 + </script> 48 + 49 + <span class={pullStatePill({ state, class: className })}> 50 + <Icon class="size-3 shrink-0" aria-hidden="true" /> 51 + <span>{labels[state ?? "open"]}</span> 52 + </span>
+11 -5
web/src/routes/[handle]/[repo]/+layout@.svelte
··· 16 16 return ["issues", "pulls", "pipelines", "settings"].includes(segment) ? segment : "overview"; 17 17 }); 18 18 19 - // commit pages break out of the reading column, everything else stays 20 - // capped 21 - const fullWidth = $derived((page.route.id ?? "").includes("/commit/")); 19 + // the pull page runs a discussion rail down the full page height, so it places 20 + // the repo header and tabs inside its own column instead of taking them here 21 + const bare = $derived(page.route.id === "/[handle]/[repo]/pulls/[aturi]"); 22 + 23 + // commit and pull pages break out of the reading column, everything else 24 + // stays capped 25 + const fullWidth = $derived(bare || (page.route.id ?? "").includes("/commit/")); 22 26 </script> 23 27 24 28 <!-- todo: og and twitter card tags, the appview has repo/fragments/og.html --> ··· 41 45 </svelte:head> 42 46 43 47 <section class={fullWidth ? "w-full px-2 py-6 sm:px-4" : "mx-auto w-full max-w-screen-lg py-6"}> 44 - <RepoHeader repo={data.repo} counts={data.counts} viewerStarRkey={data.viewerStarRkey} /> 45 - <RepoTabs repo={data.repo} counts={data.counts} active={activeTab} /> 48 + {#if !bare} 49 + <RepoHeader repo={data.repo} counts={data.counts} viewerStarRkey={data.viewerStarRkey} /> 50 + <RepoTabs repo={data.repo} counts={data.counts} active={activeTab} /> 51 + {/if} 46 52 {@render children()} 47 53 </section>
+79
web/src/routes/[handle]/[repo]/pulls/[aturi]/+page.svelte
··· 1 + <script lang="ts"> 2 + import RepoHeader from "$lib/components/repo/RepoHeader.svelte"; 3 + import RepoTabs from "$lib/components/repo/RepoTabs.svelte"; 4 + import IssueHeader from "$lib/components/repo/issues/IssueHeader.svelte"; 5 + import IssueLabelPanel from "$lib/components/repo/issues/IssueLabelPanel.svelte"; 6 + import PullDiscussion from "$lib/components/repo/pulls/PullDiscussion.svelte"; 7 + import PullInfoBar from "$lib/components/repo/pulls/PullInfoBar.svelte"; 8 + import TabPanel from "$lib/components/ui/TabPanel.svelte"; 9 + 10 + let { data } = $props(); 11 + 12 + let pull = $derived(data.pull); 13 + </script> 14 + 15 + <svelte:head> 16 + <title 17 + >{pull.title} &middot; Pull Request #{pull.rkey} &middot; {data.repo.ownerHandle}/{data.repo 18 + .name} &middot; Tangled</title 19 + > 20 + </svelte:head> 21 + 22 + <!-- 23 + unlike every other repo page this one is laid out by the page, not the layout: 24 + the discussion rail runs the full page height beside the repo chrome, and the 25 + diff below needs the whole viewport width, so only the conversation half stays 26 + inside the reading column 27 + --> 28 + <div class="flex flex-col gap-4 lg:flex-row lg:gap-2"> 29 + <div class="min-w-0 flex-1"> 30 + <!-- max-w-5xl is the layout's max-w-screen-lg, spelled the way the linter wants --> 31 + <div class="mx-auto max-w-5xl"> 32 + <RepoHeader repo={data.repo} counts={data.counts} viewerStarRkey={data.viewerStarRkey} /> 33 + <RepoTabs repo={data.repo} counts={data.counts} active="pulls" /> 34 + 35 + <div class="grid grid-cols-1 gap-4 lg:grid-cols-[4fr_1fr]"> 36 + <div class="min-w-0"> 37 + <TabPanel> 38 + <IssueHeader title={pull.title} rkey={pull.rkey} /> 39 + <PullInfoBar 40 + state={pull.state} 41 + authorHandle={pull.author.handle} 42 + authorDid={pull.author.did} 43 + ownerHandle={data.repo.ownerHandle} 44 + repoName={data.repo.name} 45 + targetBranch={pull.targetBranch} 46 + /> 47 + <!-- todo: body markup and reactions once the loader returns them --> 48 + <article class="mt-4 typography-paragraph-regular text-foreground-subtle italic"> 49 + No description provided 50 + </article> 51 + </TabPanel> 52 + </div> 53 + 54 + <!-- todo: participants, subscribe button, backlinks, external links --> 55 + <div class="min-w-0 lg:col-start-2 lg:row-span-2 lg:row-start-1"> 56 + <IssueLabelPanel /> 57 + </div> 58 + 59 + <div class="lg:col-start-1 lg:row-start-2"> 60 + <h2 class="mb-2 typography-heading-4">Commits</h2> 61 + <!-- todo: commit list, needs the base..head log in the loader --> 62 + </div> 63 + </div> 64 + </div> 65 + 66 + <!-- todo: DiffView. full-bleed on purpose, it is the reason this page places 67 + its own repo header instead of taking the layout's --> 68 + <div class="mt-4 min-h-[50vh]"></div> 69 + </div> 70 + 71 + <aside class="lg:sticky lg:top-0 lg:max-h-screen lg:w-[25vw] lg:shrink-0"> 72 + <PullDiscussion 73 + state={pull.state} 74 + versions={pull.versions} 75 + authorHandle={pull.author.handle} 76 + authorDid={pull.author.did} 77 + /> 78 + </aside> 79 + </div>
+49
web/src/routes/[handle]/[repo]/pulls/[aturi]/+page.ts
··· 1 + import { error } from "@sveltejs/kit"; 2 + import type { PullState } from "$lib/components/repo/pulls/PullStatePill.svelte"; 3 + import type { PageLoad } from "./$types"; 4 + 5 + export const load: PageLoad = async (event) => { 6 + const uri = decodeURIComponent(event.params.aturi); 7 + 8 + if (!uri.startsWith("at://")) error(404, "Pull request not found"); 9 + 10 + return { 11 + pull: { 12 + // $type: "patchView#pullViewDetailed", 13 + uri: "at://did:plc:bob/sh.tangled.pull/123", 14 + cid: "thisiscid", 15 + rkey: "123", 16 + repo: { 17 + did: "did:plc:gitrepo", 18 + owner: { 19 + // $type: "sh.tangled.actor.defs#profileViewBasic" 20 + did: "did:plc:alice", 21 + handle: "alice.pds.tngl.boltless.dev" 22 + }, 23 + slug: "core", 24 + createdAt: "" 25 + }, 26 + author: { 27 + did: "did:plc:bob", 28 + handle: "bob.pds.tngl.boltless.dev" 29 + }, 30 + title: "ref based PR", 31 + state: "open" as PullState, 32 + targetBranch: "master", 33 + versions: [ 34 + { 35 + // $type: "#patchView", 36 + base: "01117fddd3502c57bc20a65a75a66a4900d5d67d", 37 + head: "9a925efef6a0e6dfcd2d4317b4a1eee8752928b8", 38 + comments: [] 39 + }, 40 + { 41 + // $type: "#patchView", 42 + base: "4ce2be789de9ba1b9a39de3c310ae9fa35fef5f7", 43 + head: "9f7d5e39d69b030454e520f28f83afc28189ed23", 44 + comments: [] 45 + } 46 + ] 47 + } 48 + }; 49 + };