<script lang="ts">
import type { GitHubIssue } from "../../api/types/issues.js";
import { contextMenu as placeMenu } from "../../utils/menu.js";
import { copyToClipboard } from "../../utils/clipboard.js";
import AppIcon from "../layout/AppIcon.svelte";
let {
issue,
selected = false,
onclick,
}: {
issue: GitHubIssue;
selected?: boolean;
onclick: () => void;
} = $props();
let contextMenu: { x: number; y: number } | null = $state(null);
function portal(node: HTMLElement) {
document.body.appendChild(node);
return { destroy() { node.remove(); } };
}
function handleContextMenu(e: MouseEvent) {
e.preventDefault();
contextMenu = { x: e.clientX, y: e.clientY };
}
function closeContextMenu() { contextMenu = null; }
function openInBrowser() {
const [owner, repo] = issue.repo.split("/");
window.open(`https://github.com/${owner}/${repo}/issues/${issue.number}`, "_blank");
closeContextMenu();
}
function copyLink() {
const [owner, repo] = issue.repo.split("/");
void copyToClipboard(`https://github.com/${owner}/${repo}/issues/${issue.number}`).catch(() => {});
closeContextMenu();
}
function copyNumber() {
void copyToClipboard(`${issue.repo}#${issue.number}`).catch(() => {});
closeContextMenu();
}
function copyTitle() {
void copyToClipboard(issue.title).catch(() => {});
closeContextMenu();
}
$effect(() => {
if (!contextMenu) return;
function handler() { contextMenu = null; }
const id = setTimeout(() => {
document.addEventListener("click", handler, { once: true });
document.addEventListener("contextmenu", handler, { once: true });
}, 0);
return () => {
clearTimeout(id);
document.removeEventListener("click", handler);
document.removeEventListener("contextmenu", handler);
};
});
$effect(() => {
if (!contextMenu) return;
function handler(e: KeyboardEvent) { if (e.key === "Escape") contextMenu = null; }
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
});
function timeAgo(dateStr: string): string {
const now = Date.now();
const then = new Date(dateStr).getTime();
const diffMs = now - then;
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 60) return `${diffMins}m ago`;
const diffHrs = Math.floor(diffMins / 60);
if (diffHrs < 24) return `${diffHrs}h ago`;
const diffDays = Math.floor(diffHrs / 24);
if (diffDays < 30) return `${diffDays}d ago`;
const diffMonths = Math.floor(diffDays / 30);
return `${diffMonths}mo ago`;
}
</script>
<button
class="issue-item"
class:selected
{onclick}
oncontextmenu={handleContextMenu}
>
<div class="issue-top">
<span class="issue-state" class:open={issue.state === "open"} class:closed={issue.state === "closed"}>
{#if issue.state === "open"}
<AppIcon name="circle" size={10} />
{:else}
<AppIcon name="circle-check" size={10} />
{/if}
</span>
<span class="issue-title">{issue.title}</span>
</div>
<div class="issue-meta">
<span class="issue-repo">{issue.repo}</span>
<span class="issue-number">#{issue.number}</span>
<span class="issue-time">{timeAgo(issue.created_at)}</span>
{#if issue.comments > 0}
<span class="issue-comments">
<AppIcon name="message-circle" size={10} />
{issue.comments}
</span>
{/if}
</div>
{#if issue.labels.length > 0}
<div class="issue-labels">
{#each issue.labels as label}
<span
class="issue-label"
style:background="#{label.color}20"
style:color="#{label.color}"
style:border-color="#{label.color}40"
>
{label.name}
</span>
{/each}
</div>
{/if}
</button>
{#if contextMenu}
<div
class="context-menu"
use:placeMenu={contextMenu}
>
<button class="context-menu-item" onclick={openInBrowser}>Open in Browser</button>
<div class="context-menu-separator"></div>
<button class="context-menu-item" onclick={copyLink}>Copy Link</button>
<button class="context-menu-item" onclick={copyNumber}>Copy Reference</button>
<button class="context-menu-item" onclick={copyTitle}>Copy Title</button>
</div>
{/if}
<style>
.issue-item {
display: block;
width: 100%;
padding: var(--wb-spacing-10) var(--wb-spacing-14);
text-align: left;
border-bottom: 1px solid var(--vscode-widget-border);
cursor: pointer;
transition: background 0.08s;
}
.issue-item:hover {
background: var(--vscode-list-hoverBackground);
}
.issue-item.selected {
background: color-mix(in srgb, var(--vscode-focusBorder) 6%, transparent);
border-left: 2px solid var(--vscode-focusBorder);
padding-left: var(--wb-spacing-12);
}
.issue-top {
display: flex;
align-items: flex-start;
gap: var(--wb-spacing-6);
}
.issue-state {
flex-shrink: 0;
margin-top: var(--wb-spacing-2);
}
.issue-state.open {
color: var(--vscode-testing-iconPassed);
}
.issue-state.closed {
color: var(--vscode-textPreformat-foreground);
}
.issue-title {
font-size: var(--wb-font-size-13);
font-weight: 500;
color: var(--vscode-editor-foreground);
line-height: 1.3;
}
.issue-meta {
display: flex;
align-items: center;
gap: var(--wb-spacing-8);
margin-top: var(--wb-spacing-4);
padding-left: var(--wb-spacing-16);
font-size: var(--wb-font-size-11);
color: var(--vscode-disabledForeground);
}
.issue-repo {
font-family: var(--wsb-font-mono);
font-size: var(--wb-font-size-10);
}
.issue-number {
font-family: var(--wsb-font-mono);
}
.issue-comments {
display: flex;
align-items: center;
gap: var(--wb-spacing-3);
}
.issue-labels {
display: flex;
flex-wrap: wrap;
gap: var(--wb-spacing-4);
margin-top: var(--wb-spacing-6);
padding-left: var(--wb-spacing-16);
}
.issue-label {
font-size: var(--wb-font-size-10);
font-weight: 500;
padding: var(--wb-spacing-1) var(--wb-spacing-6);
border-radius: var(--wb-corner-radius-10);
border: 1px solid;
white-space: nowrap;
}
</style>