Skip to content

fix: add loading skeletons to prevent route change flash#96

Merged
thegdsks merged 3 commits intomainfrom
fix/loading-skeletons-route-flash
Apr 13, 2026
Merged

fix: add loading skeletons to prevent route change flash#96
thegdsks merged 3 commits intomainfrom
fix/loading-skeletons-route-flash

Conversation

@thegdsks
Copy link
Copy Markdown
Member

Summary

  • Add loading.tsx skeleton files for root, icon detail, categories, collections, and blog routes
  • Prevents white flash/glitch on route changes in static Cloudflare Pages deployment
  • Uses existing design tokens (bg-card/80, border-border/40, animate-pulse) with staggered animation delays

Test plan

  • Navigate between routes and verify skeleton shows instead of white flash
  • Check dark and light themes both render skeletons correctly
  • Verify mobile responsive at 375px, 768px
  • Run pnpm build to confirm static export works

thegdsks and others added 3 commits April 10, 2026 15:36
Closes #90, closes #91

Co-Authored-By: Glinr <bot@glincker.com>
Remove individual registry/<slug>.json files to stay within Cloudflare
Pages 20K file limit. Extension now fetches SVGs directly and caches
the registry. Also adds JSX/HTML/DataURI copy formats and updates
metadata to 5,600+ icons.

Co-Authored-By: Glinr <bot@glincker.com>
Co-Authored-By: Glinr <bot@glincker.com>
Copilot AI review requested due to automatic review settings April 13, 2026 19:56
@thegdsks thegdsks self-assigned this Apr 13, 2026
@thegdsks thegdsks added the ui User interface changes label Apr 13, 2026
@thegdsks thegdsks merged commit 0457f0f into main Apr 13, 2026
4 checks passed
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 13, 2026

Greptile Summary

This PR adds five Next.js loading.tsx route-level skeletons to prevent white flashes during client-side navigation on the Cloudflare Pages static deployment. Four of the five files are well-structured, but the blog skeleton has two minor issues worth a quick fix before merge.

Confidence Score: 5/5

Safe to merge — all findings are P2 style issues that don't affect runtime correctness.

The only problems are in blog/loading.tsx: a misplaced animationDelay (cosmetic, stagger won't work) and a missing sidebar placeholder (minor layout shift). Neither breaks functionality or data correctness. All other skeletons are implemented correctly.

src/app/blog/loading.tsx — animationDelay misapplied and sidebar skeleton absent.

Important Files Changed

Filename Overview
src/app/blog/loading.tsx New skeleton for the blog route; animationDelay is misapplied to the card container (no animation), so staggering is a no-op, and the layout omits the sidebar present on the actual page.
src/app/categories/loading.tsx New skeleton for the categories route; sidebar and category-grid placeholders match the real layout, and animate-pulse + animationDelay are correctly co-located.
src/app/collection/[name]/loading.tsx New skeleton for the collection route; sidebar and icon-grid placeholders look correct with properly applied animation delays.
src/app/icon/[slug]/loading.tsx New skeleton for the icon-detail route; covers sidebar, preview area, action buttons, and related-icons grid with correct staggered delays.
src/app/loading.tsx New root-level loading component; uses a centered spinner with a text label as a global fallback, which is appropriate for the catch-all case.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User navigates to route] --> B{Route segment}
    B -->|/| C[src/app/loading.tsx\nSpinner fallback]
    B -->|/blog| D[src/app/blog/loading.tsx\nPost-list skeleton]
    B -->|/categories| E[src/app/categories/loading.tsx\nSidebar + grid skeleton]
    B -->|/collection/:name| F[src/app/collection/name/loading.tsx\nSidebar + icon-grid skeleton]
    B -->|/icon/:slug| G[src/app/icon/slug/loading.tsx\nSidebar + detail skeleton]
    C --> H[Page hydrates, skeleton unmounts]
    D --> H
    E --> H
    F --> H
    G --> H
Loading

Reviews (1): Last reviewed commit: "fix: add loading skeletons to prevent ro..." | Re-trigger Greptile

Comment thread src/app/blog/loading.tsx
Comment on lines +1 to +4
export default function BlogLoading() {
return (
<div className="mx-auto max-w-4xl space-y-6 p-4 md:p-6">
<div className="h-8 w-32 animate-pulse rounded-lg bg-muted/50" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Skeleton layout doesn't match the actual page structure

The blog loading skeleton renders a standalone max-w-4xl centered container with no sidebar, but BlogPage wraps its content inside SidebarShell (which renders a 64px sidebar). When the skeleton transitions to the real page, the sidebar appears and the content shifts left — partially defeating the goal of a flash-free transition. The other four loading files all include an <aside> sidebar skeleton to prevent exactly this shift.

Comment thread src/app/blog/loading.tsx
Comment on lines +9 to +15
className="space-y-2 rounded-xl border border-border/40 bg-card/80 p-6"
style={{ animationDelay: `${i * 75}ms` }}
>
<div className="h-6 w-3/4 animate-pulse rounded bg-muted/50" />
<div className="h-4 w-1/2 animate-pulse rounded bg-muted/50" />
<div className="h-4 w-24 animate-pulse rounded bg-muted/50" />
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 animationDelay has no effect on this container

animationDelay is applied to the card wrapper (space-y-2 rounded-xl …), which carries no animation class — so the delay is a no-op. Meanwhile, the animate-pulse classes on the three inner divs have no corresponding delay, meaning all five blog cards pulse simultaneously rather than staggering. Every other loading file in this PR correctly co-locates animate-pulse and animationDelay on the same element (e.g., categories/loading.tsx lines 10–11).

The fix is to move animate-pulse up to the container div and drop it from the children, so the single animationDelay controls the whole card's pulse.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds route-level loading.tsx UI to reduce perceived flashing during route transitions, and updates the static JSON “API” outputs + Raycast extension behavior to work with static deployments (plus a couple of new icons / icon metadata normalization).

Changes:

  • Add loading.tsx components for root and several routes (icon detail, categories, collections, blog).
  • Update generate-api.ts to output only static registry.json + categories.json (no per-icon detail files) and expand registry fields.
  • Update the Raycast extension to use the new static endpoints, fetch SVGs directly, and add “Copy as JSX/HTML/Data URI/Hex” actions.

Reviewed changes

Copilot reviewed 12 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/scripts/generate-api.ts Changes static API generation outputs/shape (drops per-icon files, expands registry fields).
src/data/icons.json Normalizes various unicode strings + adds new icons/metadata.
src/app/loading.tsx Adds root-level loading UI.
src/app/icon/[slug]/loading.tsx Adds icon detail route skeleton.
src/app/collection/[name]/loading.tsx Adds collection route skeleton.
src/app/categories/loading.tsx Adds categories route skeleton.
src/app/blog/loading.tsx Adds blog route skeleton.
public/icons/jinritoutiao/default.svg Adds new icon asset.
public/icons/format-json-online/default.svg Adds new icon asset.
extensions/raycast/src/search-icons.tsx Enhances search keywords (aliases) and adds new copy-format actions section.
extensions/raycast/src/api.ts Switches Raycast data fetching to static JSON + direct SVG fetch; adds copy-format helpers.
extensions/raycast/package.json Updates extension descriptions/counts.
extensions/raycast/README.md Updates docs for new counts and copy actions.
extensions/raycast/CHANGELOG.md Documents new Raycast version features/fixes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1 to +13
/**
* Generates static JSON API files in public/api/ for the Raycast extension
* and any other consumers that need a REST-like interface.
*
* Run: npx tsx src/scripts/generate-api.ts
*
* Outputs:
* public/api/registry.json - all icons (lightweight: slug, title, categories, variant keys)
* public/api/registry.json - all icons (slug, title, aliases, categories, hex, url, variant keys)
* public/api/categories.json - category list with counts
* public/api/registry/<slug>.json - per-icon detail (includes inline SVGs)
*
* Note: Individual per-icon detail files are NOT generated to stay within
* Cloudflare Pages' 20,000 file deployment limit. Extensions should fetch
* SVG content directly from /icons/{slug}/{variant}.svg.
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description/title focuses on adding route loading skeletons, but this PR also changes the generated public API shape/outputs (including removing per-icon registry files) and updates the Raycast extension + adds icons. Please update the PR description (or split PRs) so reviewers understand the API/consumer impact (e.g., other clients may still expect /api/registry/{slug} style endpoints).

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +164
function toPascalCase(str: string): string {
return str
.replace(/[^a-zA-Z0-9]+(.)/g, (_, c: string) => c.toUpperCase())
.replace(/^(.)/, (_, c: string) => c.toUpperCase());
}

function svgToJsxAttrs(svg: string): string {
return svg
.replace(/\bclass="/g, 'className="')
.replace(/\bxmlns="[^"]*"/g, "")
.replace(
/\b([a-z]+)-([a-z])/g,
(_, a: string, b: string) => `${a}${b.toUpperCase()}`,
);
}

export function toJsx(svg: string, componentName: string): string {
const name = toPascalCase(componentName);
const jsxSvg = svgToJsxAttrs(svg);
return `export function ${name}Icon(props) {\n return (\n ${jsxSvg.replace(/<svg/, "<svg {...props}")}\n );\n}`;
}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toPascalCase/toJsx can generate an invalid JavaScript identifier for some slugs (e.g., icons like "01dotai" exist, which would produce export function 01dotaiIcon...). Please sanitize the generated component/function name so it always starts with a letter/underscore and contains only valid identifier characters (e.g., prefix with Icon when needed).

Copilot uses AI. Check for mistakes.
Comment on lines +154 to +156
.replace(
/\b([a-z]+)-([a-z])/g,
(_, a: string, b: string) => `${a}${b.toUpperCase()}`,
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

svgToJsxAttrs currently applies the kebab-case -> camelCase replacement across the entire SVG string, which will also mutate CSS inside style="..." attributes (e.g., fill-rule:evenodd becomes fillRule:evenodd, which is invalid CSS) and can break rendering for many existing icons. Consider restricting the transform to attribute names only (e.g., only when the kebab-case token is followed by =), or parsing/transforming the SVG more safely.

Suggested change
.replace(
/\b([a-z]+)-([a-z])/g,
(_, a: string, b: string) => `${a}${b.toUpperCase()}`,
.replace(/\b([a-z][a-z0-9-]*)(?==)/g, (attr: string) =>
attr.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()),

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ui User interface changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants