<script lang="ts">
import type { GitHubGist } from "../../api/types.js";
import { contextMenu as placeMenu } from "../../utils/menu.js";
import { copyToClipboard } from "../../utils/clipboard.js";
interface Props {
gists: GitHubGist[];
loading: boolean;
selectedId: string | null;
onselect: (id: string) => void;
}
let {
gists,
loading,
selectedId,
onselect,
}: Props = $props();
let contextMenu: { x: number; y: number; gist: GitHubGist } | null = $state(null);
function portal(node: HTMLElement) {
document.body.appendChild(node);
return { destroy() { node.remove(); } };
}
function handleGistContextMenu(e: MouseEvent, gist: GitHubGist) {
e.preventDefault();
contextMenu = { x: e.clientX, y: e.clientY, gist };
}
function closeContextMenu() { contextMenu = null; }
function gistOpenInBrowser() {
if (!contextMenu) return;
window.open(`https://gist.github.com/${contextMenu.gist.id}`, "_blank");
closeContextMenu();
}
function gistCopyLink() {
if (!contextMenu) return;
void copyToClipboard(`https://gist.github.com/${contextMenu.gist.id}`).catch(() => {});
closeContextMenu();
}
function gistCopyId() {
if (!contextMenu) return;
void copyToClipboard(contextMenu.gist.id).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 fileCount(gist: GitHubGist): number {
return Object.keys(gist.files ?? {}).length;
}
function ownerName(gist: GitHubGist): string {
return gist.owner?.login ?? "unknown";
}
</script>
<div class="gist-list">
{#if loading && gists.length === 0}
<div class="skel-list">
{#each { length: 5 } as _, i}
<div class="skel-gist-item">
<div class="skel" style="width: {50 + ((i * 23) % 40)}%; height: var(--wb-control-size-11);"></div>
<div class="skel" style="width: {30 + ((i * 19) % 50)}%; height: var(--wb-control-size-8); margin-top: var(--wb-spacing-6);"></div>
</div>
{/each}
</div>
{:else if gists.length === 0}
<div class="gist-list-empty">No published gists yet.</div>
{:else}
<div class="gist-list-items">
{#each gists as gist}
<button
class="gist-item"
class:active={selectedId === gist.id}
onclick={() => onselect(gist.id)}
oncontextmenu={(e) => handleGistContextMenu(e, gist)}
>
<div class="gist-item-main">
<div class="gist-item-title">
{gist.description?.trim() || "Untitled gist"}
</div>
<div class="gist-item-meta">
<span>{ownerName(gist)}</span>
<span>{fileCount(gist)} files</span>
<span>{gist.public ? "Public" : "Secret"}</span>
</div>
</div>
<time class="gist-item-time" datetime={gist.updated_at}>
{new Date(gist.updated_at).toLocaleString()}
</time>
</button>
{/each}
</div>
{/if}
</div>
{#if contextMenu}
<div
class="context-menu"
use:placeMenu={contextMenu}
>
<button class="context-menu-item" onclick={gistOpenInBrowser}>Open in Browser</button>
<div class="context-menu-separator"></div>
<button class="context-menu-item" onclick={gistCopyLink}>Copy Link</button>
<button class="context-menu-item" onclick={gistCopyId}>Copy Gist ID</button>
</div>
{/if}
<style>
.gist-list {
height: 100%;
display: flex;
flex-direction: column;
background: var(--vscode-sideBar-background);
}
.gist-list-items {
overflow-y: auto;
}
.gist-item {
width: 100%;
display: flex;
flex-direction: column;
gap: var(--wb-spacing-6);
text-align: left;
padding: var(--wb-spacing-14) var(--wb-spacing-16);
border-bottom: 1px solid var(--vscode-panel-border, var(--vscode-panel-border));
background: transparent;
cursor: pointer;
}
.gist-item:hover,
.gist-item.active {
background: color-mix(in srgb, var(--vscode-focusBorder) 8%, transparent);
}
.gist-item-title {
font-size: var(--wb-font-size-13);
font-weight: 600;
color: var(--vscode-editor-foreground);
}
.gist-item-meta {
display: flex;
gap: var(--wb-spacing-10);
flex-wrap: wrap;
font-size: var(--wb-font-size-11);
color: var(--vscode-disabledForeground);
}
.gist-item-time {
font-size: var(--wb-font-size-11);
color: var(--vscode-disabledForeground);
}
.gist-list-empty {
padding: var(--wb-spacing-24) var(--wb-spacing-16);
color: var(--vscode-disabledForeground);
font-size: var(--wb-font-size-12);
}
.skel-list {
display: flex;
flex-direction: column;
}
.skel-gist-item {
padding: var(--wb-spacing-14) var(--wb-spacing-16);
border-bottom: 1px solid var(--vscode-panel-border);
}
</style>