this repo has no description
10
fork

Configure Feed

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

feat(ui): add non-home loading skeletons

Improve perceived navigation speed with lightweight skeletons and hide screenshot carousel controls when there is only one image.

Made-with: Cursor

+244 -17
+135
assets/styles.css
··· 533 533 padding: 0 1.5rem; 534 534 } 535 535 536 + /* ================================ 537 + Page Loading Skeleton 538 + ================================ */ 539 + 540 + .page-skeleton { 541 + position: fixed; 542 + inset: 0; 543 + z-index: 1000; 544 + display: none; 545 + pointer-events: none; 546 + background: 547 + radial-gradient( 548 + ellipse 120% 80% at 20% 10%, 549 + rgba(180, 210, 255, 0.48) 0%, 550 + transparent 50% 551 + ), 552 + radial-gradient( 553 + ellipse 100% 60% at 80% 20%, 554 + rgba(200, 180, 255, 0.32) 0%, 555 + transparent 50% 556 + ), 557 + linear-gradient(180deg, #e8f0fe 0%, #dce6f8 48%, #ebe4f5 100%); 558 + } 559 + 560 + .page-skeleton--visible { 561 + display: block; 562 + } 563 + 564 + .page-skeleton-nav { 565 + position: fixed; 566 + top: 0; 567 + left: 0; 568 + right: 0; 569 + z-index: 1; 570 + min-height: var(--nav-bar-height); 571 + padding: 1rem 1.5rem; 572 + display: flex; 573 + justify-content: space-between; 574 + align-items: center; 575 + background: rgba(255, 255, 255, 0.48); 576 + border-bottom: 1px solid rgba(255, 255, 255, 0.55); 577 + backdrop-filter: blur(20px); 578 + -webkit-backdrop-filter: blur(20px); 579 + } 580 + 581 + .page-skeleton-main { 582 + width: min(100%, 960px); 583 + margin: 0 auto; 584 + padding: calc(var(--nav-bar-height) + 3rem) 1.5rem 3rem; 585 + } 586 + 587 + .page-skeleton-logo, 588 + .page-skeleton-pill, 589 + .page-skeleton-card, 590 + .page-skeleton-block { 591 + display: block; 592 + border-radius: 999px; 593 + background: linear-gradient( 594 + 110deg, 595 + rgba(255, 255, 255, 0.42) 8%, 596 + rgba(255, 255, 255, 0.78) 18%, 597 + rgba(255, 255, 255, 0.42) 33% 598 + ); 599 + background-size: 220% 100%; 600 + animation: page-skeleton-shimmer 1.15s linear infinite; 601 + border: 1px solid rgba(255, 255, 255, 0.46); 602 + } 603 + 604 + .page-skeleton-logo { 605 + width: 180px; 606 + height: 2.2rem; 607 + } 608 + 609 + .page-skeleton-pill { 610 + width: 110px; 611 + height: 2.35rem; 612 + } 613 + 614 + .page-skeleton-card { 615 + border-radius: 24px; 616 + min-height: 160px; 617 + box-shadow: 0 18px 50px rgba(20, 34, 70, 0.08); 618 + } 619 + 620 + .page-skeleton-card--hero { 621 + min-height: 280px; 622 + padding: 2rem; 623 + margin-bottom: 1rem; 624 + } 625 + 626 + .page-skeleton-grid { 627 + display: grid; 628 + grid-template-columns: repeat(3, minmax(0, 1fr)); 629 + gap: 1rem; 630 + } 631 + 632 + .page-skeleton-block { 633 + height: 1rem; 634 + margin-bottom: 0.9rem; 635 + } 636 + 637 + .page-skeleton-block--title { 638 + width: min(60%, 360px); 639 + height: 2.4rem; 640 + } 641 + 642 + .page-skeleton-block--body { 643 + width: min(86%, 620px); 644 + } 645 + 646 + .page-skeleton-block--short { 647 + width: min(48%, 340px); 648 + } 649 + 650 + @keyframes page-skeleton-shimmer { 651 + to { 652 + background-position-x: -220%; 653 + } 654 + } 655 + 656 + @media (max-width: 720px) { 657 + .page-skeleton-main { 658 + padding-left: 1rem; 659 + padding-right: 1rem; 660 + } 661 + 662 + .page-skeleton-grid { 663 + grid-template-columns: 1fr; 664 + } 665 + 666 + .page-skeleton-card:nth-child(n+2) { 667 + display: none; 668 + } 669 + } 670 + 536 671 .section { 537 672 padding: 3.5rem 0; 538 673 }
+24 -17
components/explore/ProfileScreenshots.tsx
··· 6 6 7 7 export default function ProfileScreenshots({ profile }: Props) { 8 8 if (profile.screenshots.length === 0) return null; 9 + const hasMultipleScreenshots = profile.screenshots.length > 1; 9 10 10 11 return ( 11 12 <section class="profile-screenshots" aria-label="Screenshots"> 12 13 <div class="profile-screenshots-shell" data-screenshot-carousel> 13 - <button 14 - type="button" 15 - class="profile-screenshots-arrow profile-screenshots-arrow--prev" 16 - aria-label="Previous screenshot" 17 - data-screenshot-direction="-1" 18 - > 19 - 20 - </button> 14 + {hasMultipleScreenshots && ( 15 + <button 16 + type="button" 17 + class="profile-screenshots-arrow profile-screenshots-arrow--prev" 18 + aria-label="Previous screenshot" 19 + data-screenshot-direction="-1" 20 + > 21 + 22 + </button> 23 + )} 21 24 <div class="profile-screenshots-carousel"> 22 25 {profile.screenshots.map((_, i) => ( 23 26 <a ··· 41 44 </a> 42 45 ))} 43 46 </div> 44 - <button 45 - type="button" 46 - class="profile-screenshots-arrow profile-screenshots-arrow--next" 47 - aria-label="Next screenshot" 48 - data-screenshot-direction="1" 49 - > 50 - 51 - </button> 47 + {hasMultipleScreenshots && ( 48 + <button 49 + type="button" 50 + class="profile-screenshots-arrow profile-screenshots-arrow--next" 51 + aria-label="Next screenshot" 52 + data-screenshot-direction="1" 53 + > 54 + 55 + </button> 56 + )} 52 57 </div> 53 - <script type="module" src="/profile-screenshot-carousel.js" /> 58 + {hasMultipleScreenshots && ( 59 + <script type="module" src="/profile-screenshot-carousel.js" /> 60 + )} 54 61 </section> 55 62 ); 56 63 }
+1
routes/_app.tsx
··· 300 300 <I18nProvider locale={locale}> 301 301 <Component /> 302 302 </I18nProvider> 303 + <script type="module" src="/page-skeleton.js" /> 303 304 {effectsOn && ( 304 305 <> 305 306 <script
+84
static/page-skeleton.js
··· 1 + const skeletonId = "page-loading-skeleton"; 2 + let showTimer = 0; 3 + 4 + function isNonHomePage(url) { 5 + return url.origin === globalThis.location.origin && url.pathname !== "/"; 6 + } 7 + 8 + function ensureSkeleton() { 9 + let skeleton = document.getElementById(skeletonId); 10 + if (skeleton) return skeleton; 11 + 12 + skeleton = document.createElement("div"); 13 + skeleton.id = skeletonId; 14 + skeleton.className = "page-skeleton"; 15 + skeleton.setAttribute("aria-hidden", "true"); 16 + skeleton.innerHTML = ` 17 + <div class="page-skeleton-nav"> 18 + <span class="page-skeleton-logo"></span> 19 + <span class="page-skeleton-pill"></span> 20 + </div> 21 + <main class="page-skeleton-main"> 22 + <section class="page-skeleton-card page-skeleton-card--hero"> 23 + <span class="page-skeleton-block page-skeleton-block--title"></span> 24 + <span class="page-skeleton-block page-skeleton-block--body"></span> 25 + <span class="page-skeleton-block page-skeleton-block--short"></span> 26 + </section> 27 + <section class="page-skeleton-grid"> 28 + <span class="page-skeleton-card"></span> 29 + <span class="page-skeleton-card"></span> 30 + <span class="page-skeleton-card"></span> 31 + </section> 32 + </main> 33 + `; 34 + document.body.appendChild(skeleton); 35 + return skeleton; 36 + } 37 + 38 + function showSkeleton() { 39 + ensureSkeleton().classList.add("page-skeleton--visible"); 40 + } 41 + 42 + function scheduleSkeleton() { 43 + clearTimeout(showTimer); 44 + showTimer = globalThis.setTimeout(showSkeleton, 120); 45 + } 46 + 47 + function hideSkeleton() { 48 + clearTimeout(showTimer); 49 + document.getElementById(skeletonId)?.classList.remove( 50 + "page-skeleton--visible", 51 + ); 52 + } 53 + 54 + document.addEventListener("click", (event) => { 55 + if (event.defaultPrevented || event.button !== 0) return; 56 + if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; 57 + 58 + const target = event.target; 59 + if (!(target instanceof Element)) return; 60 + const link = target.closest("a[href]"); 61 + if (!(link instanceof HTMLAnchorElement)) return; 62 + if (link.target && link.target !== "_self") return; 63 + if (link.hasAttribute("download")) return; 64 + 65 + const url = new URL(link.href, globalThis.location.href); 66 + if (url.hash && url.pathname === globalThis.location.pathname) return; 67 + if (!isNonHomePage(url)) return; 68 + 69 + scheduleSkeleton(); 70 + }); 71 + 72 + document.addEventListener("submit", (event) => { 73 + const form = event.target; 74 + if (!(form instanceof HTMLFormElement)) return; 75 + 76 + globalThis.setTimeout(() => { 77 + if (event.defaultPrevented) return; 78 + const url = new URL(form.action || globalThis.location.href); 79 + if (!isNonHomePage(url) || url.pathname.startsWith("/api/")) return; 80 + scheduleSkeleton(); 81 + }, 0); 82 + }); 83 + 84 + globalThis.addEventListener("pageshow", hideSkeleton);