@@ -49,7 +49,7 @@ const distTags = computed(() => versionSummary.value?.distTags ?? {})
4949const versionStrings = computed (() => versionSummary .value ?.versions ?? [])
5050const versionTimes = computed (() => versionSummary .value ?.time ?? {})
5151
52- // ─── Phase 2: full metadata (loaded on first group expand) ──────────────── ────
52+ // ─── Phase 2: full metadata (fired automatically after phase 1 completes) ────
5353// Fetches deprecated status, provenance, and exact times needed for version rows.
5454
5555const fullVersionMap = shallowRef < Map <
@@ -82,7 +82,6 @@ function getVersionTime(version: string): string | undefined {
8282// ─── Version groups ───────────────────────────────────────────────────────────
8383
8484const expandedGroups = ref (new Set <string >())
85- const loadingGroup = ref <string | null >(null )
8685
8786const versionGroups = computed (() => {
8887 const byKey = new Map <string , string []>()
@@ -101,27 +100,42 @@ const versionGroups = computed(() => {
101100 }))
102101})
103102
104- async function toggleGroup(groupKey : string ) {
103+ const deprecatedGroupKeys = computed (() => {
104+ if (! fullVersionMap .value ) return new Set <string >()
105+ const result = new Set <string >()
106+ for (const group of versionGroups .value ) {
107+ if (group .versions .every (v => !! fullVersionMap .value ! .get (v )?.deprecated ))
108+ result .add (group .groupKey )
109+ }
110+ return result
111+ })
112+
113+ function toggleGroup(groupKey : string ) {
105114 if (expandedGroups .value .has (groupKey )) {
106115 expandedGroups .value .delete (groupKey )
107- return
116+ } else {
117+ expandedGroups .value .add (groupKey )
108118 }
109- expandedGroups .value .add (groupKey )
110- if (! fullVersionMap .value ) {
111- loadingGroup .value = groupKey
112- try {
119+ }
120+
121+ watch (
122+ versionSummary ,
123+ async summary => {
124+ if (summary ) {
113125 await ensureFullDataLoaded ()
114- } finally {
115- loadingGroup .value = null
116126 }
117- }
118- }
127+ },
128+ { immediate: true },
129+ )
119130
120131// ─── Version filter ───────────────────────────────────────────────────────────
121132
122133const versionFilterInput = ref (' ' )
123134const versionFilter = refDebounced (versionFilterInput , 100 )
124135const isFilterActive = computed (() => versionFilter .value .trim () !== ' ' )
136+ const isInvalidRange = computed (
137+ () => isFilterActive .value && validRange (versionFilter .value .trim ()) === null ,
138+ )
125139
126140const filteredVersionSet = computed (() => {
127141 const trimmed = versionFilter .value .trim ()
@@ -198,14 +212,40 @@ const flatItems = computed<FlatItem[]>(() => {
198212 <span class =" text-fg-subtle shrink-0" >/</span >
199213 <h1 class =" text-sm text-fg-muted shrink-0" >{{ $t('package.versions.page_title') }}</h1 >
200214 </div >
201- <InputBase
202- v-model =" versionFilterInput"
203- type =" text"
204- :placeholder =" $t('package.versions.version_filter_placeholder')"
205- :aria-label =" $t('package.versions.version_filter_label')"
206- size =" sm"
207- class =" w-36 sm:w-44"
208- />
215+ <div class =" relative" >
216+ <InputBase
217+ v-model =" versionFilterInput"
218+ type =" text"
219+ :placeholder =" $t('package.versions.filter_placeholder')"
220+ :aria-label =" $t('package.versions.filter_placeholder')"
221+ :aria-invalid =" isInvalidRange ? 'true' : undefined"
222+ :aria-describedby =" isInvalidRange ? 'version-filter-error' : undefined"
223+ autocomplete =" off"
224+ size =" sm"
225+ class =" w-36 sm:w-64"
226+ :class =" isInvalidRange ? 'pe-7 !border-red-500' : ''"
227+ />
228+ <Transition
229+ enter-active-class =" transition-all duration-150"
230+ enter-from-class =" opacity-0 scale-60"
231+ leave-active-class =" transition-all duration-150"
232+ leave-to-class =" opacity-0 scale-60"
233+ >
234+ <TooltipApp
235+ v-if =" isInvalidRange"
236+ :text =" $t('package.versions.filter_invalid')"
237+ position =" bottom"
238+ class =" absolute end-0 inset-y-0 flex items-center pe-2"
239+ >
240+ <span
241+ id =" version-filter-error"
242+ class =" i-lucide:circle-alert w-3.5 h-3.5 text-red-500 block"
243+ role =" img"
244+ :aria-label =" $t('package.versions.filter_invalid')"
245+ />
246+ </TooltipApp >
247+ </Transition >
248+ </div >
209249 </div >
210250 </header >
211251
@@ -230,18 +270,26 @@ const flatItems = computed<FlatItem[]>(() => {
230270 v-for =" tag in latestTagRow!.tags.filter(t => t !== 'latest')"
231271 :key =" tag"
232272 class =" text-3xs font-semibold uppercase tracking-wide text-fg-subtle"
273+ :title =" tag"
233274 >{{ tag }}</span
234275 >
235276 </div >
236277 <LinkBase
237278 :to =" packageRoute(packageName, latestTagRow!.version)"
238279 class =" text-2xl font-semibold tracking-tight after:absolute after:inset-0 after:content-['']"
280+ :title =" latestTagRow!.version"
239281 dir =" ltr"
240282 >{{ latestTagRow!.version }}</LinkBase
241283 >
242284 </div >
243- <!-- Right: date + provenance -->
285+ <!-- Right: deprecated + date + provenance -->
244286 <div class =" flex flex-col items-end gap-1.5 shrink-0 relative z-10" >
287+ <span
288+ v-if =" fullVersionMap?.get(latestTagRow!.version)?.deprecated"
289+ 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"
290+ :title =" fullVersionMap!.get(latestTagRow!.version)!.deprecated"
291+ >deprecated</span
292+ >
245293 <ProvenanceBadge
246294 v-if =" fullVersionMap?.get(latestTagRow!.version)?.hasProvenance"
247295 :package-name =" packageName"
@@ -276,6 +324,7 @@ const flatItems = computed<FlatItem[]>(() => {
276324 v-for =" tag in row.tags"
277325 :key =" tag"
278326 class =" text-3xs font-semibold uppercase tracking-wide text-fg-subtle"
327+ :title =" tag"
279328 >{{ tag }}</span
280329 >
281330 </div >
@@ -284,30 +333,36 @@ const flatItems = computed<FlatItem[]>(() => {
284333 <LinkBase
285334 :to =" packageRoute(packageName, row.version)"
286335 class =" text-sm flex-1 min-w-0 after:absolute after:inset-0 after:content-['']"
336+ :title =" row.version"
287337 dir =" ltr"
288338 >
289339 {{ row.version }}
290340 </LinkBase >
291341
292- <!-- Date -->
293- <DateTime
294- v-if =" getVersionTime(row.version)"
295- :datetime =" getVersionTime(row.version)!"
296- class =" text-xs text-fg-subtle shrink-0 hidden sm:block"
297- year =" numeric"
298- month =" short"
299- day =" numeric"
300- />
301-
302- <!-- Provenance -->
303- <ProvenanceBadge
304- v-if =" fullVersionMap?.get(row.version)?.hasProvenance"
305- :package-name =" packageName"
306- :version =" row.version"
307- compact
308- :linked =" false"
309- class =" relative z-10 shrink-0"
310- />
342+ <!-- Deprecated + Date + Provenance -->
343+ <div class =" flex items-center gap-2 shrink-0 relative z-10" >
344+ <span
345+ v-if =" fullVersionMap?.get(row.version)?.deprecated"
346+ 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"
347+ :title =" fullVersionMap!.get(row.version)!.deprecated"
348+ >deprecated</span
349+ >
350+ <DateTime
351+ v-if =" getVersionTime(row.version)"
352+ :datetime =" getVersionTime(row.version)!"
353+ class =" text-xs text-fg-subtle hidden sm:block"
354+ year =" numeric"
355+ month =" short"
356+ day =" numeric"
357+ />
358+ <ProvenanceBadge
359+ v-if =" fullVersionMap?.get(row.version)?.hasProvenance"
360+ :package-name =" packageName"
361+ :version =" row.version"
362+ compact
363+ :linked =" false"
364+ />
365+ </div >
311366 </div >
312367 </div >
313368 </section >
@@ -351,15 +406,9 @@ const flatItems = computed<FlatItem[]>(() => {
351406 <span class =" w-4 h-4 flex items-center justify-center text-fg-subtle shrink-0" >
352407 <Transition name =" icon-swap" mode =" out-in" >
353408 <span
354- v-if =" loadingGroup === item.groupKey"
355- key =" loading"
356- class =" i-svg-spinners:ring-resize w-3 h-3"
357- aria-hidden =" true"
358- />
359- <span
360- v-else-if =" isFilterActive"
409+ v-if =" isFilterActive"
361410 key =" search"
362- class =" i-lucide:search w-3 h-3 animate-searching "
411+ class =" i-lucide:funnel w-3 h-3"
363412 aria-hidden =" true"
364413 />
365414 <span
@@ -372,9 +421,16 @@ const flatItems = computed<FlatItem[]>(() => {
372421 </Transition >
373422 </span >
374423 <span class =" text-sm font-medium" >{{ item.label }}</span >
424+ <span
425+ v-if =" deprecatedGroupKeys.has(item.groupKey)"
426+ 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"
427+ >deprecated</span
428+ >
375429 <span class =" text-xs text-fg-subtle" >({{ item.versions.length }})</span >
376430 <span class =" ms-auto flex items-center gap-3 shrink-0" >
377- <span class =" text-xs text-fg-muted" dir =" ltr" >{{ item.versions[0] }}</span >
431+ <span class =" text-xs text-fg-muted" :title =" item.versions[0]" dir =" ltr" >{{
432+ item.versions[0]
433+ }}</span >
378434 <DateTime
379435 v-if =" getVersionTime(item.versions[0])"
380436 :datetime =" getVersionTime(item.versions[0])!"
@@ -411,6 +467,11 @@ const flatItems = computed<FlatItem[]>(() => {
411467 ? 'i-lucide:octagon-alert'
412468 : undefined
413469 "
470+ :title ="
471+ fullVersionMap?.get(item.version)?.deprecated
472+ ? $t('package.versions.deprecated_title', { version: item.version })
473+ : item.version
474+ "
414475 dir =" ltr"
415476 >
416477 {{ item.version }}
@@ -424,6 +485,7 @@ const flatItems = computed<FlatItem[]>(() => {
424485 :key =" tag"
425486 class =" text-4xs font-semibold uppercase tracking-wide"
426487 :class =" tag === 'latest' ? 'text-accent' : 'text-fg-subtle'"
488+ :title =" tag"
427489 >
428490 {{ tag }}
429491 </span >
@@ -521,17 +583,4 @@ const flatItems = computed<FlatItem[]>(() => {
521583 opacity : 0 ;
522584 transform : scale (0.5 );
523585}
524-
525- @keyframes searching {
526- from {
527- transform : rotate (0deg ) translateY (-2px ) rotate (0deg );
528- }
529- to {
530- transform : rotate (360deg ) translateY (-2px ) rotate (-360deg );
531- }
532- }
533-
534- .animate-searching {
535- animation : searching 1.2s linear infinite ;
536- }
537586 </style >
0 commit comments