Skip to content

Commit da26851

Browse files
committed
feat: add toggle buttons to control prerelease & deprecated versions
1 parent 69351cf commit da26851

4 files changed

Lines changed: 225 additions & 41 deletions

File tree

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

Lines changed: 178 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {
1010
filterVersions,
1111
getVersionGroupKey,
1212
getVersionGroupLabel,
13+
isPrereleaseVersion,
1314
} from '~/utils/versions'
15+
import type { TaggedVersionRow } from '~/utils/versions'
1416
import { fetchAllPackageVersions } from '~/utils/npm/api'
1517
1618
definePageMeta({
@@ -143,10 +145,37 @@ const versionToTagsMap = computed(() => buildVersionToTagsMap(distTags.value))
143145
144146
const tagRows = computed(() => buildTaggedVersionRows(distTags.value))
145147
const 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+
146177
const 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
152181
function 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
207259
const versionFilterInput = ref('')
@@ -224,8 +276,8 @@ const filteredVersionSet = computed(() => {
224276
})
225277
226278
const 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 ───────────────────────────────────────────────── -->

app/utils/versions.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { compare, satisfies, validRange, valid } from 'semver'
1+
import { compare, prerelease, satisfies, validRange, valid } from 'semver'
22

33
/**
44
* Utilities for handling npm package versions and dist-tags
@@ -39,6 +39,15 @@ export function parseVersion(version: string): ParsedVersion {
3939
}
4040
}
4141

42+
/**
43+
* Check if a version is a pre-release (has a `-` suffix per semver).
44+
* @param version - The version string (e.g., "1.0.0-beta.1", "2.0.0")
45+
* @returns true if the version has a prerelease component
46+
*/
47+
export function isPrereleaseVersion(version: string): boolean {
48+
return prerelease(version) !== null
49+
}
50+
4251
/**
4352
* Extract the prerelease channel from a version string
4453
* @param version - The version string (e.g., "1.0.0-beta.1")

i18n/locales/en.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,16 @@
570570
},
571571
"page_title": "Version History",
572572
"current_tags": "Current Tags",
573-
"no_match_filter": "No versions match {filter}"
573+
"no_match_filter": "No versions match {filter}",
574+
"show_prereleases": "Show pre-releases",
575+
"show_deprecated": "Show deprecated",
576+
"sort_tags_by_priority": "Sort by tag",
577+
"sort_tags_by_date": "Sort by date",
578+
"sort_tags_by_date_asc": "Sort by date, oldest first",
579+
"sort_tags_by_date_desc": "Sort by date, newest first",
580+
"tags_hidden": "{count} pre-release tag hidden | {count} pre-release tags hidden",
581+
"show_all_tags": "Show all",
582+
"hide_prerelease_tags": "Hide pre-release tags"
574583
},
575584
"timeline": {
576585
"load_more": "Load more",

i18n/schema.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1716,6 +1716,33 @@
17161716
},
17171717
"no_match_filter": {
17181718
"type": "string"
1719+
},
1720+
"show_prereleases": {
1721+
"type": "string"
1722+
},
1723+
"show_deprecated": {
1724+
"type": "string"
1725+
},
1726+
"sort_tags_by_priority": {
1727+
"type": "string"
1728+
},
1729+
"sort_tags_by_date": {
1730+
"type": "string"
1731+
},
1732+
"sort_tags_by_date_asc": {
1733+
"type": "string"
1734+
},
1735+
"sort_tags_by_date_desc": {
1736+
"type": "string"
1737+
},
1738+
"tags_hidden": {
1739+
"type": "string"
1740+
},
1741+
"show_all_tags": {
1742+
"type": "string"
1743+
},
1744+
"hide_prerelease_tags": {
1745+
"type": "string"
17191746
}
17201747
},
17211748
"additionalProperties": false

0 commit comments

Comments
 (0)