Skip to content

Commit 3ae8375

Browse files
authored
feat: pin package header 📌 (#566)
1 parent 5e93353 commit 3ae8375

File tree

1 file changed

+167
-136
lines changed

1 file changed

+167
-136
lines changed

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

Lines changed: 167 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,27 @@ definePageMeta({
1919
2020
const router = useRouter()
2121
22+
const header = useTemplateRef('header')
23+
const isHeaderPinned = shallowRef(false)
24+
25+
function checkHeaderPosition() {
26+
const el = header.value
27+
if (!el) return
28+
29+
const style = getComputedStyle(el)
30+
const top = parseFloat(style.top) || 0
31+
const rect = el.getBoundingClientRect()
32+
33+
isHeaderPinned.value = Math.abs(rect.top - top) < 1
34+
}
35+
36+
useEventListener('scroll', checkHeaderPosition, { passive: true })
37+
useEventListener('resize', checkHeaderPosition)
38+
39+
onMounted(() => {
40+
checkHeaderPosition()
41+
})
42+
2243
const { packageName, requestedVersion, orgName } = usePackageRoute()
2344
const selectedPM = useSelectedPackageManager()
2445
const activePmId = computed(() => selectedPM.value ?? 'npm')
@@ -389,153 +410,157 @@ function handleClick(event: MouseEvent) {
389410
</script>
390411

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

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

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

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

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

699724
<!-- Stats grid -->
700725
<dl
701-
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"
726+
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"
702727
>
703728
<div v-if="pkg.license" class="space-y-1 sm:col-span-2">
704729
<dt class="text-xs text-fg-subtle uppercase tracking-wider">
@@ -859,7 +884,7 @@ function handleClick(event: MouseEvent) {
859884
:version="displayVersion?.version"
860885
/>
861886
</ClientOnly>
862-
</header>
887+
</section>
863888

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

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

@@ -1113,6 +1138,7 @@ function handleClick(event: MouseEvent) {
11131138
grid-template-columns: minmax(0, 1fr);
11141139
grid-template-areas:
11151140
'header'
1141+
'details'
11161142
'install'
11171143
'vulns'
11181144
'sidebar'
@@ -1125,6 +1151,7 @@ function handleClick(event: MouseEvent) {
11251151
grid-template-columns: 2fr 1fr;
11261152
grid-template-areas:
11271153
'header header'
1154+
'details details'
11281155
'install install'
11291156
'vulns vulns'
11301157
'readme sidebar';
@@ -1138,6 +1165,7 @@ function handleClick(event: MouseEvent) {
11381165
grid-template-columns: 1fr 20rem;
11391166
grid-template-areas:
11401167
'header sidebar'
1168+
'details sidebar'
11411169
'install sidebar'
11421170
'vulns sidebar'
11431171
'readme sidebar';
@@ -1146,7 +1174,10 @@ function handleClick(event: MouseEvent) {
11461174
11471175
.area-header {
11481176
grid-area: header;
1149-
overflow-x: hidden;
1177+
}
1178+
1179+
.area-details {
1180+
grid-area: details;
11501181
}
11511182
11521183
.area-install {

0 commit comments

Comments
 (0)