Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
303 changes: 167 additions & 136 deletions app/pages/[...package].vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,27 @@ definePageMeta({

const router = useRouter()

const header = useTemplateRef('header')
const isHeaderPinned = shallowRef(false)

function checkHeaderPosition() {
const el = header.value
if (!el) return

const style = getComputedStyle(el)
const top = parseFloat(style.top) || 0
const rect = el.getBoundingClientRect()

isHeaderPinned.value = Math.abs(rect.top - top) < 1
}

useEventListener('scroll', checkHeaderPosition, { passive: true })
useEventListener('resize', checkHeaderPosition)

onMounted(() => {
checkHeaderPosition()
})

const { packageName, requestedVersion, orgName } = usePackageRoute()
const selectedPM = useSelectedPackageManager()
const activePmId = computed(() => selectedPM.value ?? 'npm')
Expand Down Expand Up @@ -389,153 +410,157 @@ function handleClick(event: MouseEvent) {
</script>

<template>
<main class="container flex-1 w-full py-8 xl:py-12">
<main class="container flex-1 w-full py-8">
<PackageSkeleton v-if="status === 'pending'" />

<article v-else-if="status === 'success' && pkg" class="package-page">
<!-- Package header -->
<header class="area-header border-b border-border">
<div class="mb-4">
<!-- Package name and version -->
<div class="flex items-baseline gap-2 mb-1.5 sm:gap-3 sm:mb-2 flex-wrap min-w-0">
<h1
class="font-mono text-2xl sm:text-3xl font-medium min-w-0 break-words"
:title="pkg.name"
<header
class="area-header sticky top-14 z-1 bg-[--bg] py-2 border-border"
ref="header"
:class="{ 'border-b': isHeaderPinned }"
>
<!-- Package name and version -->
<div class="flex items-baseline gap-2 sm:gap-3 flex-wrap min-w-0">
<h1
class="font-mono text-2xl sm:text-3xl font-medium min-w-0 break-words"
:title="pkg.name"
>
<NuxtLink
v-if="orgName"
:to="{ name: 'org', params: { org: orgName } }"
class="text-fg-muted hover:text-fg transition-colors duration-200"
>@{{ orgName }}</NuxtLink
><span v-if="orgName">/</span>
<TooltipAnnounce :text="$t('common.copied')" :isVisible="copiedPkgName">
<button
@click="copyPkgName()"
aria-describedby="copy-pkg-name"
class="cursor-copy active:scale-95 transition-transform"
>
{{ orgName ? pkg.name.replace(`@${orgName}/`, '') : pkg.name }}
</button>
</TooltipAnnounce>
</h1>

<span id="copy-pkg-name" class="sr-only">{{ $t('package.copy_name') }}</span>
<span
v-if="displayVersion"
class="inline-flex items-baseline gap-1.5 font-mono text-base sm:text-lg text-fg-muted shrink-0"
>
<!-- Version resolution indicator (e.g., "latest → 4.2.0") -->
<template v-if="resolvedVersion !== requestedVersion">
<span class="font-mono text-fg-muted text-sm">{{ requestedVersion }}</span>
<span class="i-carbon:arrow-right rtl-flip w-3 h-3" aria-hidden="true" />
</template>

<NuxtLink
v-if="resolvedVersion !== requestedVersion"
:to="`/${pkg.name}/v/${displayVersion.version}`"
:title="$t('package.view_permalink')"
>{{ displayVersion.version }}</NuxtLink
>
<NuxtLink
v-if="orgName"
:to="{ name: 'org', params: { org: orgName } }"
class="text-fg-muted hover:text-fg transition-colors duration-200"
>@{{ orgName }}</NuxtLink
><span v-if="orgName">/</span>
<TooltipAnnounce :text="$t('common.copied')" :isVisible="copiedPkgName">
<button
@click="copyPkgName()"
aria-describedby="copy-pkg-name"
class="cursor-copy ms-1 mt-1 active:scale-95 transition-transform"
>
{{ orgName ? pkg.name.replace(`@${orgName}/`, '') : pkg.name }}
</button>
</TooltipAnnounce>
</h1>
<span v-else>v{{ displayVersion.version }}</span>

<span id="copy-pkg-name" class="sr-only">{{ $t('package.copy_name') }}</span>
<a
v-if="hasProvenance(displayVersion)"
:href="`https://www.npmjs.com/package/${pkg.name}/v/${displayVersion.version}#provenance`"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center justify-center gap-1.5 text-fg-muted hover:text-fg transition-colors duration-200 min-w-6 min-h-6"
:title="$t('package.verified_provenance')"
>
<span class="i-solar:shield-check-outline w-3.5 h-3.5 shrink-0" aria-hidden="true" />
</a>
<span
v-if="displayVersion"
class="inline-flex items-baseline gap-1.5 font-mono text-base sm:text-lg text-fg-muted shrink-0"
v-if="
requestedVersion &&
latestVersion &&
displayVersion.version !== latestVersion.version
"
class="text-fg-subtle text-sm shrink-0"
>{{ $t('package.not_latest') }}</span
>
<!-- Version resolution indicator (e.g., "latest → 4.2.0") -->
<template v-if="resolvedVersion !== requestedVersion">
<span class="font-mono text-fg-muted text-sm">{{ requestedVersion }}</span>
<span class="i-carbon:arrow-right rtl-flip w-3 h-3" aria-hidden="true" />
</template>
</span>

<NuxtLink
v-if="resolvedVersion !== requestedVersion"
:to="`/${pkg.name}/v/${displayVersion.version}`"
:title="$t('package.view_permalink')"
>{{ displayVersion.version }}</NuxtLink
>
<span v-else>v{{ displayVersion.version }}</span>

<a
v-if="hasProvenance(displayVersion)"
:href="`https://www.npmjs.com/package/${pkg.name}/v/${displayVersion.version}#provenance`"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center justify-center gap-1.5 text-fg-muted hover:text-fg transition-colors duration-200 min-w-6 min-h-6"
:title="$t('package.verified_provenance')"
>
<span
class="i-solar:shield-check-outline w-3.5 h-3.5 shrink-0"
aria-hidden="true"
/>
</a>
<span
v-if="
requestedVersion &&
latestVersion &&
displayVersion.version !== latestVersion.version
"
class="text-fg-subtle text-sm shrink-0"
>{{ $t('package.not_latest') }}</span
>
</span>

<!-- Package metrics (module format, types) -->
<ClientOnly>
<PackageMetricsBadges
v-if="displayVersion"
:package-name="pkg.name"
:version="displayVersion.version"
:is-binary="isBinaryOnly"
class="self-baseline ms-1 sm:ms-2"
/>
<template #fallback>
<ul class="flex items-center gap-1.5 self-baseline ms-1 sm:ms-2">
<li class="skeleton w-8 h-5 rounded" />
<li class="skeleton w-12 h-5 rounded" />
</ul>
</template>
</ClientOnly>

<!-- Internal navigation: Docs + Code + Compare (hidden on mobile, shown in external links instead) -->
<nav
<!-- Package metrics (module format, types) -->
<ClientOnly>
<PackageMetricsBadges
v-if="displayVersion"
:aria-label="$t('package.navigation')"
class="hidden sm:flex items-center gap-0.5 p-0.5 bg-bg-subtle border border-border-subtle rounded-md shrink-0 ms-auto self-center"
:package-name="pkg.name"
:version="displayVersion.version"
:is-binary="isBinaryOnly"
class="self-baseline ms-1 sm:ms-2"
/>
<template #fallback>
<ul class="flex items-center gap-1.5 self-baseline ms-1 sm:ms-2">
<li class="skeleton w-8 h-5 rounded" />
<li class="skeleton w-12 h-5 rounded" />
</ul>
</template>
</ClientOnly>

<!-- Internal navigation: Docs + Code + Compare (hidden on mobile, shown in external links instead) -->
<nav
v-if="displayVersion"
:aria-label="$t('package.navigation')"
class="hidden sm:flex items-center gap-0.5 p-0.5 bg-bg-subtle border border-border-subtle rounded-md shrink-0 ms-auto self-center"
>
<NuxtLink
v-if="docsLink"
:to="docsLink"
class="px-2 py-1.5 font-mono text-xs rounded transition-colors duration-150 border border-transparent text-fg-subtle hover:text-fg hover:bg-bg hover:shadow hover:border-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 inline-flex items-center gap-1.5"
aria-keyshortcuts="d"
>
<NuxtLink
v-if="docsLink"
:to="docsLink"
class="px-2 py-1.5 font-mono text-xs rounded transition-colors duration-150 border border-transparent text-fg-subtle hover:text-fg hover:bg-bg hover:shadow hover:border-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 inline-flex items-center gap-1.5"
aria-keyshortcuts="d"
<span class="i-carbon:document w-3 h-3" aria-hidden="true" />
{{ $t('package.links.docs') }}
<kbd
class="inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded"
aria-hidden="true"
>
<span class="i-carbon:document w-3 h-3" aria-hidden="true" />
{{ $t('package.links.docs') }}
<kbd
class="inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded"
aria-hidden="true"
>
d
</kbd>
</NuxtLink>
<NuxtLink
:to="{
name: 'code',
params: {
path: [...pkg.name.split('/'), 'v', displayVersion.version],
},
}"
class="px-2 py-1.5 font-mono text-xs rounded transition-colors duration-150 border border-transparent text-fg-subtle hover:text-fg hover:bg-bg hover:shadow hover:border-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 inline-flex items-center gap-1.5"
aria-keyshortcuts="."
d
</kbd>
</NuxtLink>
<NuxtLink
:to="{
name: 'code',
params: {
path: [...pkg.name.split('/'), 'v', displayVersion.version],
},
}"
class="px-2 py-1.5 font-mono text-xs rounded transition-colors duration-150 border border-transparent text-fg-subtle hover:text-fg hover:bg-bg hover:shadow hover:border-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 inline-flex items-center gap-1.5"
aria-keyshortcuts="."
>
<span class="i-carbon:code w-3 h-3" aria-hidden="true" />
{{ $t('package.links.code') }}
<kbd
class="inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded"
aria-hidden="true"
>
<span class="i-carbon:code w-3 h-3" aria-hidden="true" />
{{ $t('package.links.code') }}
<kbd
class="inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded"
aria-hidden="true"
>
.
</kbd>
</NuxtLink>
<NuxtLink
:to="{ path: '/compare', query: { packages: pkg.name } }"
class="px-2 py-1.5 font-mono text-xs rounded transition-colors duration-150 border border-transparent text-fg-subtle hover:text-fg hover:bg-bg hover:shadow hover:border-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 inline-flex items-center gap-1.5"
aria-keyshortcuts="c"
.
</kbd>
</NuxtLink>
<NuxtLink
:to="{ path: '/compare', query: { packages: pkg.name } }"
class="px-2 py-1.5 font-mono text-xs rounded transition-colors duration-150 border border-transparent text-fg-subtle hover:text-fg hover:bg-bg hover:shadow hover:border-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 inline-flex items-center gap-1.5"
aria-keyshortcuts="c"
>
<span class="i-carbon:compare w-3 h-3" aria-hidden="true" />
{{ $t('package.links.compare') }}
<kbd
class="inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded"
aria-hidden="true"
>
<span class="i-carbon:compare w-3 h-3" aria-hidden="true" />
{{ $t('package.links.compare') }}
<kbd
class="inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded"
aria-hidden="true"
>
c
</kbd>
</NuxtLink>
</nav>
</div>
c
</kbd>
</NuxtLink>
</nav>
</div>
</header>

<!-- Package details -->
<section class="area-details">
<div class="mb-4">
<!-- Description container with min-height to prevent CLS -->
<div class="max-w-2xl min-h-[4.5rem]">
<p v-if="pkg.description" class="text-fg-muted text-base m-0">
Expand Down Expand Up @@ -698,7 +723,7 @@ function handleClick(event: MouseEvent) {

<!-- Stats grid -->
<dl
class="grid grid-cols-2 sm:grid-cols-11 gap-3 sm:gap-4 py-4 sm:py-6 mt-4 sm:mt-6 border-t border-border"
class="grid grid-cols-2 sm:grid-cols-11 gap-3 sm:gap-4 py-4 sm:py-6 mt-4 sm:mt-6 border-t border-b border-border"
>
<div v-if="pkg.license" class="space-y-1 sm:col-span-2">
<dt class="text-xs text-fg-subtle uppercase tracking-wider">
Expand Down Expand Up @@ -859,7 +884,7 @@ function handleClick(event: MouseEvent) {
:version="displayVersion?.version"
/>
</ClientOnly>
</header>
</section>

<!-- Binary-only packages: Show only execute command (no install) -->
<section v-if="isBinaryOnly" class="area-install scroll-mt-20">
Expand Down Expand Up @@ -965,7 +990,7 @@ function handleClick(event: MouseEvent) {

<div class="area-sidebar">
<!-- Sidebar -->
<div class="sticky top-20 space-y-6 sm:space-y-8 min-w-0 overflow-hidden">
<div class="sticky top-34 space-y-6 sm:space-y-8 min-w-0 overflow-hidden xl:(top-22 pt-2)">
<!-- Maintainers (with admin actions when connected) -->
<PackageMaintainers :package-name="pkg.name" :maintainers="pkg.maintainers" />

Expand Down Expand Up @@ -1113,6 +1138,7 @@ function handleClick(event: MouseEvent) {
grid-template-columns: minmax(0, 1fr);
grid-template-areas:
'header'
'details'
'install'
'vulns'
'sidebar'
Expand All @@ -1125,6 +1151,7 @@ function handleClick(event: MouseEvent) {
grid-template-columns: 2fr 1fr;
grid-template-areas:
'header header'
'details details'
'install install'
'vulns vulns'
'readme sidebar';
Expand All @@ -1138,6 +1165,7 @@ function handleClick(event: MouseEvent) {
grid-template-columns: 1fr 20rem;
grid-template-areas:
'header sidebar'
'details sidebar'
'install sidebar'
'vulns sidebar'
'readme sidebar';
Expand All @@ -1146,7 +1174,10 @@ function handleClick(event: MouseEvent) {

.area-header {
grid-area: header;
overflow-x: hidden;
}

.area-details {
grid-area: details;
}

.area-install {
Expand Down
Loading