This repository has no description
0

Configure Feed

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

wip: repo issues slop

Signed-off-by: oppiliappan <me@oppi.li>

author
oppiliappan
date (Jul 27, 2026, 12:53 PM +0100) commit 9146cf13 parent d391344e change-id vkzzmkqz
+219
+45
web/src/lib/components/repo/issues/IssueCard.svelte
··· 1 + <script lang="ts"> 2 + import { resolve } from "$app/paths"; 3 + import User from "$lib/components/ui/User.svelte"; 4 + import TimeAgo from "$lib/components/ui/TimeAgo.svelte"; 5 + import IssueStatePill from "./IssueStatePill.svelte"; 6 + import type { IssueSummary } from "$lib/components/repo/types"; 7 + 8 + interface Props { 9 + ownerHandle: string; 10 + repoName: string; 11 + issue: IssueSummary; 12 + } 13 + 14 + let { ownerHandle, repoName, issue }: Props = $props(); 15 + 16 + const href = $derived(resolve(`/${ownerHandle}/${repoName}/issues/${issue.id}` as "/")); 17 + </script> 18 + 19 + <div class="rounded border border-border-default bg-background-default px-6 py-4 shadow-sm"> 20 + <div class="pb-2"> 21 + <a {href} class="text-foreground-default no-underline hover:underline"> 22 + {issue.title} 23 + <span class="text-foreground-subtle">#{issue.id}</span> 24 + </a> 25 + </div> 26 + 27 + <div class="flex flex-wrap items-center gap-x-1 gap-y-1.5 text-sm text-foreground-subtle"> 28 + <IssueStatePill state={issue.state} /> 29 + 30 + <span class="ml-1 flex items-center gap-1"> 31 + <User handle={issue.authorHandle} did={issue.authorDid} size="sm" /> 32 + </span> 33 + 34 + <span class="before:mr-1 before:content-['·']"> 35 + <TimeAgo value={issue.createdAt} /> 36 + </span> 37 + 38 + <span class="before:mr-1 before:content-['·']"> 39 + <a {href} class="text-foreground-subtle no-underline hover:underline"> 40 + {issue.commentCount} 41 + {issue.commentCount === 1 ? "comment" : "comments"} 42 + </a> 43 + </span> 44 + </div> 45 + </div>
+23
web/src/lib/components/repo/issues/IssueList.svelte
··· 1 + <script lang="ts"> 2 + import EmptyState from "$lib/components/ui/EmptyState.svelte"; 3 + import IssueCard from "./IssueCard.svelte"; 4 + import type { IssueSummary } from "$lib/components/repo/types"; 5 + 6 + interface Props { 7 + ownerHandle: string; 8 + repoName: string; 9 + issues: IssueSummary[]; 10 + } 11 + 12 + let { ownerHandle, repoName, issues }: Props = $props(); 13 + </script> 14 + 15 + {#if issues.length === 0} 16 + <EmptyState message="No issues found" /> 17 + {:else} 18 + <div class="flex flex-col gap-2"> 19 + {#each issues as issue (issue.id)} 20 + <IssueCard {ownerHandle} {repoName} {issue} /> 21 + {/each} 22 + </div> 23 + {/if}
+38
web/src/lib/components/repo/issues/IssueStatePill.svelte
··· 1 + <script module lang="ts"> 2 + import { tv, type VariantProps } from "tailwind-variants"; 3 + 4 + export const issueStatePill = tv({ 5 + base: "inline-flex items-center gap-1.5 rounded px-2 py-[5px] text-sm text-foreground-on-emphasis select-none", 6 + variants: { 7 + state: { 8 + open: "bg-background-success-emphasis", 9 + closed: "bg-foreground-muted" 10 + } 11 + }, 12 + defaultVariants: { 13 + state: "open" 14 + } 15 + }); 16 + 17 + export type IssueStatePillVariants = VariantProps<typeof issueStatePill>; 18 + </script> 19 + 20 + <script lang="ts"> 21 + import CircleDot from "$icon/circle-dot"; 22 + import CircleX from "$icon/circle-x"; 23 + 24 + interface Props { 25 + state: IssueStatePillVariants["state"]; 26 + class?: string; 27 + } 28 + 29 + let { state = "open", class: className }: Props = $props(); 30 + 31 + const Icon = $derived(state === "closed" ? CircleX : CircleDot); 32 + const label = $derived(state === "closed" ? "Closed" : "Open"); 33 + </script> 34 + 35 + <span class={issueStatePill({ state, class: className })}> 36 + <Icon class="size-3 shrink-0" aria-hidden="true" /> 37 + {label} 38 + </span>
+65
web/src/lib/components/repo/issues/IssueToolbar.svelte
··· 1 + <script lang="ts"> 2 + import { resolve } from "$app/paths"; 3 + import CircleDot from "$icon/circle-dot"; 4 + import CircleX from "$icon/circle-x"; 5 + import Plus from "$icon/plus"; 6 + import Search from "$icon/search"; 7 + import Button from "$lib/components/ui/Button.svelte"; 8 + import ButtonGroup from "$lib/components/ui/ButtonGroup.svelte"; 9 + 10 + interface Props { 11 + ownerHandle: string; 12 + repoName: string; 13 + state: "open" | "closed"; 14 + openCount: number; 15 + closedCount: number; 16 + } 17 + 18 + let { ownerHandle, repoName, state, openCount, closedCount }: Props = $props(); 19 + 20 + const base = $derived(`/${ownerHandle}/${repoName}/issues`); 21 + </script> 22 + 23 + <div class="grid grid-cols-1 gap-2 sm:grid-cols-[1fr_auto_auto]"> 24 + <form class="flex" method="GET"> 25 + <!-- TODO: wire up search query submission --> 26 + <div class="relative flex w-full items-center"> 27 + <Search 28 + class="pointer-events-none absolute left-2 size-4 text-foreground-placeholder" 29 + aria-hidden="true" 30 + /> 31 + <input 32 + class="h-full w-full rounded border border-border-default bg-background-default py-1.5 pr-2 pl-8 text-sm outline-none focus:border-border-strong" 33 + type="text" 34 + name="q" 35 + placeholder="Search issues..." 36 + aria-label="Search issues" 37 + /> 38 + </div> 39 + </form> 40 + 41 + <ButtonGroup> 42 + <!-- TODO: wire up state filter navigation --> 43 + <Button 44 + href={resolve(`${base}?state=open` as "/")} 45 + variant={state === "open" ? "default" : "ghost"} 46 + size="sm" 47 + icon={CircleDot} 48 + > 49 + <span class="hidden sm:inline">{openCount}</span> 50 + Open 51 + </Button> 52 + <Button 53 + href={resolve(`${base}?state=closed` as "/")} 54 + variant={state === "closed" ? "default" : "ghost"} 55 + size="sm" 56 + icon={CircleX} 57 + > 58 + <span class="hidden sm:inline">{closedCount}</span> 59 + Closed 60 + </Button> 61 + </ButtonGroup> 62 + 63 + <!-- TODO: point at the new issue route once it exists --> 64 + <Button href={resolve(`${base}/new` as "/")} variant="primary" size="sm" icon={Plus}>New</Button> 65 + </div>
+10
web/src/lib/components/repo/types.ts
··· 38 38 percentage: number; 39 39 share: number; 40 40 } 41 + 42 + export interface IssueSummary { 43 + id: number; 44 + title: string; 45 + state: "open" | "closed"; 46 + authorHandle: string; 47 + authorDid: string; 48 + createdAt: string; 49 + commentCount: number; 50 + }
+20
web/src/routes/[handle]/[repo]/issues/+page.svelte
··· 1 + <script lang="ts"> 2 + import IssueList from "$lib/components/repo/issues/IssueList.svelte"; 3 + import IssueToolbar from "$lib/components/repo/issues/IssueToolbar.svelte"; 4 + 5 + let { data } = $props(); 6 + </script> 7 + 8 + <section class="mt-2 rounded bg-background-default px-6 py-4 text-foreground-default shadow-sm"> 9 + <IssueToolbar 10 + ownerHandle={data.repo.ownerHandle} 11 + repoName={data.repo.name} 12 + state={data.state} 13 + openCount={data.openCount} 14 + closedCount={data.closedCount} 15 + /> 16 + </section> 17 + 18 + <div class="mt-2"> 19 + <IssueList ownerHandle={data.repo.ownerHandle} repoName={data.repo.name} issues={data.issues} /> 20 + </div>
+18
web/src/routes/[handle]/[repo]/issues/+page.ts
··· 1 + import type { IssueSummary } from "$lib/components/repo/types"; 2 + import type { PageLoad } from "./$types"; 3 + 4 + export const load: PageLoad = async (event) => { 5 + const parent = await event.parent(); 6 + const state = event.url.searchParams.get("state") === "closed" ? "closed" : "open"; 7 + 8 + // TODO: fetch via sh.tangled.repo.listIssues and resolve author handles 9 + const issues: IssueSummary[] = []; 10 + const closedCount = 0; 11 + 12 + return { 13 + state: state as "open" | "closed", 14 + issues, 15 + openCount: parent.counts.issues, 16 + closedCount 17 + }; 18 + };