@@ -12,6 +12,27 @@ definePageMeta({
1212
1313const 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+
1536const { packageName, requestedVersion, orgName } = usePackageRoute ()
1637const selectedPM = useSelectedPackageManager ()
1738const 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