From e7a1c0e4219d0f859f6efe0a3668a3e7682f2642 Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Sat, 14 Mar 2026 20:28:55 -0700 Subject: [PATCH 1/6] www --- .../react-grab/src/components/renderer.tsx | 1 + .../src/components/selection-label/index.tsx | 56 ++++++++++++++----- .../src/components/toolbar/index.tsx | 8 ++- packages/react-grab/src/core/index.tsx | 12 ++-- packages/react-grab/src/types.ts | 2 + 5 files changed, 55 insertions(+), 24 deletions(-) diff --git a/packages/react-grab/src/components/renderer.tsx b/packages/react-grab/src/components/renderer.tsx index 738cec8c4..a78dd4848 100644 --- a/packages/react-grab/src/components/renderer.tsx +++ b/packages/react-grab/src/components/renderer.tsx @@ -141,6 +141,7 @@ export const ReactGrabRenderer: Component = (props) => { onCancel={props.onInputCancel} onToggleExpand={props.onToggleExpand} isPendingDismiss={props.isPendingDismiss} + selectionLabelShakeCount={props.selectionLabelShakeCount} onConfirmDismiss={props.onConfirmDismiss} onCancelDismiss={props.onCancelDismiss} onOpen={() => { diff --git a/packages/react-grab/src/components/selection-label/index.tsx b/packages/react-grab/src/components/selection-label/index.tsx index 84f7aeb9a..763fde865 100644 --- a/packages/react-grab/src/components/selection-label/index.tsx +++ b/packages/react-grab/src/components/selection-label/index.tsx @@ -4,6 +4,7 @@ import { createSignal, createEffect, createMemo, + on, onMount, onCleanup, } from "solid-js"; @@ -67,6 +68,7 @@ export const SelectionLabel: Component = (props) => { const [viewportVersion, setViewportVersion] = createSignal(0); const [hadValidBounds, setHadValidBounds] = createSignal(false); const [isInternalFading, setIsInternalFading] = createSignal(false); + const [isShaking, setIsShaking] = createSignal(false); const canInteract = () => props.status !== "copying" && @@ -149,6 +151,8 @@ export const SelectionLabel: Component = (props) => { } window.addEventListener("scroll", handleViewportChange, true); window.addEventListener("resize", handleViewportChange); + window.visualViewport?.addEventListener("resize", handleViewportChange); + window.visualViewport?.addEventListener("scroll", handleViewportChange); window.addEventListener("keydown", handleGlobalKeyDown, { capture: true }); }); @@ -156,6 +160,8 @@ export const SelectionLabel: Component = (props) => { resizeObserver?.disconnect(); window.removeEventListener("scroll", handleViewportChange, true); window.removeEventListener("resize", handleViewportChange); + window.visualViewport?.removeEventListener("resize", handleViewportChange); + window.visualViewport?.removeEventListener("scroll", handleViewportChange); window.removeEventListener("keydown", handleGlobalKeyDown, { capture: true, }); @@ -200,14 +206,19 @@ export const SelectionLabel: Component = (props) => { }; } - const viewportWidth = window.visualViewport?.width ?? window.innerWidth; - const viewportHeight = window.visualViewport?.height ?? window.innerHeight; + const visualViewport = window.visualViewport; + const viewportLeft = visualViewport?.offsetLeft ?? 0; + const viewportTop = visualViewport?.offsetTop ?? 0; + const viewportRight = + viewportLeft + (visualViewport?.width ?? window.innerWidth); + const viewportBottom = + viewportTop + (visualViewport?.height ?? window.innerHeight); const isSelectionVisibleInViewport = - bounds.x + bounds.width > 0 && - bounds.x < viewportWidth && - bounds.y + bounds.height > 0 && - bounds.y < viewportHeight; + bounds.x + bounds.width > viewportLeft && + bounds.x < viewportRight && + bounds.y + bounds.height > viewportTop && + bounds.y < viewportBottom; if (!isSelectionVisibleInViewport) { return { @@ -233,24 +244,24 @@ export const SelectionLabel: Component = (props) => { const labelLeft = anchorX - labelWidth / 2; const labelRight = anchorX + labelWidth / 2; - if (labelRight > viewportWidth - VIEWPORT_MARGIN_PX) { - edgeOffsetX = viewportWidth - VIEWPORT_MARGIN_PX - labelRight; + if (labelRight > viewportRight - VIEWPORT_MARGIN_PX) { + edgeOffsetX = viewportRight - VIEWPORT_MARGIN_PX - labelRight; } - if (labelLeft + edgeOffsetX < VIEWPORT_MARGIN_PX) { - edgeOffsetX = VIEWPORT_MARGIN_PX - labelLeft; + if (labelLeft + edgeOffsetX < viewportLeft + VIEWPORT_MARGIN_PX) { + edgeOffsetX = viewportLeft + VIEWPORT_MARGIN_PX - labelLeft; } } const totalHeightNeeded = labelHeight + actualArrowHeight + LABEL_GAP_PX; const fitsBelow = - positionTop + labelHeight <= viewportHeight - VIEWPORT_MARGIN_PX; + positionTop + labelHeight <= viewportBottom - VIEWPORT_MARGIN_PX; if (!fitsBelow) { positionTop = selectionTop - totalHeightNeeded; } - if (positionTop < VIEWPORT_MARGIN_PX) { - positionTop = VIEWPORT_MARGIN_PX; + if (positionTop < viewportTop + VIEWPORT_MARGIN_PX) { + positionTop = viewportTop + VIEWPORT_MARGIN_PX; } const arrowLeftPercent = ARROW_CENTER_PERCENT; @@ -290,6 +301,18 @@ export const SelectionLabel: Component = (props) => { } }); + createEffect( + on( + () => props.selectionLabelShakeCount, + (count) => { + if (count) { + setIsShaking(true); + } + }, + { defer: true }, + ), + ); + const handleKeyDown = (event: KeyboardEvent) => { if (event.isComposing || event.keyCode === IME_COMPOSING_KEY_CODE) { return; @@ -418,10 +441,12 @@ export const SelectionLabel: Component = (props) => { class={cn( "contain-layout flex items-center gap-[5px] rounded-[10px] antialiased w-fit h-fit p-0 [font-synthesis:none] [corner-shape:superellipse(1.25)]", PANEL_STYLES, + isShaking() && "animate-shake", )} style={{ display: isCompletedStatus() && !props.error ? "none" : undefined, }} + onAnimationEnd={() => setIsShaking(false)} >
= (props) => { { + props.onCancelDismiss?.(); + inputRef?.focus(); + }} /> diff --git a/packages/react-grab/src/components/toolbar/index.tsx b/packages/react-grab/src/components/toolbar/index.tsx index 32f4ff8d5..cc1614ec3 100644 --- a/packages/react-grab/src/components/toolbar/index.tsx +++ b/packages/react-grab/src/components/toolbar/index.tsx @@ -1605,11 +1605,13 @@ export const Toolbar: Component = (props) => { > - + 0}> + class="absolute -top-1 -right-1 min-w-2.5 h-2.5 flex items-center justify-center rounded-full bg-grab-pink text-white text-[6px] font-semibold leading-none" + > + {props.historyItemCount} + diff --git a/packages/react-grab/src/core/index.tsx b/packages/react-grab/src/core/index.tsx index 32967b3d6..c22b25a01 100644 --- a/packages/react-grab/src/core/index.tsx +++ b/packages/react-grab/src/core/index.tsx @@ -273,6 +273,8 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { savedToolbarState?.enabled ?? true, ); const [toolbarShakeCount, setToolbarShakeCount] = createSignal(0); + const [selectionLabelShakeCount, setSelectionLabelShakeCount] = + createSignal(0); const [currentToolbarState, setCurrentToolbarState] = createSignal(savedToolbarState); const [isToolbarSelectHovered, setIsToolbarSelectHovered] = @@ -1643,15 +1645,10 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { actions.clearLastCopied(); if (!isPromptMode()) return; - const currentInput = store.inputText.trim(); - if (currentInput && !isPendingDismiss()) { + if (!isPendingDismiss()) { actions.setPendingDismiss(true); - return; } - - actions.clearInputText(); - actions.clearReplySessionId(); - deactivateRenderer(); + setSelectionLabelShakeCount((count) => count + 1); }; const handleConfirmDismiss = () => { @@ -4047,6 +4044,7 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { onInputCancel={handleInputCancel} onToggleExpand={handleToggleExpand} isPendingDismiss={isPendingDismiss()} + selectionLabelShakeCount={selectionLabelShakeCount()} onConfirmDismiss={handleConfirmDismiss} onCancelDismiss={handleCancelDismiss} pendingAbortSessionId={pendingAbortSessionId()} diff --git a/packages/react-grab/src/types.ts b/packages/react-grab/src/types.ts index 44135b9e6..88ded68ea 100644 --- a/packages/react-grab/src/types.ts +++ b/packages/react-grab/src/types.ts @@ -529,6 +529,7 @@ export interface ReactGrabRendererProps { onInputCancel?: () => void; onToggleExpand?: () => void; isPendingDismiss?: boolean; + selectionLabelShakeCount?: number; onConfirmDismiss?: () => void; onCancelDismiss?: () => void; pendingAbortSessionId?: string | null; @@ -688,6 +689,7 @@ export interface SelectionLabelProps { onUndo?: () => void; onFollowUpSubmit?: (prompt: string) => void; isPendingDismiss?: boolean; + selectionLabelShakeCount?: number; onConfirmDismiss?: () => void; onCancelDismiss?: () => void; isPendingAbort?: boolean; From d615813a858a54820328f182b702a09b3448cf7a Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Sat, 14 Mar 2026 23:34:00 -0700 Subject: [PATCH 2/6] www --- packages/react-grab/e2e/api-methods.spec.ts | 2 +- .../react-grab/e2e/event-callbacks.spec.ts | 17 ---- packages/react-grab/e2e/fixtures.ts | 62 ------------- .../e2e/theme-customization.spec.ts | 88 ------------------- .../e2e/toolbar-selection-hover.spec.ts | 17 ---- packages/react-grab/e2e/touch-mode.spec.ts | 20 ----- .../react-grab/e2e/visual-feedback.spec.ts | 43 --------- .../src/components/overlay-canvas.tsx | 55 +----------- .../react-grab/src/components/renderer.tsx | 1 - .../src/components/selection-label/index.tsx | 6 +- .../src/components/toolbar/index.tsx | 2 +- packages/react-grab/src/constants.ts | 1 - packages/react-grab/src/core/index.tsx | 38 +------- packages/react-grab/src/core/noop-api.ts | 1 - .../react-grab/src/core/plugin-registry.ts | 3 - packages/react-grab/src/core/theme.ts | 6 -- packages/react-grab/src/index.ts | 1 - packages/react-grab/src/types.ts | 18 ---- 18 files changed, 7 insertions(+), 374 deletions(-) diff --git a/packages/react-grab/e2e/api-methods.spec.ts b/packages/react-grab/e2e/api-methods.spec.ts index df62c90c3..a40012a99 100644 --- a/packages/react-grab/e2e/api-methods.spec.ts +++ b/packages/react-grab/e2e/api-methods.spec.ts @@ -212,7 +212,7 @@ test.describe("API Methods", () => { }) => { await reactGrab.updateOptions({ theme: { hue: 45 } }); await reactGrab.updateOptions({ - theme: { crosshair: { enabled: false } }, + theme: { elementLabel: { enabled: false } }, }); await reactGrab.activate(); diff --git a/packages/react-grab/e2e/event-callbacks.spec.ts b/packages/react-grab/e2e/event-callbacks.spec.ts index b50a6871e..1951e49b2 100644 --- a/packages/react-grab/e2e/event-callbacks.spec.ts +++ b/packages/react-grab/e2e/event-callbacks.spec.ts @@ -361,23 +361,6 @@ test.describe("Event Callbacks", () => { expect(dragBoxCalls.length).toBeGreaterThan(0); }); - test("onCrosshair should fire when crosshair moves", async ({ - reactGrab, - }) => { - await reactGrab.activate(); - await reactGrab.clearCallbackHistory(); - - await reactGrab.page.mouse.move(200, 200); - await reactGrab.page.waitForTimeout(50); - await reactGrab.page.mouse.move(300, 300); - await reactGrab.page.waitForTimeout(100); - - const history = await reactGrab.getCallbackHistory(); - const crosshairCalls = history.filter((c) => c.name === "onCrosshair"); - - expect(crosshairCalls.length).toBeGreaterThan(0); - }); - test("onGrabbedBox should fire when element is grabbed", async ({ reactGrab, }) => { diff --git a/packages/react-grab/e2e/fixtures.ts b/packages/react-grab/e2e/fixtures.ts index 10a9f0bca..ac81a5136 100644 --- a/packages/react-grab/e2e/fixtures.ts +++ b/packages/react-grab/e2e/fixtures.ts @@ -70,11 +70,6 @@ interface ReactGrabState { labelInstances: LabelInstanceInfo[]; } -interface CrosshairInfo { - isVisible: boolean; - position: { x: number; y: number } | null; -} - interface GrabbedBoxInfo { count: number; boxes: Array<{ @@ -184,8 +179,6 @@ export interface ReactGrabPageObject { waitForSelectionLabel: () => Promise; getLabelStatusText: () => Promise; - getCrosshairInfo: () => Promise; - isCrosshairVisible: () => Promise; getGrabbedBoxInfo: () => Promise; getLabelInstancesInfo: () => Promise; isGrabbedBoxVisible: () => Promise; @@ -1531,57 +1524,6 @@ const createReactGrabPageObject = (page: Page): ReactGrabPageObject => { }, ATTRIBUTE_NAME); }; - const getCrosshairInfo = async (): Promise => { - return page.evaluate((attrName) => { - const host = document.querySelector(`[${attrName}]`); - const shadowRoot = host?.shadowRoot; - if (!shadowRoot) return { isVisible: false, position: null }; - const root = shadowRoot.querySelector(`[${attrName}]`); - if (!root) return { isVisible: false, position: null }; - - const crosshairElements = Array.from( - root.querySelectorAll("div[style*='pointer-events: none']"), - ); - for (let i = 0; i < crosshairElements.length; i++) { - const element = crosshairElements[i] as HTMLElement; - const style = element.style; - if ( - style.position === "fixed" && - (style.width === "1px" || - style.height === "1px" || - style.width === "100%" || - style.height === "100%") - ) { - const transform = style.transform; - const match = transform?.match(/translate\(([^,]+)px,\s*([^)]+)px\)/); - if (match) { - return { - isVisible: true, - position: { x: parseFloat(match[1]), y: parseFloat(match[2]) }, - }; - } - } - } - return { isVisible: false, position: null }; - }, ATTRIBUTE_NAME); - }; - - const isCrosshairVisible = async (): Promise => { - return page.evaluate(() => { - const api = ( - window as { - __REACT_GRAB__?: { - getState: () => { - isCrosshairVisible: boolean; - }; - }; - } - ).__REACT_GRAB__; - - return api?.getState()?.isCrosshairVisible ?? false; - }); - }; - const getGrabbedBoxInfo = async (): Promise => { return page.evaluate(() => { const api = ( @@ -1826,7 +1768,6 @@ const createReactGrabPageObject = (page: Page): ReactGrabPageObject => { "onPromptModeChange", "onSelectionBox", "onDragBox", - "onCrosshair", "onGrabbedBox", "onContextMenu", "onOpenFile", @@ -2353,7 +2294,6 @@ const createReactGrabPageObject = (page: Page): ReactGrabPageObject => { onPromptModeChange: trackCallback("onPromptModeChange"), onSelectionBox: trackCallback("onSelectionBox"), onDragBox: trackCallback("onDragBox"), - onCrosshair: trackCallback("onCrosshair"), onGrabbedBox: trackCallback("onGrabbedBox"), onContextMenu: trackCallback("onContextMenu"), onOpenFile: trackCallback("onOpenFile"), @@ -2496,8 +2436,6 @@ const createReactGrabPageObject = (page: Page): ReactGrabPageObject => { waitForSelectionLabel, getLabelStatusText, - getCrosshairInfo, - isCrosshairVisible, getGrabbedBoxInfo, getLabelInstancesInfo, isGrabbedBoxVisible, diff --git a/packages/react-grab/e2e/theme-customization.spec.ts b/packages/react-grab/e2e/theme-customization.spec.ts index f1fcfa0f0..3363aed2f 100644 --- a/packages/react-grab/e2e/theme-customization.spec.ts +++ b/packages/react-grab/e2e/theme-customization.spec.ts @@ -160,29 +160,6 @@ test.describe("Theme Customization", () => { }); }); - test.describe("Crosshair", () => { - test("should show crosshair by default", async ({ reactGrab }) => { - await reactGrab.activate(); - await reactGrab.page.mouse.move(400, 400); - await reactGrab.page.waitForTimeout(100); - - const isVisible = await reactGrab.isCrosshairVisible(); - expect(isVisible).toBe(true); - }); - - test("should hide crosshair when disabled", async ({ reactGrab }) => { - await reactGrab.updateOptions({ - theme: { crosshair: { enabled: false } }, - }); - await reactGrab.activate(); - await reactGrab.page.mouse.move(400, 400); - await reactGrab.page.waitForTimeout(100); - - const isVisible = await reactGrab.isCrosshairVisible(); - expect(isVisible).toBe(false); - }); - }); - test.describe("Toolbar", () => { test("should show toolbar by default", async ({ reactGrab }) => { await reactGrab.page.waitForTimeout(600); @@ -215,56 +192,6 @@ test.describe("Theme Customization", () => { }); }); - test.describe("Multiple Feature Toggles", () => { - test("should apply multiple theme settings", async ({ reactGrab }) => { - await reactGrab.updateOptions({ - theme: { - hue: 45, - crosshair: { enabled: false }, - elementLabel: { enabled: false }, - }, - }); - - await reactGrab.activate(); - await reactGrab.page.mouse.move(400, 400); - await reactGrab.page.waitForTimeout(100); - - const hasFilter = await reactGrab.page.evaluate(() => { - const host = document.querySelector("[data-react-grab]"); - const shadowRoot = host?.shadowRoot; - const root = shadowRoot?.querySelector( - "[data-react-grab]", - ) as HTMLElement; - return root?.style.filter?.includes("hue-rotate(45deg)") ?? false; - }); - expect(hasFilter).toBe(true); - - const isCrosshairVisible = await reactGrab.isCrosshairVisible(); - expect(isCrosshairVisible).toBe(false); - }); - - test("should allow re-enabling disabled features", async ({ - reactGrab, - }) => { - await reactGrab.updateOptions({ - theme: { crosshair: { enabled: false } }, - }); - await reactGrab.activate(); - - const isDisabled = await reactGrab.isCrosshairVisible(); - expect(isDisabled).toBe(false); - - await reactGrab.updateOptions({ - theme: { crosshair: { enabled: true } }, - }); - await reactGrab.page.mouse.move(400, 400); - await reactGrab.page.waitForTimeout(100); - - const isEnabled = await reactGrab.isCrosshairVisible(); - expect(isEnabled).toBe(true); - }); - }); - test.describe("Theme Persistence", () => { test("theme should persist across activation cycles", async ({ reactGrab, @@ -286,20 +213,5 @@ test.describe("Theme Customization", () => { expect(hasFilter).toBe(true); }); - test("theme updates should be immediate", async ({ reactGrab }) => { - await reactGrab.activate(); - await reactGrab.page.mouse.move(400, 400); - await reactGrab.page.waitForTimeout(100); - - const isVisibleBefore = await reactGrab.isCrosshairVisible(); - expect(isVisibleBefore).toBe(true); - - await reactGrab.updateOptions({ - theme: { crosshair: { enabled: false } }, - }); - - const isVisibleAfter = await reactGrab.isCrosshairVisible(); - expect(isVisibleAfter).toBe(false); - }); }); }); diff --git a/packages/react-grab/e2e/toolbar-selection-hover.spec.ts b/packages/react-grab/e2e/toolbar-selection-hover.spec.ts index d307182b8..f9cec7ffc 100644 --- a/packages/react-grab/e2e/toolbar-selection-hover.spec.ts +++ b/packages/react-grab/e2e/toolbar-selection-hover.spec.ts @@ -89,23 +89,6 @@ test.describe("Toolbar Selection Hover", () => { .toBe(true); }); - test("should hide crosshair when hovering toolbar", async ({ - reactGrab, - }) => { - await reactGrab.activate(); - await reactGrab.hoverElement("li"); - await reactGrab.waitForSelectionBox(); - - await expect - .poll(() => reactGrab.isCrosshairVisible(), { timeout: 2000 }) - .toBe(true); - - await hoverToolbar(reactGrab.page); - - await expect - .poll(() => reactGrab.isCrosshairVisible(), { timeout: 2000 }) - .toBe(false); - }); }); test.describe("Frozen Mode", () => { diff --git a/packages/react-grab/e2e/touch-mode.spec.ts b/packages/react-grab/e2e/touch-mode.spec.ts index deab8bacf..d7a06b310 100644 --- a/packages/react-grab/e2e/touch-mode.spec.ts +++ b/packages/react-grab/e2e/touch-mode.spec.ts @@ -55,26 +55,6 @@ test.describe("Touch Mode", () => { }); test.describe("Touch Mode Behavior", () => { - test("crosshair should be hidden in touch mode", async ({ reactGrab }) => { - await reactGrab.updateOptions({ - theme: { crosshair: { enabled: true } }, - }); - await reactGrab.activate(); - - const listItem = reactGrab.page.locator("li").first(); - const box = await listItem.boundingBox(); - if (!box) throw new Error("Could not get bounding box"); - - await reactGrab.page.touchscreen.tap( - box.x + box.width / 2, - box.y + box.height / 2, - ); - await reactGrab.page.waitForTimeout(100); - - const isCrosshairVisible = await reactGrab.isCrosshairVisible(); - expect(isCrosshairVisible).toBe(false); - }); - test("touch events should update pointer position", async ({ reactGrab, }) => { diff --git a/packages/react-grab/e2e/visual-feedback.spec.ts b/packages/react-grab/e2e/visual-feedback.spec.ts index 080c26dba..b6f92e68d 100644 --- a/packages/react-grab/e2e/visual-feedback.spec.ts +++ b/packages/react-grab/e2e/visual-feedback.spec.ts @@ -154,49 +154,6 @@ test.describe("Visual Feedback", () => { }); }); - test.describe("Crosshair", () => { - test("crosshair should be visible when active", async ({ reactGrab }) => { - await reactGrab.activate(); - await reactGrab.page.mouse.move(400, 400); - await reactGrab.page.waitForTimeout(100); - - const isVisible = await reactGrab.isCrosshairVisible(); - expect(isVisible).toBe(true); - }); - - test("crosshair should follow cursor", async ({ reactGrab }) => { - await reactGrab.activate(); - - await reactGrab.page.mouse.move(200, 200); - await reactGrab.page.waitForTimeout(100); - const info1 = await reactGrab.getCrosshairInfo(); - - await reactGrab.page.mouse.move(500, 400); - await reactGrab.page.waitForTimeout(100); - const info2 = await reactGrab.getCrosshairInfo(); - - if (info1.position && info2.position) { - expect(info1.position.x).not.toBe(info2.position.x); - expect(info1.position.y).not.toBe(info2.position.y); - } - }); - - test("crosshair should be hidden during drag", async ({ reactGrab }) => { - await reactGrab.activate(); - - const listItem = reactGrab.page.locator("li").first(); - const box = await listItem.boundingBox(); - if (!box) throw new Error("Could not get bounding box"); - - await reactGrab.page.mouse.move(box.x, box.y); - await reactGrab.page.mouse.down(); - await reactGrab.page.mouse.move(box.x + 100, box.y + 100, { steps: 5 }); - - const state = await reactGrab.getState(); - expect(state.isDragging).toBe(true); - - await reactGrab.page.mouse.up(); - }); }); test.describe("Selection Label", () => { diff --git a/packages/react-grab/src/components/overlay-canvas.tsx b/packages/react-grab/src/components/overlay-canvas.tsx index 537fcb8b8..cf7f6f2b4 100644 --- a/packages/react-grab/src/components/overlay-canvas.tsx +++ b/packages/react-grab/src/components/overlay-canvas.tsx @@ -14,7 +14,6 @@ import { FADE_OUT_BUFFER_MS, MIN_DEVICE_PIXEL_RATIO, Z_INDEX_OVERLAY_CANVAS, - OVERLAY_CROSSHAIR_COLOR, OVERLAY_BORDER_COLOR_DRAG, OVERLAY_FILL_COLOR_DRAG, OVERLAY_BORDER_COLOR_DEFAULT, @@ -48,7 +47,7 @@ const LAYER_STYLES = { }, } as const; -type LayerName = "crosshair" | "drag" | "selection" | "grabbed" | "processing"; +type LayerName = "drag" | "selection" | "grabbed" | "processing"; interface OffscreenLayer { canvas: OffscreenCanvas | null; @@ -66,14 +65,7 @@ interface AnimatedBounds { isInitialized: boolean; } -interface Position { - x: number; - y: number; -} - export interface OverlayCanvasProps { - crosshairVisible?: boolean; - selectionVisible?: boolean; selectionBounds?: OverlayBounds; selectionBoundsMultiple?: OverlayBounds[]; @@ -103,15 +95,12 @@ export const OverlayCanvas: Component = (props) => { let animationFrameId: number | null = null; const layers: Record = { - crosshair: { canvas: null, context: null }, drag: { canvas: null, context: null }, selection: { canvas: null, context: null }, grabbed: { canvas: null, context: null }, processing: { canvas: null, context: null }, }; - const crosshairCurrentPosition: Position = { x: 0, y: 0 }; - let selectionAnimations: AnimatedBounds[] = []; let dragAnimation: AnimatedBounds | null = null; let grabbedAnimations: AnimatedBounds[] = []; @@ -251,26 +240,6 @@ export const OverlayCanvas: Component = (props) => { context.globalAlpha = 1; }; - const renderCrosshairLayer = () => { - const layer = layers.crosshair; - if (!layer.context) return; - - const context = layer.context; - context.clearRect(0, 0, canvasWidth, canvasHeight); - - if (!props.crosshairVisible) return; - - context.strokeStyle = OVERLAY_CROSSHAIR_COLOR; - context.lineWidth = 1; - - context.beginPath(); - context.moveTo(crosshairCurrentPosition.x, 0); - context.lineTo(crosshairCurrentPosition.x, canvasHeight); - context.moveTo(0, crosshairCurrentPosition.y); - context.lineTo(canvasWidth, crosshairCurrentPosition.y); - context.stroke(); - }; - const renderDragLayer = () => { const layer = layers.drag; if (!layer.context) return; @@ -354,14 +323,12 @@ export const OverlayCanvas: Component = (props) => { mainContext.clearRect(0, 0, canvasRef.width, canvasRef.height); mainContext.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); - renderCrosshairLayer(); renderDragLayer(); renderSelectionLayer(); renderBoundsLayer("grabbed", grabbedAnimations); renderBoundsLayer("processing", processingAnimations); const layerRenderOrder: LayerName[] = [ - "crosshair", "drag", "selection", "grabbed", @@ -517,15 +484,6 @@ export const OverlayCanvas: Component = (props) => { scheduleAnimationFrame(); }; - createEffect( - on( - () => props.crosshairVisible, - () => { - scheduleAnimationFrame(); - }, - ), - ); - createEffect( on( () => @@ -726,16 +684,6 @@ export const OverlayCanvas: Component = (props) => { initializeCanvas(); scheduleAnimationFrame(); - const handlePointerMove = (event: PointerEvent) => { - if (!event.isPrimary) return; - crosshairCurrentPosition.x = event.clientX; - crosshairCurrentPosition.y = event.clientY; - scheduleAnimationFrame(); - }; - - window.addEventListener("pointermove", handlePointerMove, { - passive: true, - }); window.addEventListener("resize", handleWindowResize); let currentDprMediaQuery: MediaQueryList | null = null; @@ -770,7 +718,6 @@ export const OverlayCanvas: Component = (props) => { setupDprMediaQuery(); onCleanup(() => { - window.removeEventListener("pointermove", handlePointerMove); window.removeEventListener("resize", handleWindowResize); if (currentDprMediaQuery) { currentDprMediaQuery.removeEventListener( diff --git a/packages/react-grab/src/components/renderer.tsx b/packages/react-grab/src/components/renderer.tsx index a78dd4848..73186e441 100644 --- a/packages/react-grab/src/components/renderer.tsx +++ b/packages/react-grab/src/components/renderer.tsx @@ -20,7 +20,6 @@ export const ReactGrabRenderer: Component = (props) => { return ( <> = (props) => { createEffect( on( () => props.selectionLabelShakeCount, - (count) => { - if (count) { - setIsShaking(true); - } - }, + () => setIsShaking(true), { defer: true }, ), ); diff --git a/packages/react-grab/src/components/toolbar/index.tsx b/packages/react-grab/src/components/toolbar/index.tsx index cc1614ec3..686365e11 100644 --- a/packages/react-grab/src/components/toolbar/index.tsx +++ b/packages/react-grab/src/components/toolbar/index.tsx @@ -1608,7 +1608,7 @@ export const Toolbar: Component = (props) => { 0}> {props.historyItemCount} diff --git a/packages/react-grab/src/constants.ts b/packages/react-grab/src/constants.ts index 224665b82..88cbb0282 100644 --- a/packages/react-grab/src/constants.ts +++ b/packages/react-grab/src/constants.ts @@ -46,7 +46,6 @@ export const FADE_OUT_BUFFER_MS = 100; export const MIN_DEVICE_PIXEL_RATIO = 2; const GRAB_PURPLE_RGB = "210, 57, 192"; -export const OVERLAY_CROSSHAIR_COLOR = `rgba(${GRAB_PURPLE_RGB}, 1)`; export const OVERLAY_BORDER_COLOR_DRAG = `rgba(${GRAB_PURPLE_RGB}, 0.4)`; export const OVERLAY_FILL_COLOR_DRAG = `rgba(${GRAB_PURPLE_RGB}, 0.05)`; export const OVERLAY_BORDER_COLOR_DEFAULT = `rgba(${GRAB_PURPLE_RGB}, 0.5)`; diff --git a/packages/react-grab/src/core/index.tsx b/packages/react-grab/src/core/index.tsx index c22b25a01..9628059c5 100644 --- a/packages/react-grab/src/core/index.tsx +++ b/packages/react-grab/src/core/index.tsx @@ -534,19 +534,6 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { const isRendererActive = createMemo(() => isActivated() && !isCopying()); - const crosshairVisible = createMemo( - () => - pluginRegistry.store.theme.enabled && - pluginRegistry.store.theme.crosshair.enabled && - isRendererActive() && - !isDragging() && - !store.isTouchMode && - !isToggleFrozen() && - !isPromptMode() && - !isToolbarSelectHovered() && - store.contextMenuPosition === null, - ); - const grabbedBoxTimeouts = new Map(); const showTemporaryGrabbedBox = ( @@ -1319,7 +1306,6 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { const dragging = isDragging(); const copying = isCopying(); const inputMode = isPromptMode(); - const crosshairState = crosshairVisible(); const target = targetElement(); const drag = dragBounds(); const themeEnabled = pluginRegistry.store.theme.enabled; @@ -1352,7 +1338,6 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { isDragging: dragging, isCopying: copying, isPromptMode: inputMode, - isCrosshairVisible: crosshairState ?? false, isSelectionBoxVisible, isDragBoxVisible, targetElement: target, @@ -1413,15 +1398,6 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { ), ); - createEffect( - on( - () => [crosshairVisible(), store.pointer.x, store.pointer.y] as const, - ([visible, x, y]) => { - pluginRegistry.hooks.onCrosshair(Boolean(visible), { x, y }); - }, - ), - ); - createEffect( on( () => @@ -1465,15 +1441,9 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { createEffect( on( - () => [isActivated(), isCopying(), isPromptMode()] as const, - ([activated, copying, inputMode]) => { - if (copying) { - setCursorOverride("progress"); - } else if (activated && !inputMode) { - setCursorOverride("crosshair"); - } else { - setCursorOverride(null); - } + () => isCopying(), + (copying) => { + setCursorOverride(copying ? "progress" : null); }, ), ); @@ -4019,7 +3989,6 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { store.frozenElements.length > 1 ? undefined : cursorPosition().x } mouseY={cursorPosition().y} - crosshairVisible={crosshairVisible()} isFrozen={ isToggleFrozen() || isActivated() || isToolbarSelectHovered() } @@ -4258,7 +4227,6 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { isDragging: isDragging(), isCopying: isCopying(), isPromptMode: isPromptMode(), - isCrosshairVisible: crosshairVisible() ?? false, isSelectionBoxVisible: selectionVisible() ?? false, isDragBoxVisible: dragVisible() ?? false, targetElement: targetElement(), diff --git a/packages/react-grab/src/core/noop-api.ts b/packages/react-grab/src/core/noop-api.ts index c4eeba10f..0e25c3fe2 100644 --- a/packages/react-grab/src/core/noop-api.ts +++ b/packages/react-grab/src/core/noop-api.ts @@ -7,7 +7,6 @@ export const createNoopApi = (): ReactGrabAPI => { isDragging: false, isCopying: false, isPromptMode: false, - isCrosshairVisible: false, isSelectionBoxVisible: false, isDragBoxVisible: false, targetElement: null, diff --git a/packages/react-grab/src/core/plugin-registry.ts b/packages/react-grab/src/core/plugin-registry.ts index 4f0e9dd15..0060cf2f4 100644 --- a/packages/react-grab/src/core/plugin-registry.ts +++ b/packages/react-grab/src/core/plugin-registry.ts @@ -14,7 +14,6 @@ import type { DragRect, ElementLabelVariant, ElementLabelContext, - CrosshairContext, ActivationMode, ActivationKey, SettableOptions, @@ -333,8 +332,6 @@ const createPluginRegistry = (initialOptions: SettableOptions = {}) => { variant: ElementLabelVariant, context: ElementLabelContext, ) => callHook("onElementLabel", visible, variant, context), - onCrosshair: (visible: boolean, context: CrosshairContext) => - callHook("onCrosshair", visible, context), onContextMenu: (element: Element, position: { x: number; y: number }) => callHook("onContextMenu", element, position), cancelPendingToolbarActions: () => callHook("cancelPendingToolbarActions"), diff --git a/packages/react-grab/src/core/theme.ts b/packages/react-grab/src/core/theme.ts index ac4d33cf9..df1fffc9f 100644 --- a/packages/react-grab/src/core/theme.ts +++ b/packages/react-grab/src/core/theme.ts @@ -15,9 +15,6 @@ export const DEFAULT_THEME: Required = { elementLabel: { enabled: true, }, - crosshair: { - enabled: true, - }, toolbar: { enabled: true, }, @@ -44,9 +41,6 @@ export const deepMergeTheme = ( enabled: partialTheme.elementLabel?.enabled ?? baseTheme.elementLabel.enabled, }, - crosshair: { - enabled: partialTheme.crosshair?.enabled ?? baseTheme.crosshair.enabled, - }, toolbar: { enabled: partialTheme.toolbar?.enabled ?? baseTheme.toolbar.enabled, }, diff --git a/packages/react-grab/src/index.ts b/packages/react-grab/src/index.ts index 03066ad27..401a51ef4 100644 --- a/packages/react-grab/src/index.ts +++ b/packages/react-grab/src/index.ts @@ -22,7 +22,6 @@ export type { DeepPartial, ElementLabelVariant, PromptModeContext, - CrosshairContext, ElementLabelContext, AgentContext, AgentSession, diff --git a/packages/react-grab/src/types.ts b/packages/react-grab/src/types.ts index 88ded68ea..bdb434d01 100644 --- a/packages/react-grab/src/types.ts +++ b/packages/react-grab/src/types.ts @@ -57,16 +57,6 @@ export interface Theme { */ enabled?: boolean; }; - /** - * The crosshair cursor overlay that helps with precise element targeting - */ - crosshair?: { - /** - * Whether to show the crosshair - * @default true - */ - enabled?: boolean; - }; /** * The floating toolbar that allows toggling React Grab activation */ @@ -84,7 +74,6 @@ export interface ReactGrabState { isDragging: boolean; isCopying: boolean; isPromptMode: boolean; - isCrosshairVisible: boolean; isSelectionBoxVisible: boolean; isDragBoxVisible: boolean; targetElement: Element | null; @@ -117,11 +106,6 @@ export interface PromptModeContext { targetElement: Element | null; } -export interface CrosshairContext { - x: number; - y: number; -} - export interface ElementLabelContext { x: number; y: number; @@ -308,7 +292,6 @@ export interface PluginHooks { variant: ElementLabelVariant, context: ElementLabelContext, ) => void; - onCrosshair?: (visible: boolean, context: CrosshairContext) => void; onContextMenu?: ( element: Element, position: { x: number; y: number }, @@ -503,7 +486,6 @@ export interface ReactGrabRendererProps { labelZIndex?: number; mouseX?: number; mouseY?: number; - crosshairVisible?: boolean; isFrozen?: boolean; inputValue?: string; isPromptMode?: boolean; From 876eed6ce320a7509a2fa7e93d7b88a676dfface Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Sun, 15 Mar 2026 02:54:19 -0700 Subject: [PATCH 3/6] vite+ --- packages/react-grab/src/core/index.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/react-grab/src/core/index.tsx b/packages/react-grab/src/core/index.tsx index 9628059c5..341796967 100644 --- a/packages/react-grab/src/core/index.tsx +++ b/packages/react-grab/src/core/index.tsx @@ -1441,9 +1441,15 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { createEffect( on( - () => isCopying(), - (copying) => { - setCursorOverride(copying ? "progress" : null); + () => [isActivated(), isCopying(), isPromptMode()] as const, + ([activated, copying, promptMode]) => { + if (copying) { + setCursorOverride("progress"); + } else if (activated && !promptMode) { + setCursorOverride("crosshair"); + } else { + setCursorOverride(null); + } }, ), ); From 685ac72c6316138ce1c50143aac98f2436e3d671 Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Sun, 15 Mar 2026 03:28:17 -0700 Subject: [PATCH 4/6] fix --- packages/react-grab/e2e/visual-feedback.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-grab/e2e/visual-feedback.spec.ts b/packages/react-grab/e2e/visual-feedback.spec.ts index b6f92e68d..5fadc8f0b 100644 --- a/packages/react-grab/e2e/visual-feedback.spec.ts +++ b/packages/react-grab/e2e/visual-feedback.spec.ts @@ -154,8 +154,6 @@ test.describe("Visual Feedback", () => { }); }); - }); - test.describe("Selection Label", () => { test("label should show tag name", async ({ reactGrab }) => { await reactGrab.activate(); From 0c67391e7e0fa7bf51980e7d7f882c65a8d7fc41 Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Sun, 15 Mar 2026 03:30:41 -0700 Subject: [PATCH 5/6] fix --- packages/react-grab/src/core/index.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/react-grab/src/core/index.tsx b/packages/react-grab/src/core/index.tsx index 341796967..3bc139717 100644 --- a/packages/react-grab/src/core/index.tsx +++ b/packages/react-grab/src/core/index.tsx @@ -1621,9 +1621,14 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { actions.clearLastCopied(); if (!isPromptMode()) return; - if (!isPendingDismiss()) { - actions.setPendingDismiss(true); + if (isPendingDismiss()) { + actions.clearInputText(); + actions.clearReplySessionId(); + deactivateRenderer(); + return; } + + actions.setPendingDismiss(true); setSelectionLabelShakeCount((count) => count + 1); }; From b0917c7febc0282c9ac756d4c4d8ea526e0a2829 Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Sun, 15 Mar 2026 16:40:34 -0700 Subject: [PATCH 6/6] fix --- packages/react-grab/e2e/input-mode.spec.ts | 1 + packages/react-grab/e2e/prompt-mode.spec.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/react-grab/e2e/input-mode.spec.ts b/packages/react-grab/e2e/input-mode.spec.ts index 7dd323526..aaeafdfe5 100644 --- a/packages/react-grab/e2e/input-mode.spec.ts +++ b/packages/react-grab/e2e/input-mode.spec.ts @@ -245,6 +245,7 @@ test.describe("Input Mode", () => { await reactGrab.setupMockAgent(); await reactGrab.enterPromptMode("li:first-child"); + await reactGrab.page.mouse.click(10, 10); await reactGrab.page.mouse.click(10, 10); await expect.poll(() => reactGrab.isPromptModeActive()).toBe(false); diff --git a/packages/react-grab/e2e/prompt-mode.spec.ts b/packages/react-grab/e2e/prompt-mode.spec.ts index f20349fa3..91fdb119b 100644 --- a/packages/react-grab/e2e/prompt-mode.spec.ts +++ b/packages/react-grab/e2e/prompt-mode.spec.ts @@ -261,6 +261,7 @@ test.describe("Prompt Mode", () => { await reactGrab.setupMockAgent(); await reactGrab.enterPromptMode("li:first-child"); + await reactGrab.page.mouse.click(10, 10); await reactGrab.page.mouse.click(10, 10); await expect.poll(() => reactGrab.isPromptModeActive()).toBe(false);