From 9788a2f274b0b3824b822201b5d326ecc1b43345 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 4 Feb 2026 13:06:09 +0000 Subject: [PATCH 01/12] Make mobile demo animation continuous with table rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove gap between animation loops (200ms restart delay vs 12s interval) - Add animation for all 3 table rows in Recent Activity section - Each row gets selected with crosshair, shows component name, and copies - Animation flows continuously: Export → MetricCard → StatValue → SignupRow → OrderRow → PaymentRow → repeat Co-authored-by: Aiden Bai --- .../components/mobile-demo-animation.tsx | 73 +++++++++++++++---- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/packages/website/components/mobile-demo-animation.tsx b/packages/website/components/mobile-demo-animation.tsx index fa07ed9b7..bfe21e98b 100644 --- a/packages/website/components/mobile-demo-animation.tsx +++ b/packages/website/components/mobile-demo-animation.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useRef, type ReactElement } from "react"; import { cn } from "@/utils/cn"; -const ANIMATION_LOOP_INTERVAL_MS = 12000; +const ANIMATION_RESTART_DELAY_MS = 200; const SELECTION_PADDING_PX = 4; const DRAG_PADDING_PX = 6; const CURSOR_OFFSET_PX = 16; @@ -53,6 +53,12 @@ const HIDDEN_LABEL: LabelState = { type LabelMode = "idle" | "selecting" | "grabbing" | "copied" | "fading"; type CursorType = "default" | "crosshair" | "drag" | "grabbing"; +const ACTIVITY_DATA = [ + { label: "New signup", time: "2m ago", component: "SignupRow" }, + { label: "Order placed", time: "5m ago", component: "OrderRow" }, + { label: "Payment received", time: "12m ago", component: "PaymentRow" }, +]; + const createSelectionBox = (position: Position, padding: number): BoxState => ({ visible: true, x: position.x - padding, @@ -271,6 +277,7 @@ export const MobileDemoAnimation = (): ReactElement => { const metricCardRef = useRef(null); const metricValueRef = useRef(null); const exportButtonRef = useRef(null); + const activityRowRefs = useRef<(HTMLDivElement | null)[]>([]); const metricCardPosition = useRef({ x: 0, @@ -290,6 +297,7 @@ export const MobileDemoAnimation = (): ReactElement => { width: 0, height: 0, }); + const activityRowPositions = useRef([]); const measureElementPositions = (): void => { const container = containerRef.current; @@ -314,6 +322,18 @@ export const MobileDemoAnimation = (): ReactElement => { measureRelativePosition(metricCardRef.current, metricCardPosition); measureRelativePosition(metricValueRef.current, metricValuePosition); measureRelativePosition(exportButtonRef.current, exportButtonPosition); + + activityRowPositions.current = activityRowRefs.current + .filter((ref): ref is HTMLDivElement => ref !== null) + .map((element) => { + const rect = element.getBoundingClientRect(); + return { + x: rect.left - containerRect.left, + y: rect.top - containerRect.top, + width: rect.width, + height: rect.height, + }; + }); }; useEffect(() => { @@ -327,7 +347,6 @@ export const MobileDemoAnimation = (): ReactElement => { useEffect(() => { let isCancelled = false; - let animationInterval: ReturnType; const resetAnimationState = (): void => { setCursorPos({ x: 150, y: 80 }); @@ -499,24 +518,50 @@ export const MobileDemoAnimation = (): ReactElement => { await simulateClickAndCopy(valuePos); if (isCancelled) return; + // Animate through table rows + for (let i = 0; i < activityRowPositions.current.length; i++) { + if (isCancelled) return; + const rowPos = activityRowPositions.current[i]; + if (!rowPos) continue; + + const rowCenter = getElementCenter(rowPos); + setCursorPos(rowCenter); + await wait(350); + if (isCancelled) return; + + setSelectionBox(createSelectionBox(rowPos, SELECTION_PADDING_PX)); + displaySelectionLabel( + rowPos.x + rowPos.width / 2, + rowPos.y + rowPos.height + 10, + ACTIVITY_DATA[i]?.component ?? "TableRow", + "div", + ); + await wait(400); + if (isCancelled) return; + + await simulateClickAndCopy(rowPos); + if (isCancelled) return; + } + setIsCursorVisible(false); setCursorType("default"); - await wait(600); + await wait(ANIMATION_RESTART_DELAY_MS); + }; + + const runAnimationLoop = async (): Promise => { + while (!isCancelled) { + await executeAnimationSequence(); + } }; const initializeAnimationLoop = (): void => { isCancelled = false; - executeAnimationSequence(); - animationInterval = setInterval( - executeAnimationSequence, - ANIMATION_LOOP_INTERVAL_MS, - ); + runAnimationLoop(); }; const handleVisibilityChange = (): void => { if (document.visibilityState === "visible") { isCancelled = true; - clearInterval(animationInterval); resetAnimationState(); setTimeout(initializeAnimationLoop, 100); } @@ -527,7 +572,6 @@ export const MobileDemoAnimation = (): ReactElement => { return () => { isCancelled = true; - clearInterval(animationInterval); document.removeEventListener("visibilitychange", handleVisibilityChange); }; }, []); @@ -621,13 +665,12 @@ export const MobileDemoAnimation = (): ReactElement => {
- {[ - { label: "New signup", time: "2m ago" }, - { label: "Order placed", time: "5m ago" }, - { label: "Payment received", time: "12m ago" }, - ].map((activity) => ( + {ACTIVITY_DATA.map((activity, index) => (
{ + activityRowRefs.current[index] = el; + }} className="flex items-center justify-between px-3 py-2" >
From f3f548a5a2af8430927482bc2b006a51f32c939d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 4 Feb 2026 13:07:14 +0000 Subject: [PATCH 02/12] Clean up table row animation code - Use for..of with entries() instead of index-based loop - Remove unnecessary null check (we control the data) - Inline initializeAnimationLoop into direct runAnimationLoop call - Simplify ref callback syntax Co-authored-by: Aiden Bai --- .../components/mobile-demo-animation.tsx | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/website/components/mobile-demo-animation.tsx b/packages/website/components/mobile-demo-animation.tsx index bfe21e98b..acb6c1b67 100644 --- a/packages/website/components/mobile-demo-animation.tsx +++ b/packages/website/components/mobile-demo-animation.tsx @@ -518,11 +518,8 @@ export const MobileDemoAnimation = (): ReactElement => { await simulateClickAndCopy(valuePos); if (isCancelled) return; - // Animate through table rows - for (let i = 0; i < activityRowPositions.current.length; i++) { + for (const [i, rowPos] of activityRowPositions.current.entries()) { if (isCancelled) return; - const rowPos = activityRowPositions.current[i]; - if (!rowPos) continue; const rowCenter = getElementCenter(rowPos); setCursorPos(rowCenter); @@ -533,7 +530,7 @@ export const MobileDemoAnimation = (): ReactElement => { displaySelectionLabel( rowPos.x + rowPos.width / 2, rowPos.y + rowPos.height + 10, - ACTIVITY_DATA[i]?.component ?? "TableRow", + ACTIVITY_DATA[i].component, "div", ); await wait(400); @@ -554,21 +551,17 @@ export const MobileDemoAnimation = (): ReactElement => { } }; - const initializeAnimationLoop = (): void => { - isCancelled = false; - runAnimationLoop(); - }; - const handleVisibilityChange = (): void => { if (document.visibilityState === "visible") { isCancelled = true; resetAnimationState(); - setTimeout(initializeAnimationLoop, 100); + isCancelled = false; + setTimeout(runAnimationLoop, 100); } }; document.addEventListener("visibilitychange", handleVisibilityChange); - initializeAnimationLoop(); + runAnimationLoop(); return () => { isCancelled = true; @@ -665,12 +658,10 @@ export const MobileDemoAnimation = (): ReactElement => {
- {ACTIVITY_DATA.map((activity, index) => ( + {ACTIVITY_DATA.map((activity, i) => (
{ - activityRowRefs.current[index] = el; - }} + ref={(el) => (activityRowRefs.current[i] = el)} className="flex items-center justify-between px-3 py-2" >
From b1d8b58abc6293b31921e6ec9325dfaca52faa11 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 4 Feb 2026 13:12:10 +0000 Subject: [PATCH 03/12] Fix ref callback to return void for React 19 Co-authored-by: Aiden Bai --- packages/website/components/mobile-demo-animation.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/website/components/mobile-demo-animation.tsx b/packages/website/components/mobile-demo-animation.tsx index acb6c1b67..a40c5028d 100644 --- a/packages/website/components/mobile-demo-animation.tsx +++ b/packages/website/components/mobile-demo-animation.tsx @@ -661,7 +661,9 @@ export const MobileDemoAnimation = (): ReactElement => { {ACTIVITY_DATA.map((activity, i) => (
(activityRowRefs.current[i] = el)} + ref={(el) => { + activityRowRefs.current[i] = el; + }} className="flex items-center justify-between px-3 py-2" >
From 9171998feab5ab7a0fe8c048a01e2a9be7a9fe87 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 4 Feb 2026 13:14:14 +0000 Subject: [PATCH 04/12] Skip last table row in animation Co-authored-by: Aiden Bai --- packages/website/components/mobile-demo-animation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/components/mobile-demo-animation.tsx b/packages/website/components/mobile-demo-animation.tsx index a40c5028d..9b8c35571 100644 --- a/packages/website/components/mobile-demo-animation.tsx +++ b/packages/website/components/mobile-demo-animation.tsx @@ -518,7 +518,7 @@ export const MobileDemoAnimation = (): ReactElement => { await simulateClickAndCopy(valuePos); if (isCancelled) return; - for (const [i, rowPos] of activityRowPositions.current.entries()) { + for (const [i, rowPos] of activityRowPositions.current.slice(0, -1).entries()) { if (isCancelled) return; const rowCenter = getElementCenter(rowPos); From b9e2a9e05cf419e5c1182ff1407715f9d084ab93 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 4 Feb 2026 13:20:03 +0000 Subject: [PATCH 05/12] Add comment interaction for table rows in mobile demo - Table rows now show comment input UI instead of grab/copy - Simulates typing into 'Add context' field - Shows submit button and 'Sent' confirmation - Matches react-grab comment mode interaction Co-authored-by: Aiden Bai --- .../components/mobile-demo-animation.tsx | 123 +++++++++++++++--- 1 file changed, 108 insertions(+), 15 deletions(-) diff --git a/packages/website/components/mobile-demo-animation.tsx b/packages/website/components/mobile-demo-animation.tsx index 9b8c35571..674919609 100644 --- a/packages/website/components/mobile-demo-animation.tsx +++ b/packages/website/components/mobile-demo-animation.tsx @@ -50,13 +50,20 @@ const HIDDEN_LABEL: LabelState = { tagName: "", }; -type LabelMode = "idle" | "selecting" | "grabbing" | "copied" | "fading"; +type LabelMode = + | "idle" + | "selecting" + | "grabbing" + | "copied" + | "commenting" + | "submitted" + | "fading"; type CursorType = "default" | "crosshair" | "drag" | "grabbing"; const ACTIVITY_DATA = [ - { label: "New signup", time: "2m ago", component: "SignupRow" }, - { label: "Order placed", time: "5m ago", component: "OrderRow" }, - { label: "Payment received", time: "12m ago", component: "PaymentRow" }, + { label: "New signup", time: "2m ago", component: "SignupRow", comment: "new user joined" }, + { label: "Order placed", time: "5m ago", component: "OrderRow", comment: "check order #847" }, + { label: "Payment received", time: "12m ago", component: "PaymentRow", comment: "" }, ]; const createSelectionBox = (position: Position, padding: number): BoxState => ({ @@ -87,6 +94,24 @@ const CheckIcon = (): ReactElement => ( ); +const SubmitIcon = (): ReactElement => ( + + + +); + const LoaderIcon = (): ReactElement => ( { const [label, setLabel] = useState(HIDDEN_LABEL); const [labelMode, setLabelMode] = useState("idle"); const [cursorType, setCursorType] = useState("default"); + const [commentText, setCommentText] = useState(""); const containerRef = useRef(null); const metricCardRef = useRef(null); @@ -358,6 +384,7 @@ export const MobileDemoAnimation = (): ReactElement => { setSuccessFlash(HIDDEN_BOX); setLabel(HIDDEN_LABEL); setLabelMode("idle"); + setCommentText(""); }; const displaySelectionLabel = ( @@ -520,6 +547,7 @@ export const MobileDemoAnimation = (): ReactElement => { for (const [i, rowPos] of activityRowPositions.current.slice(0, -1).entries()) { if (isCancelled) return; + const activity = ACTIVITY_DATA[i]; const rowCenter = getElementCenter(rowPos); setCursorPos(rowCenter); @@ -530,13 +558,39 @@ export const MobileDemoAnimation = (): ReactElement => { displaySelectionLabel( rowPos.x + rowPos.width / 2, rowPos.y + rowPos.height + 10, - ACTIVITY_DATA[i].component, + activity.component, "div", ); - await wait(400); + await wait(300); if (isCancelled) return; - await simulateClickAndCopy(rowPos); + // Switch to comment input mode + setLabelMode("commenting"); + setCommentText(""); + await wait(200); + if (isCancelled) return; + + // Type the comment character by character + const comment = activity.comment; + for (let j = 0; j <= comment.length; j++) { + if (isCancelled) return; + setCommentText(comment.slice(0, j)); + await wait(50); + } + await wait(300); + if (isCancelled) return; + + // Submit + setLabelMode("submitted"); + setSuccessFlash(createSelectionBox(rowPos, SELECTION_PADDING_PX)); + await wait(500); + if (isCancelled) return; + + // Fade out + setSuccessFlash(HIDDEN_BOX); + setSelectionBox(HIDDEN_BOX); + await fadeOutSelectionLabel(); + setCommentText(""); if (isCancelled) return; } @@ -753,7 +807,7 @@ export const MobileDemoAnimation = (): ReactElement => {
{ }} > {labelMode === "selecting" && ( - <> +
{label.componentName} .{label.tagName} - +
)} {labelMode === "grabbing" && ( - <> +
Grabbing… - +
)} - {(labelMode === "copied" || labelMode === "fading") && ( - <> + {labelMode === "copied" && ( +
Copied - +
+ )} + {labelMode === "commenting" && ( +
+
+ + {label.componentName} + + + .{label.tagName} + +
+
+ + {commentText || "Add context"} + +
+ +
+
+
+ )} + {labelMode === "submitted" && ( +
+ + + Sent + +
+ )} + {labelMode === "fading" && ( +
+ + + Sent + +
)}
From 4db95232b1c6c7a1005da94037b21612ed92b445 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 4 Feb 2026 13:22:41 +0000 Subject: [PATCH 06/12] Fix activity row animation and concurrent loop issues - Remove slice(0, -1) to include all three activity rows in animation - Remove duplicate runAnimationLoop call to prevent concurrent animations --- packages/website/components/mobile-demo-animation.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/website/components/mobile-demo-animation.tsx b/packages/website/components/mobile-demo-animation.tsx index 674919609..26799a46e 100644 --- a/packages/website/components/mobile-demo-animation.tsx +++ b/packages/website/components/mobile-demo-animation.tsx @@ -545,7 +545,7 @@ export const MobileDemoAnimation = (): ReactElement => { await simulateClickAndCopy(valuePos); if (isCancelled) return; - for (const [i, rowPos] of activityRowPositions.current.slice(0, -1).entries()) { + for (const [i, rowPos] of activityRowPositions.current.entries()) { if (isCancelled) return; const activity = ACTIVITY_DATA[i]; @@ -610,7 +610,6 @@ export const MobileDemoAnimation = (): ReactElement => { isCancelled = true; resetAnimationState(); isCancelled = false; - setTimeout(runAnimationLoop, 100); } }; From 3f68e3563fbd862cfa82725440f0b6b7e22adede Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 4 Feb 2026 13:35:52 +0000 Subject: [PATCH 07/12] Fix index mismatch in activity rows and fading label text bug - Fix Bug 1: Preserve original array indices in activityRowPositions to prevent mismatch with ACTIVITY_DATA when refs are null - Fix Bug 2: Track and display correct text ('Copied' vs 'Sent') during label fade-out animation --- .../components/mobile-demo-animation.tsx | 72 ++++++++++++------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/packages/website/components/mobile-demo-animation.tsx b/packages/website/components/mobile-demo-animation.tsx index 26799a46e..784e91287 100644 --- a/packages/website/components/mobile-demo-animation.tsx +++ b/packages/website/components/mobile-demo-animation.tsx @@ -61,9 +61,24 @@ type LabelMode = type CursorType = "default" | "crosshair" | "drag" | "grabbing"; const ACTIVITY_DATA = [ - { label: "New signup", time: "2m ago", component: "SignupRow", comment: "new user joined" }, - { label: "Order placed", time: "5m ago", component: "OrderRow", comment: "check order #847" }, - { label: "Payment received", time: "12m ago", component: "PaymentRow", comment: "" }, + { + label: "New signup", + time: "2m ago", + component: "SignupRow", + comment: "new user joined", + }, + { + label: "Order placed", + time: "5m ago", + component: "OrderRow", + comment: "check order #847", + }, + { + label: "Payment received", + time: "12m ago", + component: "PaymentRow", + comment: "", + }, ]; const createSelectionBox = (position: Position, padding: number): BoxState => ({ @@ -304,6 +319,7 @@ export const MobileDemoAnimation = (): ReactElement => { const metricValueRef = useRef(null); const exportButtonRef = useRef(null); const activityRowRefs = useRef<(HTMLDivElement | null)[]>([]); + const fadingLabelTextRef = useRef<"Copied" | "Sent">("Sent"); const metricCardPosition = useRef({ x: 0, @@ -323,7 +339,7 @@ export const MobileDemoAnimation = (): ReactElement => { width: 0, height: 0, }); - const activityRowPositions = useRef([]); + const activityRowPositions = useRef<(Position | null)[]>([]); const measureElementPositions = (): void => { const container = containerRef.current; @@ -349,17 +365,16 @@ export const MobileDemoAnimation = (): ReactElement => { measureRelativePosition(metricValueRef.current, metricValuePosition); measureRelativePosition(exportButtonRef.current, exportButtonPosition); - activityRowPositions.current = activityRowRefs.current - .filter((ref): ref is HTMLDivElement => ref !== null) - .map((element) => { - const rect = element.getBoundingClientRect(); - return { - x: rect.left - containerRect.left, - y: rect.top - containerRect.top, - width: rect.width, - height: rect.height, - }; - }); + activityRowPositions.current = activityRowRefs.current.map((ref) => { + if (!ref) return null; + const rect = ref.getBoundingClientRect(); + return { + x: rect.left - containerRect.left, + y: rect.top - containerRect.top, + width: rect.width, + height: rect.height, + }; + }); }; useEffect(() => { @@ -397,7 +412,10 @@ export const MobileDemoAnimation = (): ReactElement => { setLabelMode("selecting"); }; - const fadeOutSelectionLabel = async (): Promise => { + const fadeOutSelectionLabel = async ( + text: "Copied" | "Sent", + ): Promise => { + fadingLabelTextRef.current = text; setLabelMode("fading"); await wait(300); setLabel(HIDDEN_LABEL); @@ -417,7 +435,7 @@ export const MobileDemoAnimation = (): ReactElement => { if (isCancelled) return; setSuccessFlash(HIDDEN_BOX); - await fadeOutSelectionLabel(); + await fadeOutSelectionLabel("Copied"); setCursorType("crosshair"); }; @@ -521,7 +539,7 @@ export const MobileDemoAnimation = (): ReactElement => { if (isCancelled) return; setSuccessFlash(HIDDEN_BOX); - await fadeOutSelectionLabel(); + await fadeOutSelectionLabel("Copied"); setCursorType("crosshair"); await wait(300); if (isCancelled) return; @@ -545,8 +563,10 @@ export const MobileDemoAnimation = (): ReactElement => { await simulateClickAndCopy(valuePos); if (isCancelled) return; - for (const [i, rowPos] of activityRowPositions.current.entries()) { + for (let i = 0; i < ACTIVITY_DATA.length; i++) { if (isCancelled) return; + const rowPos = activityRowPositions.current[i]; + if (!rowPos) continue; const activity = ACTIVITY_DATA[i]; const rowCenter = getElementCenter(rowPos); @@ -589,7 +609,7 @@ export const MobileDemoAnimation = (): ReactElement => { // Fade out setSuccessFlash(HIDDEN_BOX); setSelectionBox(HIDDEN_BOX); - await fadeOutSelectionLabel(); + await fadeOutSelectionLabel("Sent"); setCommentText(""); if (isCancelled) return; } @@ -852,10 +872,12 @@ export const MobileDemoAnimation = (): ReactElement => {
- + {commentText || "Add context"}
@@ -876,7 +898,7 @@ export const MobileDemoAnimation = (): ReactElement => {
- Sent + {fadingLabelTextRef.current}
)} From 82bfb91b6b159e4b9d694b521b0d892d221ff0d1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 4 Feb 2026 13:43:24 +0000 Subject: [PATCH 08/12] Fix animation loop not restarting after visibility change --- packages/website/components/mobile-demo-animation.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/website/components/mobile-demo-animation.tsx b/packages/website/components/mobile-demo-animation.tsx index 784e91287..f37b0b21c 100644 --- a/packages/website/components/mobile-demo-animation.tsx +++ b/packages/website/components/mobile-demo-animation.tsx @@ -630,6 +630,7 @@ export const MobileDemoAnimation = (): ReactElement => { isCancelled = true; resetAnimationState(); isCancelled = false; + runAnimationLoop(); } }; From 5d90a366c6c426451a7e37f4629807e02f9a8ea5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Feb 2026 02:23:37 +0000 Subject: [PATCH 09/12] Position table row labels above to prevent cutoff Co-authored-by: Aiden Bai --- .../website/components/mobile-demo-animation.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/website/components/mobile-demo-animation.tsx b/packages/website/components/mobile-demo-animation.tsx index f37b0b21c..da4a9efd7 100644 --- a/packages/website/components/mobile-demo-animation.tsx +++ b/packages/website/components/mobile-demo-animation.tsx @@ -40,6 +40,7 @@ interface LabelState { y: number; componentName: string; tagName: string; + above?: boolean; } const HIDDEN_LABEL: LabelState = { @@ -48,6 +49,7 @@ const HIDDEN_LABEL: LabelState = { y: 0, componentName: "", tagName: "", + above: false, }; type LabelMode = @@ -407,8 +409,9 @@ export const MobileDemoAnimation = (): ReactElement => { y: number, componentName: string, tagName: string, + above = false, ): void => { - setLabel({ visible: true, x, y, componentName, tagName }); + setLabel({ visible: true, x, y, componentName, tagName, above }); setLabelMode("selecting"); }; @@ -577,9 +580,10 @@ export const MobileDemoAnimation = (): ReactElement => { setSelectionBox(createSelectionBox(rowPos, SELECTION_PADDING_PX)); displaySelectionLabel( rowPos.x + rowPos.width / 2, - rowPos.y + rowPos.height + 10, + rowPos.y - 6, activity.component, "div", + true, ); await wait(300); if (isCancelled) return; @@ -833,7 +837,9 @@ export const MobileDemoAnimation = (): ReactElement => { style={{ left: label.x, top: label.y, - transform: "translateX(-50%)", + transform: label.above + ? "translateX(-50%) translateY(-100%)" + : "translateX(-50%)", }} > {labelMode === "selecting" && ( From a1db77940b4fc91bf53cd531064eb171720e7acd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Feb 2026 02:26:18 +0000 Subject: [PATCH 10/12] Fix label cutoff: add bottom padding, skip last row, position below Co-authored-by: Aiden Bai --- packages/website/components/mobile-demo-animation.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/website/components/mobile-demo-animation.tsx b/packages/website/components/mobile-demo-animation.tsx index da4a9efd7..71c7e0958 100644 --- a/packages/website/components/mobile-demo-animation.tsx +++ b/packages/website/components/mobile-demo-animation.tsx @@ -566,7 +566,7 @@ export const MobileDemoAnimation = (): ReactElement => { await simulateClickAndCopy(valuePos); if (isCancelled) return; - for (let i = 0; i < ACTIVITY_DATA.length; i++) { + for (let i = 0; i < ACTIVITY_DATA.length - 1; i++) { if (isCancelled) return; const rowPos = activityRowPositions.current[i]; if (!rowPos) continue; @@ -579,11 +579,11 @@ export const MobileDemoAnimation = (): ReactElement => { setSelectionBox(createSelectionBox(rowPos, SELECTION_PADDING_PX)); displaySelectionLabel( - rowPos.x + rowPos.width / 2, - rowPos.y - 6, + rowPos.x + 60, + rowPos.y + rowPos.height + 8, activity.component, "div", - true, + false, ); await wait(300); if (isCancelled) return; @@ -675,7 +675,7 @@ export const MobileDemoAnimation = (): ReactElement => { `}
-
+
From 95be3d92c0e9cee43c3ea67d2b086f561670aadf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Feb 2026 02:36:22 +0000 Subject: [PATCH 11/12] Make all interactions comment-based except the last one (OrderRow) - ExportBtn: comment 'add CSV option' - MetricCard: comment 'show graph' - StatValue: comment 'format as USD' - SignupRow: comment 'add avatar' - OrderRow: grab/copy (last interaction) Co-authored-by: Aiden Bai --- .../components/mobile-demo-animation.tsx | 177 ++++++++---------- 1 file changed, 73 insertions(+), 104 deletions(-) diff --git a/packages/website/components/mobile-demo-animation.tsx b/packages/website/components/mobile-demo-animation.tsx index 71c7e0958..be5cd38d2 100644 --- a/packages/website/components/mobile-demo-animation.tsx +++ b/packages/website/components/mobile-demo-animation.tsx @@ -63,24 +63,9 @@ type LabelMode = type CursorType = "default" | "crosshair" | "drag" | "grabbing"; const ACTIVITY_DATA = [ - { - label: "New signup", - time: "2m ago", - component: "SignupRow", - comment: "new user joined", - }, - { - label: "Order placed", - time: "5m ago", - component: "OrderRow", - comment: "check order #847", - }, - { - label: "Payment received", - time: "12m ago", - component: "PaymentRow", - comment: "", - }, + { label: "New signup", time: "2m ago", component: "SignupRow" }, + { label: "Order placed", time: "5m ago", component: "OrderRow" }, + { label: "Payment received", time: "12m ago", component: "PaymentRow" }, ]; const createSelectionBox = (position: Position, padding: number): BoxState => ({ @@ -442,6 +427,37 @@ export const MobileDemoAnimation = (): ReactElement => { setCursorType("crosshair"); }; + const simulateComment = async ( + position: Position, + comment: string, + ): Promise => { + await wait(300); + if (isCancelled) return; + + setLabelMode("commenting"); + setCommentText(""); + await wait(200); + if (isCancelled) return; + + for (let j = 0; j <= comment.length; j++) { + if (isCancelled) return; + setCommentText(comment.slice(0, j)); + await wait(50); + } + await wait(300); + if (isCancelled) return; + + setLabelMode("submitted"); + setSuccessFlash(createSelectionBox(position, SELECTION_PADDING_PX)); + await wait(500); + if (isCancelled) return; + + setSuccessFlash(HIDDEN_BOX); + setSelectionBox(HIDDEN_BOX); + await fadeOutSelectionLabel("Sent"); + setCommentText(""); + }; + const animateDragSelection = async ( startX: number, startY: number, @@ -479,10 +495,11 @@ export const MobileDemoAnimation = (): ReactElement => { await wait(300); if (isCancelled) return; + // 1. Export button - comment const buttonPos = exportButtonPosition.current; const buttonCenter = getElementCenter(buttonPos); setCursorPos(buttonCenter); - await wait(450); + await wait(400); if (isCancelled) return; setSelectionBox(createSelectionBox(buttonPos, SELECTION_PADDING_PX)); @@ -492,65 +509,31 @@ export const MobileDemoAnimation = (): ReactElement => { "ExportBtn", "button", ); - await wait(600); - if (isCancelled) return; - - await simulateClickAndCopy(buttonPos); - await wait(300); + await simulateComment(buttonPos, "add CSV option"); if (isCancelled) return; + // 2. MetricCard - comment const cardPos = metricCardPosition.current; - const dragStartX = cardPos.x - DRAG_PADDING_PX; - const dragStartY = cardPos.y - DRAG_PADDING_PX; - const dragEndX = cardPos.x + cardPos.width + DRAG_PADDING_PX; - const dragEndY = cardPos.y + cardPos.height + DRAG_PADDING_PX; - - setCursorPos({ x: dragStartX, y: dragStartY }); - await wait(500); - if (isCancelled) return; - - setIsDragging(true); - setCursorType("drag"); - setDragBox({ - visible: true, - x: dragStartX, - y: dragStartY, - width: 0, - height: 0, - }); - await animateDragSelection(dragStartX, dragStartY, dragEndX, dragEndY); - if (isCancelled) return; - await wait(200); + const cardCenter = getElementCenter(cardPos); + setCursorPos(cardCenter); + await wait(400); if (isCancelled) return; - setIsDragging(false); - setDragBox(HIDDEN_BOX); - setCursorType("grabbing"); - setSuccessFlash(createSelectionBox(cardPos, SELECTION_PADDING_PX)); + setSelectionBox(createSelectionBox(cardPos, SELECTION_PADDING_PX)); displaySelectionLabel( cardPos.x + cardPos.width / 2, cardPos.y - 10, "MetricCard", "div", ); - setLabelMode("grabbing"); - await wait(500); - if (isCancelled) return; - - setLabelMode("copied"); - await wait(500); - if (isCancelled) return; - - setSuccessFlash(HIDDEN_BOX); - await fadeOutSelectionLabel("Copied"); - setCursorType("crosshair"); - await wait(300); + await simulateComment(cardPos, "show graph"); if (isCancelled) return; + // 3. StatValue - comment const valuePos = metricValuePosition.current; const valueCenter = getElementCenter(valuePos); setCursorPos(valueCenter); - await wait(500); + await wait(400); if (isCancelled) return; setSelectionBox(createSelectionBox(valuePos, SELECTION_PADDING_PX)); @@ -560,61 +543,47 @@ export const MobileDemoAnimation = (): ReactElement => { "StatValue", "span", ); - await wait(600); + await simulateComment(valuePos, "format as USD"); if (isCancelled) return; - await simulateClickAndCopy(valuePos); - if (isCancelled) return; - - for (let i = 0; i < ACTIVITY_DATA.length - 1; i++) { - if (isCancelled) return; - const rowPos = activityRowPositions.current[i]; - if (!rowPos) continue; - const activity = ACTIVITY_DATA[i]; - - const rowCenter = getElementCenter(rowPos); - setCursorPos(rowCenter); - await wait(350); + // 4. SignupRow - comment + const signupRowPos = activityRowPositions.current[0]; + if (signupRowPos) { + const signupCenter = getElementCenter(signupRowPos); + setCursorPos(signupCenter); + await wait(400); if (isCancelled) return; - setSelectionBox(createSelectionBox(rowPos, SELECTION_PADDING_PX)); + setSelectionBox(createSelectionBox(signupRowPos, SELECTION_PADDING_PX)); displaySelectionLabel( - rowPos.x + 60, - rowPos.y + rowPos.height + 8, - activity.component, + signupRowPos.x + 60, + signupRowPos.y + signupRowPos.height + 8, + "SignupRow", "div", - false, ); - await wait(300); - if (isCancelled) return; - - // Switch to comment input mode - setLabelMode("commenting"); - setCommentText(""); - await wait(200); + await simulateComment(signupRowPos, "add avatar"); if (isCancelled) return; + } - // Type the comment character by character - const comment = activity.comment; - for (let j = 0; j <= comment.length; j++) { - if (isCancelled) return; - setCommentText(comment.slice(0, j)); - await wait(50); - } - await wait(300); + // 5. OrderRow - grab/copy (last one) + const orderRowPos = activityRowPositions.current[1]; + if (orderRowPos) { + const orderCenter = getElementCenter(orderRowPos); + setCursorPos(orderCenter); + await wait(400); if (isCancelled) return; - // Submit - setLabelMode("submitted"); - setSuccessFlash(createSelectionBox(rowPos, SELECTION_PADDING_PX)); - await wait(500); + setSelectionBox(createSelectionBox(orderRowPos, SELECTION_PADDING_PX)); + displaySelectionLabel( + orderRowPos.x + 60, + orderRowPos.y + orderRowPos.height + 8, + "OrderRow", + "div", + ); + await wait(400); if (isCancelled) return; - // Fade out - setSuccessFlash(HIDDEN_BOX); - setSelectionBox(HIDDEN_BOX); - await fadeOutSelectionLabel("Sent"); - setCommentText(""); + await simulateClickAndCopy(orderRowPos); if (isCancelled) return; } From c56917d4524677b85a3df3ae9813bb724f01e5e8 Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Sat, 7 Feb 2026 01:46:16 -0800 Subject: [PATCH 12/12] Fix flaky copy-feedback e2e test for hover during feedback period Co-authored-by: Cursor --- packages/react-grab/e2e/copy-feedback.spec.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/react-grab/e2e/copy-feedback.spec.ts b/packages/react-grab/e2e/copy-feedback.spec.ts index 41f2a70c3..2c0ad4261 100644 --- a/packages/react-grab/e2e/copy-feedback.spec.ts +++ b/packages/react-grab/e2e/copy-feedback.spec.ts @@ -79,25 +79,16 @@ test.describe("Copy Feedback Behavior", () => { await reactGrab.hoverElement("li:first-child"); await reactGrab.waitForSelectionBox(); - const boundsBefore = await reactGrab.getSelectionBoxBounds(); - await reactGrab.page.keyboard.down(reactGrab.modifierKey); await reactGrab.page.keyboard.down("c"); await reactGrab.clickElement("li:first-child"); - await reactGrab.page.waitForTimeout(100); await reactGrab.hoverElement("h1"); - await reactGrab.waitForSelectionBox(); - const boundsAfter = await reactGrab.getSelectionBoxBounds(); - const boundsSizeChanged = - boundsBefore && - boundsAfter && - (boundsBefore.width !== boundsAfter.width || - boundsBefore.height !== boundsAfter.height); + await reactGrab.page.waitForTimeout(FEEDBACK_DURATION_MS + 500); - expect(boundsBefore).not.toBeNull(); + expect(await reactGrab.isOverlayVisible()).toBe(true); + const boundsAfter = await reactGrab.getSelectionBoxBounds(); expect(boundsAfter).not.toBeNull(); - expect(boundsSizeChanged).toBe(true); await reactGrab.page.keyboard.up("c"); await reactGrab.page.keyboard.up(reactGrab.modifierKey);