Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/website/app/blog/agent/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { GithubButton } from "@/components/github-button";
import { ViewDocsButton } from "@/components/view-docs-button";
import { BlogArticleLayout } from "@/components/blog-article-layout";
import { COPY_FEEDBACK_DURATION_MS } from "@/constants";
import { Button } from "@/components/ui/button";

interface HighlightedCodeBlockProps {
code: string;
Expand Down Expand Up @@ -43,13 +44,15 @@ const HighlightedCodeBlock = ({ code, lang }: HighlightedCodeBlockProps) => {

return (
<div className="group relative">
<button
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleCopy}
className="absolute right-0 top-0 text-[11px] text-white/50 opacity-0 transition-opacity hover:text-white group-hover:opacity-100 z-10"
className="absolute right-0 top-0 z-10 h-auto px-1.5 py-1 text-[11px] text-white/50 opacity-0 transition-opacity hover:bg-transparent hover:text-white group-hover:opacity-100"
>
{didCopy ? "Copied" : "Copy"}
</button>
</Button>
{highlightedHtml ? (
<div
className="overflow-x-auto font-mono text-[13px] leading-relaxed"
Expand Down
124 changes: 56 additions & 68 deletions packages/website/app/open-file/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
"use client";

import { useQueryState, parseAsStringLiteral } from "nuqs";
import { useState, useEffect, useCallback, Suspense, useRef } from "react";
import { useState, useEffect, useCallback, Suspense } from "react";
import { ReactGrabLogo } from "@/components/react-grab-logo";
import { cn } from "@/utils/cn";
import { IconCursor } from "@/components/icons/icon-cursor";
import { IconVSCode } from "@/components/icons/icon-vscode";
import { IconZed } from "@/components/icons/icon-zed";
import { IconWebStorm } from "@/components/icons/icon-webstorm";
import { ChevronDown, ArrowUpRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import Link from "next/link";

const EDITOR_OPTIONS = ["cursor", "vscode", "zed", "webstorm"] as const;
Expand All @@ -29,6 +38,13 @@ const EDITORS: EditorOption[] = [

const STORAGE_KEY = "react-grab-preferred-editor";

const parseEditorFromValue = (value: string): Editor => {
const matchingEditor = EDITOR_OPTIONS.find(
(innerEditorValue) => innerEditorValue === value,
);
return matchingEditor ?? "cursor";
};

const getEditorUrl = (
editor: Editor,
filePath: string,
Expand Down Expand Up @@ -73,24 +89,9 @@ const OpenFileContent = () => {
return getInitialEditor().editor;
});
const [didAttemptOpen, setDidAttemptOpen] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isEditorSelectOpen, setIsEditorSelectOpen] = useState(false);
const [hasSavedPreference] = useState(() => getInitialEditor().hasSaved);
const [isInfoOpen, setIsInfoOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsDropdownOpen(false);
}
};

document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);

const handleOpen = useCallback(() => {
if (!resolvedFilePath) return;
Expand All @@ -115,31 +116,30 @@ const OpenFileContent = () => {

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Enter" && !isDropdownOpen) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The Select dropdown doesn't close after selecting an editor because handleEditorChange no longer calls setIsEditorSelectOpen(false)

Fix on Vercel

if (event.key === "Enter" && !isEditorSelectOpen) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 27, 2026

Choose a reason for hiding this comment

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

P2: Pressing Enter on the editor Select trigger can unexpectedly call handleOpen() and navigate away before the Select opens, due to the global Enter handler relying only on isEditorSelectOpen.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/website/app/open-file/page.tsx, line 119:

<comment>Pressing Enter on the editor Select trigger can unexpectedly call `handleOpen()` and navigate away before the Select opens, due to the global Enter handler relying only on `isEditorSelectOpen`.</comment>

<file context>
@@ -115,31 +116,30 @@ const OpenFileContent = () => {
   useEffect(() => {
     const handleKeyDown = (event: KeyboardEvent) => {
-      if (event.key === "Enter" && !isDropdownOpen) {
+      if (event.key === "Enter" && !isEditorSelectOpen) {
         handleOpen();
       }
</file context>
Fix with Cubic

handleOpen();
}
};

document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [handleOpen, isDropdownOpen]);
}, [handleOpen, isEditorSelectOpen]);

const handleEditorChange = (editor: Editor) => {
setPreferredEditor(editor);
if (!rawParam) {
localStorage.setItem(STORAGE_KEY, editor);
}
setEditorParam(editor);
setIsDropdownOpen(false);
};

const fileName = resolvedFilePath.split("/").pop() ?? "file";
const selectedEditor = EDITORS.find((e) => e.id === preferredEditor);
const selectedEditor = EDITORS.find((innerEditor) => innerEditor.id === preferredEditor);

if (!resolvedFilePath) {
return (
<div className="flex min-h-screen items-center justify-center bg-black p-4">
<div className="w-full max-w-md rounded-lg border border-white/10 bg-[#0d0d0d] p-8 text-center shadow-[0_8px_30px_rgb(0,0,0,0.3)]">
<Card className="w-full max-w-md border-white/10 bg-[#0d0d0d] p-8 text-center text-card-foreground shadow-[0_8px_30px_rgb(0,0,0,0.3)]">
<div className="mb-6 flex justify-center">
<ReactGrabLogo width={100} height={40} />
</div>
Expand All @@ -150,7 +150,7 @@ const OpenFileContent = () => {
</code>{" "}
to the URL.
</div>
</div>
</Card>
</div>
);
}
Expand All @@ -167,7 +167,7 @@ const OpenFileContent = () => {
</Link>
</div>

<div className="w-full max-w-lg rounded-lg border border-white/10 bg-[#0d0d0d] p-8 shadow-[0_8px_30px_rgb(0,0,0,0.3)]">
<Card className="w-full max-w-lg border-white/10 bg-[#0d0d0d] p-8 shadow-[0_8px_30px_rgb(0,0,0,0.3)]">
<div className="mb-2 flex flex-wrap items-center gap-2 text-lg text-white/80">
<span>Opening</span>
<span className="inline-flex items-center rounded bg-white/10 px-2 py-0.5 font-mono text-sm text-white/90">
Expand All @@ -187,75 +187,63 @@ const OpenFileContent = () => {
{resolvedFilePath}
</div>

<div className="mb-6 inline-flex items-stretch rounded-lg border border-white/10 bg-white/5">
<div className="relative" ref={dropdownRef}>
<button
type="button"
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="flex h-full items-center gap-2 rounded-l-lg px-4 py-2.5 text-sm text-white/80 transition-colors hover:bg-white/10"
>
<div className="mb-6 inline-flex items-center gap-2">
<Select
value={preferredEditor}
onValueChange={(value) => handleEditorChange(parseEditorFromValue(value))}
onOpenChange={setIsEditorSelectOpen}
>
<SelectTrigger className="h-10 min-w-[160px] border-white/10 bg-white/5 text-sm text-white/80 hover:bg-white/10">
<span className="opacity-70">{selectedEditor?.icon}</span>
<span>{selectedEditor?.name}</span>
<ChevronDown
size={14}
className={cn(
"opacity-40 transition-transform",
isDropdownOpen && "rotate-180",
)}
/>
</button>

{isDropdownOpen && (
<div className="absolute left-0 top-full z-10 mt-1 min-w-[160px] overflow-hidden rounded-lg border border-white/10 bg-[#0d0d0d] shadow-[0_8px_30px_rgb(0,0,0,0.3)]">
{EDITORS.map((editor) => (
<button
key={editor.id}
type="button"
onClick={() => handleEditorChange(editor.id)}
className={cn(
"flex w-full items-center gap-2.5 px-4 py-2.5 text-sm transition-colors",
preferredEditor === editor.id
? "bg-white/10 text-white"
: "text-white/60 hover:bg-white/10 hover:text-white/90",
)}
>
<SelectValue placeholder="Select editor" />
</SelectTrigger>
Copy link

Choose a reason for hiding this comment

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

Duplicate editor icon in Select trigger

Medium Severity

The SelectTrigger contains an explicit icon (selectedEditor?.icon) alongside a SelectValue. Since each SelectItem wraps its full children (including the icon and name) inside SelectPrimitive.ItemText, and SelectValue renders the ItemText content of the selected item, the editor icon appears twice in the trigger — once from the explicit span and once from the SelectValue rendering.

Additional Locations (1)

Fix in Cursor Fix in Web

<SelectContent className="border-white/10 bg-[#0d0d0d] text-white">
{EDITORS.map((editor) => (
<SelectItem
key={editor.id}
value={editor.id}
className="text-sm text-white/80 focus:bg-white/10 focus:text-white"
>
<span className="flex items-center gap-2.5">
<span className="opacity-70">{editor.icon}</span>
<span>{editor.name}</span>
</button>
))}
</div>
)}
</div>

</span>
</SelectItem>
))}
</SelectContent>
</Select>
<div className="w-px bg-white/10" />

<button
<Button
type="button"
onClick={handleOpen}
className="flex items-center gap-1.5 rounded-r-lg px-4 py-2.5 text-sm text-white/80 transition-colors hover:bg-white/10"
variant="outline"
className="h-10 border-white/10 bg-white/5 text-sm text-white/80 hover:bg-white/10 hover:text-white"
>
<span>Open</span>
<ArrowUpRight size={14} className="opacity-50" />
</button>
</Button>
</div>

<div className="space-y-1 text-xs text-white/40">
<p>Your preference will be saved for future use.</p>
<p>Only open files from trusted sources.</p>
</div>
</div>
</Card>

<button
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setIsInfoOpen(!isInfoOpen)}
className="mt-8 flex items-center gap-1.5 text-xs text-white/25 transition-colors hover:text-white/40 focus:outline-none"
className="mt-8 h-auto gap-1.5 px-0 py-0 text-xs text-white/25 transition-colors hover:bg-transparent hover:text-white/40"
>
<span>What is React Grab?</span>
<ChevronDown
size={10}
className={cn("transition-transform", isInfoOpen && "rotate-180")}
/>
</button>
</Button>

{isInfoOpen && (
<p className="mt-2 text-center text-xs text-white/30">
Expand Down
102 changes: 36 additions & 66 deletions packages/website/components/benchmark-tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useState, useRef, useEffect, type ReactElement } from "react";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
import { useState, type ReactElement } from "react";
import { motion, useReducedMotion } from "motion/react";
import Link from "next/link";
import {
BENCHMARK_CONTROL_COLOR,
Expand All @@ -10,8 +10,12 @@ import {
BENCHMARK_TOOLTIP_TREATMENT_SECONDS,
BENCHMARK_TOOLTIP_MAX_SECONDS,
BENCHMARK_TOOLTIP_SPEEDUP_FACTOR,
TOOLTIP_HOVER_DELAY_MS,
} from "@/constants";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";

interface BenchmarkTooltipProps {
href: string;
Expand Down Expand Up @@ -190,71 +194,37 @@ export const BenchmarkTooltip = ({
className,
}: BenchmarkTooltipProps): ReactElement => {
const shouldReduceMotion = Boolean(useReducedMotion());
const [isHovered, setIsHovered] = useState(false);
const [isVisible, setIsVisible] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);

const handleMouseEnter = () => {
timeoutRef.current = setTimeout(() => {
setIsHovered(true);
setIsVisible(true);
}, TOOLTIP_HOVER_DELAY_MS);
};

const handleMouseLeave = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
setIsHovered(false);
setIsVisible(false);
};

useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
const [isTooltipOpen, setIsTooltipOpen] = useState(false);

return (
<span
className="relative inline-block"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<Link href={href} rel="noreferrer" className={className}>
{children}
</Link>
<AnimatePresence>
{isHovered && (
<motion.div
initial={
shouldReduceMotion ? false : { opacity: 0, y: 8, scale: 0.96 }
}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={
shouldReduceMotion ? undefined : { opacity: 0, y: 4, scale: 0.98 }
}
transition={
shouldReduceMotion
? { duration: 0 }
: { duration: 0.15, ease: "easeOut" }
}
style={{ transformOrigin: "top center" }}
className="absolute left-1/2 -translate-x-1/2 top-full mt-2 z-50 pointer-events-none"
>
<div className="absolute left-1/2 -translate-x-1/2 -top-1.5 w-3 h-3 bg-[#0a0a0a] border-l border-t border-neutral-800 rotate-45" />
<div className="bg-[#0a0a0a] border border-neutral-800 rounded-lg shadow-2xl overflow-hidden">
<MiniChart
isVisible={isVisible}
shouldReduceMotion={shouldReduceMotion}
/>
</div>
</motion.div>
)}
</AnimatePresence>
</span>
<Tooltip open={isTooltipOpen} onOpenChange={setIsTooltipOpen}>
<TooltipTrigger asChild>
<Link href={href} rel="noreferrer" className={className}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing exit animations on tooltip content motion.div causes visual snap when tooltip closes instead of smooth animation out

Fix on Vercel

{children}
</Link>
</TooltipTrigger>
<TooltipContent
side="bottom"
sideOffset={8}
className="border-neutral-800 bg-[#0a0a0a] p-0 shadow-2xl"
>
<motion.div
initial={shouldReduceMotion ? false : { opacity: 0, y: 8, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={
shouldReduceMotion
? { duration: 0 }
: { duration: 0.15, ease: "easeOut" }
}
style={{ transformOrigin: "top center" }}
>
<MiniChart
isVisible={isTooltipOpen}
shouldReduceMotion={shouldReduceMotion}
/>
</motion.div>
</TooltipContent>
</Tooltip>
);
};

Expand Down
Loading
Loading