@@ -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