Skip to content

Commit 18256fa

Browse files
committed
feat: pin package header 📌
1 parent 0d06026 commit 18256fa

1 file changed

Lines changed: 181 additions & 135 deletions

File tree

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

Lines changed: 181 additions & 135 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')
@@ -93,9 +114,24 @@ const { copied: copiedPkgName, copy: copyPkgName } = useClipboard({
93114
94115
// Fetch dependency analysis (lazy, client-side)
95116
// This is the same composable used by PackageVulnerabilityTree and PackageDeprecatedTree
96-
const { data: vulnTree, status: vulnTreeStatus } = useDependencyAnalysis(
97-
packageName,
98-
() => displayVersion.value?.version ?? '',
117+
const {
118+
data: vulnTree,
119+
status: vulnTreeStatus,
120+
fetch: fetchVulnTree,
121+
} = useDependencyAnalysis(packageName, () => displayVersion.value?.version ?? '')
122+
onMounted(() => {
123+
// Fetch vulnerability tree once displayVersion is available
124+
if (displayVersion.value) {
125+
fetchVulnTree()
126+
}
127+
})
128+
watch(
129+
() => displayVersion.value?.version,
130+
() => {
131+
if (displayVersion.value) {
132+
fetchVulnTree()
133+
}
134+
},
99135
)
100136
101137
// Keep latestVersion for comparison (to show "(latest)" badge)
@@ -372,150 +408,154 @@ function handleClick(event: MouseEvent) {
372408
</script>
373409

374410
<template>
375-
<main class="container flex-1 py-8 xl:py-12">
411+
<main class="container flex-1 py-8">
376412
<PackageSkeleton v-if="status === 'pending'" />
377413

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

405-
<span id="copy-pkg-name" class="sr-only">{{ $t('package.copy_name') }}</span>
463+
<a
464+
v-if="hasProvenance(displayVersion)"
465+
:href="`https://www.npmjs.com/package/${pkg.name}/v/${displayVersion.version}#provenance`"
466+
target="_blank"
467+
rel="noopener noreferrer"
468+
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"
469+
:title="$t('package.verified_provenance')"
470+
>
471+
<span class="i-solar:shield-check-outline w-3.5 h-3.5 shrink-0" aria-hidden="true" />
472+
</a>
406473
<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"
474+
v-if="
475+
requestedVersion &&
476+
latestVersion &&
477+
displayVersion.version !== latestVersion.version
478+
"
479+
class="text-fg-subtle text-sm shrink-0"
480+
>{{ $t('package.not_latest') }}</span
409481
>
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>
482+
</span>
415483

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>
423-
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
484+
<!-- Package metrics (module format, types) -->
485+
<ClientOnly>
486+
<PackageMetricsBadges
466487
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"
488+
:package-name="pkg.name"
489+
:version="displayVersion.version"
490+
class="self-baseline ms-1 sm:ms-2"
491+
/>
492+
<template #fallback>
493+
<ul class="flex items-center gap-1.5 self-baseline ms-1 sm:ms-2">
494+
<li class="skeleton w-8 h-5 rounded" />
495+
<li class="skeleton w-12 h-5 rounded" />
496+
</ul>
497+
</template>
498+
</ClientOnly>
499+
500+
<!-- Internal navigation: Docs + Code + Compare (hidden on mobile, shown in external links instead) -->
501+
<nav
502+
v-if="displayVersion"
503+
:aria-label="$t('package.navigation')"
504+
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"
505+
>
506+
<NuxtLink
507+
v-if="docsLink"
508+
:to="docsLink"
509+
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"
510+
aria-keyshortcuts="d"
469511
>
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"
512+
<span class="i-carbon:document w-3 h-3" aria-hidden="true" />
513+
{{ $t('package.links.docs') }}
514+
<kbd
515+
class="inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded"
516+
aria-hidden="true"
475517
>
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="."
518+
d
519+
</kbd>
520+
</NuxtLink>
521+
<NuxtLink
522+
:to="{
523+
name: 'code',
524+
params: { path: [...pkg.name.split('/'), 'v', displayVersion.version] },
525+
}"
526+
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"
527+
aria-keyshortcuts="."
528+
>
529+
<span class="i-carbon:code w-3 h-3" aria-hidden="true" />
530+
{{ $t('package.links.code') }}
531+
<kbd
532+
class="inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded"
533+
aria-hidden="true"
492534
>
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"
535+
.
536+
</kbd>
537+
</NuxtLink>
538+
<NuxtLink
539+
:to="{ path: '/compare', query: { packages: pkg.name } }"
540+
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"
541+
aria-keyshortcuts="c"
542+
>
543+
<span class="i-carbon:compare w-3 h-3" aria-hidden="true" />
544+
{{ $t('package.links.compare') }}
545+
<kbd
546+
class="inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded"
547+
aria-hidden="true"
506548
>
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>
549+
c
550+
</kbd>
551+
</NuxtLink>
552+
</nav>
553+
</div>
554+
</header>
518555

556+
<!-- Package details -->
557+
<div class="area-details border-b border-border">
558+
<div class="mb-4">
519559
<!-- Description container with min-height to prevent CLS -->
520560
<div class="max-w-2xl min-h-[4.5rem]">
521561
<p v-if="pkg.description" class="text-fg-muted text-base m-0">
@@ -826,7 +866,7 @@ function handleClick(event: MouseEvent) {
826866
</dd>
827867
</div>
828868
</dl>
829-
</header>
869+
</div>
830870

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

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

@@ -1073,6 +1113,7 @@ function handleClick(event: MouseEvent) {
10731113
grid-template-columns: minmax(0, 1fr);
10741114
grid-template-areas:
10751115
'header'
1116+
'details'
10761117
'install'
10771118
'vulns'
10781119
'sidebar'
@@ -1085,6 +1126,7 @@ function handleClick(event: MouseEvent) {
10851126
grid-template-columns: 2fr 1fr;
10861127
grid-template-areas:
10871128
'header header'
1129+
'details details'
10881130
'install install'
10891131
'vulns vulns'
10901132
'readme sidebar';
@@ -1097,6 +1139,7 @@ function handleClick(event: MouseEvent) {
10971139
grid-template-columns: 1fr 20rem;
10981140
grid-template-areas:
10991141
'header sidebar'
1142+
'details sidebar'
11001143
'install sidebar'
11011144
'vulns sidebar'
11021145
'readme sidebar';
@@ -1105,7 +1148,10 @@ function handleClick(event: MouseEvent) {
11051148
11061149
.area-header {
11071150
grid-area: header;
1108-
overflow-x: hidden;
1151+
}
1152+
1153+
.area-details {
1154+
grid-area: details;
11091155
}
11101156
11111157
.area-install {

0 commit comments

Comments
 (0)