fix: add loading skeletons to prevent route change flash#96
Conversation
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>
Greptile SummaryThis PR adds five Next.js Confidence Score: 5/5Safe 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
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
Reviews (1): Last reviewed commit: "fix: add loading skeletons to prevent ro..." | Re-trigger Greptile |
| 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" /> |
There was a problem hiding this comment.
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.
| 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> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.tsxcomponents for root and several routes (icon detail, categories, collections, blog). - Update
generate-api.tsto output only staticregistry.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.
| /** | ||
| * 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. |
There was a problem hiding this comment.
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).
| 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}`; | ||
| } |
There was a problem hiding this comment.
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).
| .replace( | ||
| /\b([a-z]+)-([a-z])/g, | ||
| (_, a: string, b: string) => `${a}${b.toUpperCase()}`, |
There was a problem hiding this comment.
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.
| .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()), |
Summary
loading.tsxskeleton files for root, icon detail, categories, collections, and blog routesbg-card/80,border-border/40,animate-pulse) with staggered animation delaysTest plan
pnpm buildto confirm static export works