Skip to content

Conversation

@maamokun
Copy link
Collaborator

@maamokun maamokun commented Oct 27, 2025

Summary by CodeRabbit

Release Notes

  • New Features

    • Added interactive cursor customisation options (click sparks, splash effect, custom cursor, target cursor).
    • Introduced settings panel with privacy and cursor toy controls.
    • Added 3D fluid glass interactive component.
  • Bug Fixes & Updates

    • Updated core framework dependencies and styling system to latest versions.
  • Removals

    • Simplified site structure by consolidating pages and navigation.
    • Removed several landing pages and UI components.

@vercel
Copy link

vercel bot commented Oct 27, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
mikn-dev Ready Ready Preview Comment Oct 28, 2025 5:36am

@coderabbitai
Copy link

coderabbitai bot commented Oct 27, 2025

Walkthrough

A major architectural refactor transitioning from Cloudflare/AWS infrastructure to @c15t framework, removing legacy page components, simplifying internationalisation, and introducing an interactive cursor effects system with new UI component library featuring advanced animations and 3D rendering capabilities.

Changes

Cohort / File(s) Summary
Infrastructure & Deployment Migration
wrangler.jsonc, open-next.config.ts, .dockerignore
Removes Cloudflare Wrangler configuration and OpenNext/Cloudflare integration config; clears .dockerignore rules.
Package Dependencies Restructuring
package.json
Shifts from Cloudflare/AWS to @c15t framework; updates Next.js (15→16), React (19.1→19.2), and related packages; adds UI/animation libraries (Radix UI, GSAP, Lucide, Tailwind animate); removes cloud-specific tooling.
Configuration Files Update
biome.json, tsconfig.json, tailwind.config.ts, renovate.json, postcss.config.mjs
Updates linter, compiler, and build tool configurations with formatting and structural changes; introduces v2.2.0 biome schema with VCS and files sections.
Next.js & App Configuration
next.config.mjs, components.json
Adds static export, custom image loader, React Compiler; introduces ShadCN UI configuration for component library.
Internationalization Simplification
src/i18n/request.ts, src/i18n/routing.ts, src/messages/en.json, src/messages/ja.json, src/middleware.ts
Hardcodes locale to "en", removes next-intl middleware, reduces translation keys to minimal placeholder structure.
Page Components Removal
src/app/[locale]/about/page.tsx, src/app/[locale]/contact/page.tsx, src/app/[locale]/cost/page.tsx, src/app/[locale]/solutions/page.tsx, src/app/[locale]/tech/page.tsx
Deletes loading spinner pages and contact form component.
Layout & Root Structure Overhaul
src/app/layout.tsx, src/app/[locale]/layout.tsx, src/app/[locale]/page.tsx, src/app/[locale]/template.tsx, src/app/not-found.tsx
Replaces complex layouts with HTML/body wrappers and providers; simplifies homepage to minimal Button; removes header/footer scaffolding in template.
Legacy Fancy Components Removal
src/components/fancy/marqee.tsx, src/components/fancy/typewriter.tsx, src/components/fancy/NumberTicker.tsx, src/components/fancy/code-comp.tsx, src/components/fancy/icon-cloud.tsx, src/components/codecomp-v3v4.tsx
Removes Marquee, Typewriter, NumberTicker, CodeComparison, IconCloud, and before/after code examples.
Navigation UI Components Removal
src/components/nUI/Header.tsx, src/components/nUI/Footer.tsx, src/components/nUI/MenuToggle.tsx, src/components/nUI/hooks.tsx
Eliminates header with mobile menu, footer, menu toggle, and useIsWide hook.
Styling & Theme Updates
src/app/globals.css
Replaces DaisyUI with token-based CSS variables, adds @theme block with semantic color/shadow mappings, introduces custom dark variant, adds tailwindcss-animate imports.
Image Loader Utilities
src/imgLoader.ts (removed), imgLoader.ts (new)
Removes Cloudflare image loader; introduces environment-based image optimizer URL builder with size/quality parameters.
Utility Functions
src/components/cn.ts, src/lib/utils.ts
Minor formatting in cn; adds new cn utility using clsx and tailwind-merge for class name composition.
Interactive Cursor Toys System
src/components/ClickSpark.tsx, src/components/SplashCursor.tsx, src/components/CustomCursor.tsx, src/components/TargetCursor.tsx, src/components/CursorToys.tsx, src/components/GlobalCursorToys.tsx
Introduces four cursor effect components (click spark particles, WebGL2 fluid simulation, PNG tracker, GSAP-animated target) orchestrated via CursorToys wrapper.
Settings & Control Components
src/components/SettingsController.tsx, src/components/consent-manager.tsx
Adds floating settings panel with cursor toy selection and privacy consent integration via @c15t.
Advanced 3D Component
src/components/FluidGlass.tsx
Introduces React Three Fiber-based interactive 3D glass effect with lens/bar/cube modes, transmission material shading, pointer following, and responsive typography/image gallery.
Animate-UI Component Library
src/components/animate-ui/components/buttons/button.tsx, src/components/animate-ui/primitives/buttons/button.tsx, src/components/animate-ui/primitives/animate/slot.tsx, src/components/animate-ui/primitives/radix/radio-group.tsx, src/components/animate-ui/icons/icon.tsx, src/components/animate-ui/icons/settings.tsx, src/components/ui/button.tsx
Implements new button and UI primitives with CVA-based styling, Framer Motion-aware slot wrapper, animated Radio components, icon animation system with variants, and Settings icon.
Context, Hooks & Utilities
src/contexts/CursorToysContext.tsx, src/hooks/use-controlled-state.tsx, src/hooks/use-is-in-view.tsx, src/lib/get-strict-context.tsx, src/interfaces/CursorToys.ts
Adds context for cursor toy selection with localStorage persistence; introduces useControlledState and useIsInView hooks; provides getStrictContext utility for strict provider enforcement.
IDE Configuration
.idea/copilot.data.migration.ask.xml, .idea/copilot.data.migration.ask2agent.xml, .idea/copilotDiffState.xml
Adds Copilot migration state markers (both marked as COMPLETED).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser
    participant SettingsController
    participant CursorToysContext
    participant CursorToys
    participant CursorEffect[Cursor Effect Component]
    
    User->>SettingsController: Click cursor toy button
    SettingsController->>CursorToysContext: setSelectedToy("spark"|"splash"|...)
    CursorToysContext->>CursorToysContext: Update state & localStorage
    CursorToysContext->>CursorToys: Re-render with new selectedToy
    CursorToys->>CursorEffect: Render selected effect component
    CursorEffect->>Browser: Mount/activate cursor effect
    
    User->>Browser: Move mouse / Click
    Browser->>CursorEffect: Fire mouse/click events
    CursorEffect->>CursorEffect: Update animation state
    CursorEffect->>Browser: Render canvas/DOM updates
Loading
sequenceDiagram
    participant App
    participant Layout["[locale]/layout.tsx"]
    participant Template["[locale]/template.tsx"]
    participant ConsentManager
    participant CursorToysProvider
    
    App->>Layout: Validate locale & render
    alt Locale not in routing.locales
        Layout->>App: notFound()
    else Locale valid
        Layout->>Template: Render with children
        Template->>ConsentManager: Wrap children
        ConsentManager->>ConsentManager: Initialize @c15t provider
        Template->>CursorToysProvider: Initialize cursor toys
        CursorToysProvider->>CursorToysProvider: Load selectedToy from localStorage
        CursorToysProvider->>App: Provide context to children
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • SplashCursor.tsx: Substantial WebGL2 fluid simulation implementation with complex shader system, FBO management, and intricate rendering loop requiring deep domain knowledge
  • FluidGlass.tsx: Advanced React Three Fiber integration with GLTF model loading, transmission material rendering, scroll controls, and responsive 3D typography/imagery positioning
  • TargetCursor.tsx: GSAP-driven animation orchestration with target element detection, parallax effects, and state synchronisation across multiple DOM elements
  • Animate-UI Icon System (icon.tsx): Sophisticated context-based animation lifecycle management with viewport-triggered effects, event composition, and variant resolution
  • Migration Completeness: Verify all Cloudflare/AWS infrastructure removal does not break deployment; confirm @c15t framework integration is complete
  • i18n Hardcoding: Locale hardcoded to "en" in request.ts—confirm this is intentional and that multi-language support is deferred or removed
  • Package.json Changes: Significant dependency restructuring across core packages (Next.js, React) and new animation libraries; verify no breaking changes in component APIs
  • Layout Changes: Root layout now uses CursorToysProvider and ConsentManager—ensure these providers are correctly nested and don't cause rendering issues

Poem

🐰 Our cursor dances with spark and splash,
Glass flows with shaders in 3D flash,
From Cloudflare clouds to @c15t's gleam,
A whisker-twitch brings the UI dream!
Settings spin, animations flow—
Watch this refactor steal the show!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.22% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The title "Refactor" is extremely vague and generic. Whilst technically the changeset is indeed a refactor, the term provides no meaningful information about the scope or nature of the changes. This pull request contains substantial architectural changes including migration from Cloudflare to @c15t framework, removal of multiple page components, addition of new interactive cursor and 3D components, major dependency updates (Next.js 16, React 19.2.0), and significant UI component restructuring. A colleague scanning the commit history would have no understanding of these major changes from the title alone, which fails to convey whether this is a minor cleanup, an architectural migration, or a feature refactor. Consider revising the title to be more descriptive and specific about the primary objective. Examples might include "Migrate from Cloudflare to @c15t framework with dependency updates", "Refactor homepage and component architecture with @c15t migration", or "Update core dependencies and migrate deployment infrastructure to @c15t". This would clearly communicate the main thrust of the changeset to anyone reviewing the project history.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @maamokun, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request undertakes a comprehensive refactoring of the project, focusing on modernizing the frontend architecture and optimizing the deployment process. Key aspects include updating critical dependencies, transitioning to a new UI component library (shadcn/ui) with a custom Tailwind CSS theme, and simplifying page structures. The changes also reflect a strategic shift in the deployment approach, moving towards static exports and away from previous Cloudflare Workers/OpenNext configurations, alongside general project cleanup.

Highlights

  • Dependency Updates: Several core dependencies in package.json have been updated, including next to ^16.0.0, react to ^19.2.0, and @biomejs/biome to ^2.3.1. New dependencies like @radix-ui/react-slot, class-variance-authority, lucide-react, tailwindcss-animate, and tw-animate-css have been added, while @opennextjs/cloudflare and @opennextjs/aws have been removed.
  • Biome Configuration Overhaul: The biome.json configuration has been significantly refactored, updating the schema version, changing formatter settings (e.g., indentStyle from tab to space, indentWidth from 4 to 2), and adding new rules and domains for linting (e.g., suspicious.noUnknownAtRules: "off", domains.next: "recommended", domains.react: "recommended").
  • Next.js and Deployment Strategy Shift: The next.config.mjs file now includes output: "export", indicating a move towards static export. The open-next.config.ts file has been removed, along with wrangler.jsonc and bunfig.toml, suggesting a shift away from Cloudflare Workers/OpenNext deployment.
  • UI Component and Styling Refactor: Multiple page files (src/app/[locale]/about/page.tsx, src/app/[locale]/contact/page.tsx, etc.) have been removed or drastically simplified. New consent management files (src/app/consent-manager.client.tsx, src/app/consent-manager.tsx) and a components.json for shadcn/ui have been added. The src/app/globals.css has been completely rewritten to remove daisyui and introduce a new custom theme with CSS variables and tailwindcss-animate.
  • Project Configuration Cleanup: The .dockerignore and bunfig.toml files have been removed, streamlining project configuration. New .idea/copilot.data.migration.ask.xml and .idea/copilot.data.migration.ask2agent.xml files have been added, likely for IDE integration with Copilot.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist
Copy link

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Oct 27, 2025

Deploying mikn-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: e04bede
Status: ✅  Deploy successful!
Preview URL: https://07292e47.mikn-dev.pages.dev
Branch Preview URL: https://refactor.mikn-dev.pages.dev

View logs

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 22

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/fancy/icon-cloud.tsx (1)

1-4: Remove renderToString import from client component and use a browser-compatible alternative.

renderToString is a server-only API in React 19 and must not be imported in client components marked with "use client". Although it may execute in the browser, doing so violates React 19's architecture, bloats the bundle, and is explicitly discouraged. Convert SVG React elements to strings using createRoot with a temporary container, or adopt the new static/prerender APIs instead. Alternatively, move icon preprocessing to a server component if feasible.

🧹 Nitpick comments (20)
src/components/fancy/icon-cloud.tsx (2)

285-293: Improve accessibility of the numbered circles fallback.

The fallback numbered circles (lines 285–293) lack semantic meaning for screen readers. Since the canvas already has role="img" and aria-label, consider:

  • Removing the fallback circles and rendering nothing whilst icons load.
  • Or, rendering a more descriptive fallback with better contrast and size.

Current approach is a minor accessibility concern because the circles are visual-only.


308-308: Review the dependency array for potential excessive re-renders.

The dependency array includes iconPositions, isDragging, mousePos, and targetRotation objects. Since these are updated on every render or animation frame, the effect may re-run more often than intended. Consider using a stable reference or narrowing dependencies:

- }, [icons, images, iconPositions, isDragging, mousePos, targetRotation]);
+ }, [icons, images]);

The animation loop uses rotationRef and other refs directly, so explicit dependency on state changes is unnecessary—the animation frame itself will capture updates via ref access.

Confirm that the animation remains smooth and responsive after reducing the dependency array.

src/messages/ja.json (1)

19-21: Polish JP localisation (typo + formalise cookie consent + correct “Legal” label).

  • Fix duplicated の (Line 19).
  • Replace “法的条約” with a more accurate “法的情報” (Line 40).
  • Make consent copy formal and compliant; “いいね!” → “同意する” (Lines 51–52).

If your ConsentManager supports “Reject”/“Manage preferences”, consider adding keys for those buttons too.

-    "SimpleNCheap": "シンプルさを追求し生まれた究極ののコストパフォマンス",
+    "SimpleNCheap": "シンプルさを追求して生まれた究極のコストパフォーマンス",

-    "legal": "法的条約",
+    "legal": "法的情報",

-    "cookieConsent": "そこの君!このサイトはCookieを使ってるぞ!!いいよね?",
-    "accept": "いいね!"
+    "cookieConsent": "当サイトでは、機能の提供および分析のためにクッキーを使用します。詳細はプライバシーポリシーをご確認ください。クッキーの使用に同意しますか?",
+    "accept": "同意する"

Also applies to: 40-42, 51-52

src/components/nUI/hooks.tsx (1)

5-12: Simplify hook: return useMedia directly; remove redundant state/effect.

Reduces re-renders and removes the need for the eslint-disable.

-export const useIsWide = () => {
-  const [isWide, setIsWide] = useState(false);
-
-  const _isWide = useMedia("(min-width: 780px)", false);
-
-  useEffect(() => {
-    setIsWide(_isWide);
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [_isWide]);
-
-  return isWide;
-};
+export const useIsWide = () => {
+  return useMedia("(min-width: 780px)", false);
+};

Additionally, remove now‑unused React imports at the top:

-import { useEffect, useState } from "react";
+// no React hooks needed here

Also applies to: 14-14

src/messages/en.json (1)

51-52: Use neutral, explicit cookie consent copy.

Improve clarity and align with consent UX.

-    "cookieConsent": "We use cookies for analytics and making stuff work. By clicking 'mmmmmmm cookies 🍪', you agree to our use of cookies.",
-    "accept": "mmmmmmm cookies 🍪"
+    "cookieConsent": "We use cookies to enable site functionality and for analytics. See our Privacy Policy. Do you consent to the use of cookies?",
+    "accept": "Accept"
src/components/fancy/marqee.tsx (2)

56-71: Guard repeat and respect reduced motion

Prevent RangeError for non-integers/negatives and add a reduced‑motion fallback for accessibility.

-      {Array(repeat)
-        .fill(0)
+      {Array.from({ length: Math.max(1, Math.floor(repeat)) })
         .map((_, i) => (
           <div
             key={i}
-            className={cn("flex shrink-0 justify-around gap-(--gap)", {
+            className={cn("flex shrink-0 justify-around gap-(--gap) motion-reduce:animate-none", {
               "animate-marquee flex-row": !vertical,
               "animate-marquee-vertical flex-col": vertical,
               "group-hover:[animation-play-state:paused]": pauseOnHover,
               "[animation-direction:reverse]": reverse,
             })}
           >

1-1: Spelling and import duplication

File name is “marqee.tsx”. Consider renaming to “marquee.tsx” and exporting from an index to avoid breaking imports. Also note two cn utilities exist (src/components/cn.ts and src/lib/utils.ts). Consolidate to a single source to avoid drift.

src/components/nUI/Header.tsx (5)

276-313: Use next/link for internal desktop navigation

<a> causes full page reloads and bypasses prefetch. Prefer <Link> for internal routes.

-                <motion.a
-                  key={item.name}
-                  href={item.href}
+                <motion.div
+                  key={item.name}
                   className="group relative text-sm font-semibold leading-6 text-on-background"
                   initial="rest"
                   whileHover="hover"
                   animate="rest"
                 >
-                  {item.name}
+                  <Link href={item.href} className="relative">
+                    {item.name}
+                  </Link>
                   {isCurrent ? (
...
-                </motion.a>
+                </motion.div>

97-103: Remove unused prop index from MobileMenuItemProps and call sites

index isn’t used inside MobileMenuItem. Drop it to keep the API tight.

-interface MobileMenuItemProps {
+interface MobileMenuItemProps {
   name: string;
   href: string;
-  index: number;
   isCurrent: boolean;
   color: string;
 }
...
-                  <MobileMenuItem
+                  <MobileMenuItem
                     key={item.name}
                     name={item.name}
                     href={item.href}
-                    index={index}
                     color={_color}
                     isCurrent={isCurrent}
                   />

Also applies to: 226-241


206-211: Optimise the logo image

Use next/image for better performance and add width/height/alt. If sticking to <img>, add loading="lazy" and decoding="async".


175-175: Avoid magic header heights; derive from ref

headerHeight = 88 and hide: { top: -88 } can drift from actual layout (h-12 / lg:h-16). Measure offsetHeight via a ref and compute dynamically.

Also applies to: 87-95


179-186: Throttle scroll updates to reduce re-render churn

Scroll fires very frequently; batching/throttling (e.g., requestAnimationFrame) can smooth performance.

src/app/[locale]/template.tsx (1)

2-5: Remove unused imports/vars and make analytics configurable

  • useTranslations, useRouter, usePathname, and their variables are unused.
  • Move the Swetrix key and API URL to env vars and optionally gate initialisation on consent (given the new ConsentManager in this PR).
  • The extra <div> wrapper isn’t necessary; return children directly.
-"use client";
-import { useTranslations } from "next-intl";
+"use client";
 import { useSwetrix } from "@swetrix/nextjs";
-import { useRouter, usePathname } from "next/navigation";
-import { ReactNode } from "react";
+import { ReactNode, useEffect } from "react";
 
 export default function PagesLayout({ children }: { children: ReactNode }) {
-  const router = useRouter();
-  const pathname = usePathname();
-  const t = useTranslations("nav");
-  useSwetrix("XxNIMaHCaVG3", { apiURL: "https://analytics.mikandev.tech/log" });
+  // TODO: read consent from context and guard initialisation
+  useSwetrix(process.env.NEXT_PUBLIC_SWETRIX_KEY!, {
+    apiURL: process.env.NEXT_PUBLIC_SWETRIX_API_URL,
+  });
 
-  return (
-    <>
-      <div>{children}</div>
-    </>
-  );
+  return <>{children}</>;
 }

Also applies to: 8-11, 13-17

src/app/layout.tsx (1)

11-15: Remove unnecessary fragment wrapper.

The fragment <>{children}</> on line 13 is redundant when wrapping a single expression.

Apply this diff:

   return (
     <ConsentManager>
-      <>{children}</>
+      {children}
     </ConsentManager>
   );
src/app/globals.css (3)

6-6: Custom dark variant selector may be overly broad; prefer ancestor-scoped form.

Use an ancestor-aware selector to avoid matching unintended contexts and align with common patterns:

-@custom-variant dark (&:is(.dark *));
+@custom-variant dark (&:where(.dark &));

This scopes “dark:” styles to elements under a .dark ancestor with better specificity behaviour. If you’re already using Tailwind’s built-in dark variant, consider removing this to avoid duplicate variants.


68-126: .dark variables duplicate :root; remove or override only differences.

The .dark block repeats all tokens with identical values as :root. This adds maintenance overhead without effect. Either delete the block or keep only actual overrides (e.g., background, foreground, accent).


181-188: Global base utilities: confirm outline colour utility is generated.

@apply outline-ring/50 relies on “ring” being a named colour token in @theme. If Tailwind doesn’t emit outline colour utilities for custom tokens in your setup, this may no-op. Verify it compiles to outline-color. If not, consider:

  • Using ring utilities instead (focus-visible:ring-... already present on Button), or
  • Switching to an explicit colour (e.g., outline-[color:oklch(...)]) for guarantees.
src/components/ui/button.tsx (1)

7-35: Variants look solid; consider exposing a data attribute for state styling (optional).

If you plan to style loading/pending states (e.g., with React Actions), exposing data-state attributes can help:

  • data-pending, data-success, data-error, etc., for Tailwind selectors (data-[pending]:opacity-50).
src/components/vrm.tsx (2)

48-60: Avoid setting static transforms every frame.

Position/scale/rotation and constant expression can be set once (e.g., useEffect) instead of on every animation frame. This reduces per‑frame work and avoids fighting external transforms.

Example:

-  useFrame(({ clock }, delta) => {
-    if (vrm) {
-      vrm.scene.position.set(0, -4.2, 0);
-      vrm.scene.scale.set(6.5, 5, 5);
-      vrm.scene.rotation.y = Math.PI;
-      vrm.expressionManager?.setValue("neutral", 1);
-      vrm.update(delta);
-    }
-    if (mixer) {
-      mixer.update(delta);
-    }
-  });
+  useEffect(() => {
+    if (!vrm) return;
+    vrm.scene.position.set(0, -4.2, 0);
+    vrm.scene.scale.set(6.5, 5, 5);
+    vrm.scene.rotation.y = Math.PI;
+    vrm.expressionManager?.setValue("neutral", 1);
+  }, [vrm]);
+
+  useFrame((_, delta) => {
+    vrm?.update(delta);
+    mixer?.update(delta);
+  });

94-104: Guard against zero/invalid weight totals in the animation picker.

Small resilience tweak: return early if total <= 0 to avoid NaNs when all weights are zero.

     function pickAnimation() {
-      const total = animations.reduce((sum, anim) => sum + anim.percentage, 0);
+      const total = animations.reduce((sum, anim) => sum + anim.percentage, 0);
+      if (total <= 0) return animations[0];
       const rand = Math.random() * total;
       let acc = 0;
       for (const anim of animations) {
         acc += anim.percentage;
         if (rand < acc) return anim;
       }
       return animations[0]; // fallback
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8fdb4a2 and 7abd5fc.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • bun.lockb is excluded by !**/bun.lockb
📒 Files selected for processing (47)
  • .dockerignore (0 hunks)
  • .idea/copilot.data.migration.ask.xml (1 hunks)
  • .idea/copilot.data.migration.ask2agent.xml (1 hunks)
  • biome.json (1 hunks)
  • bunfig.toml (0 hunks)
  • components.json (1 hunks)
  • next.config.mjs (1 hunks)
  • open-next.config.ts (0 hunks)
  • package.json (1 hunks)
  • postcss.config.mjs (1 hunks)
  • renovate.json (1 hunks)
  • src/app/[locale]/about/page.tsx (0 hunks)
  • src/app/[locale]/contact/page.tsx (0 hunks)
  • src/app/[locale]/cost/page.tsx (0 hunks)
  • src/app/[locale]/layout.tsx (1 hunks)
  • src/app/[locale]/page.tsx (1 hunks)
  • src/app/[locale]/solutions/page.tsx (0 hunks)
  • src/app/[locale]/tech/page.tsx (0 hunks)
  • src/app/[locale]/template.tsx (1 hunks)
  • src/app/consent-manager.client.tsx (1 hunks)
  • src/app/consent-manager.tsx (1 hunks)
  • src/app/globals.css (1 hunks)
  • src/app/layout.tsx (1 hunks)
  • src/app/not-found.tsx (1 hunks)
  • src/components/cn.ts (1 hunks)
  • src/components/codecomp-v3v4.tsx (0 hunks)
  • src/components/fancy/NumberTicker.tsx (1 hunks)
  • src/components/fancy/code-comp.tsx (1 hunks)
  • src/components/fancy/icon-cloud.tsx (1 hunks)
  • src/components/fancy/marqee.tsx (1 hunks)
  • src/components/fancy/typewriter.tsx (1 hunks)
  • src/components/nUI/Footer.tsx (1 hunks)
  • src/components/nUI/Header.tsx (1 hunks)
  • src/components/nUI/MenuToggle.tsx (1 hunks)
  • src/components/nUI/hooks.tsx (1 hunks)
  • src/components/ui/button.tsx (1 hunks)
  • src/components/vrm.tsx (1 hunks)
  • src/i18n/request.ts (1 hunks)
  • src/i18n/routing.ts (1 hunks)
  • src/imgLoader.ts (1 hunks)
  • src/lib/utils.ts (1 hunks)
  • src/messages/en.json (1 hunks)
  • src/messages/ja.json (1 hunks)
  • src/middleware.ts (0 hunks)
  • tailwind.config.ts (1 hunks)
  • tsconfig.json (1 hunks)
  • wrangler.jsonc (0 hunks)
💤 Files with no reviewable changes (11)
  • src/components/codecomp-v3v4.tsx
  • src/app/[locale]/tech/page.tsx
  • src/app/[locale]/cost/page.tsx
  • src/app/[locale]/contact/page.tsx
  • src/middleware.ts
  • bunfig.toml
  • src/app/[locale]/about/page.tsx
  • open-next.config.ts
  • src/app/[locale]/solutions/page.tsx
  • wrangler.jsonc
  • .dockerignore
🧰 Additional context used
🧬 Code graph analysis (9)
src/i18n/request.ts (1)
src/i18n/routing.ts (1)
  • routing (4-10)
src/components/nUI/Header.tsx (2)
src/components/nUI/hooks.tsx (1)
  • useIsWide (4-15)
src/components/nUI/MenuToggle.tsx (1)
  • MenuToggle (18-43)
src/app/layout.tsx (2)
src/app/[locale]/layout.tsx (1)
  • metadata (13-19)
src/app/consent-manager.tsx (1)
  • ConsentManager (8-20)
src/app/[locale]/layout.tsx (1)
src/i18n/routing.ts (1)
  • routing (4-10)
src/components/fancy/marqee.tsx (2)
src/components/cn.ts (1)
  • cn (4-6)
src/lib/utils.ts (1)
  • cn (4-6)
src/components/fancy/typewriter.tsx (2)
src/components/cn.ts (1)
  • cn (4-6)
src/lib/utils.ts (1)
  • cn (4-6)
src/app/[locale]/page.tsx (1)
src/components/ui/button.tsx (1)
  • Button (57-57)
src/components/fancy/NumberTicker.tsx (1)
src/components/cn.ts (1)
  • cn (4-6)
src/components/ui/button.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
🪛 ast-grep (0.39.6)
src/components/fancy/code-comp.tsx

[warning] 65-65: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🔇 Additional comments (11)
renovate.json (1)

2-3: Formatting consistency update — no functional changes.

The indentation adjustment maintains the same configuration behaviour and schema validation. No concerns with this formatting-only change.

src/imgLoader.ts (1)

1-22: Good formatting refactoring with sound logic intact.

The restructured parameter destructuring and improved whitespace maintain identical runtime behaviour whilst enhancing readability. The image loader utility is correctly implemented for Cloudflare CDN integration, with appropriate early return for development mode and optional quality parameter handling.

.idea/copilot.data.migration.ask2agent.xml (1)

1-6: Inconsistency with AI-generated summary flagged.

The AI-generated summary claims there is a typo "COMPLTED" in line 4 of this file, but the actual code shows the correct spelling "COMPLETED". This discrepancy should be noted.

src/components/fancy/icon-cloud.tsx (1)

49-49: The review comment is incorrect; type handling is already sound.

The code properly manages the React.ReactNode[] | string[] union through explicit runtime type checks at line 59 (if (images) { ... as string } else { ... as React.ReactElement }). Type assertions align precisely with control flow, and rendering logic includes appropriate guards at line 225 before canvas operations.

Additionally, the suggested fix introduces a semantic change: replacing || with ?? would alter fallback behaviour for empty arrays, which is unintended. The current implementation is type-safe and requires no modification.

Likely an incorrect or invalid review comment.

src/i18n/routing.ts (1)

5-9: Formatting only — looks good.

No semantic changes; locales and defaultLocale remain intact.

Also applies to: 15-15

tsconfig.json (2)

14-14: Correct JSX runtime for React 19.

The change from "preserve" to "react-jsx" aligns with React 19's requirement for the modern JSX transform. This enables the automatic JSX runtime without needing to import React in every file.


34-34: Good addition for Next.js dev types.

Including .next/dev/types/**/*.ts ensures TypeScript picks up Next.js-generated type definitions during development, improving type safety.

src/app/consent-manager.tsx (1)

1-20: Well-structured consent management implementation.

The component correctly wraps children with the consent provider and includes the necessary UI components (banner and dialogue). The offline mode is appropriate for a privacy-first approach, as it avoids external calls whilst managing consent locally.

src/app/consent-manager.client.tsx (1)

22-55: LGTM!

The client-side consent manager wrapper is well-structured and documented. The empty scripts and callbacks placeholders with inline examples provide clear guidance for future integrations.

src/app/globals.css (1)

3-4: Verify Tailwind plugin/import compatibility with v4.

Ensure tailwindcss-animate and tw-animate-css are v4-compatible (Oxide engine, new directives). Mismatched versions can silently drop utilities or keyframes.
Would you like a quick check script to list produced utilities at build time?

src/components/vrm.tsx (1)

4-6: Revert to investigate the @ts-ignore separately; the stated rationale does not apply to this codebase.

The review's premise is incorrect. Your TypeScript configuration does not have verbatimModuleSyntax enabled (it defaults to false), and esModuleInterop: true with moduleResolution: "bundler" already permits mixed value and type imports from the same module. The codebase uses import type patterns successfully elsewhere without .js extensions, which is the established convention here.

The @ts-ignore on line 4 may still warrant investigation—it could indicate an untyped export from three/examples/jsm/loaders/GLTFLoader—but the recommendation to add .js extensions and split the import contradicts both your TypeScript configuration and the codebase's import patterns. If there is a genuine typing issue with the GLTFLoader import, verify it separately rather than follow the suggested changes.

Likely an incorrect or invalid review comment.

Comment on lines +1 to +6
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project> No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

IDE metadata should not be committed to version control.

Files under .idea/ are JetBrains IDE configuration and state files that are typically excluded from repositories. These can cause merge conflicts, bloat the repository history, and introduce unnecessary noise. Ensure that .idea/ is properly listed in .gitignore and remove this file from the commit.

🤖 Prompt for AI Agents
In .idea/copilot.data.migration.ask.xml (lines 1-6) you committed IDE metadata;
remove this file from the repo and add .idea/ (or at least this
filename/pattern) to .gitignore. Specifically: delete the file from the commit
(git rm --cached .idea/copilot.data.migration.ask.xml), add an entry for .idea/
to .gitignore (or confirm it exists), commit the .gitignore change and the
removal, and push; optionally run git rm --cached -r .idea/ if other .idea files
were committed.

Comment on lines +1 to +6
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project> No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

IDE metadata should not be committed to version control.

Files under .idea/ are JetBrains IDE configuration and state files that are typically excluded from repositories. These can cause merge conflicts, bloat the repository history, and introduce unnecessary noise. Ensure that .idea/ is properly listed in .gitignore and remove this file from the commit.

🤖 Prompt for AI Agents
In .idea/copilot.data.migration.ask2agent.xml lines 1-6: this is IDE metadata
that should not be committed; remove this file from the repository and add
.idea/ (or at minimum this filename) to .gitignore so it won’t be tracked in
future commits; if the file is already in git history, remove it from the index
with git rm --cached and commit the removal, then push the change.

Comment on lines +8 to +11
"files": {
"ignoreUnknown": true,
"includes": ["**", "!node_modules", "!.next", "!dist", "!build"]
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify files.includes negation support

Confirm Biome v2.2 supports negation with ! in files.includes. If not, switch to files.ignore for .next, dist, build, and node_modules.


🌐 Web query:

Does Biome 2.2 support "!" negation in files.includes? What’s the recommended way to exclude .next, dist, build, and node_modules in biome.json?

💡 Result:

Short answer: Yes — Biome supports negated patterns starting with "!" in files.includes (but you must precede them with a "**" include). For generated/output folders prefer the force-ignore "!!" syntax so they are completely excluded from indexing. Node modules are ignored by default. [1][2][3]

Recommended biome.json snippet (concise):

{
"files": {
"includes": [
"",
"!
/.next",
"!!/dist",
"!!
/build",
"!**/node_modules"
]
}
}

Notes:

  • The leading "**" is required before negations in files.includes. [1][2]
  • Use "!!" for dist/build to prevent indexing (recommended for output folders). [2][3]
  • node_modules is ignored by Biome regardless of files.includes, but you can still add an explicit "!**/node_modules" for clarity. [1]

Update files.includes patterns to use proper glob syntax and force-ignore for build outputs

The negation patterns lack the required path prefix. Update to:

"includes": ["**", "!**/node_modules", "!**/.next", "!!**/dist", "!!**/build"]

Use !! (force-ignore) for dist and build to prevent indexing of generated output folders. Add **/ before each negation pattern for correct glob matching.

🤖 Prompt for AI Agents
In biome.json around lines 8 to 11, the glob negation patterns under
"files.includes" are incorrect and should include path prefixes and force-ignore
for build outputs; update the array so each negation uses the "**/" prefix
(e.g., "!**/node_modules", "!**/.next") and use double-negation force-ignore for
generated folders ("!!**/dist", "!!**/build") to ensure proper glob matching and
prevent indexing of generated output.

Comment on lines 1 to 22
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that referenced paths exist and align with project structure.

The ShadCN UI configuration references specific Tailwind and CSS paths, as well as module aliases. These should be verified to ensure they exist and are correctly configured in the TypeScript/Next.js setup.


🏁 Script executed:

#!/bin/bash
# Description: Verify that ShadCN configuration paths exist and aliases are correctly defined.

# Check if referenced Tailwind config exists
echo "=== Checking tailwind.config.ts ==="
if [ -f "tailwind.config.ts" ]; then
  echo "✓ tailwind.config.ts exists"
  head -20 tailwind.config.ts
else
  echo "✗ tailwind.config.ts not found"
fi

# Check if referenced CSS file exists
echo ""
echo "=== Checking src/app/globals.css ==="
if [ -f "src/app/globals.css" ]; then
  echo "✓ src/app/globals.css exists"
else
  echo "✗ src/app/globals.css not found"
fi

# Check if alias directories exist
echo ""
echo "=== Checking alias directories ==="
for dir in "src/components" "src/lib" "src/components/ui" "src/hooks"; do
  if [ -d "$dir" ]; then
    echo "$dir exists"
  else
    echo "$dir not found"
  fi
done

# Check TypeScript config for path aliases
echo ""
echo "=== Checking tsconfig.json for path aliases ==="
if [ -f "tsconfig.json" ]; then
  echo "✓ tsconfig.json exists"
  jq '.compilerOptions.paths' tsconfig.json 2>/dev/null || grep -A 20 '"paths"' tsconfig.json
else
  echo "✗ tsconfig.json not found"
fi

Length of output: 926


Create the src/hooks directory or remove the hooks alias from components.json.

The ShadCN configuration defines a hooks alias ("hooks": "@/hooks") that maps to src/hooks, but this directory does not exist. This will cause import failures at runtime if the alias is referenced in the codebase. Either create the directory to match the configuration or remove the alias definition if hooks are not needed.

🤖 Prompt for AI Agents
In components.json around lines 1 to 22, the "hooks": "@/hooks" alias points to
a non-existent src/hooks directory which will break imports; either create the
src/hooks directory (add an index file or relevant hook files so the alias
resolves) or remove the "hooks" alias from components.json, and if you choose
removal, also update any import paths that reference "@/hooks" and ensure
tsconfig/paths (or other alias configs) remain consistent.

Comment on lines +165 to +168
href={"https://youtube.com/@mikndotdev"}
target="_blank"
className={"block w-full h-full"}
>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

target="_blank" without rel is a security risk (reverse‑tabnabbing).

Add rel="noopener noreferrer" to external links opened in a new tab.

Apply:

-          <a
-            href={"https://youtube.com/@mikndotdev"}
-            target="_blank"
-            className={"block w-full h-full"}
-          >
+          <a
+            href="https://youtube.com/@mikndotdev"
+            target="_blank"
+            rel="noopener noreferrer"
+            className="block w-full h-full"
+          >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
href={"https://youtube.com/@mikndotdev"}
target="_blank"
className={"block w-full h-full"}
>
href="https://youtube.com/@mikndotdev"
target="_blank"
rel="noopener noreferrer"
className="block w-full h-full"
>
🤖 Prompt for AI Agents
In src/components/vrm.tsx around lines 165 to 168, the anchor uses
target="_blank" without rel attributes which risks reverse-tabnabbing; update
the <a> to include rel="noopener noreferrer" (i.e., add rel="noopener
noreferrer" alongside target="_blank") so external links opened in a new tab are
safe.

Comment on lines +177 to +201
<PiPlayPause
className="text-primary w-10 h-10 cursor-pointer"
onClick={() => {
if (!actionRef.current) return;
if (actionRef.current?.paused) {
actionRef.current.paused = false;
} else {
actionRef.current.paused = true;
}
}}
>
Pause
</PiPlayPause>
<PiRepeat
className="text-primary w-10 h-10 cursor-pointer"
onClick={() => {
if (actionRef.current) {
actionRef.current.reset();
actionRef.current.paused = false;
actionRef.current.play();
}
}}
>
Restart
</PiRepeat>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use real buttons for controls; fix inaccessible icon usage and invalid children.

Icons are not keyboard‑accessible and incorrectly include text as children. Replace with <button>s, add aria‑labels, and move the icons inside. This also improves focus styles.

Apply:

-        <PiPlayPause
-          className="text-primary w-10 h-10 cursor-pointer"
-          onClick={() => {
-            if (!actionRef.current) return;
-            if (actionRef.current?.paused) {
-              actionRef.current.paused = false;
-            } else {
-              actionRef.current.paused = true;
-            }
-          }}
-        >
-          Pause
-        </PiPlayPause>
-        <PiRepeat
-          className="text-primary w-10 h-10 cursor-pointer"
-          onClick={() => {
-            if (actionRef.current) {
-              actionRef.current.reset();
-              actionRef.current.paused = false;
-              actionRef.current.play();
-            }
-          }}
-        >
-          Restart
-        </PiRepeat>
+        <button
+          type="button"
+          aria-label="Play or pause animation"
+          className="text-primary w-10 h-10 inline-flex items-center justify-center"
+          onClick={() => {
+            const a = actionRef.current;
+            if (!a) return;
+            a.paused = !a.paused;
+          }}
+        >
+          <PiPlayPause aria-hidden="true" className="w-10 h-10" />
+        </button>
+        <button
+          type="button"
+          aria-label="Restart animation"
+          className="text-primary w-10 h-10 inline-flex items-center justify-center"
+          onClick={() => {
+            const a = actionRef.current;
+            if (!a) return;
+            a.reset();
+            a.paused = false;
+            a.play();
+          }}
+        >
+          <PiRepeat aria-hidden="true" className="w-10 h-10" />
+        </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<PiPlayPause
className="text-primary w-10 h-10 cursor-pointer"
onClick={() => {
if (!actionRef.current) return;
if (actionRef.current?.paused) {
actionRef.current.paused = false;
} else {
actionRef.current.paused = true;
}
}}
>
Pause
</PiPlayPause>
<PiRepeat
className="text-primary w-10 h-10 cursor-pointer"
onClick={() => {
if (actionRef.current) {
actionRef.current.reset();
actionRef.current.paused = false;
actionRef.current.play();
}
}}
>
Restart
</PiRepeat>
<button
type="button"
aria-label="Play or pause animation"
className="text-primary w-10 h-10 inline-flex items-center justify-center"
onClick={() => {
const a = actionRef.current;
if (!a) return;
a.paused = !a.paused;
}}
>
<PiPlayPause aria-hidden="true" className="w-10 h-10" />
</button>
<button
type="button"
aria-label="Restart animation"
className="text-primary w-10 h-10 inline-flex items-center justify-center"
onClick={() => {
const a = actionRef.current;
if (!a) return;
a.reset();
a.paused = false;
a.play();
}}
>
<PiRepeat aria-hidden="true" className="w-10 h-10" />
</button>
🤖 Prompt for AI Agents
In src/components/vrm.tsx around lines 177 to 201, the icon components are being
used as interactive controls with text children (invalid) and are not
keyboard‑accessible; replace each interactive icon usage with a semantic
<button> element (type="button") wrapping the icon component, move the icon to
be the button's child (remove text from the icon), add an appropriate aria-label
(e.g., "Pause" / "Restart"), keep the existing onClick logic on the button, and
ensure the button has the same styling classes plus a focus-visible style so
keyboard users can see focus.

Comment on lines 7 to 9
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Harden locale validation (avoid as any)

Do a proper type guard and fallback cleanly to the default locale.

-  if (!locale || !routing.locales.includes(locale as any)) {
-    locale = routing.defaultLocale;
-  }
+  const isSupportedLocale = (l: unknown): l is (typeof routing.locales)[number] =>
+    typeof l === "string" && (routing.locales as readonly string[]).includes(l);
+
+  if (!isSupportedLocale(locale)) {
+    locale = routing.defaultLocale;
+  }
🤖 Prompt for AI Agents
In src/i18n/request.ts around lines 7 to 9, the current check uses a loose cast
(as any) when validating locale; replace it with a proper type guard: verify
locale is a string (typeof locale === 'string') and that routing.locales
includes that string, otherwise assign routing.defaultLocale; ensure you do not
use any casts and keep types consistent with routing.locales' element type.

Comment on lines 11 to 21
if (process.env.NODE_ENV === "development") {
const res = await fetch(
`${process.env.TOLGEE_API_URL}/v2/projects/${process.env.TOLGEE_PROJECT_ID}/translations/${locale}`,
{
headers: {
"x-api-key": `${process.env.TOLGEE_API_KEY}`,
},
},
);
const data = await res.json();
const messages = data[locale];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling, env validation, and cache semantics to fetches

Current code can throw on non-2xx and missing envs. Add guards, no-store in dev, and a revalidate in prod. Consider an AbortController for timeouts.

-  if (process.env.NODE_ENV === "development") {
-    const res = await fetch(
+  if (process.env.NODE_ENV === "development") {
+    const { TOLGEE_API_URL, TOLGEE_PROJECT_ID, TOLGEE_API_KEY } = process.env;
+    if (!TOLGEE_API_URL || !TOLGEE_PROJECT_ID || !TOLGEE_API_KEY) {
+      console.warn("Tolgee env vars missing; falling back to empty messages in dev");
+      return { locale, messages: {} };
+    }
+    const res = await fetch(
       `${process.env.TOLGEE_API_URL}/v2/projects/${process.env.TOLGEE_PROJECT_ID}/translations/${locale}`,
       {
-        headers: {
+        cache: "no-store",
+        headers: {
           "x-api-key": `${process.env.TOLGEE_API_KEY}`,
         },
       },
     );
-    const data = await res.json();
+    if (!res.ok) {
+      console.warn("Tolgee fetch failed:", res.status, res.statusText);
+      return { locale, messages: {} };
+    }
+    const data = await res.json();
     const messages = data[locale];
 
     return {
       locale,
-      messages,
+      messages: messages ?? {},
     };
   }
 
-  const res = await fetch(`${process.env.I18N_PUBLIC_URL}/${locale}.json`);
+  const base = process.env.I18N_PUBLIC_URL;
+  if (!base) {
+    console.warn("I18N_PUBLIC_URL is missing; returning empty messages");
+    return { locale, messages: {} };
+  }
+  const res = await fetch(`${base}/${locale}.json`, { next: { revalidate: 3600 } });
+  if (!res.ok) {
+    console.warn("Static messages fetch failed:", res.status, res.statusText);
+    return { locale, messages: {} };
+  }
 
   return {
     locale,
     messages: await res.json(),
   };

Also applies to: 23-27, 29-34

🤖 Prompt for AI Agents
In src/i18n/request.ts around lines 11 to 21 (and also apply same changes to
lines 23-27 and 29-34), the fetch currently assumes required environment
variables exist, does not handle non-2xx responses, and has no cache semantics
or timeout; validate that NODE_ENV, TOLGEE_API_URL, TOLGEE_PROJECT_ID and
TOLGEE_API_KEY are present and throw or return a safe fallback when missing,
wrap the fetch in try/catch and check res.ok before calling res.json (throw or
log a descriptive error when not ok), add an AbortController with a short
timeout to cancel hung requests, and set fetch cache options to cache:
'no-store' when NODE_ENV === 'development' and to { next: { revalidate:
<seconds> } } (or equivalent) in production so dev fetches are fresh and prod
revalidates; ensure the same pattern (env validation, response.ok guard,
timeout, cache semantics, and safe fallback) is applied to the other indicated
line ranges.

Comment on lines +4 to +6
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Eliminate code duplication with src/components/cn.ts.

This cn function is identical to the one in src/components/cn.ts. Having duplicate utility functions violates the DRY principle and creates maintenance risks.

Recommended solution: Keep this implementation in src/lib/utils.ts and update src/components/cn.ts to re-export from here:

In src/components/cn.ts:

export { cn } from "@/lib/utils";

This centralises the utility in lib/utils.ts whilst maintaining backward compatibility for existing imports from components/cn.ts.

🤖 Prompt for AI Agents
In src/lib/utils.ts around lines 4 to 6, the cn function duplicates the
identical implementation in src/components/cn.ts; remove duplication by keeping
the implementation here and change src/components/cn.ts to re-export it. Update
src/components/cn.ts to export { cn } from "@/lib/utils"; so consumers importing
from components/cn.ts continue to work while the single implementation lives in
lib/utils.ts.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
package.json (2)

11-14: Remove orphaned deployment scripts.

These scripts reference opennextjs-cloudflare and wrangler which are no longer in the dependencies and will fail when executed.


53-53: Update React types to match React 19.

The @types/react version (^18) doesn't match the React version (19.2.0 on line 37), which can cause type errors with React 19 features.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7abd5fc and f4f6e38.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • src/assets/cloudfront.png is excluded by !**/*.png
📒 Files selected for processing (4)
  • imgLoader.ts (1 hunks)
  • next.config.mjs (1 hunks)
  • package.json (1 hunks)
  • src/imgLoader.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • src/imgLoader.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages

Comment on lines +1 to +2
const optimizerUrl = process.env.NEXT_PUBLIC_IMAGE_OPTIMIZER_URL || "";
const hostname = process.env.CF_PAGES_URL;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Validate required environment variables.

The loader relies on NEXT_PUBLIC_IMAGE_OPTIMIZER_URL and CF_PAGES_URL but doesn't validate their presence. If either is undefined or empty in production, the loader will generate malformed URLs, causing image loading failures.

Apply this diff to add validation:

 const optimizerUrl = process.env.NEXT_PUBLIC_IMAGE_OPTIMIZER_URL || "";
 const hostname = process.env.CF_PAGES_URL;
 
+if (process.env.NODE_ENV !== "development") {
+  if (!optimizerUrl) {
+    throw new Error("NEXT_PUBLIC_IMAGE_OPTIMIZER_URL is required in production");
+  }
+  if (!hostname) {
+    throw new Error("CF_PAGES_URL is required in production");
+  }
+}
+
 export default function ImageLoader({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const optimizerUrl = process.env.NEXT_PUBLIC_IMAGE_OPTIMIZER_URL || "";
const hostname = process.env.CF_PAGES_URL;
const optimizerUrl = process.env.NEXT_PUBLIC_IMAGE_OPTIMIZER_URL || "";
const hostname = process.env.CF_PAGES_URL;
if (process.env.NODE_ENV !== "development") {
if (!optimizerUrl) {
throw new Error("NEXT_PUBLIC_IMAGE_OPTIMIZER_URL is required in production");
}
if (!hostname) {
throw new Error("CF_PAGES_URL is required in production");
}
}
export default function ImageLoader({
🤖 Prompt for AI Agents
In imgLoader.ts around lines 1 to 2, the environment variables
NEXT_PUBLIC_IMAGE_OPTIMIZER_URL and CF_PAGES_URL are read but not validated; add
explicit checks after reading them to ensure they are non-empty strings and
throw a clear, early error (or call a startup logger and exit) if either is
missing so the loader never constructs malformed URLs; include which variable is
missing in the error message and fail fast in production builds to surface
configuration problems immediately.

}
const params = [`size=${width}`];
params.push(`quality=${quality || 75}`);
return `${optimizerUrl}/${encodeURIComponent(`${hostname}${src}`)}?${params.join("&")}`;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix URL construction to handle edge cases.

The URL construction has several potential issues:

  1. CF_PAGES_URL may not include a protocol (e.g., "example.com" instead of "https://example.com")
  2. Concatenating ${hostname}${src} assumes hostname doesn't end with / and src starts with /
  3. If either assumption is violated, you'll get malformed URLs like "example.com//path" or "example.compath"

Apply this diff to make URL construction more robust:

+  const normalizedHostname = hostname?.replace(/\/$/, "") || "";
+  const normalizedSrc = src.startsWith("/") ? src : `/${src}`;
+  const fullPath = `${normalizedHostname}${normalizedSrc}`;
+
   const params = [`size=${width}`];
   params.push(`quality=${quality || 75}`);
-  return `${optimizerUrl}/${encodeURIComponent(`${hostname}${src}`)}?${params.join("&")}`;
+  return `${optimizerUrl}/${encodeURIComponent(fullPath)}?${params.join("&")}`;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In imgLoader.ts around line 18, the URL is built by simply concatenating
optimizerUrl, hostname and src which breaks when CF_PAGES_URL lacks a protocol,
or when hostname ends with '/' or src starts with '/' (producing double slashes
or missing slash). Fix by normalizing the host to include a protocol (default to
"https://" if none), normalize/truncate leading/trailing slashes on hostname and
src (or use the URL API to set pathname) so they join with a single slash, and
ensure you pass a properly encoded path to encodeURIComponent or encode the full
URL via new URL(...) before encoding; then rebuild the final optimizer URL using
the normalized base so params.join("&") remains appended correctly.

output: "export",
images: {
loader: "custom",
loaderFile: "./imgLoader.ts",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Incorrect path to image loader file.

The loaderFile path is set to "./imgLoader.ts", but based on the file structure, the loader is located at "./src/imgLoader.ts". This mismatch will cause a build failure when Next.js attempts to load the custom image loader.

Apply this diff to correct the path:

-    loaderFile: "./imgLoader.ts",
+    loaderFile: "./src/imgLoader.ts",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
loaderFile: "./imgLoader.ts",
loaderFile: "./src/imgLoader.ts",
🤖 Prompt for AI Agents
In next.config.mjs around line 11, the loaderFile path is incorrect
("./imgLoader.ts") and should point to "./src/imgLoader.ts"; update the
loaderFile value to "./src/imgLoader.ts" so Next.js can locate the custom image
loader and avoid build failure.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (4)
package.json (1)

50-51: Update React type definitions to match React 19.

The project uses React 19.2.0 but @types/react is pinned to ^18, which is incompatible. React 19 requires @types/react@^19.0.0 and @types/react-dom@^19.0.0.

Apply this diff:

-    "@types/react": "^18",
-    "@types/react-dom": "^18",
+    "@types/react": "^19",
+    "@types/react-dom": "^19",

This mismatch will cause TypeScript errors with React 19 APIs and prevent proper type checking.

src/app/[locale]/page.tsx (3)

9-9: Remove unused translation to eliminate lint noise.

The translation function t is loaded but never used in the component, creating unnecessary overhead and potential linting warnings.

Either remove the unused import:

-  const t = await getTranslations("home");
   return (

Or use it to translate the button text:

   const t = await getTranslations("home");
   return (
     <>
-      <Button>a</Button>
+      <Button>{t("hi")}</Button>
     </>
   );

4-7: Incorrect params signature for Next.js App Router pages.

Next.js App Router pages receive an object containing params, not a Promise directly. The current signature will cause locale to be undefined at runtime, breaking locale initialisation.

Apply this diff to fix the signature:

-export default async function IndexPage(params: Promise<{ locale: string }>) {
-  const { locale } = await params;
+export default async function IndexPage({ params }: { params: Promise<{ locale: string }> }) {
+  const { locale } = await params;

Based on the past review comment, though note that in Next.js 15+, params is typically a Promise that should be awaited.


12-12: Button text appears to be a placeholder.

The button displays "a", which seems like placeholder content rather than final implementation.

Consider using a more meaningful label or the loaded translation:

-      <Button>a</Button>
+      <Button>{t("hi")}</Button>
🧹 Nitpick comments (2)
src/app/[locale]/template.tsx (2)

8-12: Simplify redundant wrapper structure.

The fragment wrapping a plain div adds unnecessary nesting. Since neither wrapper serves a functional purpose (no props, styling, or event handlers), consider returning children directly.

Apply this diff to simplify:

-  return (
-    <>
-      <div>{children}</div>
-    </>
-  );
+  return children;

Alternatively, if you need a single wrapper for future styling or layout purposes, keep just the div:

-  return (
-    <>
-      <div>{children}</div>
-    </>
-  );
+  return <div>{children}</div>;

6-6: Consider using environment variables for configuration.

The Swetrix ID and API URL are hardcoded, which reduces flexibility across different environments (development, staging, production). Consider moving these to environment variables.

Example refactor:

-  useSwetrix("XxNIMaHCaVG3", { apiURL: "https://analytics.mikandev.tech/log" });
+  useSwetrix(process.env.NEXT_PUBLIC_SWETRIX_ID!, { 
+    apiURL: process.env.NEXT_PUBLIC_SWETRIX_API_URL! 
+  });

Then add to your .env.local:

NEXT_PUBLIC_SWETRIX_ID=XxNIMaHCaVG3
NEXT_PUBLIC_SWETRIX_API_URL=https://analytics.mikandev.tech/log
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f4f6e38 and 01c4b09.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • package.json (1 hunks)
  • src/app/[locale]/page.tsx (1 hunks)
  • src/app/[locale]/template.tsx (1 hunks)
  • src/components/fancy/NumberTicker.tsx (0 hunks)
  • src/components/fancy/code-comp.tsx (0 hunks)
  • src/components/fancy/icon-cloud.tsx (0 hunks)
  • src/components/fancy/marqee.tsx (0 hunks)
  • src/components/fancy/typewriter.tsx (0 hunks)
  • src/components/nUI/Footer.tsx (0 hunks)
  • src/components/nUI/Header.tsx (0 hunks)
  • src/components/nUI/MenuToggle.tsx (0 hunks)
  • src/components/nUI/hooks.tsx (0 hunks)
  • src/i18n/request.ts (1 hunks)
  • src/messages/en.json (1 hunks)
  • tsconfig.json (1 hunks)
💤 Files with no reviewable changes (9)
  • src/components/nUI/MenuToggle.tsx
  • src/components/fancy/icon-cloud.tsx
  • src/components/nUI/Footer.tsx
  • src/components/fancy/marqee.tsx
  • src/components/fancy/NumberTicker.tsx
  • src/components/fancy/typewriter.tsx
  • src/components/nUI/hooks.tsx
  • src/components/nUI/Header.tsx
  • src/components/fancy/code-comp.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/app/[locale]/page.tsx (1)
src/components/ui/button.tsx (1)
  • Button (57-57)
🔇 Additional comments (1)
src/messages/en.json (1)

2-4: Intentional simplification to minimal translation structure.

The translation keys have been drastically reduced to a minimal placeholder. This aligns with the refactor's goal to simplify i18n handling and move to local file-based translations.

Comment on lines +3 to +9
export default getRequestConfig(async () => {
const locale = "en";

if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}

if (process.env.NODE_ENV === "development") {
const res = await fetch(
`${process.env.TOLGEE_API_URL}/v2/projects/${process.env.TOLGEE_PROJECT_ID}/translations/${locale}`,
{
headers: {
"x-api-key": `${process.env.TOLGEE_API_KEY}`,
},
},
);
const data = await res.json();
const messages = data[locale];

return {
locale,
messages,
};
}

const res = await fetch(`${process.env.I18N_PUBLIC_URL}/${locale}.json`);

return {
locale,
messages: await res.json(),
};
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Hardcoded locale ignores route parameter and breaks dynamic locale support.

The locale is hardcoded to "en", which means the [locale] route parameter in src/app/[locale]/page.tsx is effectively ignored for message loading. Whilst the page calls setRequestLocale(locale) with the URL parameter, the messages will always be loaded for English regardless of which locale is requested in the URL.

If the intention is to make this a monolingual application, consider either:

  1. Removing the [locale] dynamic route segment entirely, or
  2. Using the locale from request context to support the existing multi-locale routing structure.

If multi-locale support is intended, apply this diff:

-export default getRequestConfig(async () => {
-  const locale = "en";
+export default getRequestConfig(async ({ requestLocale }) => {
+  let locale = await requestLocale;
+  
+  // Fallback to "en" if no locale provided
+  if (!locale) {
+    locale = "en";
+  }

   return {
     locale,
     messages: (await import(`../messages/${locale}.json`)).default,
   };
 });

Note: The past review comments regarding error handling and locale validation are now obsolete as they referenced the removed remote fetch logic.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default getRequestConfig(async () => {
const locale = "en";
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
if (process.env.NODE_ENV === "development") {
const res = await fetch(
`${process.env.TOLGEE_API_URL}/v2/projects/${process.env.TOLGEE_PROJECT_ID}/translations/${locale}`,
{
headers: {
"x-api-key": `${process.env.TOLGEE_API_KEY}`,
},
},
);
const data = await res.json();
const messages = data[locale];
return {
locale,
messages,
};
}
const res = await fetch(`${process.env.I18N_PUBLIC_URL}/${locale}.json`);
return {
locale,
messages: await res.json(),
};
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
// Fallback to "en" if no locale provided
if (!locale) {
locale = "en";
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});

],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Disabling strict mode represents a type-safety regression.

Setting "strict": false disables all strict TypeScript checks, which undermines the benefits of upgrading to React 19's improved type definitions. This is especially problematic during a major version upgrade where stricter typing helps catch incompatibilities early.

Recommend reverting to "strict": true and addressing any type errors that emerge, rather than suppressing type checking altogether.

🤖 Prompt for AI Agents
In tsconfig.json around line 11, "strict" is set to false which disables all
TypeScript strict checks; change "strict" to true and rebuild/type-check, then
fix resulting type errors (address mismatched React 19 types, add explicit
types, narrow any/unknown usages, and update code or third‑party types as
needed) until the project compiles cleanly; if specific strict subflags are
problematic, enable strict and selectively relax only the minimal needed flags
(e.g., noImplicitAny or strictNullChecks) after resolving as many issues as
possible.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 22

♻️ Duplicate comments (3)
tsconfig.json (1)

7-7: ⚠️ Type-safety regression: strict mode remains disabled.

This mirrors the concern raised in the previous review. Setting "strict": false disables all TypeScript strict checks, which is especially problematic during a major React 19 migration. Rather than suppressing type checking, enable strict mode and resolve any type errors that emerge. If specific strict subflags prove problematic, selectively relax only the minimal required flags (e.g., noImplicitAny or strictNullChecks) after addressing as many issues as possible.

src/app/[locale]/page.tsx (2)

4-7: Incorrect page props signature; App Router passes { params }, not a Promise.

Accept { params } and read locale from it.

-export default async function IndexPage(params: Promise<{ locale: string }>) {
-  const { locale } = await params;
+export default async function IndexPage({ params }: { params: { locale: string } }) {
+  const { locale } = params;

9-14: Use or remove the loaded translation.

Either render a translated label or drop getTranslations to avoid lint noise.

Option A (use it):

-  const t = await getTranslations("home");
+  const t = await getTranslations("home");
   return (
     <>
-      <Button>Hello there!</Button>
+      <Button>{t("cta")}</Button>
     </>
   );

Option B (remove it):

-  const t = await getTranslations("home");
   return (
     <>
       <Button>Hello there!</Button>
     </>
   );
🧹 Nitpick comments (33)
src/components/FluidGlass.tsx (5)

30-37: Strengthen ModeProps typing and drop broad Record<string, unknown>

Current typing weakens safety (e.g. navItems is cast later). Define a concrete shape for material knobs + navItems to regain type help.

-type ModeProps = Record<string, unknown>;
+type ModeProps = {
+  // optional navigation for "bar" mode
+  navItems?: NavItem[];
+  // MeshTransmissionMaterial controls
+  scale?: number;
+  ior?: number;
+  thickness?: number;
+  anisotropy?: number;
+  chromaticAberration?: number;
+  transmission?: number;
+  roughness?: number;
+  attenuationColor?: string | number | THREE.Color;
+  attenuationDistance?: number;
+  color?: string | number | THREE.Color;
+};
-  } = modeProps as {
-    scale?: number;
-    ior?: number;
-    thickness?: number;
-    anisotropy?: number;
-    chromaticAberration?: number;
-    [key: string]: unknown;
-  };
+  } = modeProps as ModeProps;

Also applies to: 140-155


159-162: Background plane material flags

This plane is just the offscreen buffer. Disable tone mapping and depth writes to avoid subtle colour/ordering artefacts.

-        <meshBasicMaterial map={buffer.texture} transparent />
+        <meshBasicMaterial
+          map={buffer.texture}
+          transparent
+          toneMapped={false}
+          depthWrite={false}
+        />

185-206: Preload GLBs to avoid first‑interaction hitch

Proactively warm the cache for known assets.

+// Preload GLTF assets
+useGLTF.preload("https://cdn.mikn.dev/web/static/lens.glb");
+useGLTF.preload("https://cdn.mikn.dev/web/static/cube.glb");
+useGLTF.preload("https://cdn.mikn.dev/web/static/bar.glb");

Place these at module scope (e.g. end of file).

Also applies to: 209-230


270-275: Navigation approach in SPA frameworks

Directly mutating window.location bypasses client routing. Consider your router (next/router, react-router) for smoother transitions and prefetch.


63-69: <Scroll html /> with no children

This mounts an empty HTML overlay. Remove if unused.

src/interfaces/CursorToys.ts (1)

3-6: Name the props and export the union for reuse

Prefer CursorToysProps and a reusable union type to avoid string literals scattered across the codebase.

 export interface ICursorToys {
   children?: ReactNode;
-  selectedToy?: "none" | "spark" | "splash" | "custom" | "target";
+  selectedToy?: CursorToyName;
 }
+
+export type CursorToyName = "none" | "spark" | "splash" | "custom" | "target";
src/lib/get-strict-context.tsx (1)

15-24: Set displayName for easier debugging and clearer error messages

Small DX upgrade: display names improve React DevTools and errors.

   const Context = React.createContext<T | undefined>(undefined);
+  Context.displayName = `${name ?? "Anonymous"}Context`;
 
   const Provider = ({
     value,
     children,
   }: {
     value: T;
     children?: React.ReactNode;
   }) => <Context.Provider value={value}>{children}</Context.Provider>;
+  Provider.displayName = `${name ?? "Anonymous"}Provider`;
 
   const useSafeContext = () => {
     const ctx = React.useContext(Context);
     if (ctx === undefined) {
-      throw new Error(`useContext must be used within ${name ?? "a Provider"}`);
+      throw new Error(
+        `${name ?? "useContext"} must be used within ${name ?? "Provider"}`,
+      );
     }
     return ctx;
   };

Also applies to: 25-31

src/hooks/use-is-in-view.tsx (1)

10-17: Type the ref as T | null and drop the unsafe cast

The current cast can mask nulls. Align the ref types and forward the instance directly.

-function useIsInView<T extends HTMLElement = HTMLElement>(
-  ref: React.Ref<T>,
+function useIsInView<T extends HTMLElement = HTMLElement>(
+  ref: React.Ref<T | null>,
   options: UseIsInViewOptions = {},
 ) {
   const { inView, inViewOnce = false, inViewMargin = "0px" } = options;
   const localRef = React.useRef<T>(null);
-  React.useImperativeHandle(ref, () => localRef.current as T);
+  React.useImperativeHandle(ref, () => localRef.current);
src/components/animate-ui/primitives/radix/radio-group.tsx (1)

15-18: Remove redundant state; derive isChecked from context

isChecked can be computed from the group value. This avoids duplicated state and the extra setter in context.

-type RadioGroupItemContextType = {
-  isChecked: boolean;
-  setIsChecked: (isChecked: boolean) => void;
-};
+type RadioGroupItemContextType = { isChecked: boolean };
@@
 function RadioGroupItem({
   value: valueProps,
   disabled,
   required,
   ...props
 }: RadioGroupItemProps) {
   const { value } = useRadioGroup();
-  const [isChecked, setIsChecked] = React.useState(value === valueProps);
-
-  React.useEffect(() => {
-    setIsChecked(value === valueProps);
-  }, [value, valueProps]);
+  const isChecked = value === valueProps;
@@
   return (
-    <RadioGroupItemProvider value={{ isChecked, setIsChecked }}>
+    <RadioGroupItemProvider value={{ isChecked }}>

This also lets you simplify RadioGroupIndicator’s context type (no setter).

Also applies to: 87-98, 100-116, 119-130

src/components/animate-ui/icons/icon.tsx (1)

620-645: Avoid calling hooks in non-hook helpers

getVariants calls a hook from a plain function (eslint disabled). Prefer a hook wrapper or pass animationType in.

-function getVariants<
+function useVariants<
   V extends { default: T; [key: string]: T },
   T extends Record<string, Variants>,
 >(animations: V): T {
-  // eslint-disable-next-line react-hooks/rules-of-hooks
-  const { animation: animationType } = useAnimateIconContext();
+  const { animation: animationType } = useAnimateIconContext();

And update call sites accordingly (or refactor to getVariants(animations, animationType)).
This keeps the hooks rules intact.

src/components/GlobalCursorToys.tsx (1)

2-9: Code‑split TargetCursor to trim the default bundle.

Static import ships TargetCursor even when unused. Use a dynamic import with SSR disabled for this purely client visual.

Apply:

 "use client";
-import { useCursorToys } from "@/contexts/CursorToysContext";
-import TargetCursor from "@/components/TargetCursor";
+import { useCursorToys } from "@/contexts/CursorToysContext";
+import dynamic from "next/dynamic";
+const TargetCursor = dynamic(
+  () => import("@/components/TargetCursor"),
+  { ssr: false }
+);
src/components/CustomCursor.tsx (1)

9-22: Reduce re-render pressure from mousemove.

State updates on every mousemove can thrash the React tree. Throttle via requestAnimationFrame or update a ref and animate with CSS transforms.

If you want, I can provide a small rAF-based snippet to drop re-renders to ~60/sec instead of device rate.

src/components/consent-manager.tsx (1)

8-20: LGTM; consider making options overridable.

Solid wrapper. To future‑proof, allow options as a prop with a default of { mode: "offline" }.

src/app/layout.tsx (1)

5-13: You declared a font CSS variable but didn’t apply it.

You set variable: "--font-sans" but only use hsr.className. Either remove variable or apply it.

-    <html className={hsr.className}>
+    <html lang="en" className={`${hsr.className} ${hsr.variable}`}>
.idea/copilotDiffState.xml (1)

1-17: Remove IDE state files from version control.

Verification confirms that .idea/ is neither ignored nor excluded from tracking. Currently, 12 .idea/ files are tracked in Git, including .idea/copilotDiffState.xml. These environment-specific files should not be committed.

  • Add to .gitignore:
+/.idea/
  • Remove from Git history: git rm -r --cached .idea
src/components/animate-ui/primitives/buttons/button.tsx (2)

18-33: Forward ref for focus and integrations

Expose a ref so consumers can imperatively focus the button and integrate with tooltips/menus.

-function Button({
+const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
+  {
   hoverScale = 1.05,
   tapScale = 0.95,
   asChild = false,
   ...props
-}: ButtonProps) {
+}: ButtonProps, ref) {
   const Component = asChild ? Slot : motion.button;
 
   return (
     <Component
+      ref={ref}
       whileTap={{ scale: tapScale }}
       whileHover={{ scale: hoverScale }}
       {...props}
     />
   );
 }
 
 export { Button, type ButtonProps };

11-16: Polymorphic typing for asChild

ButtonProps are bound to HTMLMotionProps<"button"> even when rendering non-button children. Consider a polymorphic prop pattern (e.g., generic T extends ElementType) so the props match the rendered element when asChild is used.

src/components/SettingsController.tsx (2)

44-49: Add dialog semantics to the panel

Improve accessibility by marking the floating panel as a dialog and linking it to the trigger. Optionally manage focus on open/close.

-          <motion.div
+          <motion.div
+            role="dialog"
+            aria-modal="true"
+            id="site-settings-panel"
             initial={{ opacity: 0, y: 7 }}
             animate={{ opacity: 1, y: 0 }}
             exit={{ opacity: 0, y: 7 }}
             className="absolute bottom-full mb-5 bg-white shadow-lg rounded-lg p-4 w-80"
           >

9-11: Drop children from this component

children are rendered inside a fixed container, which is surprising and likely unused. Simplify the API by removing children from props and JSX.

-interface AccButtonProps {
-  children?: React.ReactNode;
-}
+interface AccButtonProps {}
...
-      {children}

Also applies to: 96-97

src/contexts/CursorToysContext.tsx (1)

27-30: Harden localStorage write

localStorage.setItem can throw (quota, privacy mode). Add a try/catch to avoid crashing UI.

   const setSelectedToy = (value: SelectedToy) => {
     setSelectedToyState(value);
-    localStorage.setItem("cursorToy", value);
+    try {
+      localStorage.setItem("cursorToy", value);
+    } catch {
+      // no-op
+    }
   };
src/app/[locale]/layout.tsx (1)

19-22: Fix params typing (no Promise) and drop unnecessary await

Next.js passes params as a plain object. Correct the type and remove await for clarity.

 interface LocaleLayoutProps {
-  children: ReactNode;
-  params: Promise<{ locale: string }>;
+  children: ReactNode;
+  params: { locale: string };
 }
 
 export default async function LocaleLayout({
   children,
   params,
 }: LocaleLayoutProps) {
-  const { locale } = await params;
+  const { locale } = params;

Also applies to: 28-36

src/components/animate-ui/icons/settings.tsx (1)

55-66: Consider default icon accessibility props (if IconWrapper doesn’t inject them).

Recommend adding aria-hidden="true" focusable="false" by default, and supporting an optional title/aria-label for labelled usage.

-    <motion.svg
+    <motion.svg
       xmlns="http://www.w3.org/2000/svg"
       width={size}
       height={size}
       viewBox="0 0 24 24"
       fill="none"
       stroke="currentColor"
       strokeWidth={2}
       strokeLinecap="round"
       strokeLinejoin="round"
+      aria-hidden={props["aria-label"] ? undefined : true}
+      focusable="false"
       {...props}
     >
src/components/animate-ui/components/buttons/button.tsx (1)

48-54: Prevent accidental form submits with a default type.

Unless ButtonPrimitive already sets it, add type="button" by default.

-    <ButtonPrimitive
+    <ButtonPrimitive
+      type={(props as any).type ?? "button"}
       className={cn(buttonVariants({ variant, size }), className)}
       {...props}
     />

Please confirm whether the primitive already applies a default.

src/components/ClickSpark.tsx (2)

45-51: DPR-aware canvas for crisp lines on HiDPI.

Scale the backing store by devicePixelRatio and scale the context before drawing.

-      const { width, height } = parent.getBoundingClientRect();
-      if (canvas.width !== width || canvas.height !== height) {
-        canvas.width = width;
-        canvas.height = height;
-      }
+      const { width, height } = parent.getBoundingClientRect();
+      const dpr = window.devicePixelRatio || 1;
+      const pixelW = Math.floor(width * dpr);
+      const pixelH = Math.floor(height * dpr);
+      if (canvas.width !== pixelW || canvas.height !== pixelH) {
+        canvas.width = pixelW;
+        canvas.height = pixelH;
+        canvas.style.width = `${width}px`;
+        canvas.style.height = `${height}px`;
+      }

And in the draw loop (once per frame, before drawing):

-      ctx.clearRect(0, 0, canvas.width, canvas.height);
+      const dpr = window.devicePixelRatio || 1;
+      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+      ctx.clearRect(0, 0, canvas.width, canvas.height);

Also applies to: 85-96


34-35: Remove unused startTimeRef.

It’s set but never read; drop it to reduce noise.

-  const startTimeRef = useRef<number | null>(null);
@@
-      if (!startTimeRef.current) {
-        startTimeRef.current = timestamp;
-      }
@@
-    return () => {
-      cancelAnimationFrame(animationId);
-    };
+    return () => {
+      cancelAnimationFrame(animationId);
+    };

Also applies to: 93-97, 131-133

src/components/animate-ui/primitives/animate/slot.tsx (1)

38-59: Compose event handlers instead of overriding.

mergeProps overwrites handlers like onClick/onKeyDown. Consider composing them so both fire.

 function mergeProps<T extends HTMLElement>(
   childProps: AnyProps,
   slotProps: DOMMotionProps<T>,
 ): AnyProps {
   const merged: AnyProps = { ...childProps, ...slotProps };
@@
+  // compose common event handlers if both provided
+  const handlers = [
+    "onClick","onMouseEnter","onMouseLeave","onFocus","onBlur","onKeyDown","onKeyUp",
+  ] as const;
+  handlers.forEach((key) => {
+    const c = childProps[key as keyof AnyProps] as ((e: any) => void) | undefined;
+    const s = slotProps[key as keyof DOMMotionProps<T>] as ((e: any) => void) | undefined;
+    if (c && s) {
+      merged[key] = (e: any) => {
+        c(e);
+        if (!e?.defaultPrevented) s(e);
+      };
+    }
+  });
src/components/TargetCursor.tsx (2)

274-326: Clear pending resume timeout on unmount to avoid late execution.

Ensure any scheduled resume is cancelled during cleanup.

     return () => {
       window.removeEventListener("mousemove", moveHandler);
       window.removeEventListener("mouseover", enterHandler);
       window.removeEventListener("scroll", scrollHandler);
 
       if (activeTarget) {
         cleanupTarget(activeTarget);
       }
 
+      if (resumeTimeout) {
+        clearTimeout(resumeTimeout);
+        resumeTimeout = null;
+      }
+
       spinTl.current?.kill();
       document.body.style.cursor = originalCursor;
     };

41-45: Cursor restore robustness across prop flips.

If hideDefaultCursor toggles during the component’s life, you may restore “none”. Consider storing the pre-hide value in a ref outside the effect.

-    const originalCursor = document.body.style.cursor;
+    const originalRef = { current: document.body.style.cursor };
     if (hideDefaultCursor) {
       document.body.style.cursor = "none";
     }
@@
-      document.body.style.cursor = originalCursor;
+      document.body.style.cursor = originalRef.current;
src/components/SplashCursor.tsx (5)

106-113: Respect the TRANSPARENT prop for context alpha.

Currently alpha is always true. Use the provided prop.

-      const params = {
-        alpha: true,
+      const params = {
+        alpha: TRANSPARENT,
         depth: false,
         stencil: false,
         antialias: false,
         preserveDrawingBuffer: false,
       };

56-70: Over‑high DYE_RESOLUTION (1440) will allocate very large dye FBOs (e.g., ~2.5k×1.4k on 16:9).

Lower the default and clamp by device size to avoid OOM and jank on mobiles. Manual filtering fallback alone isn’t sufficient.

@@
-  DYE_RESOLUTION = 1440,
+  DYE_RESOLUTION = 512,
@@
-    function getResolution(resolution: number) {
+    function getResolution(resolution: number) {
       const w = gl.drawingBufferWidth;
       const h = gl.drawingBufferHeight;
       const aspectRatio = w / h;
       let aspect = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio;
-      const min = Math.round(resolution);
-      const max = Math.round(resolution * aspect);
+      const maxSideCap = Math.min(1024, Math.max(128, Math.round(resolution)));
+      const min = maxSideCap;
+      const max = Math.round(maxSideCap * aspect);
       if (w > h) {
         return { width: max, height: min };
       }
       return { width: min, height: max };
     }

If you prefer, gate higher resolutions behind a media check: prefers-reduced-motion or device memory.

Also applies to: 969-980, 884-914


1282-1286: SPLAT_RADIUS appears to be scaled twice (÷100); default 0.2 becomes 0.002. Intentional?

If SPLAT_RADIUS is already a [0..1] ratio, drop the /100.

-          splatProgram.uniforms.radius,
-          correctRadius(config.SPLAT_RADIUS / 100)!,
+          splatProgram.uniforms.radius,
+          correctRadius(config.SPLAT_RADIUS)!,

Alternatively, set default SPLAT_RADIUS to 20 (percent) if you want percentage semantics.

Also applies to: 64-65


195-216: GPU resource lifetime not managed; textures/FBOs aren’t deleted on resize or unmount.

Not deleting old textures/framebuffers can accumulate GPU memory. Add delete calls in resize and effect cleanup.

Happy to draft a small disposer (deleteTexture/deleteFramebuffer) and integrate it into resizeFBO/resizeDoubleFBO and the effect cleanup.

Also applies to: 218-257, 759-838, 840-882


1496-1510: Minor: unused/under‑used props.

  • BACK_COLOR and CAPTURE_RESOLUTION are defined but unused.
  • TRANSPARENT only applied if you accept the earlier alpha change.
    Consider removing or wiring them through (e.g., clear to BACK_COLOR on the default framebuffer).
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01c4b09 and e04bede.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • .idea/copilotDiffState.xml (1 hunks)
  • components.json (1 hunks)
  • package.json (1 hunks)
  • src/app/[locale]/layout.tsx (1 hunks)
  • src/app/[locale]/page.tsx (1 hunks)
  • src/app/[locale]/template.tsx (1 hunks)
  • src/app/globals.css (1 hunks)
  • src/app/layout.tsx (1 hunks)
  • src/components/ClickSpark.tsx (1 hunks)
  • src/components/CursorToys.tsx (1 hunks)
  • src/components/CustomCursor.tsx (1 hunks)
  • src/components/FluidGlass.tsx (1 hunks)
  • src/components/GlobalCursorToys.tsx (1 hunks)
  • src/components/SettingsController.tsx (1 hunks)
  • src/components/SplashCursor.tsx (1 hunks)
  • src/components/TargetCursor.tsx (1 hunks)
  • src/components/animate-ui/components/buttons/button.tsx (1 hunks)
  • src/components/animate-ui/icons/icon.tsx (1 hunks)
  • src/components/animate-ui/icons/settings.tsx (1 hunks)
  • src/components/animate-ui/primitives/animate/slot.tsx (1 hunks)
  • src/components/animate-ui/primitives/buttons/button.tsx (1 hunks)
  • src/components/animate-ui/primitives/radix/radio-group.tsx (1 hunks)
  • src/components/consent-manager.tsx (1 hunks)
  • src/contexts/CursorToysContext.tsx (1 hunks)
  • src/hooks/use-controlled-state.tsx (1 hunks)
  • src/hooks/use-is-in-view.tsx (1 hunks)
  • src/interfaces/CursorToys.ts (1 hunks)
  • src/lib/get-strict-context.tsx (1 hunks)
  • tsconfig.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • components.json
  • src/app/globals.css
  • package.json
🧰 Additional context used
🧬 Code graph analysis (9)
src/components/GlobalCursorToys.tsx (1)
src/contexts/CursorToysContext.tsx (1)
  • useCursorToys (39-44)
src/components/CursorToys.tsx (4)
src/interfaces/CursorToys.ts (1)
  • ICursorToys (3-6)
src/contexts/CursorToysContext.tsx (1)
  • useCursorToys (39-44)
src/components/SplashCursor.tsx (1)
  • SplashCursor (55-1521)
src/components/CustomCursor.tsx (1)
  • CustomCursor (6-35)
src/components/animate-ui/components/buttons/button.tsx (1)
src/components/animate-ui/primitives/buttons/button.tsx (2)
  • ButtonProps (35-35)
  • Button (35-35)
src/components/SettingsController.tsx (1)
src/contexts/CursorToysContext.tsx (1)
  • useCursorToys (39-44)
src/components/animate-ui/primitives/radix/radio-group.tsx (2)
src/lib/get-strict-context.tsx (1)
  • getStrictContext (36-36)
src/hooks/use-controlled-state.tsx (1)
  • useControlledState (9-33)
src/components/animate-ui/primitives/buttons/button.tsx (2)
src/components/animate-ui/components/buttons/button.tsx (2)
  • ButtonProps (57-57)
  • Button (57-57)
src/components/animate-ui/primitives/animate/slot.tsx (2)
  • WithAsChild (94-94)
  • Slot (92-92)
src/app/[locale]/template.tsx (2)
src/components/CursorToys.tsx (1)
  • CursorToys (11-26)
src/components/SettingsController.tsx (1)
  • SettingsController (34-99)
src/app/layout.tsx (1)
src/contexts/CursorToysContext.tsx (1)
  • CursorToysProvider (15-37)
src/app/[locale]/layout.tsx (2)
src/i18n/routing.ts (1)
  • routing (4-10)
src/components/consent-manager.tsx (1)
  • ConsentManager (8-20)
🔇 Additional comments (12)
tsconfig.json (2)

15-15: ✓ JSX runtime update is appropriate.

Changing from "preserve" to "react-jsx" aligns with React 19 and modern Next.js setups, eliminating the need for JSX factory imports. This complements the broader framework upgrade.


25-33: Verify that narrowed exclude does not inadvertently include build artefacts.

The updated include paths appropriately prioritise Next.js-generated types. However, the exclude array has been narrowed to only ["node_modules"], removing previous broader exclusions (including legacy sst.config.ts). This risks unintentionally including build output directories (e.g., .next compiled output, dist, build, coverage, out) if they exist. Whilst .next is typically generated during dev, other directories may linger.

Verify that all necessary directories are either excluded or not present in the working tree after build steps.

src/components/FluidGlass.tsx (2)

286-289: No changes required—outlineBlur="20%" is supported

Troika's outlineBlur prop accepts percentage strings like "20%", where the percentage is treated as a percentage of the fontSize. The existing code at lines 286-289 and 374-377 is correct and requires no modification.

Likely an incorrect or invalid review comment.


170-179: Code is correct per @react-three/drei API

The code properly matches the documented API. buffer is correctly instantiated via useFBO() (line 68), and buffer.texture (a THREE.Texture) is appropriately passed to MeshTransmissionMaterial's buffer prop on line 170. No changes required.

src/components/animate-ui/primitives/radix/radio-group.tsx (1)

4-4: Confirm "radix-ui" import path works and is intentional

The import path differs from the codebase's established pattern. Other Radix components use "@radix-ui/react-*" packages (e.g., "@radix-ui/react-slot"), but this file imports from "radix-ui". Whilst "radix-ui" v1.4.3 is listed in package.json and is a legitimate npm package, the official Radix package is "@radix-ui/react-radio-group". Please verify that the RadioGroup export from "radix-ui" works correctly at runtime and confirm whether this non-standard import is intentional, or switch to the official "@radix-ui/react-radio-group" package to maintain consistency across the codebase.

src/components/CustomCursor.tsx (1)

10-21: Cursor restore race if multiple custom cursors mount.

Setting document.body.style.cursor = "auto" on unmount can override another active toy. If multiple toys are possible, guard with a ref/count or centralise cursor hiding in the provider.

Is there any path where two cursor toys can mount simultaneously? If yes, I’ll propose a tiny centralised toggle in CursorToysContext.

src/app/layout.tsx (1)

3-15: No issues detected—provider correctly configured as client component.

Verification confirms that src/contexts/CursorToysContext.tsx has "use client" as the first line, allowing it to safely use React state and hooks as required.

src/components/GlobalCursorToys.tsx (1)

5-13: No issues found. Component is not used anywhere in the codebase.

Verification confirms that GlobalCursorToys is never imported or rendered—it exists only as a definition in src/components/GlobalCursorToys.tsx. The CursorToysProvider is correctly wrapping the application in src/app/layout.tsx, and the useCursorToys() hook includes proper error handling that throws when used outside the provider. The concern about independent route mounts is not applicable since the component is currently unused.

src/components/animate-ui/primitives/buttons/button.tsx (1)

24-31: asChild + Framer Motion compatibility

When asChild is true, whileHover/whileTap will only work if the child is a motion component and accepts Motion props; otherwise these props may leak to the DOM (React warnings) or be ignored. Consider wrapping Slot with motion (if compatible) or document that the child must be a motion.* element.

src/components/CursorToys.tsx (1)

11-26: LGTM — simple, readable selection logic

The conditional rendering is clear and keeps children always present. No issues spotted.

src/app/[locale]/template.tsx (1)

10-15: Provider correctly wraps the template

The CursorToysProvider is properly mounted in the root layout (src/app/layout.tsx) and wraps all child routes, including the [locale] template. In Next.js, the root layout's provider context cascades through all nested layouts and templates, so both CursorToys and SettingsController can safely call useCursorToys(). The code is correct as-is.

src/components/animate-ui/icons/settings.tsx (1)

15-48: Animations object looks solid and type-safe.

Good use of satisfies to constrain per‑part variants and keyof typeof animations for props. No issues spotted.

</Footer>
</>
);
useSwetrix("XxNIMaHCaVG3", { apiURL: "https://analytics.mikandev.tech/log" });
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Gate analytics behind user consent

useSwetrix is called unconditionally. To meet privacy expectations, initialise analytics only after the user consents (via ConsentManager). Wire this to the consent state before calling useSwetrix.


Comment on lines +12 to +16
<html className={hsr.className}>
<body>
<CursorToysProvider>{children}</CursorToysProvider>
</body>
</html>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add lang to for a11y and SEO.

Root <html> lacks a language attribute.

Apply:

-    <html className={hsr.className}>
+    <html lang="en" className={hsr.className}>

If you have per‑locale routing, pass the locale here instead of hard‑coding.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<html className={hsr.className}>
<body>
<CursorToysProvider>{children}</CursorToysProvider>
</body>
</html>
<html lang="en" className={hsr.className}>
<body>
<CursorToysProvider>{children}</CursorToysProvider>
</body>
</html>
🤖 Prompt for AI Agents
In src/app/layout.tsx around lines 12 to 16, the root <html> element is missing
a lang attribute which harms accessibility and SEO; add a lang prop to the html
tag (use the current locale if your app supports per-locale routing, otherwise
default to "en") and ensure the locale value is read/passed in safely (e.g.,
from next/router, request, or a server prop) so the rendered <html lang="...">
reflects the active language.

function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<ButtonPrimitive
className={cn(buttonVariants({ variant, size, className }))}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

className isn’t merged; external classes are dropped.

You’re passing className into CVA options, so it’s ignored. Merge it via cn after resolving variants.

-      className={cn(buttonVariants({ variant, size, className }))}
+      className={cn(buttonVariants({ variant, size }), className)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
className={cn(buttonVariants({ variant, size, className }))}
className={cn(buttonVariants({ variant, size }), className)}
🤖 Prompt for AI Agents
In src/components/animate-ui/components/buttons/button.tsx around line 51, the
external className is being passed into the CVA options and thus ignored; update
the render to call the variant resolver without className and then merge the
result with the external className using cn (e.g., cn(buttonVariants({ variant,
size }), className)) so external classes are preserved and merged correctly.

Comment on lines +433 to +446
<AnimateIconContext.Provider
value={{
controls,
animation: currentAnimation,
loop,
loopDelay,
active: localAnimate,
animate,
initialOnAnimateEnd,
completeOnStop,
delay,
}}
>
{content}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: persistOnAnimateEnd not propagated via context

persistOnAnimateEnd is part of AnimateIconContextValue but is not provided here, breaking inheritance in nested IconWrapper/AnimateIcon trees.

   return (
     <AnimateIconContext.Provider
       value={{
         controls,
         animation: currentAnimation,
         loop,
         loopDelay,
         active: localAnimate,
         animate,
         initialOnAnimateEnd,
         completeOnStop,
+        persistOnAnimateEnd,
         delay,
       }}
     >
       {content}
     </AnimateIconContext.Provider>
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<AnimateIconContext.Provider
value={{
controls,
animation: currentAnimation,
loop,
loopDelay,
active: localAnimate,
animate,
initialOnAnimateEnd,
completeOnStop,
delay,
}}
>
{content}
<AnimateIconContext.Provider
value={{
controls,
animation: currentAnimation,
loop,
loopDelay,
active: localAnimate,
animate,
initialOnAnimateEnd,
completeOnStop,
persistOnAnimateEnd,
delay,
}}
>
{content}
</AnimateIconContext.Provider>
🤖 Prompt for AI Agents
In src/components/animate-ui/icons/icon.tsx around lines 433 to 446, the
AnimateIconContext.Provider value omits persistOnAnimateEnd which is declared on
the AnimateIconContextValue type and needed for nested IconWrapper/AnimateIcon
inheritance; add persistOnAnimateEnd to the provided context object (e.g.
include persistOnAnimateEnd: persistOnAnimateEnd) so the context propagates this
flag, and ensure the variable is defined in the surrounding scope and matches
the expected type.

Comment on lines +545 to +569
return (
<AnimateIconContext.Provider
value={{
controls,
animation: animationToUse,
loop: loopToUse,
loopDelay: loopDelayToUse,
active: parentActive,
animate: parentAnimate,
initialOnAnimateEnd: parentInitialOnAnimateEnd,
delay: parentDelay,
completeOnStop: parentCompleteOnStop,
}}
>
<IconComponent
size={size}
className={cn(
className,
(animationToUse === "path" || animationToUse === "path-loop") &&
pathClassName,
)}
{...props}
/>
</AnimateIconContext.Provider>
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Also add persistOnAnimateEnd in the IconWrapper provider

Without this, children can’t inherit the parent’s persistence behaviour.

   return (
     <AnimateIconContext.Provider
       value={{
         controls,
         animation: animationToUse,
         loop: loopToUse,
         loopDelay: loopDelayToUse,
         active: parentActive,
         animate: parentAnimate,
         initialOnAnimateEnd: parentInitialOnAnimateEnd,
+        persistOnAnimateEnd: parentPersistOnAnimateEnd,
         delay: parentDelay,
         completeOnStop: parentCompleteOnStop,
       }}
     >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<AnimateIconContext.Provider
value={{
controls,
animation: animationToUse,
loop: loopToUse,
loopDelay: loopDelayToUse,
active: parentActive,
animate: parentAnimate,
initialOnAnimateEnd: parentInitialOnAnimateEnd,
delay: parentDelay,
completeOnStop: parentCompleteOnStop,
}}
>
<IconComponent
size={size}
className={cn(
className,
(animationToUse === "path" || animationToUse === "path-loop") &&
pathClassName,
)}
{...props}
/>
</AnimateIconContext.Provider>
);
return (
<AnimateIconContext.Provider
value={{
controls,
animation: animationToUse,
loop: loopToUse,
loopDelay: loopDelayToUse,
active: parentActive,
animate: parentAnimate,
initialOnAnimateEnd: parentInitialOnAnimateEnd,
persistOnAnimateEnd: parentPersistOnAnimateEnd,
delay: parentDelay,
completeOnStop: parentCompleteOnStop,
}}
>
<IconComponent
size={size}
className={cn(
className,
(animationToUse === "path" || animationToUse === "path-loop") &&
pathClassName,
)}
{...props}
/>
</AnimateIconContext.Provider>
);
🤖 Prompt for AI Agents
In src/components/animate-ui/icons/icon.tsx around lines 545 to 569, the
AnimateIconContext.Provider value is missing the persistOnAnimateEnd property so
children can't inherit the parent's persistence behavior; add
persistOnAnimateEnd: parentPersistOnAnimateEnd (using the existing
parentPersistOnAnimateEnd variable) to the value object so the provider forwards
that flag to children.

Comment on lines +105 to +133
function getWebGLContext(canvas: HTMLCanvasElement) {
const params = {
alpha: true,
depth: false,
stencil: false,
antialias: false,
preserveDrawingBuffer: false,
};

let gl = canvas.getContext(
"webgl2",
params,
) as WebGL2RenderingContext | null;

if (!gl) {
gl = (canvas.getContext("webgl", params) ||
canvas.getContext(
"experimental-webgl",
params,
)) as WebGL2RenderingContext | null;
}

if (!gl) {
throw new Error("Unable to initialize WebGL.");
}

const isWebGL2 = "drawBuffers" in gl;

let supportLinearFiltering = false;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

WebGL2 detection and types are unsafe; may misclassify WebGL1 with extensions.

Type gl as WebGLRenderingContext | WebGL2RenderingContext and detect WebGL2 via instanceof, not "drawBuffers" in gl.

Apply this diff:

@@
-      let gl = canvas.getContext(
-        "webgl2",
-        params,
-      ) as WebGL2RenderingContext | null;
+      let gl: WebGL2RenderingContext | WebGLRenderingContext | null =
+        canvas.getContext("webgl2", params) as WebGL2RenderingContext | null;
@@
-        gl = (canvas.getContext("webgl", params) ||
-          canvas.getContext(
-            "experimental-webgl",
-            params,
-          )) as WebGL2RenderingContext | null;
+        gl =
+          (canvas.getContext("webgl", params) as WebGLRenderingContext) ||
+          (canvas.getContext("experimental-webgl", params) as WebGLRenderingContext) ||
+          null;
@@
-      const isWebGL2 = "drawBuffers" in gl;
+      const isWebGL2 =
+        typeof WebGL2RenderingContext !== "undefined" &&
+        gl instanceof WebGL2RenderingContext;
@@
-      if (isWebGL2) {
+      if (isWebGL2) {
         (gl as WebGL2RenderingContext).getExtension("EXT_color_buffer_float");
         supportLinearFiltering = !!(gl as WebGL2RenderingContext).getExtension(
           "OES_texture_float_linear",
         );
       } else {
         halfFloat = gl.getExtension("OES_texture_half_float");
         supportLinearFiltering = !!gl.getExtension(
           "OES_texture_half_float_linear",
         );
       }

Also applies to: 119-126, 131-146

🤖 Prompt for AI Agents
In src/components/SplashCursor.tsx around lines 105 to 133 (and also apply same
changes to 119-126 and 131-146), the code unsafely types gl as
WebGL2RenderingContext and detects WebGL2 by checking "drawBuffers" in gl;
instead declare gl as WebGLRenderingContext | WebGL2RenderingContext, determine
WebGL2 by using instanceof WebGL2RenderingContext, and for WebGL1 use
getExtension("WEBGL_draw_buffers") (and others) to detect optional features;
update any subsequent feature checks (like supportLinearFiltering and draw
buffer support) to branch based on the instanceof result and extension
availability so WebGL1 with extensions is handled correctly.

Comment on lines +278 to +292
function compileShader(
type: number,
source: string,
keywords: string[] | null = null,
): WebGLShader | null {
const shaderSource = addKeywords(source, keywords);
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, shaderSource);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.trace(gl.getShaderInfoLog(shader));
}
return shader;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Shader/program error handling continues with invalid objects.

Return null on failure and delete broken resources to avoid silent no‑ops later.

Apply this diff:

@@
-      gl.compileShader(shader);
-      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
-        console.trace(gl.getShaderInfoLog(shader));
-      }
-      return shader;
+      gl.compileShader(shader);
+      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+        console.trace(gl.getShaderInfoLog(shader) || "Shader compile error");
+        gl.deleteShader(shader);
+        return null;
+      }
+      return shader;
@@
-      gl.linkProgram(program);
-      if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
-        console.trace(gl.getProgramInfoLog(program));
-      }
-      return program;
+      gl.linkProgram(program);
+      if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+        console.trace(gl.getProgramInfoLog(program) || "Program link error");
+        gl.deleteProgram(program);
+        return null;
+      }
+      return program;

Also applies to: 294-308

🤖 Prompt for AI Agents
In src/components/SplashCursor.tsx around lines 278-292 and also lines 294-308,
the shader and program creation flow currently logs compile/link errors but
continues using broken objects; change the error handling to clean up and return
null: when compileShader detects a compile failure, call gl.deleteShader(shader)
before returning null (also handle createShader returning null), and for program
creation/linking, if createProgram returns null or linking fails, detach and
delete any attached shaders and delete the program, log the programInfoLog, and
then return null so callers don't use invalid WebGLProgram objects.

Comment on lines +294 to +308
function createProgram(
vertexShader: WebGLShader | null,
fragmentShader: WebGLShader | null,
): WebGLProgram | null {
if (!vertexShader || !fragmentShader) return null;
const program = gl.createProgram();
if (!program) return null;
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.trace(gl.getProgramInfoLog(program));
}
return program;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Vertex attribute not reliably bound; aPosition may not be at index 0.

Hardcoding location 0 in blit is undefined across programs. Bind attribute location 0 before linking.

Apply this diff:

@@
-      gl.attachShader(program, vertexShader);
-      gl.attachShader(program, fragmentShader);
-      gl.linkProgram(program);
+      gl.attachShader(program, vertexShader);
+      gl.attachShader(program, fragmentShader);
+      // Ensure attribute 0 is 'aPosition' for all programs to match blit() setup.
+      gl.bindAttribLocation(program, 0, "aPosition");
+      gl.linkProgram(program);
@@
-      gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
-      gl.enableVertexAttribArray(0);
+      gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
+      gl.enableVertexAttribArray(0);

Also applies to: 684-701

🤖 Prompt for AI Agents
In src/components/SplashCursor.tsx around lines 294-308 (and similarly around
684-701), the program links without binding attribute locations so the aPosition
attribute may not be at index 0; before calling gl.linkProgram(program) call
gl.bindAttribLocation(program, 0, "aPosition") (and any other fixed attribute
names you rely on) to force the attribute to location 0, then link and check the
link status as before.

Comment on lines +88 to +112
const moveHandler = (e: MouseEvent) => moveCursor(e.clientX, e.clientY);
window.addEventListener("mousemove", moveHandler);

const scrollHandler = () => {
if (!activeTarget || !cursorRef.current) return;

const mouseX = gsap.getProperty(cursorRef.current, "x") as number;
const mouseY = gsap.getProperty(cursorRef.current, "y") as number;

const elementUnderMouse = document.elementFromPoint(mouseX, mouseY);
const isStillOverTarget =
elementUnderMouse &&
(elementUnderMouse === activeTarget ||
elementUnderMouse.closest(targetSelector) === activeTarget);

if (!isStillOverTarget) {
if (currentLeaveHandler) {
currentLeaveHandler();
}
}
};

window.addEventListener("scroll", scrollHandler, { passive: true });
window.addEventListener("mousemove", moveHandler);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Duplicate mousemove listener; one remains after cleanup.

You add moveHandler twice (Lines 89 and 111) but remove it once. Remove the duplicate add.

-    window.addEventListener("mousemove", moveHandler);
@@
-    window.addEventListener("scroll", scrollHandler, { passive: true });
-    window.addEventListener("mousemove", moveHandler);
+    window.addEventListener("scroll", scrollHandler, { passive: true });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const moveHandler = (e: MouseEvent) => moveCursor(e.clientX, e.clientY);
window.addEventListener("mousemove", moveHandler);
const scrollHandler = () => {
if (!activeTarget || !cursorRef.current) return;
const mouseX = gsap.getProperty(cursorRef.current, "x") as number;
const mouseY = gsap.getProperty(cursorRef.current, "y") as number;
const elementUnderMouse = document.elementFromPoint(mouseX, mouseY);
const isStillOverTarget =
elementUnderMouse &&
(elementUnderMouse === activeTarget ||
elementUnderMouse.closest(targetSelector) === activeTarget);
if (!isStillOverTarget) {
if (currentLeaveHandler) {
currentLeaveHandler();
}
}
};
window.addEventListener("scroll", scrollHandler, { passive: true });
window.addEventListener("mousemove", moveHandler);
const moveHandler = (e: MouseEvent) => moveCursor(e.clientX, e.clientY);
const scrollHandler = () => {
if (!activeTarget || !cursorRef.current) return;
const mouseX = gsap.getProperty(cursorRef.current, "x") as number;
const mouseY = gsap.getProperty(cursorRef.current, "y") as number;
const elementUnderMouse = document.elementFromPoint(mouseX, mouseY);
const isStillOverTarget =
elementUnderMouse &&
(elementUnderMouse === activeTarget ||
elementUnderMouse.closest(targetSelector) === activeTarget);
if (!isStillOverTarget) {
if (currentLeaveHandler) {
currentLeaveHandler();
}
}
};
window.addEventListener("scroll", scrollHandler, { passive: true });
window.addEventListener("mousemove", moveHandler);
🤖 Prompt for AI Agents
In src/components/TargetCursor.tsx around lines 88 to 112 there is a duplicate
window.addEventListener("mousemove", moveHandler) call (added at lines ~89 and
~111) which causes one listener to remain after cleanup; remove the
second/duplicate addEventListener call so moveHandler is only registered once
(and verify the existing cleanup/removeEventListener matches that single
registration).

Comment on lines +8 to +19
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useControlledState<T, Rest extends any[] = []>(
props: CommonControlledStateProps<T> & {
onChange?: (value: T, ...args: Rest) => void;
},
): readonly [T, (next: T, ...args: Rest) => void] {
const { value, defaultValue, onChange } = props;

const [state, setInternalState] = React.useState<T>(
value !== undefined ? value : (defaultValue as T),
);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid unsafe initial cast; allow undefined state and tighten generics

Current typing hides an undefined initial state behind a cast, risking runtime issues for consumers expecting T to be always defined.

-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export function useControlledState<T, Rest extends any[] = []>(
+export function useControlledState<T, Rest extends unknown[] = []>(
   props: CommonControlledStateProps<T> & {
     onChange?: (value: T, ...args: Rest) => void;
   },
-): readonly [T, (next: T, ...args: Rest) => void] {
+): readonly [T | undefined, (next: T, ...args: Rest) => void] {
   const { value, defaultValue, onChange } = props;
 
-  const [state, setInternalState] = React.useState<T>(
-    value !== undefined ? value : (defaultValue as T),
-  );
+  const [state, setInternalState] = React.useState<T | undefined>(
+    value !== undefined ? value : defaultValue,
+  );
 
   React.useEffect(() => {
-    if (value !== undefined) setInternalState(value);
+    if (value !== undefined) setInternalState(value);
   }, [value]);

Optional: add dev-time warning if the hook switches between controlled and uncontrolled during its lifetime.

Also applies to: 24-33

🤖 Prompt for AI Agents
In src/hooks/use-controlled-state.tsx around lines 8 to 19 (and similarly 24 to
33), the hook unsafely casts defaultValue to T which hides an undefined initial
state; change the state type to T | undefined, initialize useState with value
!== undefined ? value : defaultValue (no cast) so the initial state can be
undefined safely, tighten generics so the hook returns readonly [T | undefined,
(next: T | undefined, ...args: Rest) => void], and (optionally) add a dev-only
useRef + useEffect check that detects and warns if the hook switches between
controlled and uncontrolled during its lifecycle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants