Skip to content

Commit 36461d7

Browse files
Adebesin-Cellclaude
andcommitted
fix: resolve hydration mismatch on package page
- Safe access for tarballUrl in dependency-resolver (dist?.tarball ?? '') to prevent runtime errors during install-size resolution - Remove useId() from DownloadButton to avoid SSR/client ID mismatch - Remove unnecessary explicit composable imports (auto-imported by Nuxt) - Add prefersReducedMotion via useMediaQuery for animation control - Fix dropdown positioning (left-aligned instead of right offset) - Use ul/li with role=menu/menuitem for proper a11y semantics Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c8e7774 commit 36461d7

File tree

3 files changed

+25
-22
lines changed

3 files changed

+25
-22
lines changed

app/components/Package/DownloadButton.vue

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
import type { SlimPackumentVersion, InstallSizeResult } from '#shared/types'
3-
import { onClickOutside, useEventListener } from '@vueuse/core'
3+
import { onClickOutside, useEventListener, useMediaQuery } from '@vueuse/core'
44
55
const props = withDefaults(
66
defineProps<{
@@ -18,10 +18,10 @@ const triggerRef = useTemplateRef('triggerRef')
1818
const listRef = useTemplateRef('listRef')
1919
const isOpen = shallowRef(false)
2020
const highlightedIndex = shallowRef(-1)
21-
const dropdownPosition = shallowRef<{ top: number; right: number } | null>(null)
21+
const dropdownPosition = shallowRef<{ top: number; left: number } | null>(null)
2222
2323
const { t } = useI18n()
24-
const menuId = `${useId()}-download-menu`
24+
const menuId = 'download-menu'
2525
const menuItems = computed(() => {
2626
const items = [{ id: 'package', label: t('package.download.package'), icon: 'i-lucide:package' }]
2727
if (props.installSize) {
@@ -34,11 +34,13 @@ const menuItems = computed(() => {
3434
return items
3535
})
3636
37+
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)')
38+
3739
function getDropdownStyle(): Record<string, string> {
3840
if (!dropdownPosition.value) return {}
3941
return {
4042
top: `${dropdownPosition.value.top}px`,
41-
right: `${document.documentElement.clientWidth - dropdownPosition.value.right}px`,
43+
left: `${dropdownPosition.value.left}px`,
4244
}
4345
}
4446
@@ -50,7 +52,7 @@ function toggle() {
5052
if (rect) {
5153
dropdownPosition.value = {
5254
top: rect.bottom + 4,
53-
right: rect.right,
55+
left: rect.left,
5456
}
5557
}
5658
isOpen.value = true
@@ -200,36 +202,40 @@ defineOptions({
200202
>
201203
{{ $t('package.download.button') }}
202204
<span
203-
class="i-lucide:chevron-down ms-1 transition-transform duration-200 motion-reduce:transition-none"
204-
:class="[size === 'small' ? 'w-3 h-3' : 'w-3.5 h-3.5', { 'rotate-180': isOpen }]"
205+
class="i-lucide:chevron-down ms-1"
206+
:class="[
207+
size === 'small' ? 'w-3 h-3' : 'w-3.5 h-3.5',
208+
{ 'rotate-180': isOpen },
209+
prefersReducedMotion ? '' : 'transition-transform duration-200',
210+
]"
205211
aria-hidden="true"
206212
/>
207213
</ButtonBase>
208214

209215
<Teleport to="body">
210216
<Transition
211-
enter-active-class="transition-opacity duration-150 motion-reduce:duration-0"
212-
enter-from-class="opacity-0"
217+
:enter-active-class="prefersReducedMotion ? '' : 'transition-opacity duration-150'"
218+
:enter-from-class="prefersReducedMotion ? '' : 'opacity-0'"
213219
enter-to-class="opacity-100"
214-
leave-active-class="transition-opacity duration-100 motion-reduce:duration-0"
220+
:leave-active-class="prefersReducedMotion ? '' : 'transition-opacity duration-100'"
215221
leave-from-class="opacity-100"
216-
leave-to-class="opacity-0"
222+
:leave-to-class="prefersReducedMotion ? '' : 'opacity-0'"
217223
>
218-
<div
224+
<ul
219225
v-if="isOpen"
220226
:id="menuId"
221227
ref="listRef"
222228
role="menu"
229+
:aria-label="$t('package.download.button')"
223230
:style="getDropdownStyle()"
224-
class="fixed bg-bg-subtle border border-border rounded-md shadow-lg z-50 py-1 w-64 overscroll-contain"
231+
class="fixed bg-bg-subtle border border-border rounded-md shadow-lg z-50"
225232
@keydown="handleKeydown"
226233
>
227-
<button
234+
<li
228235
v-for="(item, index) in menuItems"
229236
:key="item.id"
230237
role="menuitem"
231-
type="button"
232-
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-fg-muted transition-colors duration-150"
238+
class="cursor-pointer flex items-center gap-2 px-3 py-1.5 text-sm text-fg-muted transition-colors duration-150"
233239
:class="[
234240
highlightedIndex === index
235241
? 'bg-bg-elevated text-fg'
@@ -240,8 +246,8 @@ defineOptions({
240246
>
241247
<span :class="item.icon" class="w-4 h-4" aria-hidden="true" />
242248
{{ item.label }}
243-
</button>
244-
</div>
249+
</li>
250+
</ul>
245251
</Transition>
246252
</Teleport>
247253
</template>

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
<script setup lang="ts">
22
import { assertValidPackageName } from '#shared/utils/npm'
3-
import { usePackageHeaderHeight } from '~/composables/usePackageHeaderHeight'
4-
import { useRepositoryUrl } from '~/composables/useRepositoryUrl'
5-
import { useViewOnGitProvider } from '~/composables/useViewOnGitProvider'
63
import { getDependencyCount } from '~/utils/npm/dependency-count'
74
85
defineOgImageComponent('Package', {

server/utils/dependency-resolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ export async function resolveDependencyTree(
153153
if (!matchesPlatform(versionData)) return
154154

155155
const size = (versionData.dist as { unpackedSize?: number })?.unpackedSize ?? 0
156-
const tarballUrl = versionData.dist.tarball
156+
const tarballUrl = versionData.dist?.tarball ?? ''
157157
const key = `${name}@${version}`
158158

159159
// Build path for this package (path to parent + this package with version)

0 commit comments

Comments
 (0)