From 0bed08a765d187492a657d1ba5519f3c3dba7bce Mon Sep 17 00:00:00 2001 From: Vordgi Date: Tue, 10 Mar 2026 22:45:46 +0000 Subject: [PATCH 01/12] refactor: move package-header to new component --- app/components/Package/Header.vue | 355 ++++++++++++++++++++++ app/composables/usePackageHeaderHeight.ts | 3 + app/pages/package/[[org]]/[name].vue | 353 +-------------------- 3 files changed, 372 insertions(+), 339 deletions(-) create mode 100644 app/components/Package/Header.vue create mode 100644 app/composables/usePackageHeaderHeight.ts diff --git a/app/components/Package/Header.vue b/app/components/Package/Header.vue new file mode 100644 index 0000000000..b0fb55425d --- /dev/null +++ b/app/components/Package/Header.vue @@ -0,0 +1,355 @@ + + + + + diff --git a/app/composables/usePackageHeaderHeight.ts b/app/composables/usePackageHeaderHeight.ts new file mode 100644 index 0000000000..d33733b84c --- /dev/null +++ b/app/composables/usePackageHeaderHeight.ts @@ -0,0 +1,3 @@ +export function usePackageHeaderHeight() { + return useState('package-header-height', () => 0) +} diff --git a/app/pages/package/[[org]]/[name].vue b/app/pages/package/[[org]]/[name].vue index a57774f110..eabc810769 100644 --- a/app/pages/package/[[org]]/[name].vue +++ b/app/pages/package/[[org]]/[name].vue @@ -17,9 +17,6 @@ import { areUrlsEquivalent } from '#shared/utils/url' import { isEditableElement } from '~/utils/input' import { getDependencyCount } from '~/utils/npm/dependency-count' import { detectPublishSecurityDowngradeForVersion } from '~/utils/publish-security' -import { useModal } from '~/composables/useModal' -import { useAtproto } from '~/composables/atproto/useAtproto' -import { togglePackageLike } from '~/utils/atproto/likes' import { useInstallSizeDiff } from '~/composables/useInstallSizeDiff' import { useViewOnGitProvider } from '~/composables/useViewOnGitProvider' import type { RouteLocationRaw } from 'vue-router' @@ -32,15 +29,10 @@ defineOgImageComponent('Package', { const router = useRouter() -const header = useTemplateRef('header') -const isHeaderPinned = shallowRef(false) const readmeHeader = useTemplateRef('readmeHeader') const isReadmeHeaderPinned = shallowRef(false) -const navExtraOffset = shallowRef(0) -const isMobile = useMediaQuery('(max-width: 639.9px)') - -const headerBounds = useElementBounding(header) -const readmeStickyTop = computed(() => `${56 + headerBounds.height.value}px`) +const packageHeaderHeight = usePackageHeaderHeight() +const readmeStickyTop = computed(() => `${56 + packageHeaderHeight.value}px`) function isStickyPinned(el: HTMLElement | null): boolean { if (!el) return false @@ -53,58 +45,16 @@ function isStickyPinned(el: HTMLElement | null): boolean { } function checkHeaderPosition() { - isHeaderPinned.value = isStickyPinned(header.value) isReadmeHeaderPinned.value = isStickyPinned(readmeHeader.value) } 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 - - pauseFooterObserver() - - watch( - isMobile, - value => { - if (value) { - resumeFooterObserver() - } else { - pauseFooterObserver() - navExtraOffset.value = 0 - } - }, - { immediate: true }, - ) -} - onMounted(() => { checkHeaderPosition() - initFooterObserver() }) -const navExtraOffsetStyle = computed(() => ({ - '--package-nav-extra': `${navExtraOffset.value}px`, -})) - const { packageName, requestedVersion, orgName } = usePackageRoute() const { data: resolvedVersion, status: resolvedStatus } = await useResolvedVersion( @@ -318,25 +268,6 @@ const pkgDescription = useMarkdown(() => ({ packageName: pkg.value?.name, })) -//copy package name -const { copied: copiedPkgName, copy: copyPkgName } = useClipboard({ - source: packageName, - copiedDuring: 2000, -}) - -//copy version name -const { copied: copiedVersion, copy: copyVersion } = useClipboard({ - source: () => resolvedVersion.value ?? '', - copiedDuring: 2000, -}) - -const { scrollToTop, isTouchDeviceClient } = useScrollToTop() - -const { y: scrollY } = useScroll(window) -const showScrollToTop = computed( - () => isTouchDeviceClient.value && scrollY.value > SCROLL_TO_TOP_THRESHOLD, -) - // Fetch dependency analysis (lazy, client-side) // This is the same composable used by PackageVulnerabilityTree and PackageDeprecatedTree const { data: vulnTree, status: vulnTreeStatus } = useDependencyAnalysis( @@ -602,69 +533,6 @@ const canonicalUrl = computed(() => { return requestedVersion.value ? `${base}/v/${requestedVersion.value}` : base }) -//atproto -// TODO: Maybe set this where it's not loaded here every load? -const { user } = useAtproto() - -const authModal = useModal('auth-modal') - -const { data: likesData, status: likeStatus } = useFetch( - () => `/api/social/likes/${packageName.value}`, - { - default: () => ({ totalLikes: 0, userHasLiked: false }), - server: false, - }, -) -const isLoadingLikeData = computed( - () => likeStatus.value === 'pending' || likeStatus.value === 'idle', -) - -const isLikeActionPending = shallowRef(false) - -const likeAction = async () => { - if (user.value?.handle == null) { - authModal.open() - return - } - - if (isLikeActionPending.value) return - - const currentlyLiked = likesData.value?.userHasLiked ?? false - const currentLikes = likesData.value?.totalLikes ?? 0 - - // Optimistic update - likesData.value = { - totalLikes: currentlyLiked ? currentLikes - 1 : currentLikes + 1, - userHasLiked: !currentlyLiked, - } - - isLikeActionPending.value = true - - try { - const result = await togglePackageLike(packageName.value, currentlyLiked, user.value?.handle) - - isLikeActionPending.value = false - - if (result.success) { - // Update with server response - likesData.value = result.data - } else { - // Revert on error - likesData.value = { - totalLikes: currentLikes, - userHasLiked: currentlyLiked, - } - } - } catch { - // Revert on error - likesData.value = { - totalLikes: currentLikes, - userHasLiked: currentlyLiked, - } - isLikeActionPending.value = false - } -} - const dependencyCount = computed(() => getDependencyCount(displayVersion.value)) const numberFormatter = useNumberFormatter() @@ -772,190 +640,18 @@ const showSkeleton = shallowRef(false) />
- -
- -
- -

- - @{{ orgName }} - - / - - {{ orgName ? pkg.name.replace(`@${orgName}/`, '') : pkg.name }} - -

-
- - - - - - {{ resolvedVersion }} - v{{ resolvedVersion }} - - - {{ $t('package.not_latest') }} - - - - - - -
- - - - - - - -
-
-
+
@@ -1632,18 +1328,6 @@ const showSkeleton = shallowRef(false) grid-area: header; } -/* Improve package name wrapping for narrow screens */ -.areaHeader h1 { - overflow-wrap: anywhere; -} - -/* Ensure description text wraps properly */ -.areaHeader p { - word-wrap: break-word; - overflow-wrap: break-word; - word-break: break-word; -} - .areaDetails { grid-area: details; } @@ -1676,16 +1360,7 @@ const showSkeleton = shallowRef(false) grid-area: sidebar; } -/* Mobile floating nav: safe-area positioning + kbd hiding */ @media (max-width: 639.9px) { - .packageNav { - bottom: calc(1.25rem + var(--package-nav-extra, 0px) + env(safe-area-inset-bottom, 0px)); - } - - .packageNav > :global(a kbd) { - display: none; - } - .packagePage { padding-bottom: calc(4.5rem + env(safe-area-inset-bottom, 0px)); } From 06b5b0519ecb107eb0ee0031cb0b84378d78c477 Mon Sep 17 00:00:00 2001 From: Vordgi Date: Tue, 10 Mar 2026 23:12:57 +0000 Subject: [PATCH 02/12] test: add a11y test for package header --- app/pages/package/[[org]]/[name].vue | 2 +- test/nuxt/a11y.spec.ts | 31 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/app/pages/package/[[org]]/[name].vue b/app/pages/package/[[org]]/[name].vue index eabc810769..92ee712790 100644 --- a/app/pages/package/[[org]]/[name].vue +++ b/app/pages/package/[[org]]/[name].vue @@ -55,7 +55,7 @@ onMounted(() => { checkHeaderPosition() }) -const { packageName, requestedVersion, orgName } = usePackageRoute() +const { packageName, requestedVersion } = usePackageRoute() const { data: resolvedVersion, status: resolvedStatus } = await useResolvedVersion( packageName, diff --git a/test/nuxt/a11y.spec.ts b/test/nuxt/a11y.spec.ts index 9a079f91c8..ba8345807a 100644 --- a/test/nuxt/a11y.spec.ts +++ b/test/nuxt/a11y.spec.ts @@ -171,6 +171,7 @@ import { PackageCompatibility, PackageDependencies, PackageDeprecatedTree, + PackageHeader, PackageInstallScripts, PackageKeywords, PackageList, @@ -725,6 +726,36 @@ describe('component accessibility audits', () => { }) }) + describe('PackageHeader', () => { + it('should have no accessibility violations', async () => { + const component = await mountSuspended(PackageHeader, { + props: { + pkg: { name: 'vue' }, + resolvedVersion: '3.5.0', + displayVersion: { + _id: '1234567890', + _npmVersion: '3.5.0', + name: 'vue', + version: '3.5.0', + dist: { + shasum: '1234567890', + signatures: [], + tarball: 'https://npmx.dev/package/vue/tarball', + }, + }, + latestVersion: { version: '3.5.0', tags: [] }, + provenanceData: null, + provenanceStatus: 'idle', + docsLink: null, + codeLink: null, + isBinaryOnly: false, + }, + }) + const results = await runAxe(component) + expect(results.violations).toEqual([]) + }) + }) + // Note: PackageWeeklyDownloadStats tests are skipped because vue-data-ui VueUiSparkline // component has issues in the test environment (requires DOM measurements that aren't // available during SSR-like test mounting). From 08cacfbf8f099b8ce2cdd756d3eb127f89e8c3e5 Mon Sep 17 00:00:00 2001 From: Vordgi Date: Wed, 11 Mar 2026 17:08:38 +0000 Subject: [PATCH 03/12] feat: update package header ui --- app/components/Link/Base.vue | 2 +- app/components/Package/Header.vue | 234 +++--- app/components/Package/Sidebar.vue | 5 + app/pages/package/[[org]]/[name].vue | 1101 +++++++++++++------------- 4 files changed, 662 insertions(+), 680 deletions(-) diff --git a/app/components/Link/Base.vue b/app/components/Link/Base.vue index 5bc5328364..ba62e47ca8 100644 --- a/app/components/Link/Base.vue +++ b/app/components/Link/Base.vue @@ -124,7 +124,7 @@ const keyboardShortcutsEnabled = useKeyboardShortcuts()