[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: improve package page skeleton loading state (#1115)

authored by

Daniel Roe and committed by
GitHub
4baf904e a06538e6

+246 -138
+246 -138
app/components/Package/Skeleton.vue
··· 4 4 <article 5 5 aria-busy="true" 6 6 :aria-label="$t('package.skeleton.loading')" 7 - class="motion-safe:animate-fade-in" 7 + class="package-page motion-safe:animate-fade-in" 8 8 > 9 - <!-- Package header - matches header in [...name].vue --> 10 - <header class="mb-8 pb-8 border-b border-border"> 11 - <div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4 mb-4"> 12 - <div class="flex-1 min-w-0"> 13 - <!-- Package name: h1 font-mono text-2xl sm:text-3xl font-medium mb-2 --> 14 - <h1 class="font-mono text-2xl sm:text-3xl font-medium mb-2"> 9 + <!-- Package header — matches area-header in [...name].vue --> 10 + <header class="area-header sticky top-14 z-1 bg-[--bg] py-2"> 11 + <div class="flex items-baseline gap-2 sm:gap-3 flex-wrap min-w-0"> 12 + <!-- Package name --> 13 + <div class="min-w-0"> 14 + <h1 class="font-mono text-2xl sm:text-3xl font-medium"> 15 15 <SkeletonInline class="h-9 w-48" /> 16 16 </h1> 17 - <!-- Description: fixed height container matching min-h-[4.5rem] (72px) to prevent CLS --> 18 - <div class="relative max-w-2xl min-h-[4.5rem]"> 19 - <div class="space-y-2"> 20 - <SkeletonBlock class="h-5 w-full" /> 21 - <SkeletonBlock class="h-5 w-4/5" /> 22 - <SkeletonBlock class="h-5 w-3/5" /> 23 - </div> 17 + </div> 18 + <!-- Version --> 19 + <span class="inline-flex items-baseline font-mono text-base sm:text-lg shrink-0"> 20 + <SkeletonInline class="h-6 w-20" /> 21 + </span> 22 + <!-- Metrics badges placeholder --> 23 + <div class="flex items-center gap-1.5 relative top-[5px] self-baseline ms-1 sm:ms-2"> 24 + <SkeletonBlock class="w-16 h-5.5 rounded" /> 25 + <SkeletonBlock class="w-13 h-5.5 rounded" /> 26 + <SkeletonBlock class="w-13 h-5.5 rounded" /> 27 + <SkeletonBlock class="w-13 h-5.5 rounded bg-bg-subtle" /> 28 + </div> 29 + <!-- Internal navigation placeholder (hidden on mobile) --> 30 + <div 31 + class="hidden sm:flex items-center gap-0.5 p-0.5 bg-bg-subtle border border-border-subtle rounded-md shrink-0 ms-auto self-center" 32 + > 33 + <SkeletonInline class="h-7 w-16 rounded" /> 34 + <SkeletonInline class="h-7 w-14 rounded" /> 35 + <SkeletonInline class="h-7 w-20 rounded" /> 36 + </div> 37 + </div> 38 + </header> 39 + 40 + <!-- Package details — matches area-details in [...name].vue --> 41 + <section class="area-details"> 42 + <div class="mb-4"> 43 + <!-- Description container with min-height to prevent CLS --> 44 + <div class="max-w-2xl min-h-[4.5rem]"> 45 + <div class="space-y-2"> 46 + <SkeletonBlock class="h-5 w-full" /> 47 + <SkeletonBlock class="h-5 w-4/5" /> 48 + <SkeletonBlock class="h-5 w-3/5" /> 24 49 </div> 25 50 </div> 26 51 27 - <!-- Version badge: shrink-0 px-3 py-1 font-mono text-sm bg-bg-muted border border-border rounded-md --> 28 - <SkeletonInline class="shrink-0 h-8 w-20 rounded-md" /> 52 + <!-- External links --> 53 + <ul class="flex flex-wrap items-center gap-x-3 gap-y-1.5 sm:gap-4 list-none m-0 p-0 mt-3"> 54 + <li><SkeletonInline class="h-5 w-28" /></li> 55 + <li><SkeletonInline class="h-5 w-14" /></li> 56 + <li><SkeletonInline class="h-5 w-16" /></li> 57 + <li><SkeletonInline class="h-5 w-10" /></li> 58 + </ul> 29 59 </div> 30 60 31 - <!-- Stats grid: grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-4 mt-6 --> 32 - <dl class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-4 mt-6"> 61 + <!-- Stats grid — matches dl in [...name].vue --> 62 + <dl 63 + class="grid grid-cols-2 sm:grid-cols-7 md:grid-cols-11 gap-3 sm:gap-4 py-4 sm:py-6 mt-4 sm:mt-6 border-t border-b border-border" 64 + > 33 65 <!-- License --> 34 - <div class="space-y-1"> 66 + <div class="space-y-1 sm:col-span-2"> 35 67 <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 36 - {{ $t('package.skeleton.license') }} 68 + {{ $t('package.stats.license') }} 37 69 </dt> 38 70 <dd class="font-mono text-sm"> 39 71 <SkeletonInline class="h-5 w-12" /> 40 72 </dd> 41 73 </div> 42 74 43 - <!-- Weekly --> 44 - <div class="space-y-1"> 75 + <!-- Deps --> 76 + <div class="space-y-1 sm:col-span-2"> 45 77 <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 46 - {{ $t('package.skeleton.weekly') }} 78 + {{ $t('package.stats.deps') }} 47 79 </dt> 48 80 <dd class="font-mono text-sm"> 49 - <SkeletonInline class="h-5 w-20" /> 81 + <SkeletonInline class="h-5 w-12" /> 50 82 </dd> 51 83 </div> 52 84 53 - <!-- Size --> 54 - <div class="space-y-1"> 85 + <!-- Install Size --> 86 + <div class="space-y-1 sm:col-span-3"> 55 87 <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 56 - {{ $t('package.skeleton.size') }} 88 + {{ $t('package.stats.install_size') }} 57 89 </dt> 58 90 <dd class="font-mono text-sm"> 59 91 <SkeletonInline class="h-5 w-16" /> 60 92 </dd> 61 93 </div> 62 94 63 - <!-- Deps --> 64 - <div class="space-y-1"> 95 + <!-- Vulns --> 96 + <div class="space-y-1 sm:col-span-2"> 65 97 <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 66 - {{ $t('package.skeleton.deps') }} 98 + {{ $t('package.stats.vulns') }} 67 99 </dt> 68 - <dd class="font-mono text-sm"> 69 - <SkeletonInline class="h-5 w-8" /> 70 - </dd> 100 + <dd class="font-mono text-sm text-fg-subtle">-</dd> 71 101 </div> 72 102 73 103 <!-- Published --> 74 - <div class="space-y-1 col-span-2"> 104 + <div class="space-y-1 sm:col-span-2"> 75 105 <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 76 - {{ $t('package.skeleton.published') }} 106 + {{ $t('package.stats.published') }} 77 107 </dt> 78 108 <dd class="font-mono text-sm"> 79 109 <SkeletonInline class="h-5 w-28" /> 80 110 </dd> 81 111 </div> 82 112 </dl> 83 - 84 - <!-- Links: mt-6, flex flex-wrap items-center gap-4 --> 85 - <nav aria-label="Package links" class="mt-6"> 86 - <ul class="flex flex-wrap items-center gap-4 list-none m-0 p-0"> 87 - <li> 88 - <SkeletonInline class="h-5 w-14" /> 89 - </li> 90 - <li> 91 - <SkeletonInline class="h-5 w-20" /> 92 - </li> 93 - <li> 94 - <SkeletonInline class="h-5 w-16" /> 95 - </li> 96 - <li> 97 - <SkeletonInline class="h-5 w-12" /> 98 - </li> 99 - </ul> 100 - </nav> 101 - </header> 113 + </section> 102 114 103 - <!-- Install section: mb-8 --> 104 - <section class="mb-8"> 105 - <h2 106 - id="install-heading-skeleton" 107 - class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 108 - > 109 - {{ $t('package.skeleton.get_started') }} 110 - </h2> 111 - <!-- code-block with relative positioning for copy button --> 115 + <!-- Install section — matches area-install in [...name].vue --> 116 + <section class="area-install scroll-mt-20"> 117 + <div class="flex flex-wrap items-center justify-between mb-3"> 118 + <h2 class="text-xs text-fg-subtle uppercase tracking-wider"> 119 + {{ $t('package.get_started.title') }} 120 + </h2> 121 + <!-- Package manager select placeholder --> 122 + <SkeletonInline class="h-7 w-24 rounded" /> 123 + </div> 124 + <!-- Install command code block --> 112 125 <div class="relative"> 113 126 <div 114 127 class="bg-bg-muted border border-border rounded-md p-4 font-mono text-sm overflow-x-auto pe-16" ··· 119 132 </div> 120 133 </section> 121 134 122 - <!-- Two column layout: grid lg:grid-cols-3 gap-8 --> 123 - <div class="grid lg:grid-cols-3 gap-8"> 124 - <!-- Main content (README): lg:col-span-2 order-2 lg:order-1 --> 125 - <div class="lg:col-span-2 order-2 lg:order-1"> 126 - <section> 127 - <h2 128 - id="readme-heading-skeleton" 129 - class="text-xs text-fg-subtle uppercase tracking-wider mb-4" 130 - > 131 - {{ $t('package.skeleton.readme') }} 132 - </h2> 133 - <!-- Simulated README content --> 134 - <div class="space-y-4"> 135 - <!-- Heading --> 136 - <SkeletonBlock class="h-7 w-2/3" /> 137 - <!-- Paragraphs --> 138 - <SkeletonBlock class="h-4 w-full" /> 139 - <SkeletonBlock class="h-4 w-full" /> 140 - <SkeletonBlock class="h-4 w-4/5" /> 141 - <!-- Gap for section break --> 142 - <SkeletonBlock class="h-6 w-1/2 mt-6" /> 143 - <SkeletonBlock class="h-4 w-full" /> 144 - <SkeletonBlock class="h-4 w-full" /> 145 - <SkeletonBlock class="h-4 w-3/4" /> 146 - <!-- Code block placeholder --> 147 - <SkeletonBlock class="h-24 w-full rounded-lg mt-4" /> 148 - <SkeletonBlock class="h-4 w-full" /> 149 - <SkeletonBlock class="h-4 w-5/6" /> 150 - </div> 151 - </section> 152 - </div> 135 + <!-- Vulns area (empty placeholder to hold grid space) --> 136 + <div class="area-vulns" /> 153 137 154 - <!-- Sidebar: order-1 lg:order-2 space-y-8 --> 155 - <div class="order-1 lg:order-2 space-y-8"> 156 - <!-- Maintainers --> 157 - <section> 158 - <h2 159 - id="maintainers-heading-skeleton" 160 - class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 161 - > 162 - {{ $t('package.skeleton.maintainers') }} 163 - </h2> 164 - <ul class="space-y-2 list-none m-0 p-0"> 165 - <li> 166 - <SkeletonInline class="h-5 w-28" /> 167 - </li> 168 - <li> 169 - <SkeletonInline class="h-5 w-24" /> 170 - </li> 171 - </ul> 172 - </section> 138 + <!-- README — matches area-readme in [...name].vue --> 139 + <section class="area-readme min-w-0 scroll-mt-20"> 140 + <div class="flex flex-wrap items-center justify-between mb-3 px-1"> 141 + <h2 class="text-xs text-fg-subtle uppercase tracking-wider"> 142 + {{ $t('package.readme.title') }} 143 + </h2> 144 + </div> 145 + <!-- Simulated README content --> 146 + <div class="space-y-4"> 147 + <!-- Heading --> 148 + <SkeletonBlock class="h-7 w-2/3" /> 149 + <!-- Paragraphs --> 150 + <SkeletonBlock class="h-4 w-full" /> 151 + <SkeletonBlock class="h-4 w-full" /> 152 + <SkeletonBlock class="h-4 w-4/5" /> 153 + <!-- Gap for section break --> 154 + <SkeletonBlock class="h-6 w-1/2 mt-6" /> 155 + <SkeletonBlock class="h-4 w-full" /> 156 + <SkeletonBlock class="h-4 w-full" /> 157 + <SkeletonBlock class="h-4 w-3/4" /> 158 + <!-- Code block placeholder --> 159 + <SkeletonBlock class="h-24 w-full rounded-lg mt-4" /> 160 + <SkeletonBlock class="h-4 w-full" /> 161 + <SkeletonBlock class="h-4 w-5/6" /> 162 + </div> 163 + </section> 173 164 174 - <!-- Keywords --> 165 + <!-- Sidebar — matches area-sidebar in [...name].vue --> 166 + <div class="area-sidebar"> 167 + <div 168 + class="sidebar-scroll sticky top-34 space-y-6 sm:space-y-8 min-w-0 overflow-y-auto pe-2.5 lg:(max-h-[calc(100dvh-8.5rem)] overscroll-contain) xl:(top-22 pt-2 max-h-[calc(100dvh-6rem)])" 169 + > 170 + <!-- Download stats --> 175 171 <section> 176 - <h2 177 - id="keywords-heading-skeleton" 178 - class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 179 - > 180 - {{ $t('package.skeleton.keywords') }} 172 + <h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-3"> 173 + {{ $t('package.skeleton.weekly') }} 181 174 </h2> 182 - <!-- flex flex-wrap gap-1.5 --> 183 - <ul class="flex flex-wrap gap-1.5 list-none m-0 p-0"> 184 - <li><SkeletonInline class="h-6 w-16 rounded" /></li> 185 - <li><SkeletonInline class="h-6 w-12 rounded" /></li> 186 - <li><SkeletonInline class="h-6 w-20 rounded" /></li> 187 - <li><SkeletonInline class="h-6 w-14 rounded" /></li> 188 - <li><SkeletonInline class="h-6 w-18 rounded" /></li> 189 - <li><SkeletonInline class="h-6 w-10 rounded" /></li> 190 - </ul> 175 + <!-- Chart placeholder --> 176 + <SkeletonBlock class="h-32 w-full rounded-lg" /> 191 177 </section> 192 178 193 179 <!-- Versions --> 194 180 <section> 195 - <h2 196 - id="versions-heading-skeleton" 197 - class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 198 - > 181 + <h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-3"> 199 182 {{ $t('package.skeleton.versions') }} 200 183 </h2> 201 - <!-- space-y-1, each row: flex items-center justify-between py-1.5 text-sm --> 202 184 <div class="space-y-1"> 203 185 <div class="flex items-center justify-between py-1.5 text-sm"> 204 186 <SkeletonInline class="h-4 w-16" /> ··· 225 207 226 208 <!-- Dependencies --> 227 209 <section> 228 - <h2 229 - id="dependencies-heading-skeleton" 230 - class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 231 - > 210 + <h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-3"> 232 211 {{ $t('package.skeleton.dependencies') }} 233 212 </h2> 234 - <!-- space-y-1, each: flex items-center justify-between py-1 text-sm --> 235 213 <ul class="space-y-1 list-none m-0 p-0"> 236 214 <li class="flex items-center justify-between py-1 text-sm"> 237 215 <SkeletonInline class="h-4 w-24" /> ··· 251 229 </li> 252 230 </ul> 253 231 </section> 232 + 233 + <!-- Keywords --> 234 + <section> 235 + <h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-3"> 236 + {{ $t('package.skeleton.keywords') }} 237 + </h2> 238 + <ul class="flex flex-wrap gap-1.5 list-none m-0 p-0"> 239 + <li><SkeletonInline class="h-6 w-16 rounded" /></li> 240 + <li><SkeletonInline class="h-6 w-12 rounded" /></li> 241 + <li><SkeletonInline class="h-6 w-20 rounded" /></li> 242 + <li><SkeletonInline class="h-6 w-14 rounded" /></li> 243 + <li><SkeletonInline class="h-6 w-18 rounded" /></li> 244 + <li><SkeletonInline class="h-6 w-10 rounded" /></li> 245 + </ul> 246 + </section> 247 + 248 + <!-- Maintainers --> 249 + <section> 250 + <h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-3"> 251 + {{ $t('package.skeleton.maintainers') }} 252 + </h2> 253 + <ul class="space-y-2 list-none m-0 p-0"> 254 + <li><SkeletonInline class="h-5 w-28" /></li> 255 + <li><SkeletonInline class="h-5 w-24" /></li> 256 + </ul> 257 + </section> 254 258 </div> 255 259 </div> 256 260 </article> 257 261 </template> 262 + 263 + <style scoped> 264 + .package-page { 265 + display: grid; 266 + gap: 2rem; 267 + 268 + /* Mobile: single column, sidebar above readme */ 269 + grid-template-columns: minmax(0, 1fr); 270 + grid-template-areas: 271 + 'header' 272 + 'details' 273 + 'install' 274 + 'vulns' 275 + 'sidebar' 276 + 'readme'; 277 + } 278 + 279 + /* Tablet/medium: header/install/vulns full width, readme+sidebar side by side */ 280 + @media (min-width: 1024px) { 281 + .package-page { 282 + grid-template-columns: 2fr 1fr; 283 + grid-template-areas: 284 + 'header header' 285 + 'details details' 286 + 'install install' 287 + 'vulns vulns' 288 + 'readme sidebar'; 289 + grid-template-rows: auto auto auto auto 1fr; 290 + } 291 + } 292 + 293 + /* Desktop: floating sidebar alongside all content */ 294 + @media (min-width: 1280px) { 295 + .package-page { 296 + grid-template-columns: 1fr 20rem; 297 + grid-template-areas: 298 + 'header sidebar' 299 + 'details sidebar' 300 + 'install sidebar' 301 + 'vulns sidebar' 302 + 'readme sidebar'; 303 + } 304 + } 305 + 306 + .area-header { 307 + grid-area: header; 308 + } 309 + 310 + .area-details { 311 + grid-area: details; 312 + } 313 + 314 + .area-install { 315 + grid-area: install; 316 + } 317 + 318 + .area-vulns { 319 + grid-area: vulns; 320 + } 321 + 322 + .area-readme { 323 + grid-area: readme; 324 + overflow-x: hidden; 325 + } 326 + 327 + .area-sidebar { 328 + grid-area: sidebar; 329 + } 330 + 331 + /* Sidebar scrollbar: hidden by default, shown on hover/focus */ 332 + @media (min-width: 1024px) { 333 + .sidebar-scroll { 334 + scrollbar-gutter: stable; 335 + scrollbar-width: 8px; 336 + scrollbar-color: transparent transparent; 337 + } 338 + 339 + .sidebar-scroll::-webkit-scrollbar { 340 + width: 8px; 341 + height: 8px; 342 + } 343 + 344 + .sidebar-scroll::-webkit-scrollbar-track, 345 + .sidebar-scroll::-webkit-scrollbar-thumb { 346 + background: transparent; 347 + } 348 + 349 + .sidebar-scroll:hover, 350 + .sidebar-scroll:focus-within { 351 + scrollbar-color: var(--border) transparent; 352 + } 353 + 354 + .sidebar-scroll:hover::-webkit-scrollbar-thumb, 355 + .sidebar-scroll:focus-within::-webkit-scrollbar-thumb { 356 + background-color: var(--border); 357 + border-radius: 9999px; 358 + } 359 + 360 + .sidebar-scroll:hover::-webkit-scrollbar-track, 361 + .sidebar-scroll:focus-within::-webkit-scrollbar-track { 362 + background: transparent; 363 + } 364 + } 365 + </style>