From fa162788025b11ce4309f5105197f16c73da265a Mon Sep 17 00:00:00 2001 From: ackzell Date: Tue, 10 Feb 2026 21:17:16 +0100 Subject: [PATCH 1/3] fix: re-position the buttongroup if the footer is visible (#1340) --- app/pages/package/[[org]]/[name].vue | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/app/pages/package/[[org]]/[name].vue b/app/pages/package/[[org]]/[name].vue index 3ade232d3..82a2a374b 100644 --- a/app/pages/package/[[org]]/[name].vue +++ b/app/pages/package/[[org]]/[name].vue @@ -28,6 +28,7 @@ const router = useRouter() const header = useTemplateRef('header') const isHeaderPinned = shallowRef(false) +const navExtraOffset = shallowRef(0) function checkHeaderPosition() { const el = header.value @@ -40,13 +41,37 @@ function checkHeaderPosition() { isHeaderPinned.value = Math.abs(rect.top - top) < 1 } +let footerEl: HTMLElement | null = null +let footerObserver: IntersectionObserver | null = null + useEventListener('scroll', checkHeaderPosition, { passive: true }) useEventListener('resize', checkHeaderPosition) onMounted(() => { checkHeaderPosition() + footerEl = document.querySelector('footer') + if (!footerEl) return + const thresholdValues = Array.from({ length: 101 }, (_, index) => index / 100) + footerObserver = new IntersectionObserver( + entries => { + const entry = entries[0] + if (!entry) return + navExtraOffset.value = entry.isIntersecting ? entry.intersectionRect.height : 0 + }, + { threshold: thresholdValues }, + ) + footerObserver.observe(footerEl) }) +onBeforeUnmount(() => { + footerObserver?.disconnect() + footerObserver = null +}) + +const navExtraOffsetStyle = computed(() => ({ + '--package-nav-extra': `${navExtraOffset.value}px`, +})) + const { packageName, requestedVersion, orgName } = usePackageRoute() const selectedPM = useSelectedPackageManager() const activePmId = computed(() => selectedPM.value ?? 'npm') @@ -647,6 +672,7 @@ onKeyStroke( as="nav" :aria-label="$t('package.navigation')" class="hidden sm:flex max-sm:flex max-sm:fixed max-sm:z-40 max-sm:inset-is-1/2 max-sm:-translate-x-1/2 max-sm:rtl:translate-x-1/2 max-sm:bg-[--bg]/90 max-sm:backdrop-blur-md max-sm:border max-sm:border-border max-sm:rounded-md max-sm:shadow-md" + :style="navExtraOffsetStyle" :class="$style.packageNav" > :global(a kbd) { From d6de9567217c8b07cb81ddaf4f2b60c39e58ea49 Mon Sep 17 00:00:00 2001 From: ackzell Date: Tue, 10 Feb 2026 21:31:41 +0100 Subject: [PATCH 2/3] Only using the intersection observer when on narrower screens. --- app/pages/package/[[org]]/[name].vue | 42 ++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/app/pages/package/[[org]]/[name].vue b/app/pages/package/[[org]]/[name].vue index 82a2a374b..226f34009 100644 --- a/app/pages/package/[[org]]/[name].vue +++ b/app/pages/package/[[org]]/[name].vue @@ -43,13 +43,16 @@ function checkHeaderPosition() { let footerEl: HTMLElement | null = null let footerObserver: IntersectionObserver | null = null +let footerMediaQuery: MediaQueryList | null = null +const footerMediaHandler = (event: MediaQueryListEvent) => { + if (event.matches) { + setupFooterObserver() + } else { + teardownFooterObserver() + } +} -useEventListener('scroll', checkHeaderPosition, { passive: true }) -useEventListener('resize', checkHeaderPosition) - -onMounted(() => { - checkHeaderPosition() - footerEl = document.querySelector('footer') +function setupFooterObserver() { if (!footerEl) return const thresholdValues = Array.from({ length: 101 }, (_, index) => index / 100) footerObserver = new IntersectionObserver( @@ -61,11 +64,34 @@ onMounted(() => { { threshold: thresholdValues }, ) footerObserver.observe(footerEl) -}) +} -onBeforeUnmount(() => { +function teardownFooterObserver() { footerObserver?.disconnect() footerObserver = null + navExtraOffset.value = 0 +} + +useEventListener('scroll', checkHeaderPosition, { passive: true }) +useEventListener('resize', checkHeaderPosition) + +onMounted(() => { + checkHeaderPosition() + footerEl = document.querySelector('footer') + if (!footerEl) return + footerMediaQuery = window.matchMedia('(max-width: 639.9px)') + if (footerMediaQuery.matches) { + setupFooterObserver() + } else { + teardownFooterObserver() + } + footerMediaQuery.addEventListener('change', footerMediaHandler) +}) + +onBeforeUnmount(() => { + teardownFooterObserver() + footerMediaQuery?.removeEventListener('change', footerMediaHandler) + footerMediaQuery = null }) const navExtraOffsetStyle = computed(() => ({ From b6bc0128a452e5f6cac70345722cd174411bbd41 Mon Sep 17 00:00:00 2001 From: ackzell Date: Tue, 10 Feb 2026 23:32:47 +0100 Subject: [PATCH 3/3] refactor: Implementing useIntersectionObserver composable --- app/pages/package/[[org]]/[name].vue | 80 +++++++++++++--------------- 1 file changed, 36 insertions(+), 44 deletions(-) diff --git a/app/pages/package/[[org]]/[name].vue b/app/pages/package/[[org]]/[name].vue index 226f34009..cddc7a70c 100644 --- a/app/pages/package/[[org]]/[name].vue +++ b/app/pages/package/[[org]]/[name].vue @@ -29,6 +29,7 @@ const router = useRouter() const header = useTemplateRef('header') const isHeaderPinned = shallowRef(false) const navExtraOffset = shallowRef(0) +const isMobile = useMediaQuery('(max-width: 639.9px)') function checkHeaderPosition() { const el = header.value @@ -41,57 +42,48 @@ function checkHeaderPosition() { isHeaderPinned.value = Math.abs(rect.top - top) < 1 } -let footerEl: HTMLElement | null = null -let footerObserver: IntersectionObserver | null = null -let footerMediaQuery: MediaQueryList | null = null -const footerMediaHandler = (event: MediaQueryListEvent) => { - if (event.matches) { - setupFooterObserver() - } else { - teardownFooterObserver() - } -} +useEventListener('scroll', checkHeaderPosition, { passive: true }) +useEventListener('resize', checkHeaderPosition) + +const footerTarget = ref(null) +const footerThresholds = Array.from({ length: 11 }, (_, i) => i / 10) + +const { pause: pauseFooterObserver, resume: resumeFooterObserver } = useIntersectionObserver( + footerTarget, + ([entry]) => { + if (!entry) return + + navExtraOffset.value = entry.isIntersecting ? entry.intersectionRect.height : 0 + }, + { + threshold: footerThresholds, + immediate: false, + }, +) + +function initFooterObserver() { + footerTarget.value = document.querySelector('footer') + if (!footerTarget.value) return -function setupFooterObserver() { - if (!footerEl) return - const thresholdValues = Array.from({ length: 101 }, (_, index) => index / 100) - footerObserver = new IntersectionObserver( - entries => { - const entry = entries[0] - if (!entry) return - navExtraOffset.value = entry.isIntersecting ? entry.intersectionRect.height : 0 + pauseFooterObserver() + + watch( + isMobile, + value => { + if (value) { + resumeFooterObserver() + } else { + pauseFooterObserver() + navExtraOffset.value = 0 + } }, - { threshold: thresholdValues }, + { immediate: true }, ) - footerObserver.observe(footerEl) -} - -function teardownFooterObserver() { - footerObserver?.disconnect() - footerObserver = null - navExtraOffset.value = 0 } -useEventListener('scroll', checkHeaderPosition, { passive: true }) -useEventListener('resize', checkHeaderPosition) - onMounted(() => { checkHeaderPosition() - footerEl = document.querySelector('footer') - if (!footerEl) return - footerMediaQuery = window.matchMedia('(max-width: 639.9px)') - if (footerMediaQuery.matches) { - setupFooterObserver() - } else { - teardownFooterObserver() - } - footerMediaQuery.addEventListener('change', footerMediaHandler) -}) - -onBeforeUnmount(() => { - teardownFooterObserver() - footerMediaQuery?.removeEventListener('change', footerMediaHandler) - footerMediaQuery = null + initFooterObserver() }) const navExtraOffsetStyle = computed(() => ({