Skip to content

Commit ed609e8

Browse files
committed
version selector
1 parent 9c33f0a commit ed609e8

2 files changed

Lines changed: 145 additions & 1 deletion

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<script setup lang="ts">
2+
import { onClickOutside } from '@vueuse/core'
3+
import { compareVersions } from '~/utils/versions'
4+
5+
const props = defineProps<{
6+
packageName: string
7+
currentVersion: string
8+
versions: Record<string, unknown>
9+
distTags: Record<string, string>
10+
}>()
11+
12+
const isOpen = ref(false)
13+
const dropdownRef = ref<HTMLElement>()
14+
15+
onClickOutside(dropdownRef, () => {
16+
isOpen.value = false
17+
})
18+
19+
/** Maximum number of versions to show in dropdown */
20+
const MAX_VERSIONS = 10
21+
22+
/** Get sorted list of recent versions with their tags */
23+
const recentVersions = computed(() => {
24+
const versionList = Object.keys(props.versions)
25+
.sort((a, b) => compareVersions(b, a))
26+
.slice(0, MAX_VERSIONS)
27+
28+
// Create a map of version -> tags
29+
const versionTags = new Map<string, string[]>()
30+
for (const [tag, version] of Object.entries(props.distTags)) {
31+
const existing = versionTags.get(version)
32+
if (existing) {
33+
existing.push(tag)
34+
} else {
35+
versionTags.set(version, [tag])
36+
}
37+
}
38+
39+
return versionList.map(version => ({
40+
version,
41+
tags: versionTags.get(version) ?? [],
42+
isCurrent: version === props.currentVersion,
43+
}))
44+
})
45+
46+
const latestVersion = computed(() => props.distTags.latest)
47+
48+
function getDocsUrl(version: string): string {
49+
return `/docs/${props.packageName}/v/${version}`
50+
}
51+
52+
function handleKeydown(event: KeyboardEvent) {
53+
if (event.key === 'Escape') {
54+
isOpen.value = false
55+
}
56+
}
57+
</script>
58+
59+
<template>
60+
<div ref="dropdownRef" class="relative">
61+
<button
62+
type="button"
63+
aria-haspopup="listbox"
64+
:aria-expanded="isOpen"
65+
class="flex items-center gap-1.5 text-fg-subtle font-mono text-sm hover:text-fg transition-colors"
66+
@click="isOpen = !isOpen"
67+
@keydown="handleKeydown"
68+
>
69+
<span>{{ currentVersion }}</span>
70+
<span
71+
v-if="currentVersion === latestVersion"
72+
class="text-[10px] px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-400 font-sans font-medium"
73+
>
74+
latest
75+
</span>
76+
<span
77+
class="i-carbon-chevron-down w-3.5 h-3.5 transition-transform duration-200"
78+
:class="{ 'rotate-180': isOpen }"
79+
aria-hidden="true"
80+
/>
81+
</button>
82+
83+
<Transition
84+
enter-active-class="transition duration-150 ease-out"
85+
enter-from-class="opacity-0 scale-95"
86+
enter-to-class="opacity-100 scale-100"
87+
leave-active-class="transition duration-100 ease-in"
88+
leave-from-class="opacity-100 scale-100"
89+
leave-to-class="opacity-0 scale-95"
90+
>
91+
<div
92+
v-if="isOpen"
93+
role="listbox"
94+
:aria-activedescendant="`version-${currentVersion}`"
95+
class="absolute top-full left-0 mt-2 min-w-[180px] bg-bg-elevated border border-border rounded-lg shadow-lg z-50 py-1 max-h-[300px] overflow-y-auto"
96+
@keydown="handleKeydown"
97+
>
98+
<NuxtLink
99+
v-for="{ version, tags, isCurrent } in recentVersions"
100+
:id="`version-${version}`"
101+
:key="version"
102+
:to="getDocsUrl(version)"
103+
role="option"
104+
:aria-selected="isCurrent"
105+
class="flex items-center justify-between gap-3 px-3 py-2 text-sm font-mono hover:bg-bg-muted transition-colors"
106+
:class="isCurrent ? 'text-fg bg-bg-muted' : 'text-fg-muted'"
107+
@click="isOpen = false"
108+
>
109+
<span class="truncate">{{ version }}</span>
110+
<span v-if="tags.length > 0" class="flex items-center gap-1 shrink-0">
111+
<span
112+
v-for="tag in tags"
113+
:key="tag"
114+
class="text-[10px] px-1.5 py-0.5 rounded font-sans font-medium"
115+
:class="tag === 'latest' ? 'bg-emerald-500/10 text-emerald-400' : 'bg-bg-muted text-fg-subtle'"
116+
>
117+
{{ tag }}
118+
</span>
119+
</span>
120+
</NuxtLink>
121+
122+
<div
123+
v-if="Object.keys(versions).length > MAX_VERSIONS"
124+
class="border-t border-border mt-1 pt-1 px-3 py-2"
125+
>
126+
<NuxtLink
127+
:to="`/${packageName}`"
128+
class="text-xs text-fg-subtle hover:text-fg transition-colors"
129+
@click="isOpen = false"
130+
>
131+
View all {{ Object.keys(versions).length }} versions
132+
</NuxtLink>
133+
</div>
134+
</div>
135+
</Transition>
136+
</div>
137+
</template>

app/pages/docs/[...path].vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,14 @@ const showEmptyState = computed(() => docsData.value?.status !== 'ok')
101101
>
102102
{{ packageName }}
103103
</NuxtLink>
104-
<span v-if="resolvedVersion" class="text-fg-subtle font-mono text-sm shrink-0">
104+
<DocsVersionSelector
105+
v-if="resolvedVersion && pkg?.versions && pkg?.['dist-tags']"
106+
:package-name="packageName"
107+
:current-version="resolvedVersion"
108+
:versions="pkg.versions"
109+
:dist-tags="pkg['dist-tags']"
110+
/>
111+
<span v-else-if="resolvedVersion" class="text-fg-subtle font-mono text-sm shrink-0">
105112
{{ resolvedVersion }}
106113
</span>
107114
</div>

0 commit comments

Comments
 (0)