@@ -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' )
@@ -372,150 +393,154 @@ function handleClick(event: MouseEvent) {
372393 </script >
373394
374395<template >
375- <main class =" container flex-1 py-8 xl:py-12 " >
396+ <main class =" container flex-1 py-8" >
376397 <PackageSkeleton v-if =" status === 'pending'" />
377398
378399 <article v-else-if =" status === 'success' && pkg" class =" package-page" >
379400 <!-- 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"
401+ <header
402+ class =" area-header sticky top-14 z-1 bg-[--bg] py-2 border-border"
403+ ref =" header"
404+ :class =" { 'border-b': isHeaderPinned }"
405+ >
406+ <!-- Package name and version -->
407+ <div class =" flex items-baseline gap-2 sm:gap-3 flex-wrap min-w-0" >
408+ <h1
409+ class =" font-mono text-2xl sm:text-3xl font-medium min-w-0 break-words"
410+ :title =" pkg.name"
411+ >
412+ <NuxtLink
413+ v-if =" orgName"
414+ :to =" { name: 'org', params: { org: orgName } }"
415+ class =" text-fg-muted hover:text-fg transition-colors duration-200"
416+ >@{{ orgName }}</NuxtLink
417+ ><span v-if =" orgName" >/</span >
418+ <AnnounceTooltip :text =" $t('common.copied')" :isVisible =" copiedPkgName" >
419+ <button
420+ @click =" copyPkgName()"
421+ aria-describedby =" copy-pkg-name"
422+ class =" cursor-copy active:scale-95 transition-transform"
423+ >
424+ {{ orgName ? pkg.name.replace(`@${orgName}/`, '') : pkg.name }}
425+ </button >
426+ </AnnounceTooltip >
427+ </h1 >
428+
429+ <span id =" copy-pkg-name" class =" sr-only" >{{ $t('package.copy_name') }}</span >
430+ <span
431+ v-if =" displayVersion"
432+ class =" inline-flex items-baseline gap-1.5 font-mono text-base sm:text-lg text-fg-muted shrink-0"
433+ >
434+ <!-- Version resolution indicator (e.g., "latest → 4.2.0") -->
435+ <template v-if =" resolvedVersion !== requestedVersion " >
436+ <span class =" font-mono text-fg-muted text-sm" >{{ requestedVersion }}</span >
437+ <span class =" i-carbon:arrow-right rtl-flip w-3 h-3" aria-hidden =" true" />
438+ </template >
439+
440+ <NuxtLink
441+ v-if =" resolvedVersion !== requestedVersion"
442+ :to =" `/${pkg.name}/v/${displayVersion.version}`"
443+ :title =" $t('package.view_permalink')"
444+ >{{ displayVersion.version }}</NuxtLink
387445 >
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 >
446+ <span v-else >v{{ displayVersion.version }}</span >
404447
405- <span id =" copy-pkg-name" class =" sr-only" >{{ $t('package.copy_name') }}</span >
448+ <a
449+ v-if =" hasProvenance(displayVersion)"
450+ :href =" `https://www.npmjs.com/package/${pkg.name}/v/${displayVersion.version}#provenance`"
451+ target =" _blank"
452+ rel =" noopener noreferrer"
453+ 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"
454+ :title =" $t('package.verified_provenance')"
455+ >
456+ <span class =" i-solar:shield-check-outline w-3.5 h-3.5 shrink-0" aria-hidden =" true" />
457+ </a >
406458 <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"
459+ v-if ="
460+ requestedVersion &&
461+ latestVersion &&
462+ displayVersion.version !== latestVersion.version
463+ "
464+ class =" text-fg-subtle text-sm shrink-0"
465+ >{{ $t('package.not_latest') }}</span
409466 >
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 >
415-
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 >
467+ </span >
423468
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
469+ <!-- Package metrics (module format, types) -->
470+ <ClientOnly >
471+ <PackageMetricsBadges
466472 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"
473+ :package-name =" pkg.name"
474+ :version =" displayVersion.version"
475+ class =" self-baseline ms-1 sm:ms-2"
476+ />
477+ <template #fallback >
478+ <ul class =" flex items-center gap-1.5 self-baseline ms-1 sm:ms-2" >
479+ <li class =" skeleton w-8 h-5 rounded" />
480+ <li class =" skeleton w-12 h-5 rounded" />
481+ </ul >
482+ </template >
483+ </ClientOnly >
484+
485+ <!-- Internal navigation: Docs + Code + Compare (hidden on mobile, shown in external links instead) -->
486+ <nav
487+ v-if =" displayVersion"
488+ :aria-label =" $t('package.navigation')"
489+ 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+ >
491+ <NuxtLink
492+ v-if =" docsLink"
493+ :to =" docsLink"
494+ 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"
495+ aria-keyshortcuts =" d"
469496 >
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 "
497+ <span class = " i-carbon:document w-3 h-3 " aria-hidden = " true " />
498+ {{ $t('package.links.docs') }}
499+ < kbd
500+ class =" inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded "
501+ aria-hidden = " true "
475502 >
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 = " . "
503+ d
504+ </ kbd >
505+ </ NuxtLink >
506+ < NuxtLink
507+ :to = " {
508+ name: 'code',
509+ params: { path: [...pkg.name.split('/'), 'v', displayVersion.version] },
510+ } "
511+ 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 "
512+ aria-keyshortcuts = " . "
513+ >
514+ < span class = " i-carbon:code w-3 h-3 " aria-hidden = " true " />
515+ {{ $t('package.links.code') }}
516+ < kbd
517+ class =" inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded "
518+ aria-hidden = " true "
492519 >
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 "
520+ .
521+ </ kbd >
522+ </ NuxtLink >
523+ < NuxtLink
524+ :to = " { path: '/compare', query: { packages: pkg.name } } "
525+ 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 "
526+ aria-keyshortcuts = " c "
527+ >
528+ <span class = " i-carbon:compare w-3 h-3 " aria-hidden = " true " / >
529+ {{ $t('package.links.compare') }}
530+ < kbd
531+ class =" inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded "
532+ aria-hidden = " true "
506533 >
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 >
534+ c
535+ </kbd >
536+ </NuxtLink >
537+ </nav >
538+ </div >
539+ </header >
518540
541+ <!-- Package details -->
542+ <section class =" area-details" >
543+ <div class =" mb-4" >
519544 <!-- Description container with min-height to prevent CLS -->
520545 <div class =" max-w-2xl min-h-[4.5rem]" >
521546 <p v-if =" pkg.description" class =" text-fg-muted text-base m-0" >
@@ -826,7 +851,7 @@ function handleClick(event: MouseEvent) {
826851 </dd >
827852 </div >
828853 </dl >
829- </header >
854+ </section >
830855
831856 <!-- Binary-only packages: Show only execute command (no install) -->
832857 <section v-if =" isBinaryOnly" class =" area-install scroll-mt-20" >
@@ -937,7 +962,7 @@ function handleClick(event: MouseEvent) {
937962
938963 <div class =" area-sidebar" >
939964 <!-- Sidebar -->
940- <div class =" sticky top-20 space-y-6 sm:space-y-8 min-w-0 overflow-hidden" >
965+ <div class =" sticky top-34 space-y-6 sm:space-y-8 min-w-0 overflow-hidden xl:(top-22 pt-2) " >
941966 <!-- Maintainers (with admin actions when connected) -->
942967 <PackageMaintainers :package-name =" pkg.name" :maintainers =" pkg.maintainers" />
943968
@@ -1073,6 +1098,7 @@ function handleClick(event: MouseEvent) {
10731098 grid-template-columns : minmax (0 , 1fr );
10741099 grid-template-areas :
10751100 ' header'
1101+ ' details'
10761102 ' install'
10771103 ' vulns'
10781104 ' sidebar'
@@ -1085,6 +1111,7 @@ function handleClick(event: MouseEvent) {
10851111 grid-template-columns : 2fr 1fr ;
10861112 grid-template-areas :
10871113 ' header header'
1114+ ' details details'
10881115 ' install install'
10891116 ' vulns vulns'
10901117 ' readme sidebar' ;
@@ -1097,6 +1124,7 @@ function handleClick(event: MouseEvent) {
10971124 grid-template-columns : 1fr 20rem ;
10981125 grid-template-areas :
10991126 ' header sidebar'
1127+ ' details sidebar'
11001128 ' install sidebar'
11011129 ' vulns sidebar'
11021130 ' readme sidebar' ;
@@ -1105,7 +1133,10 @@ function handleClick(event: MouseEvent) {
11051133
11061134.area-header {
11071135 grid-area : header;
1108- overflow-x : hidden ;
1136+ }
1137+
1138+ .area-details {
1139+ grid-area : details;
11091140}
11101141
11111142.area-install {
0 commit comments