@@ -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+ onMounted (() => {
30+ useEventListener (' scroll' , checkHeaderPosition , { passive: true })
31+ useEventListener (' resize' , checkHeaderPosition )
32+
33+ checkHeaderPosition ()
34+ })
35+
1536const { packageName, requestedVersion, orgName } = usePackageRoute ()
1637const selectedPM = useSelectedPackageManager ()
1738const activePmId = computed (() => selectedPM .value ?? ' npm' )
@@ -387,150 +408,154 @@ function handleClick(event: MouseEvent) {
387408 </script >
388409
389410<template >
390- <main class =" container flex-1 py-8 xl:py-12 " >
411+ <main class =" container flex-1 py-8" >
391412 <PackageSkeleton v-if =" status === 'pending'" />
392413
393414 <article v-else-if =" status === 'success' && pkg" class =" package-page" >
394415 <!-- 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"
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
402460 >
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 >
461+ <span v-else >v{{ displayVersion.version }}</span >
419462
420- <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 >
421473 <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"
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
424481 >
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 >
482+ </span >
438483
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
484+ <!-- Package metrics (module format, types) -->
485+ <ClientOnly >
486+ <PackageMetricsBadges
481487 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"
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"
484511 >
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 "
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 "
490517 >
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 = " . "
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 "
507534 >
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 "
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 "
521548 >
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 >
549+ c
550+ </kbd >
551+ </NuxtLink >
552+ </nav >
553+ </div >
554+ </header >
533555
556+ <!-- Package details -->
557+ <div class =" area-details border-b border-border" >
558+ <div class =" mb-4" >
534559 <!-- Description container with min-height to prevent CLS -->
535560 <div class =" max-w-2xl min-h-[4.5rem]" >
536561 <p v-if =" pkg.description" class =" text-fg-muted text-base m-0" >
@@ -841,7 +866,7 @@ function handleClick(event: MouseEvent) {
841866 </dd >
842867 </div >
843868 </dl >
844- </header >
869+ </div >
845870
846871 <!-- Binary-only packages: Show only execute command (no install) -->
847872 <section v-if =" isBinaryOnly" class =" area-install scroll-mt-20" >
@@ -952,7 +977,7 @@ function handleClick(event: MouseEvent) {
952977
953978 <div class =" area-sidebar" >
954979 <!-- Sidebar -->
955- <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 " >
956981 <!-- Maintainers (with admin actions when connected) -->
957982 <PackageMaintainers :package-name =" pkg.name" :maintainers =" pkg.maintainers" />
958983
@@ -1088,6 +1113,7 @@ function handleClick(event: MouseEvent) {
10881113 grid-template-columns : minmax (0 , 1fr );
10891114 grid-template-areas :
10901115 ' header'
1116+ ' details'
10911117 ' install'
10921118 ' vulns'
10931119 ' sidebar'
@@ -1100,6 +1126,7 @@ function handleClick(event: MouseEvent) {
11001126 grid-template-columns : 2fr 1fr ;
11011127 grid-template-areas :
11021128 ' header header'
1129+ ' details details'
11031130 ' install install'
11041131 ' vulns vulns'
11051132 ' readme sidebar' ;
@@ -1112,6 +1139,7 @@ function handleClick(event: MouseEvent) {
11121139 grid-template-columns : 1fr 20rem ;
11131140 grid-template-areas :
11141141 ' header sidebar'
1142+ ' details sidebar'
11151143 ' install sidebar'
11161144 ' vulns sidebar'
11171145 ' readme sidebar' ;
@@ -1120,7 +1148,10 @@ function handleClick(event: MouseEvent) {
11201148
11211149.area-header {
11221150 grid-area : header;
1123- overflow-x : hidden ;
1151+ }
1152+
1153+ .area-details {
1154+ grid-area : details;
11241155}
11251156
11261157.area-install {
0 commit comments