@@ -19,6 +19,27 @@ definePageMeta({
1919
2020const router = useRouter ()
2121
22+ const header = useTemplateRef (' header' )
23+ const isHeaderPinned = shallowRef (false )
24+
25+ function checkHeaderPosition() {
26+ const el = header .value
27+ if (! el ) return
28+
29+ const style = getComputedStyle (el )
30+ const top = parseFloat (style .top ) || 0
31+ const rect = el .getBoundingClientRect ()
32+
33+ isHeaderPinned .value = Math .abs (rect .top - top ) < 1
34+ }
35+
36+ useEventListener (' scroll' , checkHeaderPosition , { passive: true })
37+ useEventListener (' resize' , checkHeaderPosition )
38+
39+ onMounted (() => {
40+ checkHeaderPosition ()
41+ })
42+
2243const { packageName, requestedVersion, orgName } = usePackageRoute ()
2344const selectedPM = useSelectedPackageManager ()
2445const activePmId = computed (() => selectedPM .value ?? ' npm' )
@@ -389,153 +410,157 @@ function handleClick(event: MouseEvent) {
389410 </script >
390411
391412<template >
392- <main class =" container flex-1 w-full py-8 xl:py-12 " >
413+ <main class =" container flex-1 w-full py-8" >
393414 <PackageSkeleton v-if =" status === 'pending'" />
394415
395416 <article v-else-if =" status === 'success' && pkg" class =" package-page" >
396417 <!-- Package header -->
397- <header class =" area-header border-b border-border" >
398- <div class =" mb-4" >
399- <!-- Package name and version -->
400- <div class =" flex items-baseline gap-2 mb-1.5 sm:gap-3 sm:mb-2 flex-wrap min-w-0" >
401- <h1
402- class =" font-mono text-2xl sm:text-3xl font-medium min-w-0 break-words"
403- :title =" pkg.name"
418+ <header
419+ class =" area-header sticky top-14 z-1 bg-[--bg] py-2 border-border"
420+ ref =" header"
421+ :class =" { 'border-b': isHeaderPinned }"
422+ >
423+ <!-- Package name and version -->
424+ <div class =" flex items-baseline gap-2 sm:gap-3 flex-wrap min-w-0" >
425+ <h1
426+ class =" font-mono text-2xl sm:text-3xl font-medium min-w-0 break-words"
427+ :title =" pkg.name"
428+ >
429+ <NuxtLink
430+ v-if =" orgName"
431+ :to =" { name: 'org', params: { org: orgName } }"
432+ class =" text-fg-muted hover:text-fg transition-colors duration-200"
433+ >@{{ orgName }}</NuxtLink
434+ ><span v-if =" orgName" >/</span >
435+ <TooltipAnnounce :text =" $t('common.copied')" :isVisible =" copiedPkgName" >
436+ <button
437+ @click =" copyPkgName()"
438+ aria-describedby =" copy-pkg-name"
439+ class =" cursor-copy active:scale-95 transition-transform"
440+ >
441+ {{ orgName ? pkg.name.replace(`@${orgName}/`, '') : pkg.name }}
442+ </button >
443+ </TooltipAnnounce >
444+ </h1 >
445+
446+ <span id =" copy-pkg-name" class =" sr-only" >{{ $t('package.copy_name') }}</span >
447+ <span
448+ v-if =" displayVersion"
449+ class =" inline-flex items-baseline gap-1.5 font-mono text-base sm:text-lg text-fg-muted shrink-0"
450+ >
451+ <!-- Version resolution indicator (e.g., "latest → 4.2.0") -->
452+ <template v-if =" resolvedVersion !== requestedVersion " >
453+ <span class =" font-mono text-fg-muted text-sm" >{{ requestedVersion }}</span >
454+ <span class =" i-carbon:arrow-right rtl-flip w-3 h-3" aria-hidden =" true" />
455+ </template >
456+
457+ <NuxtLink
458+ v-if =" resolvedVersion !== requestedVersion"
459+ :to =" `/${pkg.name}/v/${displayVersion.version}`"
460+ :title =" $t('package.view_permalink')"
461+ >{{ displayVersion.version }}</NuxtLink
404462 >
405- <NuxtLink
406- v-if =" orgName"
407- :to =" { name: 'org', params: { org: orgName } }"
408- class =" text-fg-muted hover:text-fg transition-colors duration-200"
409- >@{{ orgName }}</NuxtLink
410- ><span v-if =" orgName" >/</span >
411- <TooltipAnnounce :text =" $t('common.copied')" :isVisible =" copiedPkgName" >
412- <button
413- @click =" copyPkgName()"
414- aria-describedby =" copy-pkg-name"
415- class =" cursor-copy ms-1 mt-1 active:scale-95 transition-transform"
416- >
417- {{ orgName ? pkg.name.replace(`@${orgName}/`, '') : pkg.name }}
418- </button >
419- </TooltipAnnounce >
420- </h1 >
463+ <span v-else >v{{ displayVersion.version }}</span >
421464
422- <span id =" copy-pkg-name" class =" sr-only" >{{ $t('package.copy_name') }}</span >
465+ <a
466+ v-if =" hasProvenance(displayVersion)"
467+ :href =" `https://www.npmjs.com/package/${pkg.name}/v/${displayVersion.version}#provenance`"
468+ target =" _blank"
469+ rel =" noopener noreferrer"
470+ 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"
471+ :title =" $t('package.verified_provenance')"
472+ >
473+ <span class =" i-solar:shield-check-outline w-3.5 h-3.5 shrink-0" aria-hidden =" true" />
474+ </a >
423475 <span
424- v-if =" displayVersion"
425- class =" inline-flex items-baseline gap-1.5 font-mono text-base sm:text-lg text-fg-muted shrink-0"
476+ v-if ="
477+ requestedVersion &&
478+ latestVersion &&
479+ displayVersion.version !== latestVersion.version
480+ "
481+ class =" text-fg-subtle text-sm shrink-0"
482+ >{{ $t('package.not_latest') }}</span
426483 >
427- <!-- Version resolution indicator (e.g., "latest → 4.2.0") -->
428- <template v-if =" resolvedVersion !== requestedVersion " >
429- <span class =" font-mono text-fg-muted text-sm" >{{ requestedVersion }}</span >
430- <span class =" i-carbon:arrow-right rtl-flip w-3 h-3" aria-hidden =" true" />
431- </template >
484+ </span >
432485
433- <NuxtLink
434- v-if =" resolvedVersion !== requestedVersion"
435- :to =" `/${pkg.name}/v/${displayVersion.version}`"
436- :title =" $t('package.view_permalink')"
437- >{{ displayVersion.version }}</NuxtLink
438- >
439- <span v-else >v{{ displayVersion.version }}</span >
440-
441- <a
442- v-if =" hasProvenance(displayVersion)"
443- :href =" `https://www.npmjs.com/package/${pkg.name}/v/${displayVersion.version}#provenance`"
444- target =" _blank"
445- rel =" noopener noreferrer"
446- 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"
447- :title =" $t('package.verified_provenance')"
448- >
449- <span
450- class =" i-solar:shield-check-outline w-3.5 h-3.5 shrink-0"
451- aria-hidden =" true"
452- />
453- </a >
454- <span
455- v-if ="
456- requestedVersion &&
457- latestVersion &&
458- displayVersion.version !== latestVersion.version
459- "
460- class =" text-fg-subtle text-sm shrink-0"
461- >{{ $t('package.not_latest') }}</span
462- >
463- </span >
464-
465- <!-- Package metrics (module format, types) -->
466- <ClientOnly >
467- <PackageMetricsBadges
468- v-if =" displayVersion"
469- :package-name =" pkg.name"
470- :version =" displayVersion.version"
471- :is-binary =" isBinaryOnly"
472- class =" self-baseline ms-1 sm:ms-2"
473- />
474- <template #fallback >
475- <ul class =" flex items-center gap-1.5 self-baseline ms-1 sm:ms-2" >
476- <li class =" skeleton w-8 h-5 rounded" />
477- <li class =" skeleton w-12 h-5 rounded" />
478- </ul >
479- </template >
480- </ClientOnly >
481-
482- <!-- Internal navigation: Docs + Code + Compare (hidden on mobile, shown in external links instead) -->
483- <nav
486+ <!-- Package metrics (module format, types) -->
487+ <ClientOnly >
488+ <PackageMetricsBadges
484489 v-if =" displayVersion"
485- :aria-label =" $t('package.navigation')"
486- 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+ :package-name =" pkg.name"
491+ :version =" displayVersion.version"
492+ :is-binary =" isBinaryOnly"
493+ class =" self-baseline ms-1 sm:ms-2"
494+ />
495+ <template #fallback >
496+ <ul class =" flex items-center gap-1.5 self-baseline ms-1 sm:ms-2" >
497+ <li class =" skeleton w-8 h-5 rounded" />
498+ <li class =" skeleton w-12 h-5 rounded" />
499+ </ul >
500+ </template >
501+ </ClientOnly >
502+
503+ <!-- Internal navigation: Docs + Code + Compare (hidden on mobile, shown in external links instead) -->
504+ <nav
505+ v-if =" displayVersion"
506+ :aria-label =" $t('package.navigation')"
507+ 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"
508+ >
509+ <NuxtLink
510+ v-if =" docsLink"
511+ :to =" docsLink"
512+ 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"
513+ aria-keyshortcuts =" d"
487514 >
488- <NuxtLink
489- v-if = " docsLink "
490- :to = " docsLink "
491- 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 "
492- aria-keyshortcuts = " d "
515+ <span class = " i-carbon:document w-3 h-3 " aria-hidden = " true " />
516+ {{ $t('package.links.docs') }}
517+ < kbd
518+ class =" inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded "
519+ aria-hidden = " true "
493520 >
494- < span class = " i-carbon:document w-3 h-3 " aria-hidden = " true " />
495- {{ $t('package.links.docs') }}
496- < kbd
497- class = " inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded "
498- aria-hidden = " true "
499- >
500- d
501- </ kbd >
502- </ NuxtLink >
503- < NuxtLink
504- :to = " {
505- name: 'code',
506- params: {
507- path: [...pkg.name.split('/'), 'v', displayVersion.version],
508- },
509- } "
510- 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 "
511- aria-keyshortcuts = " . "
521+ d
522+ </ kbd >
523+ </ NuxtLink >
524+ < NuxtLink
525+ :to = " {
526+ name: 'code',
527+ params: {
528+ path: [...pkg.name.split('/'), 'v', displayVersion.version],
529+ },
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 "
512539 >
513- < span class = " i-carbon:code w-3 h-3 " aria-hidden = " true " />
514- {{ $t('package.links.code') }}
515- < kbd
516- class = " inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded "
517- aria-hidden = " true "
518- >
519- .
520- </ kbd >
521- </ NuxtLink >
522- < NuxtLink
523- :to = " { path: '/compare', query: { packages: pkg.name } } "
524- 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 "
525- 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 "
526553 >
527- <span class =" i-carbon:compare w-3 h-3" aria-hidden =" true" />
528- {{ $t('package.links.compare') }}
529- <kbd
530- class =" inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded"
531- aria-hidden =" true"
532- >
533- c
534- </kbd >
535- </NuxtLink >
536- </nav >
537- </div >
554+ c
555+ </kbd >
556+ </NuxtLink >
557+ </nav >
558+ </div >
559+ </header >
538560
561+ <!-- Package details -->
562+ <section class =" area-details" >
563+ <div class =" mb-4" >
539564 <!-- Description container with min-height to prevent CLS -->
540565 <div class =" max-w-2xl min-h-[4.5rem]" >
541566 <p v-if =" pkg.description" class =" text-fg-muted text-base m-0" >
@@ -698,7 +723,7 @@ function handleClick(event: MouseEvent) {
698723
699724 <!-- Stats grid -->
700725 <dl
701- class =" grid grid-cols-2 sm:grid-cols-11 gap-3 sm:gap-4 py-4 sm:py-6 mt-4 sm:mt-6 border-t border-border"
726+ class =" grid grid-cols-2 sm:grid-cols-11 gap-3 sm:gap-4 py-4 sm:py-6 mt-4 sm:mt-6 border-t border-b border- border"
702727 >
703728 <div v-if =" pkg.license" class =" space-y-1 sm:col-span-2" >
704729 <dt class =" text-xs text-fg-subtle uppercase tracking-wider" >
@@ -859,7 +884,7 @@ function handleClick(event: MouseEvent) {
859884 :version =" displayVersion?.version"
860885 />
861886 </ClientOnly >
862- </header >
887+ </section >
863888
864889 <!-- Binary-only packages: Show only execute command (no install) -->
865890 <section v-if =" isBinaryOnly" class =" area-install scroll-mt-20" >
@@ -965,7 +990,7 @@ function handleClick(event: MouseEvent) {
965990
966991 <div class =" area-sidebar" >
967992 <!-- Sidebar -->
968- <div class =" sticky top-20 space-y-6 sm:space-y-8 min-w-0 overflow-hidden" >
993+ <div class =" sticky top-34 space-y-6 sm:space-y-8 min-w-0 overflow-hidden xl:(top-22 pt-2) " >
969994 <!-- Maintainers (with admin actions when connected) -->
970995 <PackageMaintainers :package-name =" pkg.name" :maintainers =" pkg.maintainers" />
971996
@@ -1113,6 +1138,7 @@ function handleClick(event: MouseEvent) {
11131138 grid-template-columns : minmax (0 , 1fr );
11141139 grid-template-areas :
11151140 ' header'
1141+ ' details'
11161142 ' install'
11171143 ' vulns'
11181144 ' sidebar'
@@ -1125,6 +1151,7 @@ function handleClick(event: MouseEvent) {
11251151 grid-template-columns : 2fr 1fr ;
11261152 grid-template-areas :
11271153 ' header header'
1154+ ' details details'
11281155 ' install install'
11291156 ' vulns vulns'
11301157 ' readme sidebar' ;
@@ -1138,6 +1165,7 @@ function handleClick(event: MouseEvent) {
11381165 grid-template-columns : 1fr 20rem ;
11391166 grid-template-areas :
11401167 ' header sidebar'
1168+ ' details sidebar'
11411169 ' install sidebar'
11421170 ' vulns sidebar'
11431171 ' readme sidebar' ;
@@ -1146,7 +1174,10 @@ function handleClick(event: MouseEvent) {
11461174
11471175.area-header {
11481176 grid-area : header;
1149- overflow-x : hidden ;
1177+ }
1178+
1179+ .area-details {
1180+ grid-area : details;
11501181}
11511182
11521183.area-install {
0 commit comments