[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: animate only using motion-safe (#177)

authored by

Matteo Gabriele and committed by
GitHub
2e5345c2 86a1818e

+35 -20
+6 -1
app/components/AppFooter.vue
··· 137 137 z-index: 40; 138 138 /* Hidden by default (translated off-screen) */ 139 139 transform: translateY(100%); 140 - transition: transform 0.3s ease-out; 140 + } 141 + 142 + @media (prefers-reduced-motion: no-preference) { 143 + .footer-scroll-state { 144 + transition: transform 0.3s ease-out; 145 + } 141 146 } 142 147 143 148 /* Show footer when user can scroll up (meaning they've scrolled down) */
+3 -1
app/components/LoadingSpinner.vue
··· 9 9 10 10 <template> 11 11 <div aria-busy="true" class="flex items-center gap-3 text-fg-muted font-mono text-sm py-8"> 12 - <span class="w-4 h-4 border-2 border-fg-subtle border-t-fg rounded-full animate-spin" /> 12 + <span 13 + class="w-4 h-4 border-2 border-fg-subtle border-t-fg rounded-full motion-safe:animate-spin" 14 + /> 13 15 {{ text ?? t('common.loading') }} 14 16 </div> 15 17 </template>
+1 -1
app/components/OrgMembersPanel.vue
··· 309 309 > 310 310 <span 311 311 class="i-carbon-renew block w-4 h-4" 312 - :class="{ 'animate-spin': isLoading || isLoadingTeams }" 312 + :class="{ 'motion-safe:animate-spin': isLoading || isLoadingTeams }" 313 313 aria-hidden="true" 314 314 /> 315 315 </button>
+2 -2
app/components/OrgTeamsPanel.vue
··· 331 331 <!-- Loading state --> 332 332 <div v-if="isLoadingTeams && teams.length === 0" class="p-8 text-center"> 333 333 <span 334 - class="i-carbon-rotate-180 block w-5 h-5 text-fg-muted animate-spin mx-auto" 334 + class="i-carbon-rotate-180 block w-5 h-5 text-fg-muted motion-safe:animate-spin mx-auto" 335 335 aria-hidden="true" 336 336 /> 337 337 <p class="font-mono text-sm text-fg-muted mt-2">{{ $t('org.teams.loading') }}</p> ··· 390 390 </span> 391 391 <span 392 392 v-if="isLoadingUsers[teamName]" 393 - class="i-carbon-rotate-180 w-3 h-3 text-fg-muted animate-spin" 393 + class="i-carbon-rotate-180 w-3 h-3 text-fg-muted motion-safe:animate-spin" 394 394 aria-hidden="true" 395 395 /> 396 396 </button>
+1 -1
app/components/PackageAccessControls.vue
··· 164 164 > 165 165 <span 166 166 class="i-carbon-renew block w-3.5 h-3.5" 167 - :class="{ 'animate-spin': isLoadingCollaborators }" 167 + :class="{ 'motion-safe:animate-spin': isLoadingCollaborators }" 168 168 aria-hidden="true" 169 169 /> 170 170 </button>
+4 -2
app/components/PackageList.vue
··· 111 111 :show-publisher="showPublisher" 112 112 :selected="index === (selectedIndex ?? -1)" 113 113 :index="index" 114 - class="animate-fade-in animate-fill-both" 114 + class="motion-safe:animate-fade-in motion-safe:animate-fill-both" 115 115 :style="{ animationDelay: `${Math.min(index * 0.02, 0.3)}s` }" 116 116 @focus="emit('select', $event)" 117 117 /> ··· 122 122 <!-- Loading indicator --> 123 123 <div v-if="isLoading" class="py-4 flex items-center justify-center"> 124 124 <div class="flex items-center gap-3 text-fg-muted font-mono text-sm"> 125 - <span class="w-4 h-4 border-2 border-fg-subtle border-t-fg rounded-full animate-spin" /> 125 + <span 126 + class="w-4 h-4 border-2 border-fg-subtle border-t-fg rounded-full motion-safe:animate-spin" 127 + /> 126 128 {{ $t('common.loading_more') }} 127 129 </div> 128 130 </div>
+5 -1
app/components/PackageSkeleton.vue
··· 3 3 </script> 4 4 5 5 <template> 6 - <article aria-busy="true" :aria-label="t('package.skeleton.loading')" class="animate-fade-in"> 6 + <article 7 + aria-busy="true" 8 + :aria-label="t('package.skeleton.loading')" 9 + class="motion-safe:animate-fade-in" 10 + > 7 11 <!-- Package header - matches header in [...name].vue --> 8 12 <header class="mb-8 pb-8 border-b border-border"> 9 13 <div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4 mb-4">
+4 -2
app/components/PackageVersions.vue
··· 317 317 > 318 318 <span 319 319 v-if="loadingTags.has(row.tag)" 320 - class="i-carbon-rotate-180 w-3 h-3 animate-spin" 320 + class="i-carbon-rotate-180 w-3 h-3 motion-safe:animate-spin" 321 + data-testid="loading-spinner" 321 322 aria-hidden="true" 322 323 /> 323 324 <span ··· 457 458 > 458 459 <span 459 460 v-if="otherVersionsLoading" 460 - class="i-carbon-rotate-180 w-3 h-3 animate-spin" 461 + class="i-carbon-rotate-180 w-3 h-3 motion-safe:animate-spin" 462 + data-testid="loading-spinner" 461 463 aria-hidden="true" 462 464 /> 463 465 <span
+2 -2
app/pages/[...package].vue
··· 401 401 <main class="container py-8 sm:py-12 overflow-hidden w-full"> 402 402 <PackageSkeleton v-if="status === 'pending'" /> 403 403 404 - <article v-else-if="status === 'success' && pkg" class="animate-fade-in min-w-0"> 404 + <article v-else-if="status === 'success' && pkg" class="motion-safe:animate-fade-in min-w-0"> 405 405 <!-- Package header --> 406 406 <header class="mb-8 pb-8 border-b border-border"> 407 407 <div class="mb-4"> ··· 627 627 class="inline-flex items-center gap-1 text-fg-subtle" 628 628 > 629 629 <span 630 - class="i-carbon-circle-dash w-3 h-3 animate-spin motion-reduce:animate-none" 630 + class="i-carbon-circle-dash w-3 h-3 motion-safe:animate-spin" 631 631 aria-hidden="true" 632 632 /> 633 633 </span>
+4 -4
app/pages/index.vue
··· 25 25 <header class="flex-1 flex flex-col items-center justify-center text-center py-20"> 26 26 <!-- Animated title --> 27 27 <h1 28 - class="font-mono text-5xl sm:text-7xl md:text-8xl font-medium tracking-tight mb-4 animate-fade-in animate-fill-both" 28 + class="font-mono text-5xl sm:text-7xl md:text-8xl font-medium tracking-tight mb-4 motion-safe:animate-fade-in motion-safe:animate-fill-both" 29 29 > 30 30 <span class="text-accent"><span class="-tracking-0.2em">.</span>/</span>npmx 31 31 </h1> 32 32 33 33 <p 34 - class="text-fg-muted text-lg sm:text-xl max-w-md mb-12 animate-slide-up animate-fill-both" 34 + class="text-fg-muted text-lg sm:text-xl max-w-md mb-12 motion-safe:animate-slide-up motion-safe:animate-fill-both" 35 35 style="animation-delay: 0.1s" 36 36 > 37 37 {{ $t('tagline') }} ··· 39 39 40 40 <!-- Search form with micro-interactions --> 41 41 <search 42 - class="w-full max-w-xl animate-slide-up animate-fill-both" 42 + class="w-full max-w-xl motion-safe:animate-slide-up motion-safe:animate-fill-both" 43 43 style="animation-delay: 0.2s" 44 44 > 45 45 <form role="search" class="relative" @submit.prevent="handleSearch"> ··· 90 90 <!-- Popular packages --> 91 91 <nav 92 92 :aria-label="$t('nav.popular_packages')" 93 - class="pb-20 text-center animate-fade-in animate-fill-both" 93 + class="pb-20 text-center motion-safe:animate-fade-in motion-safe:animate-fill-both" 94 94 style="animation-delay: 0.3s" 95 95 > 96 96 <ul class="flex flex-wrap items-center justify-center gap-x-6 gap-y-3 list-none m-0 p-0">
+1 -1
app/pages/search.vue
··· 415 415 </div> 416 416 <button 417 417 type="button" 418 - class="shrink-0 px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-colors duration-200 hover:bg-fg/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 418 + class="shrink-0 px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md motion-safe:transition-colors motion-safe:duration-200 hover:bg-fg/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 419 419 @click="claimModalOpen = true" 420 420 > 421 421 {{ t('search.claim_button', { name: query }) }}
+2 -2
test/nuxt/components/PackageVersions.spec.ts
··· 782 782 783 783 // Should show loading spinner (animate-spin class) 784 784 await vi.waitFor(() => { 785 - expect(component.find('.animate-spin').exists()).toBe(true) 785 + expect(component.find('[data-testid="loading-spinner"]').exists()).toBe(true) 786 786 }) 787 787 788 788 // Resolve the promise to clean up ··· 816 816 817 817 // Should show loading spinner 818 818 await vi.waitFor(() => { 819 - expect(component.find('.animate-spin').exists()).toBe(true) 819 + expect(component.find('[data-testid="loading-spinner"]').exists()).toBe(true) 820 820 }) 821 821 822 822 // Resolve the promise to clean up