@@ -12,6 +12,32 @@ definePageMeta({
1212
1313const 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+
1541const { packageName, requestedVersion, orgName } = usePackageRoute ()
1642const selectedPM = useSelectedPackageManager ()
1743const 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