mirror of
https://github.com/pesde-pkg/pesde.git
synced 2025-05-04 10:33:47 +01:00
feat(website): add package metadata
This commit is contained in:
parent
56fa47afce
commit
5fb0a03297
5 changed files with 183 additions and 64 deletions
|
@ -15,7 +15,7 @@ export type PackageResponse = {
|
||||||
targets: TargetInfo[]
|
targets: TargetInfo[]
|
||||||
description: string
|
description: string
|
||||||
published_at: string
|
published_at: string
|
||||||
license: string
|
license?: string
|
||||||
authors?: string[]
|
authors?: string[]
|
||||||
repository?: string
|
repository?: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,11 @@ const fetchPackage = async (fetcher: typeof fetch, options: FetchPackageOptions)
|
||||||
export const load: LayoutServerLoad = async ({ params }) => {
|
export const load: LayoutServerLoad = async ({ params }) => {
|
||||||
const { scope, name, version, target } = params
|
const { scope, name, version, target } = params
|
||||||
|
|
||||||
const options = version ? { scope, name, version, target } : { scope, name }
|
if (version !== undefined && target === undefined) {
|
||||||
|
error(404, "Not Found")
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = version !== undefined ? { scope, name, version, target } : { scope, name }
|
||||||
|
|
||||||
const pkg = await fetchPackage(fetch, options)
|
const pkg = await fetchPackage(fetch, options)
|
||||||
|
|
||||||
|
|
|
@ -1,34 +1,50 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { formatDistanceToNow } from "date-fns"
|
import { formatDistanceToNow } from "date-fns"
|
||||||
import { Check, ChevronDownIcon, Clipboard } from "lucide-svelte"
|
import { BinaryIcon, Icon, LibraryIcon } from "lucide-svelte"
|
||||||
import Tab from "./Tab.svelte"
|
import Tab from "./Tab.svelte"
|
||||||
import { page } from "$app/stores"
|
import { page } from "$app/stores"
|
||||||
import { goto } from "$app/navigation"
|
import type { TargetInfo } from "$lib/registry-api"
|
||||||
|
import type { ComponentType } from "svelte"
|
||||||
|
import Command from "./Command.svelte"
|
||||||
|
import TargetSelector from "./TargetSelector.svelte"
|
||||||
|
|
||||||
let { children, data } = $props()
|
let { children, data } = $props()
|
||||||
|
|
||||||
let didCopy = $state(false)
|
const [scope, name] = $derived(data.pkg.name.split("/"))
|
||||||
|
|
||||||
const [scope, name] = data.pkg.name.split("/")
|
const installCommand = $derived(`pesde add ${data.pkg.name}`)
|
||||||
|
const xCommand = $derived(`pesde x ${data.pkg.name}`)
|
||||||
const installCommand = `pesde add ${data.pkg.name}`
|
|
||||||
|
|
||||||
const defaultTarget = $derived(
|
const defaultTarget = $derived(
|
||||||
"target" in $page.params ? $page.params.target : data.pkg.targets[0].kind,
|
"target" in $page.params ? $page.params.target : data.pkg.targets[0].kind,
|
||||||
)
|
)
|
||||||
|
const currentTarget = $derived(data.pkg.targets.find((target) => target.kind === defaultTarget))
|
||||||
|
|
||||||
const basePath = $derived.by(() => {
|
const repositoryUrl = $derived(
|
||||||
const { scope, name } = $page.params
|
data.pkg.repository !== undefined ? new URL(data.pkg.repository) : undefined,
|
||||||
if ("target" in $page.params) {
|
)
|
||||||
const { version } = $page.params
|
const isGithub = $derived(repositoryUrl?.hostname === "github.com")
|
||||||
return `/packages/${scope}/${name}/${version}`
|
const githubRepo = $derived(
|
||||||
}
|
repositoryUrl?.pathname
|
||||||
return `/packages/${scope}/${name}/latest`
|
.split("/")
|
||||||
})
|
.slice(1, 3)
|
||||||
|
.join("/")
|
||||||
|
.replace(/\.git$/, ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
const exportNames: Partial<Record<keyof TargetInfo, string>> = {
|
||||||
|
lib: "Library",
|
||||||
|
bin: "Binary",
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportIcons: Partial<Record<keyof TargetInfo, ComponentType<Icon>>> = {
|
||||||
|
lib: LibraryIcon,
|
||||||
|
bin: BinaryIcon,
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto flex max-w-screen-lg px-4 py-16">
|
<div class="mx-auto flex max-w-prose flex-col px-4 py-16 lg:max-w-screen-lg lg:flex-row">
|
||||||
<div class="flex-grow pr-4">
|
<div class="flex-grow lg:pr-4">
|
||||||
<h1 class="text-3xl font-bold">
|
<h1 class="text-3xl font-bold">
|
||||||
<span class="text-heading">{scope}/</span><span class="text-light">{name}</span>
|
<span class="text-heading">{scope}/</span><span class="text-light">{name}</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -45,6 +61,10 @@
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-6 max-w-prose">{data.pkg.description}</p>
|
<p class="mb-6 max-w-prose">{data.pkg.description}</p>
|
||||||
|
|
||||||
|
<div class="mb-12 lg:hidden">
|
||||||
|
<TargetSelector id="target-selector-sidebar" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<nav class="flex w-full border-b-2">
|
<nav class="flex w-full border-b-2">
|
||||||
<Tab tab="">Readme</Tab>
|
<Tab tab="">Readme</Tab>
|
||||||
<Tab tab="versions">Versions</Tab>
|
<Tab tab="versions">Versions</Tab>
|
||||||
|
@ -52,56 +72,69 @@
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
<aside class="ml-auto w-full max-w-[22rem] flex-shrink-0 border-l pl-4">
|
<aside
|
||||||
|
class="w-full flex-shrink-0 border-t pt-16 lg:ml-auto lg:max-w-[22rem] lg:border-l lg:border-t-0 lg:pl-4 lg:pt-0"
|
||||||
|
>
|
||||||
<h2 class="mb-1 text-lg font-semibold text-heading">Install</h2>
|
<h2 class="mb-1 text-lg font-semibold text-heading">Install</h2>
|
||||||
<div class="mb-4 flex h-11 items-center overflow-hidden rounded border text-sm">
|
<Command command={installCommand} class="mb-4" />
|
||||||
<code class="truncate px-4">{installCommand}</code>
|
|
||||||
<button
|
|
||||||
class="ml-auto flex size-11 items-center justify-center border-l bg-card/40 hover:bg-card/60"
|
|
||||||
onclick={() => {
|
|
||||||
navigator.clipboard.writeText(installCommand)
|
|
||||||
|
|
||||||
if (didCopy) return
|
<div class="hidden lg:block">
|
||||||
|
<TargetSelector id="target-selector-sidebar" />
|
||||||
didCopy = true
|
|
||||||
setTimeout(() => {
|
|
||||||
didCopy = false
|
|
||||||
}, 1000)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#if didCopy}
|
|
||||||
<Check class="size-5" />
|
|
||||||
{:else}
|
|
||||||
<Clipboard class="size-5" />
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="mb-1 text-lg font-semibold text-heading">
|
{#if data.pkg.license !== undefined}
|
||||||
<label for="target-select">Target</label>
|
<h2 class="mb-1 text-lg font-semibold text-heading">License</h2>
|
||||||
</h2>
|
<div class="mb-6">{data.pkg.license}</div>
|
||||||
<div
|
{/if}
|
||||||
class="relative flex h-11 w-full items-center rounded border border-input-border bg-input-bg ring-0 ring-primary-bg/20 transition focus-within:border-primary focus-within:ring-4 has-[:disabled]:opacity-50"
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
class="absolute inset-0 appearance-none bg-transparent px-4 outline-none"
|
|
||||||
id="target-select"
|
|
||||||
onchange={(e) => {
|
|
||||||
const select = e.currentTarget
|
|
||||||
|
|
||||||
select.disabled = true
|
{#if data.pkg.repository !== undefined}
|
||||||
goto(`${basePath}/${e.currentTarget.value}`).finally(() => {
|
<h2 class="mb-1 text-lg font-semibold text-heading">Repository</h2>
|
||||||
select.disabled = false
|
<div class="mb-6">
|
||||||
})
|
<a
|
||||||
}}
|
href={data.pkg.repository}
|
||||||
>
|
class="inline-flex items-center space-x-2 underline"
|
||||||
{#each data.pkg.targets as target}
|
target="_blank"
|
||||||
<option value={target.kind} class="bg-card" selected={target.kind === defaultTarget}>
|
rel="noreferrer noopener"
|
||||||
{target.kind[0].toUpperCase() + target.kind.slice(1)}
|
>
|
||||||
</option>
|
{#if isGithub}
|
||||||
{/each}
|
<svg
|
||||||
</select>
|
class="size-5 text-primary"
|
||||||
<ChevronDownIcon class="pointer-events-none absolute right-4 h-5 w-5" />
|
role="img"
|
||||||
</div>
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<title>GitHub</title>
|
||||||
|
<path
|
||||||
|
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
{githubRepo}
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
{data.pkg.repository}
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<h2 class="mb-1 text-lg font-semibold text-heading">Exports</h2>
|
||||||
|
<ul class="mb-6 space-y-0.5">
|
||||||
|
{#each Object.entries(exportNames).filter(([key]) => !!currentTarget?.[key as keyof TargetInfo]) as [exportKey, exportName]}
|
||||||
|
{@const Icon = exportIcons[exportKey as keyof TargetInfo]}
|
||||||
|
<li class="flex items-center">
|
||||||
|
<Icon class="mr-2 size-5 text-primary" />
|
||||||
|
{exportName}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{#if currentTarget?.bin}
|
||||||
|
<p class="-mt-3 mb-4 text-sm text-body/80">
|
||||||
|
This package provides a binary that can be executed after installation, or globally via:
|
||||||
|
</p>
|
||||||
|
<Command command={xCommand} class="mb-6" />
|
||||||
|
{/if}
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Check, Clipboard } from "lucide-svelte"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
command: string
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { command, class: classname = "" }: Props = $props()
|
||||||
|
|
||||||
|
let didCopy = $state(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={`flex h-11 items-center overflow-hidden rounded border text-sm ${classname}`}>
|
||||||
|
<code class="truncate px-4">{command}</code>
|
||||||
|
<button
|
||||||
|
class="ml-auto flex size-11 items-center justify-center border-l bg-card/40 hover:bg-card/60"
|
||||||
|
onclick={() => {
|
||||||
|
navigator.clipboard.writeText(command)
|
||||||
|
|
||||||
|
if (didCopy) return
|
||||||
|
|
||||||
|
didCopy = true
|
||||||
|
setTimeout(() => {
|
||||||
|
didCopy = false
|
||||||
|
}, 1000)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if didCopy}
|
||||||
|
<Check class="size-5" />
|
||||||
|
{:else}
|
||||||
|
<Clipboard class="size-5" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
|
@ -0,0 +1,47 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation"
|
||||||
|
import { page } from "$app/stores"
|
||||||
|
import { ChevronDownIcon } from "lucide-svelte"
|
||||||
|
|
||||||
|
const { id }: { id: string } = $props()
|
||||||
|
|
||||||
|
const defaultTarget = $derived(
|
||||||
|
"target" in $page.params ? $page.params.target : $page.data.pkg.targets[0].kind,
|
||||||
|
)
|
||||||
|
|
||||||
|
const basePath = $derived.by(() => {
|
||||||
|
const { scope, name } = $page.params
|
||||||
|
if ("target" in $page.params) {
|
||||||
|
const { version } = $page.params
|
||||||
|
return `/packages/${scope}/${name}/${version}`
|
||||||
|
}
|
||||||
|
return `/packages/${scope}/${name}/latest`
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-1 text-lg font-semibold text-heading">
|
||||||
|
<label for={id}>Target</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="relative mb-6 flex h-11 w-full items-center rounded border border-input-border bg-input-bg ring-0 ring-primary-bg/20 transition focus-within:border-primary focus-within:ring-4 has-[:disabled]:opacity-50"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
class="absolute inset-0 appearance-none bg-transparent px-4 outline-none"
|
||||||
|
{id}
|
||||||
|
onchange={(e) => {
|
||||||
|
const select = e.currentTarget
|
||||||
|
|
||||||
|
select.disabled = true
|
||||||
|
goto(`${basePath}/${e.currentTarget.value}`).finally(() => {
|
||||||
|
select.disabled = false
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#each $page.data.pkg.targets as target}
|
||||||
|
<option value={target.kind} class="bg-card" selected={target.kind === defaultTarget}>
|
||||||
|
{target.kind[0].toUpperCase() + target.kind.slice(1)}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<ChevronDownIcon class="pointer-events-none absolute right-4 h-5 w-5" />
|
||||||
|
</div>
|
Loading…
Add table
Reference in a new issue