Skip to content

Commit 8b74db0

Browse files
okineadevdanielroejonathanyeong9romiseuserquin
authored
feat: allow linking to individual headers on package page (#224)
Co-authored-by: Daniel Roe <daniel@roe.dev> Co-authored-by: Jonathan Yeong <hey@jonathanyeong.com> Co-authored-by: Vida Xie <vida_2020@163.com> Co-authored-by: Joaquín Sánchez <userquin@gmail.com>
1 parent ca8f6e4 commit 8b74db0

6 files changed

Lines changed: 193 additions & 46 deletions

File tree

app/components/PackageDependencies.vue

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,26 @@ const sortedOptionalDependencies = computed(() => {
4848
<template>
4949
<div class="space-y-8">
5050
<!-- Dependencies -->
51-
<section v-if="sortedDependencies.length > 0" aria-labelledby="dependencies-heading">
52-
<h2 id="dependencies-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-3">
53-
{{ $t('package.dependencies.title', { count: sortedDependencies.length }) }}
51+
<section
52+
id="dependencies"
53+
v-if="sortedDependencies.length > 0"
54+
aria-labelledby="dependencies-heading"
55+
class="scroll-mt-20"
56+
>
57+
<h2
58+
id="dependencies-heading"
59+
class="group text-xs text-fg-subtle uppercase tracking-wider mb-3"
60+
>
61+
<a
62+
href="#dependencies"
63+
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
64+
>
65+
{{ $t('package.dependencies.title', { count: sortedDependencies.length }) }}
66+
<span
67+
class="i-carbon-link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
68+
aria-hidden="true"
69+
/>
70+
</a>
5471
</h2>
5572
<ul class="space-y-1 list-none m-0 p-0" :aria-label="$t('package.dependencies.list_label')">
5673
<li
@@ -99,12 +116,26 @@ const sortedOptionalDependencies = computed(() => {
99116
</section>
100117

101118
<!-- Peer Dependencies -->
102-
<section v-if="sortedPeerDependencies.length > 0" aria-labelledby="peer-dependencies-heading">
119+
<section
120+
id="peer-dependencies"
121+
v-if="sortedPeerDependencies.length > 0"
122+
aria-labelledby="peer-dependencies-heading"
123+
class="scroll-mt-20"
124+
>
103125
<h2
104126
id="peer-dependencies-heading"
105-
class="text-xs text-fg-subtle uppercase tracking-wider mb-3"
127+
class="group text-xs text-fg-subtle uppercase tracking-wider mb-3"
106128
>
107-
{{ $t('package.peer_dependencies.title', { count: sortedPeerDependencies.length }) }}
129+
<a
130+
href="#peer-dependencies"
131+
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
132+
>
133+
{{ $t('package.peer_dependencies.title', { count: sortedPeerDependencies.length }) }}
134+
<span
135+
class="i-carbon-link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
136+
aria-hidden="true"
137+
/>
138+
</a>
108139
</h2>
109140
<ul
110141
class="space-y-1 list-none m-0 p-0"
@@ -154,16 +185,27 @@ const sortedOptionalDependencies = computed(() => {
154185

155186
<!-- Optional Dependencies -->
156187
<section
188+
id="optional-dependencies"
157189
v-if="sortedOptionalDependencies.length > 0"
158190
aria-labelledby="optional-dependencies-heading"
191+
class="scroll-mt-20"
159192
>
160193
<h2
161194
id="optional-dependencies-heading"
162-
class="text-xs text-fg-subtle uppercase tracking-wider mb-3"
195+
class="group text-xs text-fg-subtle uppercase tracking-wider mb-3"
163196
>
164-
{{
165-
$t('package.optional_dependencies.title', { count: sortedOptionalDependencies.length })
166-
}}
197+
<a
198+
href="#optional-dependencies"
199+
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
200+
>
201+
{{
202+
$t('package.optional_dependencies.title', { count: sortedOptionalDependencies.length })
203+
}}
204+
<span
205+
class="i-carbon-link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
206+
aria-hidden="true"
207+
/>
208+
</a>
167209
</h2>
168210
<ul
169211
class="space-y-1 list-none m-0 p-0"

app/components/PackageMaintainers.vue

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,23 @@ watch(
153153
</script>
154154

155155
<template>
156-
<section v-if="maintainers?.length" aria-labelledby="maintainers-heading">
157-
<h2 id="maintainers-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-3">
158-
{{ $t('package.maintainers.title') }}
156+
<section
157+
id="maintainers"
158+
v-if="maintainers?.length"
159+
aria-labelledby="maintainers-heading"
160+
class="scroll-mt-20"
161+
>
162+
<h2 id="maintainers-heading" class="group text-xs text-fg-subtle uppercase tracking-wider mb-3">
163+
<a
164+
href="#maintainers"
165+
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
166+
>
167+
{{ $t('package.maintainers.title') }}
168+
<span
169+
class="i-carbon-link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
170+
aria-hidden="true"
171+
/>
172+
</a>
159173
</h2>
160174
<ul class="space-y-2 list-none m-0 p-0" :aria-label="$t('package.maintainers.list_label')">
161175
<li

app/components/PackageVersions.vue

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,9 +305,23 @@ function getTagVersions(tag: string): VersionDisplay[] {
305305
</script>
306306

307307
<template>
308-
<section v-if="allTagRows.length > 0" aria-labelledby="versions-heading" class="overflow-hidden">
309-
<h2 id="versions-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-3">
310-
{{ $t('package.versions.title') }}
308+
<section
309+
id="versions"
310+
v-if="allTagRows.length > 0"
311+
aria-labelledby="versions-heading"
312+
class="overflow-hidden scroll-mt-20"
313+
>
314+
<h2 id="versions-heading" class="group text-xs text-fg-subtle uppercase tracking-wider mb-3">
315+
<a
316+
href="#versions"
317+
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
318+
>
319+
{{ $t('package.versions.title') }}
320+
<span
321+
class="i-carbon-link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
322+
aria-hidden="true"
323+
/>
324+
</a>
311325
</h2>
312326

313327
<div class="space-y-0.5 min-w-0">

app/components/PackageWeeklyDownloadStats.vue

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,19 @@ const config = computed(() => {
144144

145145
<template>
146146
<div class="space-y-8">
147-
<section>
147+
<section id="downloads" class="scroll-mt-20">
148148
<div class="flex items-center justify-between mb-3">
149-
<h2 class="text-xs text-fg-subtle uppercase tracking-wider">
150-
{{ $t('package.downloads.title') }}
149+
<h2 class="group text-xs text-fg-subtle uppercase tracking-wider">
150+
<a
151+
href="#downloads"
152+
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
153+
>
154+
{{ $t('package.downloads.title') }}
155+
<span
156+
class="i-carbon-link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
157+
aria-hidden="true"
158+
/>
159+
</a>
151160
</h2>
152161
<button
153162
type="button"
@@ -211,7 +220,7 @@ const config = computed(() => {
211220
class="w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg flex items-center justify-center text-fg-muted hover:text-fg transition-colors"
212221
:aria-label="$t('common.close')"
213222
>
214-
<span class="w-5 h-5 i-carbon-close" />
223+
<span class="w-5 h-5 i-carbon-close" aria-hidden="true" />
215224
</button>
216225
</div>
217226
</template>

app/pages/[...package].vue

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -685,10 +685,19 @@ defineOgImageComponent('Package', {
685685
/>
686686

687687
<!-- Install command with package manager selector -->
688-
<section aria-labelledby="install-heading" class="area-install">
688+
<section id="install" aria-labelledby="install-heading" class="area-install scroll-mt-20">
689689
<div class="flex flex-wrap items-center justify-between mb-3">
690-
<h2 id="install-heading" class="text-xs text-fg-subtle uppercase tracking-wider">
691-
{{ $t('package.install.title') }}
690+
<h2 id="install-heading" class="group text-xs text-fg-subtle uppercase tracking-wider">
691+
<a
692+
href="#install"
693+
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
694+
>
695+
{{ $t('package.install.title') }}
696+
<span
697+
class="i-carbon-link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
698+
aria-hidden="true"
699+
/>
700+
</a>
692701
</h2>
693702
<!-- Package manager tabs -->
694703
<div
@@ -787,9 +796,22 @@ defineOgImageComponent('Package', {
787796
</section>
788797

789798
<!-- README -->
790-
<section aria-labelledby="readme-heading" class="area-readme min-w-0">
791-
<h2 id="readme-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-4">
792-
{{ $t('package.readme.title') }}
799+
<section
800+
id="readme"
801+
aria-labelledby="readme-heading"
802+
class="area-readme min-w-0 scroll-mt-20"
803+
>
804+
<h2 id="readme-heading" class="group text-xs text-fg-subtle uppercase tracking-wider mb-4">
805+
<a
806+
href="#readme"
807+
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
808+
>
809+
{{ $t('package.readme.title') }}
810+
<span
811+
class="i-carbon-link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
812+
aria-hidden="true"
813+
/>
814+
</a>
793815
</h2>
794816
<!-- eslint-disable vue/no-v-html -- HTML is sanitized server-side -->
795817
<div
@@ -817,9 +839,26 @@ defineOgImageComponent('Package', {
817839
</ClientOnly>
818840

819841
<!-- Keywords -->
820-
<section v-if="displayVersion?.keywords?.length" aria-labelledby="keywords-heading">
821-
<h2 id="keywords-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-3">
822-
{{ $t('package.keywords_title') }}
842+
<section
843+
id="keywords"
844+
v-if="displayVersion?.keywords?.length"
845+
aria-labelledby="keywords-heading"
846+
class="scroll-mt-20"
847+
>
848+
<h2
849+
id="keywords-heading"
850+
class="group text-xs text-fg-subtle uppercase tracking-wider mb-3"
851+
>
852+
<a
853+
href="#keywords"
854+
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
855+
>
856+
{{ $t('package.keywords_title') }}
857+
<span
858+
class="i-carbon-link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
859+
aria-hidden="true"
860+
/>
861+
</a>
823862
</h2>
824863
<ul class="flex flex-wrap gap-1.5 list-none m-0 p-0">
825864
<li v-for="keyword in displayVersion.keywords.slice(0, 15)" :key="keyword">
@@ -840,16 +879,27 @@ defineOgImageComponent('Package', {
840879
/>
841880

842881
<section
882+
id="compatibility"
843883
v-if="
844884
displayVersion?.engines && (displayVersion.engines.node || displayVersion.engines.npm)
845885
"
846886
aria-labelledby="compatibility-heading"
887+
class="scroll-mt-20"
847888
>
848889
<h2
849890
id="compatibility-heading"
850-
class="text-xs text-fg-subtle uppercase tracking-wider mb-3"
891+
class="group text-xs text-fg-subtle uppercase tracking-wider mb-3"
851892
>
852-
{{ $t('package.compatibility') }}
893+
<a
894+
href="#compatibility"
895+
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
896+
>
897+
{{ $t('package.compatibility') }}
898+
<span
899+
class="i-carbon-link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
900+
aria-hidden="true"
901+
/>
902+
</a>
853903
</h2>
854904
<dl class="space-y-2">
855905
<div v-if="displayVersion.engines.node" class="flex justify-between gap-4 py-1">

test/nuxt/components/PackageVersions.spec.ts

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,12 @@ describe('PackageVersions', () => {
8282
},
8383
})
8484

85-
const link = component.find('a')
86-
expect(link.exists()).toBe(true)
87-
expect(link.text()).toBe('2.0.0')
85+
// Find version links (exclude anchor links that start with #)
86+
const versionLinks = component
87+
.findAll('a')
88+
.filter(a => !a.attributes('href')?.startsWith('#'))
89+
expect(versionLinks.length).toBeGreaterThan(0)
90+
expect(versionLinks[0]?.text()).toBe('2.0.0')
8891
})
8992

9093
it('renders scoped package version links correctly', async () => {
@@ -99,9 +102,12 @@ describe('PackageVersions', () => {
99102
},
100103
})
101104

102-
const link = component.find('a')
103-
expect(link.exists()).toBe(true)
104-
expect(link.text()).toBe('1.0.0')
105+
// Find version links (exclude anchor links that start with #)
106+
const versionLinks = component
107+
.findAll('a')
108+
.filter(a => !a.attributes('href')?.startsWith('#'))
109+
expect(versionLinks.length).toBeGreaterThan(0)
110+
expect(versionLinks[0]?.text()).toBe('1.0.0')
105111
})
106112
})
107113

@@ -190,8 +196,11 @@ describe('PackageVersions', () => {
190196
},
191197
})
192198

193-
const links = component.findAll('a')
194-
const versions = links.map(l => l.text())
199+
// Find version links (exclude anchor links that start with #)
200+
const versionLinks = component
201+
.findAll('a')
202+
.filter(a => !a.attributes('href')?.startsWith('#'))
203+
const versions = versionLinks.map(l => l.text())
195204
// Should be sorted by version descending
196205
expect(versions[0]).toBe('2.0.0')
197206
})
@@ -210,8 +219,12 @@ describe('PackageVersions', () => {
210219
},
211220
})
212221

213-
const link = component.find('a')
214-
expect(link.classes()).toContain('text-red-400')
222+
// Find version links (exclude anchor links that start with #)
223+
const versionLinks = component
224+
.findAll('a')
225+
.filter(a => !a.attributes('href')?.startsWith('#'))
226+
expect(versionLinks.length).toBeGreaterThan(0)
227+
expect(versionLinks[0]?.classes()).toContain('text-red-400')
215228
})
216229

217230
it('shows deprecated version in title attribute', async () => {
@@ -226,8 +239,12 @@ describe('PackageVersions', () => {
226239
},
227240
})
228241

229-
const link = component.find('a')
230-
expect(link.attributes('title')).toContain('deprecated')
242+
// Find version links (exclude anchor links that start with #)
243+
const versionLinks = component
244+
.findAll('a')
245+
.filter(a => !a.attributes('href')?.startsWith('#'))
246+
expect(versionLinks.length).toBeGreaterThan(0)
247+
expect(versionLinks[0]?.attributes('title')).toContain('deprecated')
231248
})
232249

233250
it('filters deprecated tags from visible list when package is not deprecated', async () => {
@@ -552,9 +569,10 @@ describe('PackageVersions', () => {
552569
},
553570
})
554571

555-
// Count visible version links (excluding "Other versions" section)
556-
// The first set of links before the "Other versions" button
557-
const visibleLinks = component.findAll('a')
572+
// Count visible version links (excluding anchor links that start with #)
573+
const visibleLinks = component
574+
.findAll('a')
575+
.filter(a => !a.attributes('href')?.startsWith('#'))
558576
// Should have max 10 visible links in the main section
559577
expect(visibleLinks.length).toBeLessThanOrEqual(10)
560578
})

0 commit comments

Comments
 (0)