@@ -10,7 +10,9 @@ import {
1010 filterVersions ,
1111 getVersionGroupKey ,
1212 getVersionGroupLabel ,
13+ isPrereleaseVersion ,
1314} from ' ~/utils/versions'
15+ import type { TaggedVersionRow } from ' ~/utils/versions'
1416import { fetchAllPackageVersions } from ' ~/utils/npm/api'
1517
1618definePageMeta ({
@@ -143,10 +145,37 @@ const versionToTagsMap = computed(() => buildVersionToTagsMap(distTags.value))
143145
144146const tagRows = computed (() => buildTaggedVersionRows (distTags .value ))
145147const latestTagRow = computed (() => tagRows .value .find (r => r .tags .includes (' latest' )) ?? null )
148+
149+ const otherTagRowsAll = computed (() => tagRows .value .filter (r => ! r .tags .includes (' latest' )))
150+ const stableOtherTagRows = computed (() =>
151+ otherTagRowsAll .value .filter (r => ! isPrereleaseVersion (r .version )),
152+ )
153+ const hiddenPrereleaseTagCount = computed (
154+ () => otherTagRowsAll .value .length - stableOtherTagRows .value .length ,
155+ )
156+
157+ function sortTagRows(rows : TaggedVersionRow []): TaggedVersionRow [] {
158+ if (tagsSortMode .value === ' date' ) {
159+ const dir = tagsSortOrder .value === ' desc' ? 1 : - 1
160+ return [... rows ].sort ((rowA , rowB ) => {
161+ const timeA = versionTimes .value [rowA .version ] ?? ' '
162+ const timeB = versionTimes .value [rowB .version ] ?? ' '
163+ return dir * timeB .localeCompare (timeA )
164+ })
165+ }
166+ return [... rows ].sort ((rowA , rowB ) => compareTagRows (rowA , rowB , versionTimes .value ))
167+ }
168+
169+ function selectTagsSort(mode : ' priority' | ' date' ) {
170+ if (tagsSortMode .value === mode && mode === ' date' ) {
171+ tagsSortOrder .value = tagsSortOrder .value === ' desc' ? ' asc' : ' desc'
172+ return
173+ }
174+ tagsSortMode .value = mode
175+ }
176+
146177const otherTagRows = computed (() =>
147- tagRows .value
148- .filter (r => ! r .tags .includes (' latest' ))
149- .sort ((rowA , rowB ) => compareTagRows (rowA , rowB , versionTimes .value )),
178+ sortTagRows (showHiddenTags .value ? otherTagRowsAll .value : stableOtherTagRows .value ),
150179)
151180
152181function getVersionTime(version : string ): string | undefined {
@@ -202,6 +231,29 @@ watch(
202231 { immediate: true },
203232)
204233
234+ // ─── View toggles ─────────────────────────────────────────────────────────────
235+
236+ const showPrereleases = ref (false )
237+ const showDeprecated = ref (false )
238+ const tagsSortMode = ref <' priority' | ' date' >(' priority' )
239+ const tagsSortOrder = ref <' asc' | ' desc' >(' desc' )
240+ const showHiddenTags = ref (false )
241+
242+ const visibleVersionGroups = computed (() => {
243+ if (showPrereleases .value && showDeprecated .value ) return versionGroups .value
244+ return versionGroups .value
245+ .map (group =>
246+ Object .assign ({}, group , {
247+ versions: group .versions .filter (v => {
248+ if (! showPrereleases .value && isPrereleaseVersion (v )) return false
249+ if (! showDeprecated .value && fullVersionMap .value ?.get (v )?.deprecated ) return false
250+ return true
251+ }),
252+ }),
253+ )
254+ .filter (group => group .versions .length > 0 )
255+ })
256+
205257// ─── Version filter ───────────────────────────────────────────────────────────
206258
207259const versionFilterInput = ref (' ' )
@@ -224,8 +276,8 @@ const filteredVersionSet = computed(() => {
224276})
225277
226278const filteredGroups = computed (() => {
227- if (! isFilterActive .value || ! filteredVersionSet .value ) return versionGroups .value
228- return versionGroups .value
279+ if (! isFilterActive .value || ! filteredVersionSet .value ) return visibleVersionGroups .value
280+ return visibleVersionGroups .value
229281 .map (group =>
230282 Object .assign ({}, group , {
231283 versions: group .versions .filter (v => filteredVersionSet .value ! .has (v )),
@@ -286,39 +338,71 @@ const flatItems = computed<FlatItem[]>(() => {
286338 <span class =" text-fg-subtle shrink-0" >/</span >
287339 <h1 class =" text-sm text-fg-muted shrink-0" >{{ $t('package.versions.page_title') }}</h1 >
288340 </div >
289- <div class =" relative" >
290- <InputBase
291- v-model =" versionFilterInput"
292- type =" text"
293- :placeholder =" $t('package.versions.filter_placeholder')"
294- :aria-label =" $t('package.versions.filter_placeholder')"
295- :aria-invalid =" isInvalidRange ? 'true' : undefined"
296- :aria-describedby =" isInvalidRange ? 'version-filter-error' : undefined"
297- autocomplete =" off"
298- size =" sm"
299- class =" w-36 sm:w-64"
300- :class =" isInvalidRange ? 'pe-7 !border-red-500' : ''"
301- />
302- <Transition
303- enter-active-class =" transition-all duration-150"
304- enter-from-class =" opacity-0 scale-60"
305- leave-active-class =" transition-all duration-150"
306- leave-to-class =" opacity-0 scale-60"
341+ <div class =" flex items-center gap-2" >
342+ <div
343+ class =" flex items-center gap-1"
344+ role =" group"
345+ :aria-label =" $t('package.versions.page_title')"
307346 >
308- <TooltipApp
309- v-if =" isInvalidRange"
310- :text =" $t('package.versions.filter_invalid')"
311- position =" bottom"
312- class =" absolute end-0 inset-y-0 flex items-center pe-2"
313- >
314- <span
315- id =" version-filter-error"
316- class =" i-lucide:circle-alert w-3.5 h-3.5 text-red-500 block"
317- role =" img"
318- :aria-label =" $t('package.versions.filter_invalid')"
319- />
347+ <TooltipApp :text =" $t('package.versions.show_prereleases')" position =" bottom" >
348+ <button
349+ type =" button"
350+ class =" inline-flex items-center gap-1.5 px-2 py-1 text-xs font-medium rounded-md border transition-colors cursor-pointer aria-pressed:(bg-fg/10 border-fg/20 text-fg) bg-transparent border-transparent text-fg-muted hover:bg-bg-subtle hover:text-fg"
351+ :aria-pressed =" showPrereleases"
352+ :aria-label =" $t('package.versions.show_prereleases')"
353+ @click =" showPrereleases = !showPrereleases"
354+ >
355+ <span class =" i-lucide:flask-conical w-3.5 h-3.5 shrink-0" aria-hidden =" true" />
356+ <span class =" hidden sm:inline" >{{ $t('package.versions.show_prereleases') }}</span >
357+ </button >
358+ </TooltipApp >
359+ <TooltipApp :text =" $t('package.versions.show_deprecated')" position =" bottom" >
360+ <button
361+ type =" button"
362+ class =" inline-flex items-center gap-1.5 px-2 py-1 text-xs font-medium rounded-md border transition-colors cursor-pointer aria-pressed:(bg-fg/10 border-fg/20 text-fg) bg-transparent border-transparent text-fg-muted hover:bg-bg-subtle hover:text-fg"
363+ :aria-pressed =" showDeprecated"
364+ :aria-label =" $t('package.versions.show_deprecated')"
365+ @click =" showDeprecated = !showDeprecated"
366+ >
367+ <span class =" i-lucide:archive w-3.5 h-3.5 shrink-0" aria-hidden =" true" />
368+ <span class =" hidden sm:inline" >{{ $t('package.versions.show_deprecated') }}</span >
369+ </button >
320370 </TooltipApp >
321- </Transition >
371+ </div >
372+ <div class =" relative" >
373+ <InputBase
374+ v-model =" versionFilterInput"
375+ type =" text"
376+ :placeholder =" $t('package.versions.filter_placeholder')"
377+ :aria-label =" $t('package.versions.filter_placeholder')"
378+ :aria-invalid =" isInvalidRange ? 'true' : undefined"
379+ :aria-describedby =" isInvalidRange ? 'version-filter-error' : undefined"
380+ autocomplete =" off"
381+ size =" sm"
382+ class =" w-36 sm:w-64"
383+ :class =" isInvalidRange ? 'pe-7 !border-red-500' : ''"
384+ />
385+ <Transition
386+ enter-active-class =" transition-all duration-150"
387+ enter-from-class =" opacity-0 scale-60"
388+ leave-active-class =" transition-all duration-150"
389+ leave-to-class =" opacity-0 scale-60"
390+ >
391+ <TooltipApp
392+ v-if =" isInvalidRange"
393+ :text =" $t('package.versions.filter_invalid')"
394+ position =" bottom"
395+ class =" absolute end-0 inset-y-0 flex items-center pe-2"
396+ >
397+ <span
398+ id =" version-filter-error"
399+ class =" i-lucide:circle-alert w-3.5 h-3.5 text-red-500 block"
400+ role =" img"
401+ :aria-label =" $t('package.versions.filter_invalid')"
402+ />
403+ </TooltipApp >
404+ </Transition >
405+ </div >
322406 </div >
323407 </div >
324408 </header >
@@ -327,9 +411,45 @@ const flatItems = computed<FlatItem[]>(() => {
327411 <div class =" container w-full py-8 space-y-8" >
328412 <!-- ── Current Tags ───────────────────────────────────────────────────── -->
329413 <section class =" space-y-3" >
330- <h2 class =" text-xs text-fg-subtle uppercase tracking-wider px-4 sm:px-6 ps-1" >
331- {{ $t('package.versions.current_tags') }}
332- </h2 >
414+ <div class =" flex items-center justify-between gap-2 px-4 sm:px-6 ps-1" >
415+ <h2 class =" text-xs text-fg-subtle uppercase tracking-wider" >
416+ {{ $t('package.versions.current_tags') }}
417+ </h2 >
418+ <div
419+ v-if =" otherTagRowsAll.length > 1"
420+ class =" flex items-center gap-0.5"
421+ role =" group"
422+ :aria-label =" $t('package.versions.sort_tags_by_priority')"
423+ >
424+ <button
425+ type =" button"
426+ class =" text-3xs font-semibold uppercase tracking-wide px-2 py-0.5 rounded transition-colors cursor-pointer text-fg-muted hover:text-fg aria-pressed:(bg-bg-muted text-fg)"
427+ :aria-pressed =" tagsSortMode === 'priority'"
428+ @click =" selectTagsSort('priority')"
429+ >
430+ {{ $t('package.versions.sort_tags_by_priority') }}
431+ </button >
432+ <button
433+ type =" button"
434+ class =" inline-flex items-center gap-1 text-3xs font-semibold uppercase tracking-wide px-2 py-0.5 rounded transition-colors cursor-pointer text-fg-muted hover:text-fg aria-pressed:(bg-bg-muted text-fg)"
435+ :aria-pressed =" tagsSortMode === 'date'"
436+ :aria-label ="
437+ tagsSortMode === 'date' && tagsSortOrder === 'asc'
438+ ? $t('package.versions.sort_tags_by_date_asc')
439+ : $t('package.versions.sort_tags_by_date_desc')
440+ "
441+ @click =" selectTagsSort('date')"
442+ >
443+ {{ $t('package.versions.sort_tags_by_date') }}
444+ <span
445+ v-if =" tagsSortMode === 'date'"
446+ :class =" tagsSortOrder === 'desc' ? 'i-lucide:arrow-down' : 'i-lucide:arrow-up'"
447+ class =" w-3 h-3"
448+ aria-hidden =" true"
449+ />
450+ </button >
451+ </div >
452+ </div >
333453
334454 <!-- Latest — featured card -->
335455 <div
@@ -468,6 +588,25 @@ const flatItems = computed<FlatItem[]>(() => {
468588 </div >
469589 </div >
470590 </div >
591+
592+ <!-- Hidden pre-release tags notice -->
593+ <button
594+ v-if =" hiddenPrereleaseTagCount > 0"
595+ type =" button"
596+ class =" text-xs text-fg-muted hover:text-fg transition-colors cursor-pointer inline-flex items-center gap-1.5 px-4 sm:px-6 ps-1"
597+ @click =" showHiddenTags = !showHiddenTags"
598+ >
599+ <span
600+ :class =" showHiddenTags ? 'i-lucide:eye-off' : 'i-lucide:eye'"
601+ class =" w-3 h-3"
602+ aria-hidden =" true"
603+ />
604+ <span v-if =" !showHiddenTags" >
605+ {{ $t('package.versions.tags_hidden', hiddenPrereleaseTagCount) }} —
606+ <span class =" underline" >{{ $t('package.versions.show_all_tags') }}</span >
607+ </span >
608+ <span v-else >{{ $t('package.versions.hide_prerelease_tags') }}</span >
609+ </button >
471610 </section >
472611
473612 <!-- ── Version History ───────────────────────────────────────────────── -->
0 commit comments