|
1 | 1 | <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' |
3 | 8 |
|
4 | 9 | definePageMeta({ |
5 | 10 | name: 'package-versions', |
@@ -49,6 +54,41 @@ function getVersionTime(version: string): string | undefined { |
49 | 54 | return versionHistory.value.find(v => v.version === version)?.time |
50 | 55 | } |
51 | 56 |
|
| 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 | +
|
52 | 92 | // ─── Changelog side panel ───────────────────────────────────────────────────── |
53 | 93 |
|
54 | 94 | const selectedChangelogVersion = ref<string | null>(null) |
@@ -247,111 +287,144 @@ watch(jumpVersion, () => { |
247 | 287 |
|
248 | 288 | <!-- List + changelog side panel --> |
249 | 289 | <div class="flex"> |
250 | | - <!-- Version list --> |
| 290 | + <!-- Version list (grouped by major) --> |
251 | 291 | <div |
252 | 292 | class="flex-1 min-w-0 border-y sm:border border-border sm:rounded-lg sm:overflow-hidden" |
253 | 293 | > |
254 | 294 | <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" |
259 | 298 | > |
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)" |
263 | 306 | > |
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"> |
319 | 308 | <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' : ''" |
322 | 311 | aria-hidden="true" |
323 | 312 | /> |
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> |
326 | 320 | <DateTime |
327 | | - v-if="v.time" |
328 | | - :datetime="v.time" |
| 321 | + v-if="group.versions[0]?.time" |
| 322 | + :datetime="group.versions[0].time" |
329 | 323 | class="text-xs text-fg-subtle hidden sm:block" |
330 | 324 | year="numeric" |
331 | 325 | month="short" |
332 | 326 | day="numeric" |
333 | 327 | /> |
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> |
343 | 330 |
|
344 | | - <!-- Mobile inline changelog (below the row, sm and up uses side panel) --> |
| 331 | + <!-- Expanded versions --> |
345 | 332 | <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]'" |
351 | 335 | > |
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> |
355 | 428 | </div> |
356 | 429 | </div> |
357 | 430 | </div> |
|
0 commit comments