11<script setup lang="ts">
22import { WindowVirtualizer } from ' virtua/vue'
33import { getVersions } from ' fast-npm-meta'
4+ import { validRange } from ' semver'
45import {
56 buildVersionToTagsMap ,
67 buildTaggedVersionRows ,
8+ filterVersions ,
79 getVersionGroupKey ,
810 getVersionGroupLabel ,
911} from ' ~/utils/versions'
@@ -17,7 +19,6 @@ definePageMeta({
1719const SSR_COUNT = 20
1820
1921const route = useRoute ()
20- const router = useRouter ()
2122
2223const packageName = computed (() => {
2324 const { org, name } = route .params as { org? : string ; name: string }
@@ -125,6 +126,33 @@ async function toggleGroup(groupKey: string) {
125126 }
126127}
127128
129+ // ─── Version filter ───────────────────────────────────────────────────────────
130+
131+ const versionFilter = ref (' ' )
132+ const isFilterActive = computed (() => versionFilter .value .trim () !== ' ' )
133+
134+ const filteredVersionSet = computed (() => {
135+ const trimmed = versionFilter .value .trim ()
136+ if (! trimmed ) return null
137+ // Try semver range first (e.g. "^2.0.0", ">=1 <3")
138+ if (validRange (trimmed )) {
139+ return filterVersions (versionStrings .value , trimmed )
140+ }
141+ // Fallback: substring match (e.g. "2.4", "beta")
142+ const lower = trimmed .toLowerCase ()
143+ return new Set (versionStrings .value .filter (v => v .toLowerCase ().includes (lower )))
144+ })
145+
146+ const filteredGroups = computed (() => {
147+ if (! isFilterActive .value || ! filteredVersionSet .value ) return versionGroups .value
148+ return versionGroups .value
149+ .map (group => ({
150+ ... group ,
151+ versions: group .versions .filter (v => filteredVersionSet .value ! .has (v )),
152+ }))
153+ .filter (group => group .versions .length > 0 )
154+ })
155+
128156// ─── Flat list for virtual rendering ──────────────────────────────────────────
129157
130158type FlatItem =
@@ -133,14 +161,14 @@ type FlatItem =
133161
134162const flatItems = computed <FlatItem []>(() => {
135163 const items: FlatItem [] = []
136- for (const group of versionGroups .value ) {
164+ for (const group of filteredGroups .value ) {
137165 items .push ({
138166 type: ' header' ,
139167 groupKey: group .groupKey ,
140168 label: group .label ,
141169 versions: group .versions ,
142170 })
143- if (expandedGroups .value .has (group .groupKey )) {
171+ if (expandedGroups .value .has (group .groupKey ) || isFilterActive . value ) {
144172 for (const version of group .versions ) {
145173 items .push ({ type: ' version' , version , groupKey: group .groupKey })
146174 }
@@ -161,26 +189,6 @@ const selectedChangelogContent = computed(() => {
161189// function toggleChangelog(version: string) {
162190// selectedChangelogVersion.value = selectedChangelogVersion.value === version ? null : version
163191// }
164-
165- // ─── Jump to version ──────────────────────────────────────────────────────────
166-
167- const jumpVersion = ref (' ' )
168- const jumpError = ref (' ' )
169-
170- function navigateToVersion() {
171- const v = jumpVersion .value .trim ()
172- if (! v ) return
173- if (! versionStrings .value .includes (v )) {
174- jumpError .value = ` "${v }" not found `
175- return
176- }
177- jumpError .value = ' '
178- router .push (packageRoute (packageName .value , v ))
179- }
180-
181- watch (jumpVersion , () => {
182- jumpError .value = ' '
183- })
184192 </script >
185193
186194<template >
@@ -201,35 +209,14 @@ watch(jumpVersion, () => {
201209 <span class =" text-fg-subtle shrink-0" >/</span >
202210 <span class =" font-mono text-sm text-fg-muted shrink-0" >Version History</span >
203211 </div >
204- <div class =" flex flex-col items-end gap-1 shrink-0" >
205- <div class =" flex items-center gap-2" >
206- <InputBase
207- v-model =" jumpVersion"
208- type =" text"
209- placeholder =" Jump to version…"
210- aria-label =" Jump to version"
211- size =" small"
212- class =" w-36 sm:w-44 font-mono"
213- @keydown.enter =" navigateToVersion"
214- />
215- <ButtonBase
216- variant =" secondary"
217- size =" small"
218- classicon =" i-lucide:arrow-right"
219- :disabled =" !jumpVersion.trim()"
220- @click =" navigateToVersion"
221- >
222- Go
223- </ButtonBase >
224- </div >
225- <p
226- v-if =" jumpError"
227- role =" alert"
228- class =" text-red-500 dark:text-red-400 text-xs leading-none"
229- >
230- {{ jumpError }}
231- </p >
232- </div >
212+ <InputBase
213+ v-model =" versionFilter"
214+ type =" text"
215+ placeholder =" Filter versions…"
216+ aria-label =" Filter versions"
217+ size =" small"
218+ class =" w-36 sm:w-44 font-mono"
219+ />
233220 </div >
234221 </header >
235222
@@ -345,8 +332,18 @@ watch(jumpVersion, () => {
345332 </span >
346333 </h2 >
347334
335+ <!-- No filter matches -->
336+ <div
337+ v-if =" isFilterActive && filteredGroups.length === 0"
338+ class =" px-1 py-4 text-sm text-fg-subtle"
339+ role =" status"
340+ aria-live =" polite"
341+ >
342+ No versions match <span class =" font-mono" >{{ versionFilter }}</span >
343+ </div >
344+
348345 <!-- List + changelog side panel -->
349- <div class =" flex" >
346+ <div v-else class =" flex" >
350347 <!-- Version list (grouped by major, virtualized) -->
351348 <div
352349 class =" flex-1 min-w-0 border-y sm:border border-border sm:rounded-lg sm:overflow-hidden"
0 commit comments