Files
TypstDrive/src/routes/packages/+page.svelte
T

107 lines
4.2 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import Icon from '@iconify/svelte';
import Navbar from '$lib/components/dashboard/Navbar.svelte';
interface Package {
id: string;
name: string;
description?: string;
owner_name?: string;
latest_version?: string;
}
let packages = $state<Package[]>([]);
let loading = $state(true);
let copied = $state('');
async function load() {
loading = true;
const res = await fetch('/api/packages');
packages = res.ok ? await res.json() : [];
loading = false;
}
function importSnippet(pkg: Package): string {
return `#import "@typstdrive/${pkg.name}:${pkg.latest_version ?? '0.1.0'}": *`;
}
async function copy(pkg: Package) {
await navigator.clipboard.writeText(importSnippet(pkg));
copied = pkg.id;
setTimeout(() => (copied = ''), 2000);
}
async function remove(pkg: Package) {
if (!confirm(`Delete package "${pkg.name}" and all its versions?`)) return;
const res = await fetch(`/api/packages/${pkg.name}`, { method: 'DELETE' });
if (res.ok) packages = packages.filter((p) => p.id !== pkg.id);
}
onMount(load);
</script>
<svelte:head>
<title>Packages - TypstDrive</title>
</svelte:head>
<div class="min-h-screen bg-[var(--color-surface-muted)]">
<Navbar />
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-6">
<button onclick={() => goto('/dashboard')} class="text-sm font-medium text-[var(--color-ink-muted)] hover:text-[var(--color-ink)] transition-colors mb-2 flex items-center gap-1.5">
<Icon icon="mdi:arrow-left" class="text-lg" />
Back to Dashboard
</button>
<h2 class="text-2xl font-bold text-[var(--color-ink)] flex items-center gap-2">
<Icon icon="mdi:package-variant-closed" class="text-purple-500" />
Packages
</h2>
<p class="text-sm text-[var(--color-ink-muted)] mt-1">
Instance-local Typst packages, published from Projects and importable as
<code class="font-mono text-xs bg-[var(--color-surface-sunken)] px-1.5 py-0.5 rounded">@typstdrive/&lt;name&gt;:&lt;version&gt;</code>.
</p>
</div>
{#if loading}
<p class="text-[var(--color-ink-muted)]">Loading…</p>
{:else if packages.length === 0}
<div class="text-center py-16 text-[var(--color-ink-muted)]">
<Icon icon="mdi:package-variant" class="text-5xl mx-auto mb-3 opacity-50" />
<p>No packages published yet. Open a Project and use “Publish” to create one.</p>
</div>
{:else}
<div class="space-y-3">
{#each packages as pkg (pkg.id)}
<div class="bg-[var(--color-surface)] rounded-xl border border-[var(--color-line)] p-4 flex items-start justify-between gap-4">
<div class="min-w-0">
<div class="flex items-center gap-2">
<p class="font-semibold text-[var(--color-ink)] truncate">@typstdrive/{pkg.name}</p>
{#if pkg.latest_version}
<span class="text-xs font-mono bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 px-1.5 py-0.5 rounded">v{pkg.latest_version}</span>
{/if}
</div>
{#if pkg.description}
<p class="text-sm text-[var(--color-ink-muted)] mt-1 truncate">{pkg.description}</p>
{/if}
<p class="text-xs text-[var(--color-ink-muted)] mt-1">by {pkg.owner_name ?? 'unknown'}</p>
<pre class="mt-2 text-xs font-mono bg-[var(--color-surface-muted)] border border-[var(--color-line)] rounded px-2 py-1 overflow-x-auto">{importSnippet(pkg)}</pre>
</div>
<div class="flex flex-col items-end gap-2 flex-shrink-0">
<button onclick={() => copy(pkg)} class="text-xs px-2 py-1 rounded-md bg-[var(--color-surface-muted)] hover:bg-[var(--color-surface-sunken)] flex items-center gap-1">
<Icon icon={copied === pkg.id ? 'mdi:check' : 'mdi:content-copy'} class="text-sm" />
{copied === pkg.id ? 'Copied' : 'Copy'}
</button>
<button onclick={() => remove(pkg)} title="Delete" class="text-xs px-2 py-1 rounded-md text-[var(--color-ink-muted)] hover:text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10 flex items-center gap-1">
<Icon icon="mdi:trash-can-outline" class="text-sm" /> Delete
</button>
</div>
</div>
{/each}
</div>
{/if}
</div>
</div>