Skip to content

Commit 00288a2

Browse files
committed
feat: grouped versions
1 parent 0dd1bfa commit 00288a2

1 file changed

Lines changed: 161 additions & 88 deletions

File tree

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

Lines changed: 161 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
<script setup lang="ts">
2-
import { buildVersionToTagsMap, buildTaggedVersionRows } from '~/utils/versions'
2+
import {
3+
buildVersionToTagsMap,
4+
buildTaggedVersionRows,
5+
getVersionGroupKey,
6+
getVersionGroupLabel,
7+
} from '~/utils/versions'
38
49
definePageMeta({
510
name: 'package-versions',
@@ -49,6 +54,41 @@ function getVersionTime(version: string): string | undefined {
4954
return versionHistory.value.find(v => v.version === version)?.time
5055
}
5156
57+
// ─── Version groups ───────────────────────────────────────────────────────────
58+
59+
const expandedGroups = ref(new Set<string>())
60+
61+
const versionGroups = computed(() => {
62+
const byKey = new Map<string, typeof sortedVersions.value>()
63+
for (const v of sortedVersions.value) {
64+
const key = getVersionGroupKey(v.version)
65+
if (!byKey.has(key)) byKey.set(key, [])
66+
byKey.get(key)!.push(v)
67+
}
68+
69+
return Array.from(byKey.keys())
70+
.sort((a, b) => {
71+
const [aMajor, aMinor] = a.split('.').map(Number)
72+
const [bMajor, bMinor] = b.split('.').map(Number)
73+
if (aMajor !== bMajor) return (bMajor ?? 0) - (aMajor ?? 0)
74+
return (bMinor ?? -1) - (aMinor ?? -1)
75+
})
76+
.map(groupKey => ({
77+
groupKey,
78+
label: getVersionGroupLabel(groupKey),
79+
versions: byKey.get(groupKey)!,
80+
}))
81+
})
82+
83+
function toggleGroup(groupKey: string) {
84+
if (expandedGroups.value.has(groupKey)) {
85+
expandedGroups.value.delete(groupKey)
86+
} else {
87+
expandedGroups.value.add(groupKey)
88+
}
89+
expandedGroups.value = new Set(expandedGroups.value)
90+
}
91+
5292
// ─── Changelog side panel ─────────────────────────────────────────────────────
5393
5494
const selectedChangelogVersion = ref<string | null>(null)
@@ -247,111 +287,144 @@ watch(jumpVersion, () => {
247287

248288
<!-- List + changelog side panel -->
249289
<div class="flex">
250-
<!-- Version list -->
290+
<!-- Version list (grouped by major) -->
251291
<div
252292
class="flex-1 min-w-0 border-y sm:border border-border sm:rounded-lg sm:overflow-hidden"
253293
>
254294
<div
255-
v-for="v in sortedVersions"
256-
:key="v.version"
257-
class="border-b border-border last:border-0 transition-colors"
258-
:class="selectedChangelogVersion === v.version ? 'bg-bg-subtle' : ''"
295+
v-for="group in versionGroups"
296+
:key="group.groupKey"
297+
class="border-b border-border last:border-0"
259298
>
260-
<div
261-
class="flex items-center gap-3 px-4 py-2.5 group relative"
262-
:class="selectedChangelogVersion === v.version ? '' : 'hover:bg-bg-subtle'"
299+
<!-- Group header -->
300+
<button
301+
type="button"
302+
class="flex items-center gap-3 px-4 py-2.5 w-full text-start hover:bg-bg-subtle transition-colors"
303+
:aria-expanded="expandedGroups.has(group.groupKey)"
304+
:aria-label="`${expandedGroups.has(group.groupKey) ? 'Collapse' : 'Expand'} ${group.label}`"
305+
@click="toggleGroup(group.groupKey)"
263306
>
264-
<!-- Version + badges -->
265-
<div class="flex-1 min-w-0 flex items-center gap-2 flex-wrap">
266-
<LinkBase
267-
:to="packageRoute(packageName, v.version)"
268-
class="font-mono text-sm after:absolute after:inset-0 after:content-['']"
269-
:class="v.deprecated ? 'text-red-700 dark:text-red-400' : ''"
270-
:classicon="v.deprecated ? 'i-lucide:octagon-alert' : undefined"
271-
dir="ltr"
272-
>
273-
{{ v.version }}
274-
</LinkBase>
275-
<div
276-
v-if="v.tags?.length"
277-
class="flex items-center gap-1 flex-wrap relative z-10"
278-
>
279-
<span
280-
v-for="tag in v.tags"
281-
:key="tag"
282-
class="text-4xs font-semibold uppercase tracking-wide"
283-
:class="tag === 'latest' ? 'text-accent' : 'text-fg-subtle'"
284-
>
285-
{{ tag }}
286-
</span>
287-
</div>
288-
<span
289-
v-if="v.deprecated"
290-
class="text-3xs font-medium text-red-700 dark:text-red-400 bg-red-100 dark:bg-red-900/30 px-1.5 py-0.5 rounded relative z-10"
291-
:title="v.deprecated"
292-
>
293-
deprecated
294-
</span>
295-
</div>
296-
297-
<!-- Right side -->
298-
<div class="flex items-center gap-2 shrink-0 relative z-10">
299-
<!-- Changelog toggle button -->
300-
<!-- TODO(atriiy): changelog would be implemented later -->
301-
<!-- <button
302-
v-if="v.hasChangelog"
303-
type="button"
304-
class="flex items-center gap-1.5 text-xs px-2 py-1 rounded border transition-colors focus-visible:outline-accent/70"
305-
:class="
306-
selectedChangelogVersion === v.version
307-
? 'border-accent/50 bg-accent/8 text-accent'
308-
: 'border-border text-fg-subtle hover:text-fg hover:border-border-hover'
309-
"
310-
:aria-expanded="selectedChangelogVersion === v.version"
311-
:aria-label="`Toggle changelog for v${v.version}`"
312-
@click.stop="toggleChangelog(v.version)"
313-
>
314-
<span class="i-lucide:scroll-text w-3.5 h-3.5 shrink-0" aria-hidden="true" />
315-
<span class="hidden sm:inline">Changelog</span>
316-
</button> -->
317-
318-
<!-- Divider -->
307+
<span class="w-4 h-4 flex items-center justify-center text-fg-subtle shrink-0">
319308
<span
320-
v-if="v.hasChangelog"
321-
class="w-px h-3.5 bg-border shrink-0 hidden sm:block"
309+
class="i-lucide:chevron-right w-3 h-3 transition-transform duration-200 rtl-flip"
310+
:class="expandedGroups.has(group.groupKey) ? 'rotate-90' : ''"
322311
aria-hidden="true"
323312
/>
324-
325-
<!-- Metadata: date + provenance -->
313+
</span>
314+
<span class="font-mono text-sm font-medium">{{ group.label }}</span>
315+
<span class="text-xs text-fg-subtle">({{ group.versions.length }})</span>
316+
<span class="ms-auto flex items-center gap-3 shrink-0">
317+
<span class="font-mono text-xs text-fg-muted" dir="ltr">{{
318+
group.versions[0]?.version
319+
}}</span>
326320
<DateTime
327-
v-if="v.time"
328-
:datetime="v.time"
321+
v-if="group.versions[0]?.time"
322+
:datetime="group.versions[0].time"
329323
class="text-xs text-fg-subtle hidden sm:block"
330324
year="numeric"
331325
month="short"
332326
day="numeric"
333327
/>
334-
<ProvenanceBadge
335-
v-if="v.hasProvenance"
336-
:package-name="packageName"
337-
:version="v.version"
338-
compact
339-
:linked="false"
340-
/>
341-
</div>
342-
</div>
328+
</span>
329+
</button>
343330

344-
<!-- Mobile inline changelog (below the row, sm and up uses side panel) -->
331+
<!-- Expanded versions -->
345332
<div
346-
v-if="v.hasChangelog"
347-
class="grid sm:hidden transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none"
348-
:class="
349-
selectedChangelogVersion === v.version ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
350-
"
333+
class="grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none"
334+
:class="expandedGroups.has(group.groupKey) ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'"
351335
>
352-
<div class="overflow-hidden">
353-
<div class="changelog-body border-t border-border px-4 py-3 text-sm">
354-
{{ selectedChangelogVersion === v.version ? selectedChangelogContent : '' }}
336+
<div class="overflow-hidden border-t border-border">
337+
<div
338+
v-for="v in group.versions"
339+
:key="v.version"
340+
class="border-b border-border last:border-0 transition-colors"
341+
:class="selectedChangelogVersion === v.version ? 'bg-bg-subtle' : ''"
342+
>
343+
<div
344+
class="flex items-center gap-3 px-4 ps-11 py-2.5 group relative"
345+
:class="selectedChangelogVersion === v.version ? '' : 'hover:bg-bg-subtle'"
346+
>
347+
<!-- Version + badges -->
348+
<div class="flex-1 min-w-0 flex items-center gap-2 flex-wrap">
349+
<LinkBase
350+
:to="packageRoute(packageName, v.version)"
351+
class="font-mono text-sm after:absolute after:inset-0 after:content-['']"
352+
:class="v.deprecated ? 'text-red-700 dark:text-red-400' : ''"
353+
:classicon="v.deprecated ? 'i-lucide:octagon-alert' : undefined"
354+
dir="ltr"
355+
>
356+
{{ v.version }}
357+
</LinkBase>
358+
<div
359+
v-if="v.tags?.length"
360+
class="flex items-center gap-1 flex-wrap relative z-10"
361+
>
362+
<span
363+
v-for="tag in v.tags"
364+
:key="tag"
365+
class="text-4xs font-semibold uppercase tracking-wide"
366+
:class="tag === 'latest' ? 'text-accent' : 'text-fg-subtle'"
367+
>
368+
{{ tag }}
369+
</span>
370+
</div>
371+
<span
372+
v-if="v.deprecated"
373+
class="text-3xs font-medium text-red-700 dark:text-red-400 bg-red-100 dark:bg-red-900/30 px-1.5 py-0.5 rounded relative z-10"
374+
:title="v.deprecated"
375+
>
376+
deprecated
377+
</span>
378+
</div>
379+
380+
<!-- Right side -->
381+
<div class="flex items-center gap-2 shrink-0 relative z-10">
382+
<!-- TODO(atriiy): changelog would be implemented later -->
383+
384+
<!-- Divider -->
385+
<span
386+
v-if="v.hasChangelog"
387+
class="w-px h-3.5 bg-border shrink-0 hidden sm:block"
388+
aria-hidden="true"
389+
/>
390+
391+
<!-- Metadata: date + provenance -->
392+
<DateTime
393+
v-if="v.time"
394+
:datetime="v.time"
395+
class="text-xs text-fg-subtle hidden sm:block"
396+
year="numeric"
397+
month="short"
398+
day="numeric"
399+
/>
400+
<ProvenanceBadge
401+
v-if="v.hasProvenance"
402+
:package-name="packageName"
403+
:version="v.version"
404+
compact
405+
:linked="false"
406+
/>
407+
</div>
408+
</div>
409+
410+
<!-- Mobile inline changelog (below the row, sm and up uses side panel) -->
411+
<div
412+
v-if="v.hasChangelog"
413+
class="grid sm:hidden transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none"
414+
:class="
415+
selectedChangelogVersion === v.version
416+
? 'grid-rows-[1fr]'
417+
: 'grid-rows-[0fr]'
418+
"
419+
>
420+
<div class="overflow-hidden">
421+
<div class="changelog-body border-t border-border px-4 py-3 text-sm">
422+
{{
423+
selectedChangelogVersion === v.version ? selectedChangelogContent : ''
424+
}}
425+
</div>
426+
</div>
427+
</div>
355428
</div>
356429
</div>
357430
</div>

0 commit comments

Comments
 (0)