@@ -156,21 +156,27 @@ const filteredGroups = computed(() => {
156156// ─── Flat list for virtual rendering ──────────────────────────────────────────
157157
158158type FlatItem =
159- | { type: ' header' ; groupKey: string ; label: string ; versions: string [] }
160- | { type: ' version' ; version: string ; groupKey: string }
159+ | { type: ' header' ; key : string ; groupKey: string ; label: string ; versions: string [] }
160+ | { type: ' version' ; key : string ; version: string ; groupKey: string }
161161
162162const flatItems = computed <FlatItem []>(() => {
163163 const items: FlatItem [] = []
164164 for (const group of filteredGroups .value ) {
165165 items .push ({
166166 type: ' header' ,
167+ key: ` header:${group .groupKey } ` ,
167168 groupKey: group .groupKey ,
168169 label: group .label ,
169170 versions: group .versions ,
170171 })
171172 if (expandedGroups .value .has (group .groupKey ) || isFilterActive .value ) {
172173 for (const version of group .versions ) {
173- items .push ({ type: ' version' , version , groupKey: group .groupKey })
174+ items .push ({
175+ type: ' version' ,
176+ key: ` version:${version } ` ,
177+ version ,
178+ groupKey: group .groupKey ,
179+ })
174180 }
175181 }
176182 }
@@ -351,141 +357,147 @@ const selectedChangelogContent = computed(() => {
351357 <ClientOnly >
352358 <WindowVirtualizer :data =" flatItems" >
353359 <template #default =" { item , index } " >
354- <!-- ── Group header ── -->
355- <button
356- v-if =" item.type === 'header'"
357- type =" button"
358- class =" flex items-center gap-3 px-4 py-2.5 w-full text-start hover:bg-bg-subtle transition-colors"
359- :class =" index < flatItems.length - 1 ? 'border-b border-border' : ''"
360- :aria-expanded =" expandedGroups.has(item.groupKey)"
361- :aria-label =" `${expandedGroups.has(item.groupKey) ? 'Collapse' : 'Expand'} ${item.label}`"
362- @click =" toggleGroup(item.groupKey)"
363- >
364- <span class =" w-4 h-4 flex items-center justify-center text-fg-subtle shrink-0" >
365- <span
366- v-if =" loadingGroup === item.groupKey"
367- class =" i-svg-spinners:ring-resize w-3 h-3"
368- aria-hidden =" true"
369- />
370- <span
371- v-else
372- class =" i-lucide:chevron-right w-3 h-3 transition-transform duration-200 rtl-flip"
373- :class =" expandedGroups.has(item.groupKey) ? 'rotate-90' : ''"
374- aria-hidden =" true"
375- />
376- </span >
377- <span class =" font-mono text-sm font-medium" >{{ item.label }}</span >
378- <span class =" text-xs text-fg-subtle" >({{ item.versions.length }})</span >
379- <span class =" ms-auto flex items-center gap-3 shrink-0" >
380- <span class =" font-mono text-xs text-fg-muted" dir =" ltr" >{{
381- item.versions[0]
382- }}</span >
383- <DateTime
384- v-if =" getVersionTime(item.versions[0])"
385- :datetime =" getVersionTime(item.versions[0])!"
386- class =" text-xs text-fg-subtle hidden sm:block"
387- year =" numeric"
388- month =" short"
389- day =" numeric"
390- />
391- </span >
392- </button >
393-
394- <!-- ── Version row ── -->
395- <div
396- v-else
397- class =" transition-colors"
398- :class =" [
399- index < flatItems.length - 1 ? 'border-b border-border' : '',
400- selectedChangelogVersion === item.version ? 'bg-bg-subtle' : '',
401- ]"
402- >
403- <div
404- class =" flex items-center gap-3 px-4 ps-11 py-2.5 group relative"
405- :class =" selectedChangelogVersion === item.version ? '' : 'hover:bg-bg-subtle'"
360+ <div :key =" item.key" >
361+ <!-- ── Group header ── -->
362+ <button
363+ v-if =" item.type === 'header'"
364+ type =" button"
365+ class =" flex items-center gap-3 px-4 py-2.5 w-full text-start hover:bg-bg-subtle transition-colors"
366+ :class =" index < flatItems.length - 1 ? 'border-b border-border' : ''"
367+ :aria-expanded =" expandedGroups.has(item.groupKey)"
368+ :aria-label =" `${expandedGroups.has(item.groupKey) ? 'Collapse' : 'Expand'} ${item.label}`"
369+ @click =" toggleGroup(item.groupKey)"
406370 >
407- <!-- Version + badges -->
408- <div class =" flex-1 min-w-0 flex items-center gap-2 flex-wrap" >
409- <LinkBase
410- :to =" packageRoute(packageName, item.version)"
411- :prefetch =" false"
412- class =" font-mono text-sm after:absolute after:inset-0 after:content-['']"
413- :class ="
414- fullVersionMap?.get(item.version)?.deprecated
415- ? 'text-red-700 dark:text-red-400'
416- : ''
417- "
418- :classicon ="
419- fullVersionMap?.get(item.version)?.deprecated
420- ? 'i-lucide:octagon-alert'
421- : undefined
422- "
423- dir =" ltr"
424- >
425- {{ item.version }}
426- </LinkBase >
427- <div
428- v-if =" versionToTagsMap.get(item.version)?.length"
429- class =" flex items-center gap-1 flex-wrap relative z-10"
430- >
431- <span
432- v-for =" tag in versionToTagsMap.get(item.version)"
433- :key =" tag"
434- class =" text-4xs font-semibold uppercase tracking-wide"
435- :class =" tag === 'latest' ? 'text-accent' : 'text-fg-subtle'"
436- >
437- {{ tag }}
438- </span >
439- </div >
371+ <span
372+ class =" w-4 h-4 flex items-center justify-center text-fg-subtle shrink-0"
373+ >
440374 <span
441- v-if =" fullVersionMap?.get(item.version)?.deprecated"
442- 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"
443- :title =" fullVersionMap.get(item.version)!.deprecated"
444- >
445- deprecated
446- </span >
447- </div >
448-
449- <!-- Right side -->
450- <div class =" flex items-center gap-2 shrink-0 relative z-10" >
451- <!-- TODO(atriiy): changelog would be implemented later -->
452-
453- <!-- Metadata: date + provenance -->
375+ v-if =" loadingGroup === item.groupKey"
376+ class =" i-svg-spinners:ring-resize w-3 h-3"
377+ aria-hidden =" true"
378+ />
379+ <span
380+ v-else
381+ class =" i-lucide:chevron-right w-3 h-3 transition-transform duration-200 rtl-flip"
382+ :class =" expandedGroups.has(item.groupKey) ? 'rotate-90' : ''"
383+ aria-hidden =" true"
384+ />
385+ </span >
386+ <span class =" font-mono text-sm font-medium" >{{ item.label }}</span >
387+ <span class =" text-xs text-fg-subtle" >({{ item.versions.length }})</span >
388+ <span class =" ms-auto flex items-center gap-3 shrink-0" >
389+ <span class =" font-mono text-xs text-fg-muted" dir =" ltr" >{{
390+ item.versions[0]
391+ }}</span >
454392 <DateTime
455- v-if =" getVersionTime(item.version )"
456- :datetime =" getVersionTime(item.version )!"
393+ v-if =" getVersionTime(item.versions[0] )"
394+ :datetime =" getVersionTime(item.versions[0] )!"
457395 class =" text-xs text-fg-subtle hidden sm:block"
458396 year =" numeric"
459397 month =" short"
460398 day =" numeric"
461399 />
462- <ProvenanceBadge
463- v-if =" fullVersionMap?.get(item.version)?.hasProvenance"
464- :package-name =" packageName"
465- :version =" item.version"
466- compact
467- :linked =" false"
468- />
469- </div >
470- </div >
400+ </span >
401+ </button >
471402
472- <!-- Mobile inline changelog (below the row, sm and up uses side panel) -->
403+ <!-- ── Version row ── -->
473404 <div
474- v-if =" item.version in mockChangelogs"
475- class =" grid sm:hidden transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none"
476- :class ="
477- selectedChangelogVersion === item.version
478- ? 'grid-rows-[1fr]'
479- : 'grid-rows-[0fr]'
480- "
405+ v-else
406+ class =" transition-colors"
407+ :class =" [
408+ index < flatItems.length - 1 ? 'border-b border-border' : '',
409+ selectedChangelogVersion === item.version ? 'bg-bg-subtle' : '',
410+ ]"
481411 >
482- <div class =" overflow-hidden" >
483- <div class =" changelog-body border-t border-border px-4 py-3 text-sm" >
484- {{
485- selectedChangelogVersion === item.version
486- ? selectedChangelogContent
487- : ''
488- }}
412+ <div
413+ class =" flex items-center gap-3 px-4 ps-11 py-2.5 group relative"
414+ :class ="
415+ selectedChangelogVersion === item.version ? '' : 'hover:bg-bg-subtle'
416+ "
417+ >
418+ <!-- Version + badges -->
419+ <div class =" flex-1 min-w-0 flex items-center gap-2 flex-wrap" >
420+ <LinkBase
421+ :to =" packageRoute(packageName, item.version)"
422+ :prefetch =" false"
423+ class =" font-mono text-sm after:absolute after:inset-0 after:content-['']"
424+ :class ="
425+ fullVersionMap?.get(item.version)?.deprecated
426+ ? 'text-red-700 dark:text-red-400'
427+ : ''
428+ "
429+ :classicon ="
430+ fullVersionMap?.get(item.version)?.deprecated
431+ ? 'i-lucide:octagon-alert'
432+ : undefined
433+ "
434+ dir =" ltr"
435+ >
436+ {{ item.version }}
437+ </LinkBase >
438+ <div
439+ v-if =" versionToTagsMap.get(item.version)?.length"
440+ class =" flex items-center gap-1 flex-wrap relative z-10"
441+ >
442+ <span
443+ v-for =" tag in versionToTagsMap.get(item.version)"
444+ :key =" tag"
445+ class =" text-4xs font-semibold uppercase tracking-wide"
446+ :class =" tag === 'latest' ? 'text-accent' : 'text-fg-subtle'"
447+ >
448+ {{ tag }}
449+ </span >
450+ </div >
451+ <span
452+ v-if =" fullVersionMap?.get(item.version)?.deprecated"
453+ 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"
454+ :title =" fullVersionMap.get(item.version)!.deprecated"
455+ >
456+ deprecated
457+ </span >
458+ </div >
459+
460+ <!-- Right side -->
461+ <div class =" flex items-center gap-2 shrink-0 relative z-10" >
462+ <!-- TODO(atriiy): changelog would be implemented later -->
463+
464+ <!-- Metadata: date + provenance -->
465+ <DateTime
466+ v-if =" getVersionTime(item.version)"
467+ :datetime =" getVersionTime(item.version)!"
468+ class =" text-xs text-fg-subtle hidden sm:block"
469+ year =" numeric"
470+ month =" short"
471+ day =" numeric"
472+ />
473+ <ProvenanceBadge
474+ v-if =" fullVersionMap?.get(item.version)?.hasProvenance"
475+ :package-name =" packageName"
476+ :version =" item.version"
477+ compact
478+ :linked =" false"
479+ />
480+ </div >
481+ </div >
482+
483+ <!-- Mobile inline changelog (below the row, sm and up uses side panel) -->
484+ <div
485+ v-if =" item.version in mockChangelogs"
486+ class =" grid sm:hidden transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none"
487+ :class ="
488+ selectedChangelogVersion === item.version
489+ ? 'grid-rows-[1fr]'
490+ : 'grid-rows-[0fr]'
491+ "
492+ >
493+ <div class =" overflow-hidden" >
494+ <div class =" changelog-body border-t border-border px-4 py-3 text-sm" >
495+ {{
496+ selectedChangelogVersion === item.version
497+ ? selectedChangelogContent
498+ : ''
499+ }}
500+ </div >
489501 </div >
490502 </div >
491503 </div >
0 commit comments