Skip to content

Commit 37ede33

Browse files
committed
feat: link to org page and better display dependencies
1 parent 4e170ce commit 37ede33

2 files changed

Lines changed: 189 additions & 57 deletions

File tree

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<script setup lang="ts">
2+
const props = defineProps<{
3+
packageName: string
4+
dependencies?: Record<string, string>
5+
peerDependencies?: Record<string, string>
6+
peerDependenciesMeta?: Record<string, { optional?: boolean }>
7+
}>()
8+
9+
// Expanded state for each section
10+
const depsExpanded = ref(false)
11+
const peerDepsExpanded = ref(false)
12+
13+
// Sort dependencies alphabetically
14+
const sortedDependencies = computed(() => {
15+
if (!props.dependencies) return []
16+
return Object.entries(props.dependencies)
17+
.sort(([a], [b]) => a.localeCompare(b))
18+
})
19+
20+
// Sort peer dependencies alphabetically, with required first then optional
21+
const sortedPeerDependencies = computed(() => {
22+
if (!props.peerDependencies) return []
23+
24+
return Object.entries(props.peerDependencies)
25+
.map(([name, version]) => ({
26+
name,
27+
version,
28+
optional: props.peerDependenciesMeta?.[name]?.optional ?? false,
29+
}))
30+
.sort((a, b) => {
31+
// Required first, then optional
32+
if (a.optional !== b.optional) return a.optional ? 1 : -1
33+
return a.name.localeCompare(b.name)
34+
})
35+
})
36+
37+
// Check if a version string is "long" (multiple alternatives)
38+
function isLongVersion(version: string): boolean {
39+
return version.length > 20 || version.includes('||')
40+
}
41+
42+
// Truncate long version strings for display
43+
function truncateVersion(version: string, maxLength = 20): string {
44+
if (version.length <= maxLength) return version
45+
return `${version.slice(0, maxLength)}…`
46+
}
47+
</script>
48+
49+
<template>
50+
<div class="space-y-8">
51+
<!-- Dependencies -->
52+
<section
53+
v-if="sortedDependencies.length > 0"
54+
aria-labelledby="dependencies-heading"
55+
>
56+
<div class="flex items-center justify-between mb-3">
57+
<h2
58+
id="dependencies-heading"
59+
class="text-xs text-fg-subtle uppercase tracking-wider"
60+
>
61+
Dependencies ({{ sortedDependencies.length }})
62+
</h2>
63+
<a
64+
:href="`https://npmgraph.js.org/?q=${packageName}`"
65+
target="_blank"
66+
rel="noopener noreferrer"
67+
class="link-subtle text-fg-subtle"
68+
aria-label="View dependency graph"
69+
title="View dependency graph"
70+
>
71+
<span class="text-xs uppercase tracking-wider">
72+
Graph
73+
</span>
74+
</a>
75+
</div>
76+
<ul
77+
class="space-y-1 list-none m-0 p-0"
78+
aria-label="Package dependencies"
79+
>
80+
<li
81+
v-for="[dep, version] in sortedDependencies.slice(0, depsExpanded ? undefined : 10)"
82+
:key="dep"
83+
class="flex items-center justify-between py-1 text-sm gap-2"
84+
>
85+
<NuxtLink
86+
:to="`/package/${dep}`"
87+
class="font-mono text-fg-muted hover:text-fg transition-colors duration-200 truncate min-w-0"
88+
>
89+
{{ dep }}
90+
</NuxtLink>
91+
<span
92+
class="font-mono text-xs text-fg-subtle max-w-[50%] text-right"
93+
:class="isLongVersion(version) ? 'truncate' : 'shrink-0'"
94+
:title="isLongVersion(version) ? version : undefined"
95+
>
96+
{{ isLongVersion(version) ? truncateVersion(version) : version }}
97+
</span>
98+
</li>
99+
</ul>
100+
<button
101+
v-if="sortedDependencies.length > 10 && !depsExpanded"
102+
type="button"
103+
class="mt-2 font-mono text-xs text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
104+
@click="depsExpanded = true"
105+
>
106+
show all {{ sortedDependencies.length }} deps
107+
</button>
108+
</section>
109+
110+
<!-- Peer Dependencies -->
111+
<section
112+
v-if="sortedPeerDependencies.length > 0"
113+
aria-labelledby="peer-dependencies-heading"
114+
>
115+
<h2
116+
id="peer-dependencies-heading"
117+
class="text-xs text-fg-subtle uppercase tracking-wider mb-3"
118+
>
119+
Peer Dependencies ({{ sortedPeerDependencies.length }})
120+
</h2>
121+
<ul
122+
class="space-y-1 list-none m-0 p-0"
123+
aria-label="Package peer dependencies"
124+
>
125+
<li
126+
v-for="peer in sortedPeerDependencies.slice(0, peerDepsExpanded ? undefined : 10)"
127+
:key="peer.name"
128+
class="flex items-center justify-between py-1 text-sm gap-2"
129+
>
130+
<div class="flex items-center gap-2 min-w-0">
131+
<NuxtLink
132+
:to="`/package/${peer.name}`"
133+
class="font-mono text-fg-muted hover:text-fg transition-colors duration-200 truncate"
134+
>
135+
{{ peer.name }}
136+
</NuxtLink>
137+
<span
138+
v-if="peer.optional"
139+
class="px-1 py-0.5 font-mono text-[10px] text-fg-subtle bg-bg-muted border border-border rounded shrink-0"
140+
title="Optional peer dependency"
141+
>
142+
optional
143+
</span>
144+
</div>
145+
<span
146+
class="font-mono text-xs text-fg-subtle max-w-[40%] text-right"
147+
:class="isLongVersion(peer.version) ? 'truncate' : 'shrink-0'"
148+
:title="isLongVersion(peer.version) ? peer.version : undefined"
149+
>
150+
{{ isLongVersion(peer.version) ? truncateVersion(peer.version) : peer.version }}
151+
</span>
152+
</li>
153+
</ul>
154+
<button
155+
v-if="sortedPeerDependencies.length > 10 && !peerDepsExpanded"
156+
type="button"
157+
class="mt-2 font-mono text-xs text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
158+
@click="peerDepsExpanded = true"
159+
>
160+
show all {{ sortedPeerDependencies.length }} peer deps
161+
</button>
162+
</section>
163+
</div>
164+
</template>

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

Lines changed: 25 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ const parsedRoute = computed(() => {
3232
const packageName = computed(() => parsedRoute.value.packageName)
3333
const requestedVersion = computed(() => parsedRoute.value.requestedVersion)
3434
35+
// Extract org name from scoped package (e.g., "@nuxt/kit" -> "nuxt")
36+
const orgName = computed(() => {
37+
const name = packageName.value
38+
if (!name.startsWith('@')) return null
39+
const match = name.match(/^@([^/]+)\//)
40+
return match ? match[1] : null
41+
})
42+
3543
const { data: pkg, status, error } = usePackage(packageName)
3644
3745
const { data: downloads } = usePackageDownloads(packageName, 'last-week')
@@ -75,11 +83,11 @@ const sortedVersions = computed(() => {
7583
.slice(0, 20)
7684
})
7785
78-
// Sort dependencies alphabetically
79-
const sortedDependencies = computed(() => {
80-
if (!displayVersion.value?.dependencies) return []
81-
return Object.entries(displayVersion.value.dependencies)
82-
.sort(([a], [b]) => a.localeCompare(b))
86+
const hasDependencies = computed(() => {
87+
if (!displayVersion.value) return false
88+
const deps = displayVersion.value.dependencies
89+
const peerDeps = displayVersion.value.peerDependencies
90+
return (deps && Object.keys(deps).length > 0) || (peerDeps && Object.keys(peerDeps).length > 0)
8391
})
8492
8593
const repositoryUrl = computed(() => {
@@ -185,9 +193,6 @@ const descriptionExpanded = ref(false)
185193
const descriptionRef = ref<HTMLDivElement>()
186194
const descriptionOverflows = ref(false)
187195
188-
// Expandable dependencies
189-
const depsExpanded = ref(false)
190-
191196
// Check if description overflows on mount/update
192197
function checkDescriptionOverflow() {
193198
if (descriptionRef.value) {
@@ -235,7 +240,11 @@ defineOgImageComponent('Package', {
235240
<!-- Package name and version -->
236241
<div class="flex items-center gap-3 mb-2 flex-wrap">
237242
<h1 class="font-mono text-2xl sm:text-3xl font-medium">
238-
{{ pkg.name }}
243+
<NuxtLink
244+
v-if="orgName"
245+
:to="`/org/${orgName}`"
246+
class="text-fg-muted hover:text-fg transition-colors duration-200"
247+
>@{{ orgName }}</NuxtLink><span v-if="orgName">/</span>{{ orgName ? pkg.name.replace(`@${orgName}/`, '') : pkg.name }}
239248
</h1>
240249
<a
241250
v-if="displayVersion"
@@ -659,54 +668,13 @@ defineOgImageComponent('Package', {
659668
</section>
660669

661670
<!-- Dependencies -->
662-
<section
663-
v-if="sortedDependencies.length > 0"
664-
aria-labelledby="dependencies-heading"
665-
>
666-
<div class="flex items-center justify-between mb-3">
667-
<h2
668-
id="dependencies-heading"
669-
class="text-xs text-fg-subtle uppercase tracking-wider"
670-
>
671-
Dependencies ({{ sortedDependencies.length }})
672-
</h2>
673-
<a
674-
:href="`https://npmgraph.js.org/?q=${pkg.name}`"
675-
target="_blank"
676-
rel="noopener noreferrer"
677-
class="link-subtle text-fg-subtle"
678-
aria-label="View dependency graph"
679-
title="View dependency graph"
680-
>
681-
<span class="text-xs uppercase tracking-wider">
682-
Graph
683-
</span>
684-
</a>
685-
</div>
686-
<ul class="space-y-1 list-none m-0 p-0">
687-
<li
688-
v-for="[dep, version] in sortedDependencies.slice(0, depsExpanded ? undefined : 10)"
689-
:key="dep"
690-
class="flex items-center justify-between py-1 text-sm"
691-
>
692-
<NuxtLink
693-
:to="`/package/${dep}`"
694-
class="font-mono text-fg-muted hover:text-fg transition-colors duration-200 truncate mr-2"
695-
>
696-
{{ dep }}
697-
</NuxtLink>
698-
<span class="font-mono text-xs text-fg-subtle shrink-0">{{ version }}</span>
699-
</li>
700-
</ul>
701-
<button
702-
v-if="sortedDependencies.length > 10 && !depsExpanded"
703-
type="button"
704-
class="mt-2 font-mono text-xs text-fg-muted hover:text-fg transition-colors duration-200"
705-
@click="depsExpanded = true"
706-
>
707-
show all {{ sortedDependencies.length }} deps
708-
</button>
709-
</section>
671+
<PackageDependencies
672+
v-if="hasDependencies"
673+
:package-name="pkg.name"
674+
:dependencies="displayVersion?.dependencies"
675+
:peer-dependencies="displayVersion?.peerDependencies"
676+
:peer-dependencies-meta="displayVersion?.peerDependenciesMeta"
677+
/>
710678
</aside>
711679
</div>
712680
</article>

0 commit comments

Comments
 (0)