107 lines
4.2 KiB
Svelte
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/<name>:<version></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>
|