Skip to content

Commit 13300d9

Browse files
authored
Merge branch 'main' into docs/dev-cache-busting
2 parents 0ec4b7a + 4b98444 commit 13300d9

File tree

9 files changed

+414
-316
lines changed

9 files changed

+414
-316
lines changed

app/components/Package/TrendsChart.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,7 +1410,7 @@ const chartConfig = computed<VueUiXyConfig>(() => {
14101410
chart: {
14111411
height: isMobile.value ? 950 : 600,
14121412
backgroundColor: colors.value.bg,
1413-
padding: { bottom: displayedGranularity.value === 'yearly' ? 84 : 64, right: 100 }, // padding right is set to leave space of last datapoint label(s)
1413+
padding: { bottom: displayedGranularity.value === 'yearly' ? 84 : 64, right: 128 }, // padding right is set to leave space of last datapoint label(s)
14141414
userOptions: {
14151415
buttons: {
14161416
pdf: false,
@@ -1682,7 +1682,11 @@ watch(selectedMetric, value => {
16821682
</h2>
16831683

16841684
<!-- Chart panel (active metric) -->
1685-
<div role="region" aria-labelledby="trends-chart-title" class="min-h-[260px]">
1685+
<div
1686+
role="region"
1687+
aria-labelledby="trends-chart-title"
1688+
:class="isMobile === false && width > 0 ? 'min-h-[567px]' : 'min-h-[260px]'"
1689+
>
16861690
<ClientOnly v-if="chartData.dataset">
16871691
<div :data-pending="pending" :data-minimap-visible="maxDatapoints > 6">
16881692
<VueUiXy

app/components/Package/WeeklyDownloadStats.vue

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ const config = computed<VueUiSparklineConfig>(() => {
264264
</script>
265265

266266
<template>
267-
<div class="space-y-8">
267+
<div class="space-y-8 h-[110px] motion-safe:h-[140px]">
268268
<CollapsibleSection id="downloads" :title="$t('package.downloads.title')">
269269
<template #actions>
270270
<ButtonBase
@@ -307,6 +307,10 @@ const config = computed<VueUiSparklineConfig>(() => {
307307
<SkeletonInline class="h-px w-full" />
308308
</div>
309309
</div>
310+
<!-- Animation toggle placeholder -->
311+
<div class="w-full hidden motion-safe:flex flex-1 items-end justify-end">
312+
<SkeletonInline class="h-[20px] w-30" />
313+
</div>
310314
</div>
311315
</template>
312316
</ClientOnly>
@@ -351,10 +355,7 @@ const config = computed<VueUiSparklineConfig>(() => {
351355

352356
<!-- This placeholder bears the same dimensions as the PackageTrendsChart component -->
353357
<!-- Avoids CLS when the dialog has transitioned -->
354-
<div
355-
v-if="!hasChartModalTransitioned"
356-
class="w-full aspect-[390/634.5] sm:aspect-[718/622.797]"
357-
/>
358+
<div v-if="!hasChartModalTransitioned" class="w-full aspect-[390/634.5] sm:aspect-[718/647]" />
358359
</PackageChartModal>
359360
</template>
360361

app/components/ScrollToTop.client.vue

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,32 @@ const route = useRoute()
33
44
// Pages where scroll-to-top should NOT be shown
55
const excludedRoutes = new Set(['index', 'code'])
6+
const isPackagePage = computed(() => route.name === 'package' || route.name === 'package-version')
67
7-
const isActive = computed(() => !excludedRoutes.has(route.name as string))
8+
const isActive = computed(() => !excludedRoutes.has(route.name as string) && !isPackagePage.value)
89
910
const isMounted = useMounted()
10-
const isVisible = shallowRef(false)
11-
const scrollThreshold = 300
11+
const { scrollToTop, isTouchDeviceClient } = useScrollToTop()
12+
13+
const { y: scrollTop } = useScroll(window)
14+
const isVisible = computed(() => {
15+
if (supportsScrollStateQueries.value) return false
16+
return scrollTop.value > SCROLL_TO_TOP_THRESHOLD
17+
})
1218
const { isSupported: supportsScrollStateQueries } = useCssSupports(
1319
'container-type',
1420
'scroll-state',
1521
{ ssrValue: false },
1622
)
17-
18-
function onScroll() {
19-
if (!supportsScrollStateQueries.value) {
20-
return
21-
}
22-
isVisible.value = window.scrollY > scrollThreshold
23-
}
24-
25-
function scrollToTop() {
26-
window.scrollTo({ top: 0, behavior: 'smooth' })
27-
}
28-
29-
useEventListener('scroll', onScroll, { passive: true })
30-
31-
onMounted(() => {
32-
onScroll()
33-
})
23+
const shouldShowButton = computed(() => isActive.value && isTouchDeviceClient.value)
3424
</script>
3525

3626
<template>
3727
<!-- When CSS scroll-state is supported, use CSS-only visibility -->
3828
<button
39-
v-if="isActive && supportsScrollStateQueries"
29+
v-if="shouldShowButton && supportsScrollStateQueries"
4030
type="button"
41-
class="scroll-to-top-css fixed bottom-4 inset-ie-4 z-50 w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg md:hidden flex items-center justify-center text-fg-muted hover:text-fg transition-colors active:scale-95"
31+
class="scroll-to-top-css fixed bottom-4 inset-ie-4 z-50 w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg flex items-center justify-center text-fg-muted hover:text-fg transition-colors active:scale-95"
4232
:aria-label="$t('common.scroll_to_top')"
4333
@click="scrollToTop"
4434
>
@@ -56,11 +46,11 @@ onMounted(() => {
5646
leave-to-class="opacity-0 translate-y-2"
5747
>
5848
<button
59-
v-if="isActive && isMounted && isVisible"
49+
v-if="shouldShowButton && isMounted && isVisible"
6050
type="button"
61-
class="fixed bottom-4 inset-ie-4 z-50 w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg md:hidden flex items-center justify-center text-fg-muted hover:text-fg transition-colors active:scale-95"
51+
class="fixed bottom-4 inset-ie-4 z-50 w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg flex items-center justify-center text-fg-muted hover:text-fg transition-colors active:scale-95"
6252
:aria-label="$t('common.scroll_to_top')"
63-
@click="scrollToTop"
53+
@click="() => scrollToTop()"
6454
>
6555
<span class="i-lucide:arrow-up w-5 h-5" aria-hidden="true" />
6656
</button>

app/composables/npm/useResolvedVersion.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ export function useResolvedVersion(
44
packageName: MaybeRefOrGetter<string>,
55
requestedVersion: MaybeRefOrGetter<string | null>,
66
) {
7-
return useFetch(
8-
() => {
7+
return useAsyncData(
8+
() => `resolved-version:${toValue(packageName)}:${toValue(requestedVersion) ?? 'latest'}`,
9+
async () => {
910
const version = toValue(requestedVersion)
10-
return version
11-
? `https://npm.antfu.dev/${toValue(packageName)}@${version}`
12-
: `https://npm.antfu.dev/${toValue(packageName)}`
13-
},
14-
{
15-
transform: (data: ResolvedPackageVersion) => data.version,
11+
const name = toValue(packageName)
12+
const url = version
13+
? `https://npm.antfu.dev/${name}@${version}`
14+
: `https://npm.antfu.dev/${name}`
15+
const data = await $fetch<ResolvedPackageVersion>(url)
16+
return data.version
1617
},
18+
{ default: () => null },
1719
)
1820
}

app/composables/useScrollToTop.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Easing function for the scroll animation
2+
const easeOutQuad = (t: number) => t * (2 - t)
3+
4+
export const SCROLL_TO_TOP_THRESHOLD = 300
5+
const SCROLL_TO_TOP_DURATION = 500
6+
7+
/**
8+
* Scroll to the top of the page with a smooth animation.
9+
* @param options - Configuration options for the scroll animation.
10+
* @returns An object containing the scrollToTop function and a cancel function.
11+
*/
12+
export const useScrollToTop = createSharedComposable(function useScrollToTop() {
13+
// Check if prefers-reduced-motion is enabled
14+
const preferReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)')
15+
16+
/**
17+
* Active requestAnimationFrame id for the current auto-scroll animation
18+
*/
19+
let rafId: number | null = null
20+
const isScrolling = ref(false)
21+
22+
/**
23+
* Stop any in-flight auto-scroll before starting a new one.
24+
*/
25+
function cancel() {
26+
if (rafId !== null) {
27+
cancelAnimationFrame(rafId)
28+
rafId = null
29+
}
30+
isScrolling.value = false
31+
}
32+
33+
// Cancel scroll on user interaction
34+
const onInteraction = () => {
35+
if (isScrolling.value) {
36+
cancel()
37+
}
38+
}
39+
40+
if (import.meta.client) {
41+
const listenerOptions = { passive: true }
42+
useEventListener(window, 'wheel', onInteraction, listenerOptions)
43+
useEventListener(window, 'touchstart', onInteraction, listenerOptions)
44+
useEventListener(window, 'mousedown', onInteraction, listenerOptions)
45+
}
46+
47+
function scrollToTop() {
48+
cancel()
49+
50+
if (preferReducedMotion.value) {
51+
window.scrollTo({ top: 0, behavior: 'instant' })
52+
return
53+
}
54+
55+
const start = window.scrollY
56+
if (start <= 0) return
57+
58+
isScrolling.value = true
59+
60+
const startTime = performance.now()
61+
const change = -start
62+
63+
// Start the frame-by-frame scroll animation.
64+
function animate() {
65+
const elapsed = performance.now() - startTime
66+
const t = Math.min(elapsed / SCROLL_TO_TOP_DURATION, 1)
67+
const y = start + change * easeOutQuad(t)
68+
69+
window.scrollTo({ top: y })
70+
71+
if (t < 1 && isScrolling.value) {
72+
rafId = requestAnimationFrame(animate)
73+
} else {
74+
cancel()
75+
}
76+
}
77+
78+
rafId = requestAnimationFrame(animate)
79+
}
80+
81+
tryOnScopeDispose(cancel)
82+
83+
const isTouchDeviceClient = shallowRef(false)
84+
onMounted(() => {
85+
isTouchDeviceClient.value = isTouchDevice()
86+
})
87+
88+
return {
89+
scrollToTop,
90+
cancel,
91+
isTouchDeviceClient,
92+
}
93+
})

app/pages/package/[[org]]/[name].vue

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -195,18 +195,9 @@ const { data: skillsData } = useLazyFetch<SkillsListResponse>(
195195
const { data: packageAnalysis } = usePackageAnalysis(packageName, requestedVersion)
196196
const { data: moduleReplacement } = useModuleReplacement(packageName)
197197
198-
const {
199-
data: resolvedVersion,
200-
status: versionStatus,
201-
error: versionError,
202-
} = await useResolvedVersion(packageName, requestedVersion)
203-
204-
if (
205-
versionStatus.value === 'error' &&
206-
versionError.value?.statusCode &&
207-
versionError.value.statusCode >= 400 &&
208-
versionError.value.statusCode < 500
209-
) {
198+
const { data: resolvedVersion } = await useResolvedVersion(packageName, requestedVersion)
199+
200+
if (resolvedVersion.value === null) {
210201
throw createError({
211202
statusCode: 404,
212203
statusMessage: $t('package.not_found'),
@@ -251,6 +242,13 @@ const { copied: copiedVersion, copy: copyVersion } = useClipboard({
251242
copiedDuring: 2000,
252243
})
253244
245+
const { scrollToTop, isTouchDeviceClient } = useScrollToTop()
246+
247+
const { y: scrollY } = useScroll(window)
248+
const showScrollToTop = computed(
249+
() => isTouchDeviceClient.value && scrollY.value > SCROLL_TO_TOP_THRESHOLD,
250+
)
251+
254252
// Fetch dependency analysis (lazy, client-side)
255253
// This is the same composable used by PackageVulnerabilityTree and PackageDeprecatedTree
256254
const { data: vulnTree, status: vulnTreeStatus } = useDependencyAnalysis(
@@ -786,26 +784,37 @@ const showSkeleton = shallowRef(false)
786784
:to="docsLink"
787785
aria-keyshortcuts="d"
788786
classicon="i-lucide:file-text"
787+
:title="$t('package.links.docs')"
789788
>
790-
{{ $t('package.links.docs') }}
789+
<span class="max-sm:sr-only">{{ $t('package.links.docs') }}</span>
791790
</LinkBase>
792791
<LinkBase
793792
v-if="codeLink"
794793
variant="button-secondary"
795794
:to="codeLink"
796795
aria-keyshortcuts="."
797796
classicon="i-lucide:code"
797+
:title="$t('package.links.code')"
798798
>
799-
{{ $t('package.links.code') }}
799+
<span class="max-sm:sr-only">{{ $t('package.links.code') }}</span>
800800
</LinkBase>
801801
<LinkBase
802802
variant="button-secondary"
803803
:to="{ name: 'compare', query: { packages: pkg.name } }"
804804
aria-keyshortcuts="c"
805805
classicon="i-lucide:git-compare"
806+
:title="$t('package.links.compare')"
806807
>
807-
{{ $t('package.links.compare') }}
808+
<span class="max-sm:sr-only">{{ $t('package.links.compare') }}</span>
808809
</LinkBase>
810+
<ButtonBase
811+
v-if="showScrollToTop"
812+
variant="secondary"
813+
:title="$t('common.scroll_to_top')"
814+
:aria-label="$t('common.scroll_to_top')"
815+
@click="() => scrollToTop()"
816+
classicon="i-lucide:arrow-up"
817+
/>
809818
</ButtonGroup>
810819

811820
<!-- Package metrics -->

nuxt.config.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,10 @@ export default defineNuxtConfig({
135135
},
136136
},
137137
// pages
138-
'/package/:name': getISRConfig(60, { fallback: 'html' }),
138+
'/package/**': getISRConfig(60, { fallback: 'html' }),
139139
'/package/:name/_payload.json': getISRConfig(60, { fallback: 'json' }),
140-
'/package/:name/v/:version': getISRConfig(60, { fallback: 'html' }),
141140
'/package/:name/v/:version/_payload.json': getISRConfig(60, { fallback: 'json' }),
142-
'/package/:org/:name': getISRConfig(60, { fallback: 'html' }),
143141
'/package/:org/:name/_payload.json': getISRConfig(60, { fallback: 'json' }),
144-
'/package/:org/:name/v/:version': getISRConfig(60, { fallback: 'html' }),
145142
'/package/:org/:name/v/:version/_payload.json': getISRConfig(60, { fallback: 'json' }),
146143
// infinite cache (versioned - doesn't change)
147144
'/package-code/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
"virtua": "0.48.6",
110110
"vite-plugin-pwa": "1.2.0",
111111
"vite-plus": "0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab",
112-
"vue": "3.5.28",
112+
"vue": "3.5.29",
113113
"vue-data-ui": "3.15.6"
114114
},
115115
"devDependencies": {

0 commit comments

Comments
 (0)