@@ -16,24 +16,6 @@ interface VersionDisplay {
1616 hasProvenance: boolean
1717}
1818
19- /** A dist-tag row */
20- interface TagRow {
21- id: string
22- tag: string
23- primaryVersion: VersionDisplay
24- /** Versions in this tag's channel (same major + same prerelease type) */
25- allVersions: VersionDisplay []
26- loading: boolean
27- expanded: boolean
28- }
29-
30- /** Unclaimed major version group */
31- interface MajorGroup {
32- major: number
33- versions: VersionDisplay []
34- expanded: boolean
35- }
36-
3719// Check if a version has provenance/attestations
3820function hasProvenance(version : PackumentVersion | undefined ): boolean {
3921 if (! version ?.dist ) return false
@@ -77,11 +59,6 @@ function getPrereleaseChannel(version: string): string {
7759 return match ? match [1 ]! .toLowerCase () : ' '
7860}
7961
80- // Cached full version list
81- const allVersionsCache = ref <PackageVersionInfo [] | null >(null )
82- const loadingVersions = ref (false )
83- const hasLoadedAll = ref (false )
84-
8562// Version to tag lookup
8663const versionToTag = computed (() => {
8764 const map = new Map <string , string >()
@@ -94,16 +71,9 @@ const versionToTag = computed(() => {
9471 return map
9572})
9673
97- // Dist-tag rows (stable structure)
98- const tagRows = ref <TagRow []>([])
99-
100- // Unclaimed versions section
101- const otherVersionsExpanded = ref (false )
102- const otherMajorGroups = ref <MajorGroup []>([])
103-
104- // Initialize tag rows from props
105- watchEffect (() => {
106- const rows: TagRow [] = Object .entries (props .distTags )
74+ // Initial tag rows derived from props (SSR-safe)
75+ const initialTagRows = computed (() => {
76+ return Object .entries (props .distTags )
10777 .map (([tag , version ]) => {
10878 const versionData = props .versions [version ]
10979 return {
@@ -114,17 +84,26 @@ watchEffect(() => {
11484 time: props .time [version ],
11585 tag ,
11686 hasProvenance: hasProvenance (versionData ),
117- },
118- allVersions: [],
119- loading: false ,
120- expanded: false ,
87+ } as VersionDisplay ,
12188 }
12289 })
12390 .sort ((a , b ) => compareVersions (b .primaryVersion .version , a .primaryVersion .version ))
124-
125- tagRows .value = rows
12691})
12792
93+ // Client-side state for expansion and loaded versions
94+ const expandedTags = ref <Set <string >>(new Set ())
95+ const tagVersions = ref <Map <string , VersionDisplay []>>(new Map ())
96+ const loadingTags = ref <Set <string >>(new Set ())
97+
98+ const otherVersionsExpanded = ref (false )
99+ const otherMajorGroups = ref <Array <{ major: number , versions: VersionDisplay [], expanded: boolean }>>([])
100+ const otherVersionsLoading = ref (false )
101+
102+ // Cached full version list
103+ const allVersionsCache = ref <PackageVersionInfo [] | null >(null )
104+ const loadingVersions = ref (false )
105+ const hasLoadedAll = ref (false )
106+
128107// npm registry packument type (simplified)
129108interface NpmPackument {
130109 versions: Record <string , unknown >
@@ -162,26 +141,27 @@ async function loadAllVersions(): Promise<PackageVersionInfo[]> {
162141 .map (version => ({
163142 version ,
164143 time: data .time [version ],
165- hasProvenance: false , // We don't have this info from the basic packument
144+ hasProvenance: false ,
166145 }))
167146 .sort ((a , b ) => compareVersions (b .version , a .version ))
168147
169148 allVersionsCache .value = versions
149+ hasLoadedAll .value = true
170150 return versions
171151 }
172152 finally {
173153 loadingVersions .value = false
174154 }
175155}
176156
177- // Process loaded versions - populate tag rows and find unclaimed versions
157+ // Process loaded versions
178158function processLoadedVersions(allVersions : PackageVersionInfo []) {
179159 const distTags = props .distTags
180160
181161 // For each tag, find versions in its channel (same major + same prerelease channel)
182162 const claimedVersions = new Set <string >()
183163
184- for (const row of tagRows .value ) {
164+ for (const row of initialTagRows .value ) {
185165 const tagVersion = distTags [row .tag ]
186166 if (! tagVersion ) continue
187167
@@ -202,7 +182,7 @@ function processLoadedVersions(allVersions: PackageVersionInfo[]) {
202182 hasProvenance: v .hasProvenance ,
203183 }))
204184
205- row .allVersions = channelVersions
185+ tagVersions . value . set ( row .tag , channelVersions )
206186
207187 for (const v of channelVersions ) {
208188 claimedVersions .add (v .version )
@@ -239,22 +219,19 @@ function processLoadedVersions(allVersions: PackageVersionInfo[]) {
239219 versions: byMajor .get (major )! ,
240220 expanded: false ,
241221 }))
242-
243- hasLoadedAll .value = true
244222}
245223
246224// Expand a tag row
247- async function expandTagRow(index : number ) {
248- const row = tagRows .value [index ]
249- if (! row ) return
250-
251- if (row .expanded ) {
252- row .expanded = false
225+ async function expandTagRow(tag : string ) {
226+ if (expandedTags .value .has (tag )) {
227+ expandedTags .value .delete (tag )
228+ expandedTags .value = new Set (expandedTags .value )
253229 return
254230 }
255231
256232 if (! hasLoadedAll .value ) {
257- row .loading = true
233+ loadingTags .value .add (tag )
234+ loadingTags .value = new Set (loadingTags .value )
258235 try {
259236 const allVersions = await loadAllVersions ()
260237 processLoadedVersions (allVersions )
@@ -263,11 +240,13 @@ async function expandTagRow(index: number) {
263240 console .error (' Failed to load versions:' , error )
264241 }
265242 finally {
266- row .loading = false
243+ loadingTags .value .delete (tag )
244+ loadingTags .value = new Set (loadingTags .value )
267245 }
268246 }
269247
270- row .expanded = true
248+ expandedTags .value .add (tag )
249+ expandedTags .value = new Set (expandedTags .value )
271250}
272251
273252// Expand "Other versions" section
@@ -278,13 +257,17 @@ async function expandOtherVersions() {
278257 }
279258
280259 if (! hasLoadedAll .value ) {
260+ otherVersionsLoading .value = true
281261 try {
282262 const allVersions = await loadAllVersions ()
283263 processLoadedVersions (allVersions )
284264 }
285265 catch (error ) {
286266 console .error (' Failed to load versions:' , error )
287267 }
268+ finally {
269+ otherVersionsLoading .value = false
270+ }
288271 }
289272
290273 otherVersionsExpanded .value = true
@@ -298,6 +281,11 @@ function toggleMajorGroup(index: number) {
298281 }
299282}
300283
284+ // Get versions for a tag (from loaded data or empty)
285+ function getTagVersions(tag : string ): VersionDisplay [] {
286+ return tagVersions .value .get (tag ) ?? []
287+ }
288+
301289function formatDate(dateStr : string ): string {
302290 return new Date (dateStr ).toLocaleDateString (' en-US' , {
303291 year: ' numeric' ,
@@ -309,7 +297,7 @@ function formatDate(dateStr: string): string {
309297
310298<template >
311299 <section
312- v-if =" tagRows .length > 0"
300+ v-if =" initialTagRows .length > 0"
313301 aria-labelledby =" versions-heading"
314302 >
315303 <h2
@@ -322,27 +310,27 @@ function formatDate(dateStr: string): string {
322310 <div class =" space-y-0.5" >
323311 <!-- Dist-tag rows -->
324312 <div
325- v-for =" ( row, index) in tagRows "
313+ v-for =" row in initialTagRows "
326314 :key =" row.id"
327315 >
328316 <div class =" flex items-center gap-2" >
329317 <!-- Expand button (only if there are more versions to show) -->
330318 <button
331- v-if =" row.allVersions .length > 1 || !hasLoadedAll"
319+ v-if =" getTagVersions( row.tag) .length > 1 || !hasLoadedAll"
332320 type =" button"
333321 class =" w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg transition-colors"
334- :aria-expanded =" row.expanded "
335- :aria-label =" row.expanded ? `Collapse ${row.tag}` : `Expand ${row.tag}`"
336- @click =" expandTagRow(index )"
322+ :aria-expanded =" expandedTags.has( row.tag) "
323+ :aria-label =" expandedTags.has( row.tag) ? `Collapse ${row.tag}` : `Expand ${row.tag}`"
324+ @click =" expandTagRow(row.tag )"
337325 >
338326 <span
339- v-if =" row.loading "
327+ v-if =" loadingTags.has( row.tag) "
340328 class =" i-carbon-rotate w-3 h-3 animate-spin"
341329 />
342330 <span
343331 v-else
344332 class =" w-3 h-3 transition-transform duration-200"
345- :class =" row.expanded ? 'i-carbon-chevron-down' : 'i-carbon-chevron-right'"
333+ :class =" expandedTags.has( row.tag) ? 'i-carbon-chevron-down' : 'i-carbon-chevron-right'"
346334 />
347335 </button >
348336 <span
@@ -383,11 +371,11 @@ function formatDate(dateStr: string): string {
383371
384372 <!-- Expanded versions -->
385373 <div
386- v-if =" row.expanded && row.allVersions .length > 1"
374+ v-if =" expandedTags.has( row.tag) && getTagVersions( row.tag) .length > 1"
387375 class =" ml-4 pl-2 border-l border-border space-y-0.5"
388376 >
389377 <div
390- v-for =" v in row.allVersions .slice(1)"
378+ v-for =" v in getTagVersions( row.tag) .slice(1)"
391379 :key =" v.version"
392380 class =" flex items-center justify-between py-1 text-sm gap-2"
393381 >
@@ -434,7 +422,7 @@ function formatDate(dateStr: string): string {
434422 >
435423 <span class =" w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg transition-colors" >
436424 <span
437- v-if =" loadingVersions && !hasLoadedAll "
425+ v-if =" otherVersionsLoading "
438426 class =" i-carbon-rotate w-3 h-3 animate-spin"
439427 />
440428 <span
0 commit comments