Skip to content

Commit 7ac42c6

Browse files
committed
feat: pin package header 📌
1 parent 8825b0a commit 7ac42c6

1 file changed

Lines changed: 168 additions & 132 deletions

File tree

‎app/pages/[...package].vue‎

Lines changed: 168 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,32 @@ definePageMeta({
1212
1313
const router = useRouter()
1414
15+
const header = templateRef('header')
16+
const isHeaderPinned = ref(false)
17+
18+
function checkHeaderPosition() {
19+
const el = header.value
20+
if (!el) return
21+
22+
const style = getComputedStyle(el)
23+
const top = parseFloat(style.top) || 0
24+
const rect = el.getBoundingClientRect()
25+
26+
isHeaderPinned.value = Math.abs(rect.top - top) < 1
27+
}
28+
29+
onMounted(() => {
30+
window.addEventListener('scroll', checkHeaderPosition, { passive: true })
31+
window.addEventListener('resize', checkHeaderPosition)
32+
33+
checkHeaderPosition()
34+
})
35+
36+
onBeforeUnmount(() => {
37+
window.removeEventListener('scroll', checkHeaderPosition)
38+
window.removeEventListener('resize', checkHeaderPosition)
39+
})
40+
1541
const { packageName, requestedVersion, orgName } = usePackageRoute()
1642
const selectedPM = useSelectedPackageManager()
1743
const activePmId = computed(() => selectedPM.value ?? 'npm')
@@ -387,150 +413,154 @@ function handleClick(event: MouseEvent) {
387413
</script>
388414

389415
<template>
390-
<main class="container flex-1 py-8 xl:py-12">
416+
<main class="container flex-1 py-8">
391417
<PackageSkeleton v-if="status === 'pending'" />
392418

393419
<article v-else-if="status === 'success' && pkg" class="package-page">
394420
<!-- Package header -->
395-
<header class="area-header border-b border-border">
396-
<div class="mb-4">
397-
<!-- Package name and version -->
398-
<div class="flex items-baseline gap-2 mb-1.5 sm:gap-3 sm:mb-2 flex-wrap min-w-0">
399-
<h1
400-
class="font-mono text-2xl sm:text-3xl font-medium min-w-0 break-words"
401-
:title="pkg.name"
421+
<header
422+
class="area-header sticky top-14 z-1 bg-[--bg] py-2 border-border"
423+
ref="header"
424+
:class="{ 'border-b': isHeaderPinned }"
425+
>
426+
<!-- Package name and version -->
427+
<div class="flex items-baseline gap-2 sm:gap-3 flex-wrap min-w-0">
428+
<h1
429+
class="font-mono text-2xl sm:text-3xl font-medium min-w-0 break-words"
430+
:title="pkg.name"
431+
>
432+
<NuxtLink
433+
v-if="orgName"
434+
:to="{ name: 'org', params: { org: orgName } }"
435+
class="text-fg-muted hover:text-fg transition-colors duration-200"
436+
>@{{ orgName }}</NuxtLink
437+
><span v-if="orgName">/</span>
438+
<AnnounceTooltip :text="$t('common.copied')" :isVisible="copiedPkgName">
439+
<button
440+
@click="copyPkgName()"
441+
aria-describedby="copy-pkg-name"
442+
class="cursor-cop active:scale-95 transition-transform"
443+
>
444+
{{ orgName ? pkg.name.replace(`@${orgName}/`, '') : pkg.name }}
445+
</button>
446+
</AnnounceTooltip>
447+
</h1>
448+
449+
<span id="copy-pkg-name" class="sr-only">{{ $t('package.copy_name') }}</span>
450+
<span
451+
v-if="displayVersion"
452+
class="inline-flex items-baseline gap-1.5 font-mono text-base sm:text-lg text-fg-muted shrink-0"
453+
>
454+
<!-- Version resolution indicator (e.g., "latest → 4.2.0") -->
455+
<template v-if="resolvedVersion !== requestedVersion">
456+
<span class="font-mono text-fg-muted text-sm">{{ requestedVersion }}</span>
457+
<span class="i-carbon:arrow-right rtl-flip w-3 h-3" aria-hidden="true" />
458+
</template>
459+
460+
<NuxtLink
461+
v-if="resolvedVersion !== requestedVersion"
462+
:to="`/${pkg.name}/v/${displayVersion.version}`"
463+
:title="$t('package.view_permalink')"
464+
>{{ displayVersion.version }}</NuxtLink
402465
>
403-
<NuxtLink
404-
v-if="orgName"
405-
:to="{ name: 'org', params: { org: orgName } }"
406-
class="text-fg-muted hover:text-fg transition-colors duration-200"
407-
>@{{ orgName }}</NuxtLink
408-
><span v-if="orgName">/</span>
409-
<AnnounceTooltip :text="$t('common.copied')" :isVisible="copiedPkgName">
410-
<button
411-
@click="copyPkgName()"
412-
aria-describedby="copy-pkg-name"
413-
class="cursor-copy ms-1 mt-1 active:scale-95 transition-transform"
414-
>
415-
{{ orgName ? pkg.name.replace(`@${orgName}/`, '') : pkg.name }}
416-
</button>
417-
</AnnounceTooltip>
418-
</h1>
466+
<span v-else>v{{ displayVersion.version }}</span>
419467

420-
<span id="copy-pkg-name" class="sr-only">{{ $t('package.copy_name') }}</span>
468+
<a
469+
v-if="hasProvenance(displayVersion)"
470+
:href="`https://www.npmjs.com/package/${pkg.name}/v/${displayVersion.version}#provenance`"
471+
target="_blank"
472+
rel="noopener noreferrer"
473+
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"
474+
:title="$t('package.verified_provenance')"
475+
>
476+
<span class="i-solar:shield-check-outline w-3.5 h-3.5 shrink-0" aria-hidden="true" />
477+
</a>
421478
<span
422-
v-if="displayVersion"
423-
class="inline-flex items-baseline gap-1.5 font-mono text-base sm:text-lg text-fg-muted shrink-0"
479+
v-if="
480+
requestedVersion &&
481+
latestVersion &&
482+
displayVersion.version !== latestVersion.version
483+
"
484+
class="text-fg-subtle text-sm shrink-0"
485+
>{{ $t('package.not_latest') }}</span
424486
>
425-
<!-- Version resolution indicator (e.g., "latest → 4.2.0") -->
426-
<template v-if="resolvedVersion !== requestedVersion">
427-
<span class="font-mono text-fg-muted text-sm">{{ requestedVersion }}</span>
428-
<span class="i-carbon:arrow-right rtl-flip w-3 h-3" aria-hidden="true" />
429-
</template>
430-
431-
<NuxtLink
432-
v-if="resolvedVersion !== requestedVersion"
433-
:to="`/${pkg.name}/v/${displayVersion.version}`"
434-
:title="$t('package.view_permalink')"
435-
>{{ displayVersion.version }}</NuxtLink
436-
>
437-
<span v-else>v{{ displayVersion.version }}</span>
487+
</span>
438488

439-
<a
440-
v-if="hasProvenance(displayVersion)"
441-
:href="`https://www.npmjs.com/package/${pkg.name}/v/${displayVersion.version}#provenance`"
442-
target="_blank"
443-
rel="noopener noreferrer"
444-
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"
445-
:title="$t('package.verified_provenance')"
446-
>
447-
<span
448-
class="i-solar:shield-check-outline w-3.5 h-3.5 shrink-0"
449-
aria-hidden="true"
450-
/>
451-
</a>
452-
<span
453-
v-if="
454-
requestedVersion &&
455-
latestVersion &&
456-
displayVersion.version !== latestVersion.version
457-
"
458-
class="text-fg-subtle text-sm shrink-0"
459-
>{{ $t('package.not_latest') }}</span
460-
>
461-
</span>
462-
463-
<!-- Package metrics (module format, types) -->
464-
<ClientOnly>
465-
<PackageMetricsBadges
466-
v-if="displayVersion"
467-
:package-name="pkg.name"
468-
:version="displayVersion.version"
469-
class="self-baseline ms-1 sm:ms-2"
470-
/>
471-
<template #fallback>
472-
<ul class="flex items-center gap-1.5 self-baseline ms-1 sm:ms-2">
473-
<li class="skeleton w-8 h-5 rounded" />
474-
<li class="skeleton w-12 h-5 rounded" />
475-
</ul>
476-
</template>
477-
</ClientOnly>
478-
479-
<!-- Internal navigation: Docs + Code + Compare (hidden on mobile, shown in external links instead) -->
480-
<nav
489+
<!-- Package metrics (module format, types) -->
490+
<ClientOnly>
491+
<PackageMetricsBadges
481492
v-if="displayVersion"
482-
:aria-label="$t('package.navigation')"
483-
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"
493+
:package-name="pkg.name"
494+
:version="displayVersion.version"
495+
class="self-baseline ms-1 sm:ms-2"
496+
/>
497+
<template #fallback>
498+
<ul class="flex items-center gap-1.5 self-baseline ms-1 sm:ms-2">
499+
<li class="skeleton w-8 h-5 rounded" />
500+
<li class="skeleton w-12 h-5 rounded" />
501+
</ul>
502+
</template>
503+
</ClientOnly>
504+
505+
<!-- Internal navigation: Docs + Code + Compare (hidden on mobile, shown in external links instead) -->
506+
<nav
507+
v-if="displayVersion"
508+
:aria-label="$t('package.navigation')"
509+
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"
510+
>
511+
<NuxtLink
512+
v-if="docsLink"
513+
:to="docsLink"
514+
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"
515+
aria-keyshortcuts="d"
484516
>
485-
<NuxtLink
486-
v-if="docsLink"
487-
:to="docsLink"
488-
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"
489-
aria-keyshortcuts="d"
517+
<span class="i-carbon:document w-3 h-3" aria-hidden="true" />
518+
{{ $t('package.links.docs') }}
519+
<kbd
520+
class="inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded"
521+
aria-hidden="true"
490522
>
491-
<span class="i-carbon:document w-3 h-3" aria-hidden="true" />
492-
{{ $t('package.links.docs') }}
493-
<kbd
494-
class="inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded"
495-
aria-hidden="true"
496-
>
497-
d
498-
</kbd>
499-
</NuxtLink>
500-
<NuxtLink
501-
:to="{
502-
name: 'code',
503-
params: { path: [...pkg.name.split('/'), 'v', displayVersion.version] },
504-
}"
505-
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"
506-
aria-keyshortcuts="."
523+
d
524+
</kbd>
525+
</NuxtLink>
526+
<NuxtLink
527+
:to="{
528+
name: 'code',
529+
params: { path: [...pkg.name.split('/'), 'v', displayVersion.version] },
530+
}"
531+
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"
532+
aria-keyshortcuts="."
533+
>
534+
<span class="i-carbon:code w-3 h-3" aria-hidden="true" />
535+
{{ $t('package.links.code') }}
536+
<kbd
537+
class="inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded"
538+
aria-hidden="true"
507539
>
508-
<span class="i-carbon:code w-3 h-3" aria-hidden="true" />
509-
{{ $t('package.links.code') }}
510-
<kbd
511-
class="inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded"
512-
aria-hidden="true"
513-
>
514-
.
515-
</kbd>
516-
</NuxtLink>
517-
<NuxtLink
518-
:to="{ path: '/compare', query: { packages: pkg.name } }"
519-
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"
520-
aria-keyshortcuts="c"
540+
.
541+
</kbd>
542+
</NuxtLink>
543+
<NuxtLink
544+
:to="{ path: '/compare', query: { packages: pkg.name } }"
545+
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"
546+
aria-keyshortcuts="c"
547+
>
548+
<span class="i-carbon:compare w-3 h-3" aria-hidden="true" />
549+
{{ $t('package.links.compare') }}
550+
<kbd
551+
class="inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded"
552+
aria-hidden="true"
521553
>
522-
<span class="i-carbon:compare w-3 h-3" aria-hidden="true" />
523-
{{ $t('package.links.compare') }}
524-
<kbd
525-
class="inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded"
526-
aria-hidden="true"
527-
>
528-
c
529-
</kbd>
530-
</NuxtLink>
531-
</nav>
532-
</div>
554+
c
555+
</kbd>
556+
</NuxtLink>
557+
</nav>
558+
</div>
559+
</header>
533560

561+
<!-- Package details -->
562+
<div class="area-details border-b border-border">
563+
<div class="mb-4">
534564
<!-- Description container with min-height to prevent CLS -->
535565
<div class="max-w-2xl min-h-[4.5rem]">
536566
<p v-if="pkg.description" class="text-fg-muted text-base m-0">
@@ -841,7 +871,7 @@ function handleClick(event: MouseEvent) {
841871
</dd>
842872
</div>
843873
</dl>
844-
</header>
874+
</div>
845875

846876
<!-- Binary-only packages: Show only execute command (no install) -->
847877
<section v-if="isBinaryOnly" class="area-install scroll-mt-20">
@@ -952,7 +982,7 @@ function handleClick(event: MouseEvent) {
952982

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

@@ -1088,6 +1118,7 @@ function handleClick(event: MouseEvent) {
10881118
grid-template-columns: minmax(0, 1fr);
10891119
grid-template-areas:
10901120
'header'
1121+
'details'
10911122
'install'
10921123
'vulns'
10931124
'sidebar'
@@ -1100,6 +1131,7 @@ function handleClick(event: MouseEvent) {
11001131
grid-template-columns: 2fr 1fr;
11011132
grid-template-areas:
11021133
'header header'
1134+
'details details'
11031135
'install install'
11041136
'vulns vulns'
11051137
'readme sidebar';
@@ -1112,6 +1144,7 @@ function handleClick(event: MouseEvent) {
11121144
grid-template-columns: 1fr 20rem;
11131145
grid-template-areas:
11141146
'header sidebar'
1147+
'details sidebar'
11151148
'install sidebar'
11161149
'vulns sidebar'
11171150
'readme sidebar';
@@ -1120,7 +1153,10 @@ function handleClick(event: MouseEvent) {
11201153
11211154
.area-header {
11221155
grid-area: header;
1123-
overflow-x: hidden;
1156+
}
1157+
1158+
.area-details {
1159+
grid-area: details;
11241160
}
11251161
11261162
.area-install {

0 commit comments

Comments
 (0)