Skip to content

Commit 0579ad8

Browse files
committed
feat: pin package header 📌
1 parent 0d06026 commit 0579ad8

File tree

1 file changed

+163
-132
lines changed

1 file changed

+163
-132
lines changed

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

Lines changed: 163 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,27 @@ definePageMeta({
1212
1313
const router = useRouter()
1414
15+
const header = useTemplateRef('header')
16+
const isHeaderPinned = shallowRef(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+
useEventListener('scroll', checkHeaderPosition, { passive: true })
30+
useEventListener('resize', checkHeaderPosition)
31+
32+
onMounted(() => {
33+
checkHeaderPosition()
34+
})
35+
1536
const { packageName, requestedVersion, orgName } = usePackageRoute()
1637
const selectedPM = useSelectedPackageManager()
1738
const activePmId = computed(() => selectedPM.value ?? 'npm')
@@ -372,150 +393,154 @@ function handleClick(event: MouseEvent) {
372393
</script>
373394

374395
<template>
375-
<main class="container flex-1 py-8 xl:py-12">
396+
<main class="container flex-1 py-8">
376397
<PackageSkeleton v-if="status === 'pending'" />
377398

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

405-
<span id="copy-pkg-name" class="sr-only">{{ $t('package.copy_name') }}</span>
448+
<a
449+
v-if="hasProvenance(displayVersion)"
450+
:href="`https://www.npmjs.com/package/${pkg.name}/v/${displayVersion.version}#provenance`"
451+
target="_blank"
452+
rel="noopener noreferrer"
453+
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"
454+
:title="$t('package.verified_provenance')"
455+
>
456+
<span class="i-solar:shield-check-outline w-3.5 h-3.5 shrink-0" aria-hidden="true" />
457+
</a>
406458
<span
407-
v-if="displayVersion"
408-
class="inline-flex items-baseline gap-1.5 font-mono text-base sm:text-lg text-fg-muted shrink-0"
459+
v-if="
460+
requestedVersion &&
461+
latestVersion &&
462+
displayVersion.version !== latestVersion.version
463+
"
464+
class="text-fg-subtle text-sm shrink-0"
465+
>{{ $t('package.not_latest') }}</span
409466
>
410-
<!-- Version resolution indicator (e.g., "latest → 4.2.0") -->
411-
<template v-if="resolvedVersion !== requestedVersion">
412-
<span class="font-mono text-fg-muted text-sm">{{ requestedVersion }}</span>
413-
<span class="i-carbon:arrow-right rtl-flip w-3 h-3" aria-hidden="true" />
414-
</template>
415-
416-
<NuxtLink
417-
v-if="resolvedVersion !== requestedVersion"
418-
:to="`/${pkg.name}/v/${displayVersion.version}`"
419-
:title="$t('package.view_permalink')"
420-
>{{ displayVersion.version }}</NuxtLink
421-
>
422-
<span v-else>v{{ displayVersion.version }}</span>
467+
</span>
423468

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

541+
<!-- Package details -->
542+
<section class="area-details">
543+
<div class="mb-4">
519544
<!-- Description container with min-height to prevent CLS -->
520545
<div class="max-w-2xl min-h-[4.5rem]">
521546
<p v-if="pkg.description" class="text-fg-muted text-base m-0">
@@ -826,7 +851,7 @@ function handleClick(event: MouseEvent) {
826851
</dd>
827852
</div>
828853
</dl>
829-
</header>
854+
</section>
830855

831856
<!-- Binary-only packages: Show only execute command (no install) -->
832857
<section v-if="isBinaryOnly" class="area-install scroll-mt-20">
@@ -937,7 +962,7 @@ function handleClick(event: MouseEvent) {
937962

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

@@ -1073,6 +1098,7 @@ function handleClick(event: MouseEvent) {
10731098
grid-template-columns: minmax(0, 1fr);
10741099
grid-template-areas:
10751100
'header'
1101+
'details'
10761102
'install'
10771103
'vulns'
10781104
'sidebar'
@@ -1085,6 +1111,7 @@ function handleClick(event: MouseEvent) {
10851111
grid-template-columns: 2fr 1fr;
10861112
grid-template-areas:
10871113
'header header'
1114+
'details details'
10881115
'install install'
10891116
'vulns vulns'
10901117
'readme sidebar';
@@ -1097,6 +1124,7 @@ function handleClick(event: MouseEvent) {
10971124
grid-template-columns: 1fr 20rem;
10981125
grid-template-areas:
10991126
'header sidebar'
1127+
'details sidebar'
11001128
'install sidebar'
11011129
'vulns sidebar'
11021130
'readme sidebar';
@@ -1105,7 +1133,10 @@ function handleClick(event: MouseEvent) {
11051133
11061134
.area-header {
11071135
grid-area: header;
1108-
overflow-x: hidden;
1136+
}
1137+
1138+
.area-details {
1139+
grid-area: details;
11091140
}
11101141
11111142
.area-install {

0 commit comments

Comments
 (0)