Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 25 additions & 10 deletions app/pages/package/[[org]]/[name]/versions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
filterVersions,
getVersionGroupKey,
getVersionGroupLabel,
getTagPriority,
} from '~/utils/versions'
import { fetchAllPackageVersions } from '~/utils/npm/api'

Expand Down Expand Up @@ -64,7 +65,21 @@ async function ensureFullDataLoaded() {
// ─── Derived data ─────────────────────────────────────────────────────────────

const versionToTagsMap = computed(() => buildVersionToTagsMap(distTags.value))

const tagRows = computed(() => buildTaggedVersionRows(distTags.value))
const latestTagRow = computed(() => tagRows.value.find(r => r.tags.includes('latest')) ?? null)
const otherTagRows = computed(() =>
tagRows.value
.filter(r => !r.tags.includes('latest'))
.sort((rowA, rowB) => {
const priorityA = Math.min(...rowA.tags.map(getTagPriority))
const priorityB = Math.min(...rowB.tags.map(getTagPriority))
if (priorityA !== priorityB) return priorityA - priorityB
const timeA = versionTimes.value[rowA.version] ?? ''
const timeB = versionTimes.value[rowB.version] ?? ''
return timeB.localeCompare(timeA)
}),
)

function getVersionTime(version: string): string | undefined {
return versionTimes.value[version]
Expand Down Expand Up @@ -215,39 +230,39 @@ const flatItems = computed<FlatItem[]>(() => {

<!-- Latest — featured card -->
<div
v-if="tagRows[0]"
v-if="latestTagRow"
class="border-y sm:rounded-lg sm:border border-accent/40 bg-accent/5 px-5 py-4 relative flex items-center justify-between gap-4 hover:bg-accent/8 transition-colors"
>
<!-- Left: tags + version -->
<div>
<div class="flex items-center gap-2 mb-1.5 flex-wrap">
<span class="text-3xs font-bold uppercase tracking-widest text-accent">latest</span>
<span
v-for="tag in tagRows[0].tags.filter(t => t !== 'latest')"
v-for="tag in latestTagRow!.tags.filter(t => t !== 'latest')"
:key="tag"
class="text-3xs font-semibold uppercase tracking-wide text-fg-subtle"
>{{ tag }}</span
>
</div>
<LinkBase
:to="packageRoute(packageName, tagRows[0].version)"
:to="packageRoute(packageName, latestTagRow!.version)"
class="text-2xl font-semibold tracking-tight after:absolute after:inset-0 after:content-['']"
dir="ltr"
>{{ tagRows[0].version }}</LinkBase
>{{ latestTagRow!.version }}</LinkBase
>
</div>
<!-- Right: date + provenance -->
<div class="flex flex-col items-end gap-1.5 shrink-0 relative z-10">
<ProvenanceBadge
v-if="fullVersionMap?.get(tagRows[0].version)?.hasProvenance"
v-if="fullVersionMap?.get(latestTagRow!.version)?.hasProvenance"
:package-name="packageName"
:version="tagRows[0].version"
:version="latestTagRow!.version"
compact
:linked="false"
/>
<DateTime
v-if="getVersionTime(tagRows[0].version)"
:datetime="getVersionTime(tagRows[0].version)!"
v-if="getVersionTime(latestTagRow!.version)"
:datetime="getVersionTime(latestTagRow!.version)!"
class="text-xs text-fg-subtle"
year="numeric"
month="short"
Expand All @@ -258,11 +273,11 @@ const flatItems = computed<FlatItem[]>(() => {

<!-- Other tags — compact list (hidden when only latest exists) -->
<div
v-if="tagRows.length > 1"
v-if="otherTagRows.length > 0"
class="border-y sm:rounded-lg sm:border border-border sm:overflow-hidden"
>
<div
v-for="row in tagRows.slice(1)"
v-for="row in otherTagRows"
:key="row.id"
class="flex items-center gap-4 px-4 py-2.5 border-b border-border last:border-0 hover:bg-bg-subtle transition-colors relative"
>
Expand Down
32 changes: 32 additions & 0 deletions app/utils/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,38 @@ export function getPrereleaseChannel(version: string): string {
return match ? match[1]!.toLowerCase() : ''
}

/**
* Priority order for well-known dist-tags.
* Lower number = higher priority in display order.
* Unknown tags fall back to Infinity and are sorted by publish date descending.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was reading through the code and wondering how it's possible that "latest" is not a well-known tag. It might be worth having a comment here explaining that it's handled manually elsewhere :)

Copy link
Contributor Author

@ShroXd ShroXd Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was reading through the code and wondering how it's possible that "latest" is not a well-known tag. It might be worth having a comment here explaining that it's handled manually elsewhere :)

Nice point, thanks for bringing it up! I reconsidered TAG_PRIORITY — it's definitely weird to exclude latest from it. Although we filter it out in the UI, it doesn't make sense for a general constant to be affected by specific UI logic.
So I've added latest: 0 and shifted the other priorities by 1.

*/
export const TAG_PRIORITY: Record<string, number> = {
latest: 0,
stable: 1,
rc: 2,
beta: 3,
next: 4,
alpha: 5,
canary: 6,
nightly: 7,
experimental: 8,
legacy: 9,
}

/**
* Get the display priority for a dist-tag.
* Uses fuzzy matching so e.g. "v2-legacy" matches "legacy".
* @param tag - The tag name (e.g., "beta", "v2-legacy")
* @returns Numeric priority (lower = higher priority); Infinity for unknown tags
*/
export function getTagPriority(tag: string | undefined): number {
if (!tag) return Infinity
for (const [key, priority] of Object.entries(TAG_PRIORITY)) {
if (tag.toLowerCase().includes(key)) return priority
}
return Infinity
}

/**
* Sort tags with 'latest' first, then alphabetically
* @param tags - Array of tag names
Expand Down
Loading