Skip to content

Conversation

@drfarrell
Copy link
Collaborator

@drfarrell drfarrell commented Sep 17, 2025

image

Implement drag selection functionality allowing users to select multiple webframes by clicking and dragging on the canvas, similar to Figma and tldraw.

  • Add DragSelectOverlay component for visual selection rectangle
  • Implement drag selection logic in Canvas component with proper coordinate transformation
  • Add visual feedback with teal outline for selected frames
  • Support multi-select with Shift key modifier
  • Integrate with existing frame selection system for chat context

🤖 Generated with Claude Code

Description

Related Issues

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Release
  • Refactor
  • Other (please describe):

Testing

Screenshots (if applicable)

Additional Notes


Important

Add drag-to-select functionality for multiple frames on the canvas with visual feedback and multi-select support.

  • New Feature:
    • Implement drag-to-select functionality in Canvas component, allowing users to select multiple frames by dragging a selection rectangle.
    • Add DragSelectOverlay component for visual feedback of the selection rectangle.
    • Support multi-select with Shift key and integrate with existing frame selection system.
  • State Management:
    • Add isDragSelecting state to StateManager in state/index.ts to manage drag selection state.
  • Frame Components:
    • Update FrameView and TopBar components to handle drag selection state and provide visual feedback with teal outlines.
    • Add createMouseMoveHandler in helpers.ts to manage frame dragging logic.
  • Utility Functions:
    • Add getFramesInSelection and getSelectedFrameData in selection-utils.ts to calculate frames within the selection rectangle.

This description was created by Ellipsis for f9bf313. You can customize this summary. It will automatically update as commits are pushed.


Summary by CodeRabbit

  • New Features
    • Drag-to-select in Design mode with live selection rectangle, real-time frame highlighting, multi-select with Shift, and selection on mouse release; hover suppressed during drag for stable feedback.
    • Drag-selection state now propagates to frames and their top bars for consistent live highlight visuals.
  • Style
    • Teal outlines and semi-transparent teal drag-fill for highlighted frames and selection rectangle; 4px rounded corners.

Implement drag selection functionality allowing users to select multiple webframes
by clicking and dragging on the canvas, similar to Figma and tldraw.

- Add DragSelectOverlay component for visual selection rectangle
- Implement drag selection logic in Canvas component with proper coordinate transformation
- Add visual feedback with teal outline for selected frames
- Support multi-select with Shift key modifier
- Integrate with existing frame selection system for chat context

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@vercel
Copy link

vercel bot commented Sep 17, 2025

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

Project Deployment Preview Comments Updated (UTC)
web Ready Ready Preview Comment Sep 17, 2025 10:53pm
1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
docs Skipped Skipped Sep 17, 2025 10:53pm

@supabase
Copy link

supabase bot commented Sep 17, 2025

This pull request has been ignored for the connected project wowaemfasoptxrdjhilu because there are no changes detected in apps/backend/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai
Copy link

coderabbitai bot commented Sep 17, 2025

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds drag-to-select in DESIGN mode: new editor state flag, drag lifecycle (start/move/end) with throttled moves, selection rectangle overlay, selection intersection utilities, per-frame drag-highlight props, and centralized multi-frame drag move/snapping helper.

Changes

Cohort / File(s) Summary
Canvas drag-select core
apps/web/client/src/app/project/[id]/_components/canvas/index.tsx, apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx, apps/web/client/src/app/project/[id]/_components/canvas/selection-utils.ts
Adds full drag-selection lifecycle (start/move/end), container↔canvas coordinate mapping, throttled mousemove, DragSelectOverlay, utilities getFramesInSelection/getSelectedFrameData, and tracks framesInSelection: Set<string>.
Frames surface & per-frame props
apps/web/client/src/app/project/[id]/_components/canvas/frames.tsx, apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx, apps/web/client/src/app/project/[id]/_components/canvas/frame/view.tsx
Threads framesInDragSelection → per-frame isInDragSelection; updates component signatures to accept isInDragSelection; applies conditional teal outline/styling when participating in drag-selection.
Top-bar / multi-frame drag helper
apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/helpers.ts, apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx
Introduces createMouseMoveHandler and MouseMoveHandlerOptions to centralize multi-frame move logic, canvas-scale aware deltas, 5px deadzone, snapping via editorEngine.snap, snap-line display/hide, and replaces inline top-bar drag logic; adds click-vs-drag detection.
Gesture suppression & frame gesture guard
apps/web/client/src/app/project/[id]/_components/canvas/frame/gesture.tsx
Adds guard to skip hover/mousemove handling while editorEngine.state.isDragSelecting is true, preventing hover effects during drag-select.
Editor state
apps/web/client/src/components/store/editor/state/index.ts
Adds public boolean isDragSelecting (initialized false) to StateManager for drag-select lifecycle signaling.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Canvas
  participant State as EditorState
  participant Overlay as DragSelectOverlay
  participant Frames
  participant TopBar as TopBar/Helpers
  participant Frame as FrameView

  User->>Canvas: mousedown (DESIGN, LMB)
  Canvas->>State: set isDragSelecting = true
  Canvas->>Canvas: record dragStart/dragEnd (container coords)
  Canvas->>Overlay: render selection rect
  loop dragging (throttled ~16ms)
    User-->>Canvas: mousemove
    Canvas->>Canvas: update dragEnd, compute selection rect → canvas coords
    Canvas->>Frames: compute framesInDragSelection (ids)
    Frames->>Frame: pass isInDragSelection per-frame
    Canvas->>Overlay: update overlay position/size
  end
  User->>Canvas: mouseup
  Canvas->>Canvas: finalize intersecting frames (Shift-aware)
  Canvas->>Frames: apply selection
  Canvas->>State: set isDragSelecting = false
  Canvas->>Overlay: hide overlay
  Note right of TopBar: TopBar drag uses createMouseMoveHandler for direct frame dragging & snapping
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

I nibble teal upon the screen,
I trace a box where frames convene.
No hover stirs while edges grow,
Release the paw — the choices show.
A rabbit hops: the canvas gleams. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "feat: add drag-to-select for multiple webframes" succinctly and accurately summarizes the primary change in this PR—adding drag-to-select to allow selecting multiple webframes on the canvas. It is concise, uses the conventional "feat:" prefix, and clearly communicates the main intent for reviewers scanning history.
Description Check ✅ Passed The PR contains a clear top-level summary, an "IMPORTANT" breakdown of behavior/components/state changes, a screenshot, and correctly marks the Type of Change as a new feature, but the repository template sections 'Description', 'Related Issues', and 'Testing' are left as placeholders or empty so there are no concrete testing steps or linked issue references. The provided information is otherwise detailed and actionable for reviewers, so the description is largely sufficient but not fully compliant with the template.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4087cb6 and f9bf313.

📒 Files selected for processing (3)
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/helpers.ts (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (3 hunks)
  • apps/web/client/src/app/project/[id]/_components/canvas/selection-utils.ts (1 hunks)

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.

Comment on lines 58 to 67
const handleCanvasMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
if (!isDragSelecting || !containerRef.current) {
return;
}

const rect = containerRef.current.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
setDragSelectEnd({ x, y });
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Mouse move handler has no bounds checking and will cause performance issues. The handler executes on every mouse movement even when not drag selecting, and when drag selecting, it performs expensive DOM operations (getBoundingClientRect) on every pixel movement without throttling. This will cause UI lag and poor user experience. Add early return when not drag selecting and consider throttling the coordinate updates.

Suggested change
const handleCanvasMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
if (!isDragSelecting || !containerRef.current) {
return;
}
const rect = containerRef.current.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
setDragSelectEnd({ x, y });
};
const handleCanvasMouseMove = useCallback(
throttle((event: React.MouseEvent<HTMLDivElement>) => {
if (!isDragSelecting || !containerRef.current) {
return;
}
const rect = containerRef.current.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
setDragSelectEnd({ x, y });
}, 16), // ~60fps
[isDragSelecting]
);

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

onMouseDown={handleCanvasMouseDown}
onMouseMove={handleCanvasMouseMove}
onMouseUp={handleCanvasMouseUp}
onMouseLeave={handleCanvasMouseUp}
Copy link
Contributor

Choose a reason for hiding this comment

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

Mouse leave handler will incorrectly terminate drag selection when user drags outside the canvas container but is still dragging. This breaks the expected drag selection behavior where users should be able to drag beyond the visible canvas area. The onMouseLeave should not call handleCanvasMouseUp, or should only do so when the mouse button is not pressed.

Suggested change
onMouseLeave={handleCanvasMouseUp}
onMouseLeave={(e) => {
// Only terminate drag if no mouse button is pressed
if (e.buttons === 0) {
handleCanvasMouseUp(e);
}
}}

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Changed from border-2 (2px) to border (1px) for a more subtle appearance
@vercel vercel bot temporarily deployed to Preview – docs September 17, 2025 05:22 Inactive
Replace Tailwind's blue-500 with Onlook's design system colors from @onlook/ui/tokens
@vercel vercel bot temporarily deployed to Preview – docs September 17, 2025 05:23 Inactive
Comment on lines 39 to 55
if (editorEngine.state.editorMode === EditorMode.DESIGN && event.button === 0) {
const rect = containerRef.current.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;

setIsDragSelecting(true);
setDragSelectStart({ x, y });
setDragSelectEnd({ x, y });

// Clear existing selections if not shift-clicking
if (!event.shiftKey) {
editorEngine.clearUI();
editorEngine.frames.deselectAll();
}
} else {
editorEngine.clearUI();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Logic error: The drag selection logic only triggers when event.button === 0 (left mouse button), but the else clause at line 53-55 calls editorEngine.clearUI() for ALL other mouse buttons including right-click and middle-click. This will incorrectly clear the UI when users right-click to open context menus or middle-click to pan. The else clause should either be removed or should only handle specific cases where clearing UI is actually desired.

Suggested change
if (editorEngine.state.editorMode === EditorMode.DESIGN && event.button === 0) {
const rect = containerRef.current.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
setIsDragSelecting(true);
setDragSelectStart({ x, y });
setDragSelectEnd({ x, y });
// Clear existing selections if not shift-clicking
if (!event.shiftKey) {
editorEngine.clearUI();
editorEngine.frames.deselectAll();
}
} else {
editorEngine.clearUI();
}
if (editorEngine.state.editorMode === EditorMode.DESIGN && event.button === 0) {
const rect = containerRef.current.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
setIsDragSelecting(true);
setDragSelectStart({ x, y });
setDragSelectEnd({ x, y });
// Clear existing selections if not shift-clicking
if (!event.shiftKey) {
editorEngine.clearUI();
editorEngine.frames.deselectAll();
}
}
// Removed the else clause that was incorrectly clearing UI for all non-left clicks

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

- Add throttling to mouse move handler (16ms/~60fps) to prevent performance issues
- Fix mouse leave handler to only terminate drag when no button is pressed
- Fix logic error that was clearing UI on right/middle clicks
- Add proper cleanup for throttled function

Addresses review feedback from Graphite
@vercel vercel bot temporarily deployed to Preview – docs September 17, 2025 05:27 Inactive
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: 1

🧹 Nitpick comments (6)
apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx (1)

64-67: Keep highlight thickness constant across zoom; avoid inline styles.

Currently the outline scales with canvas zoom. Compute width inversely to editorEngine.canvas.scale and prefer classes over inline styles.

-            <div className="relative" style={{
-                outline: isSelected ? '2px solid rgb(94, 234, 212)' : 'none',
-                borderRadius: '4px',
-            }}>
+            <div
+              className={`relative ${isSelected ? 'rounded' : ''}`}
+              style={{
+                outline: isSelected
+                  ? `${Math.max(1, 2 / editorEngine.canvas.scale)}px solid rgb(94, 234, 212)`
+                  : 'none',
+                borderRadius: 4,
+              }}
+            >

Please verify that this still matches the intended design tokens (e.g., if there is a themed ring/outline class to use instead of a raw color).

apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx (3)

1-1: Avoid duplicating client boundaries.

Per our guideline, keep a single client boundary at the feature entry. Since Canvas is already a client component, this file doesn’t need its own "use client".

-'use client';
+

If this component is imported anywhere outside the Canvas client tree, keep "use client" here.


23-31: Ensure overlay renders above frames.

Frames use fixed/transform stacking; add an explicit z-index so the selection rectangle is never hidden.

-        <div
-            className="absolute border-2 border-blue-500 bg-blue-500/10 pointer-events-none"
+        <div
+            className="absolute z-40 border-2 border-blue-500 bg-blue-500/10 pointer-events-none"

13-34: observer is unnecessary here.

The overlay renders from props only and doesn’t dereference observables; drop observer to avoid extra MobX tracking.

-import { observer } from 'mobx-react-lite';
+// no mobx imports needed

-export const DragSelectOverlay = observer(({ startX, startY, endX, endY, isSelecting }: DragSelectOverlayProps) => {
+export const DragSelectOverlay = ({ startX, startY, endX, endY, isSelecting }: DragSelectOverlayProps) => {
   ...
-});
+};

Confirm this component isn’t subscribed to store state via context elsewhere.

apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (2)

58-67: Prevent default during drag to avoid accidental text/image selection.

-    const handleCanvasMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
+    const handleCanvasMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
         if (!isDragSelecting || !containerRef.current) {
             return;
         }
-        
+        event.preventDefault();

If you see selection artifacts in iframes, consider switching to Pointer Events and calling setPointerCapture on pointerdown.


69-117: Robustness: finalize on outside mouseup and ignore tiny drags.

  • Capture mouseup outside the container to avoid stuck state.
  • Treat sub-threshold rectangles as clicks (no selection).
-    const handleCanvasMouseUp = (event: React.MouseEvent<HTMLDivElement>) => {
+    const handleCanvasMouseUp = (event: React.MouseEvent<HTMLDivElement> | MouseEvent) => {
         if (!isDragSelecting) {
             return;
         }
         
         // Calculate which frames are within the selection rectangle
         const selectionRect = {
             left: Math.min(dragSelectStart.x, dragSelectEnd.x),
             top: Math.min(dragSelectStart.y, dragSelectEnd.y),
             right: Math.max(dragSelectStart.x, dragSelectEnd.x),
             bottom: Math.max(dragSelectStart.y, dragSelectEnd.y),
         };
+        const width = selectionRect.right - selectionRect.left;
+        const height = selectionRect.bottom - selectionRect.top;
+        if (width < 3 && height < 3) {
+            setIsDragSelecting(false);
+            return;
+        }

And when starting a drag, attach a one-off document mouseup:

             setDragSelectStart({ x, y });
             setDragSelectEnd({ x, y });
+            const onUp = (e: MouseEvent) => {
+                handleCanvasMouseUp(e);
+                document.removeEventListener('mouseup', onUp);
+            };
+            document.addEventListener('mouseup', onUp);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3932f9e and d20e7c6.

📒 Files selected for processing (3)
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx (2 hunks)
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (3 hunks)
  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
apps/web/client/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/app/**/*.tsx: Default to Server Components; add 'use client' when using events, state/effects, browser APIs, or client‑only libraries
Do not use process.env in client code; import env from @/env instead

Avoid hardcoded user-facing text; use next-intl messages/hooks

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*
Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Use path aliases @/* and ~/* for imports mapping to src/*

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
apps/web/client/src/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable references across renders
Keep the active MobX store in a useRef and perform async cleanup with setTimeout(() => storeRef.current?.clear(), 0) to avoid route-change races
Avoid useMemo for creating MobX store instances
Avoid putting the MobX store instance in effect dependency arrays if it causes loops; split concerns by domain

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable identities across renders
Keep the active MobX store in a useRef and clean up asynchronously with setTimeout(() => storeRef.current?.clear(), 0)
Do not use useMemo to create MobX stores
Avoid placing MobX store instances in effect dependency arrays if it causes loops; split concerns instead
observer components must be client components; place a single client boundary at the feature entry; child observers need not repeat 'use client'

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
apps/web/client/src/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Default to Server Components; add 'use client' only when using events, state/effects, browser APIs, or client-only libs

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using the any type unless absolutely necessary

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
🧠 Learnings (1)
📚 Learning: 2025-09-16T19:22:52.423Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.423Z
Learning: Applies to apps/web/client/src/**/*.tsx : observer components must be client components; place a single client boundary at the feature entry; child observers need not repeat 'use client'

Applied to files:

  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx
🧬 Code graph analysis (2)
apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx (1)
apps/web/client/src/components/store/editor/frames/manager.ts (1)
  • isSelected (77-79)
apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (2)
apps/web/client/src/app/project/[id]/_components/canvas/frames.tsx (1)
  • Frames (8-19)
apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx (1)
  • DragSelectOverlay (13-34)
🔇 Additional comments (4)
apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx (1)

20-20: Selection reactivity looks good.

Using editorEngine.frames.isSelected(frame.id) will re-render under MobX when selected flips.

apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (3)

29-31: State for drag-select looks fine.

Simple, explicit shape for start/end points.


252-255: onMouseLeave → finalize is fine, but keep document-level mouseup as primary.

Using both prevents stuck overlay when the pointer exits the canvas abruptly.


260-266: Pass through; consider matching overlay z-index with other overlays.

Once DragSelectOverlay adds z-40, this usage is good. No prop/state leaks spotted.

Comment on lines 260 to 265
onMouseLeave={(e) => {
// Only terminate drag if no mouse button is pressed
if (e.buttons === 0) {
handleCanvasMouseUp(e);
}
}}
Copy link
Contributor

Choose a reason for hiding this comment

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

The current onMouseLeave handler only terminates drag selection when no mouse button is pressed (e.buttons === 0), but doesn't handle the case where the user drags outside the canvas and then releases the mouse button elsewhere. This could leave the application in a perpetual drag selecting state.

Consider adding a global mouse up event listener when drag selecting starts:

useEffect(() => {
  if (isDragSelecting) {
    const handleGlobalMouseUp = () => {
      setIsDragSelecting(false);
    };
    
    window.addEventListener('mouseup', handleGlobalMouseUp);
    return () => window.removeEventListener('mouseup', handleGlobalMouseUp);
  }
}, [isDragSelecting]);

This would ensure the drag selection state is properly reset regardless of where the mouse button is released.

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

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: 0

♻️ Duplicate comments (1)
apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (1)

34-57: Fix target check: drag-select rarely starts; use currentTarget/canvas-container and preventDefault.

Strict equality with event.target blocks drags when clicking the inner canvas or descendants. Use event.currentTarget/canvas container id and call preventDefault() to avoid text selection. Also guard containerRef.current.

-    const handleCanvasMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
-        if (event.target !== containerRef.current) {
-            return;
-        }
-        
-        // Start drag selection only in design mode and left mouse button
-        if (editorEngine.state.editorMode === EditorMode.DESIGN && event.button === 0) {
-            const rect = containerRef.current.getBoundingClientRect();
-            const x = event.clientX - rect.left;
-            const y = event.clientY - rect.top;
-            
-            setIsDragSelecting(true);
-            setDragSelectStart({ x, y });
-            setDragSelectEnd({ x, y });
-            
-            // Clear existing selections if not shift-clicking
-            if (!event.shiftKey) {
-                editorEngine.clearUI();
-                editorEngine.frames.deselectAll();
-            }
-        } else if (event.button === 0) {
-            // Only clear UI for left clicks that don't start drag selection
-            editorEngine.clearUI();
-        }
-    };
+    const handleCanvasMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
+        const target = event.target as HTMLElement | null;
+        const isBackground =
+            target === event.currentTarget ||
+            target?.id === EditorAttributes.CANVAS_CONTAINER_ID;
+        if (!isBackground) return;
+        if (!containerRef.current) return;
+
+        // Start drag selection only in design mode and left mouse button
+        if (editorEngine.state.editorMode === EditorMode.DESIGN && event.button === 0) {
+            event.preventDefault(); // avoid text selection
+            const rect = containerRef.current.getBoundingClientRect();
+            const x = event.clientX - rect.left;
+            const y = event.clientY - rect.top;
+
+            setIsDragSelecting(true);
+            setDragSelectStart({ x, y });
+            setDragSelectEnd({ x, y });
+
+            // Clear existing selections if not shift-clicking
+            if (!event.shiftKey) {
+                editorEngine.clearUI();
+                editorEngine.frames.deselectAll();
+            }
+        } else if (event.button === 0) {
+            // Only clear UI for left clicks that don't start drag selection
+            editorEngine.clearUI();
+        }
+    };
🧹 Nitpick comments (3)
apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (3)

60-73: Don’t pass React events into throttled handler; throttle plain coords instead.

Safer and cheaper to throttle numeric coords; avoids any gotchas with synthetic events and makes cancellation explicit.

-    const handleCanvasMouseMove = useCallback(
-        throttle((event: React.MouseEvent<HTMLDivElement>) => {
-            if (!isDragSelecting || !containerRef.current) {
-                return;
-            }
-            
-            const rect = containerRef.current.getBoundingClientRect();
-            const x = event.clientX - rect.left;
-            const y = event.clientY - rect.top;
-            setDragSelectEnd({ x, y });
-        }, 16), // ~60fps
-        [isDragSelecting]
-    );
+    const handleCanvasMouseMoveThrottled = useMemo(
+        () =>
+            throttle((clientX: number, clientY: number) => {
+                if (!containerRef.current) return;
+                const rect = containerRef.current.getBoundingClientRect();
+                const x = clientX - rect.left;
+                const y = clientY - rect.top;
+                setDragSelectEnd({ x, y });
+            }, 16),
+        []
+    );
+    const handleCanvasMouseMove = useCallback(
+        (event: React.MouseEvent<HTMLDivElement>) => {
+            if (!isDragSelecting) return;
+            handleCanvasMouseMoveThrottled(event.clientX, event.clientY);
+        },
+        [isDragSelecting, handleCanvasMouseMoveThrottled]
+    );
-                onMouseMove={handleCanvasMouseMove}
+                onMouseMove={handleCanvasMouseMove}
-                handleCanvasMouseMove.cancel?.(); // Clean up throttled function
+                handleCanvasMouseMoveThrottled.cancel?.(); // Clean up throttled function

Also applies to: 247-251, 258-258


6-6: Reduce bundle size: import throttle by path.

Avoid pulling all of lodash on the client.

-import { throttle } from 'lodash';
+import throttle from 'lodash/throttle';
+// or: import throttle from 'lodash-es/throttle';

34-73: Allow dragging outside the canvas: use Pointer Events with capture.

Dragging stops updating once the cursor leaves the container. Switch to onPointerDown/Move/Up and call setPointerCapture on pointerdown to keep receiving events beyond bounds; release on pointerup.

- onMouseDown={handleCanvasMouseDown}
- onMouseMove={handleCanvasMouseMove}
- onMouseUp={handleCanvasMouseUp}
+ onPointerDown={handleCanvasPointerDown}
+ onPointerMove={handleCanvasPointerMove}
+ onPointerUp={handleCanvasPointerUp}
// New handlers (outline)
const handleCanvasPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
  // ...same background checks...
  if (editorEngine.state.editorMode === EditorMode.DESIGN && e.button === 0) {
    (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
    e.preventDefault();
    // compute start/end...
  }
};
const handleCanvasPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
  if (!isDragSelecting) return;
  // update end using e.clientX/Y (use throttled coords as above)
};
const handleCanvasPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
  if (!isDragSelecting) return;
  (e.currentTarget as HTMLElement).releasePointerCapture?.(e.pointerId);
  // finalize selection...
};

Also applies to: 252-266

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d20e7c6 and 0497fed.

📒 Files selected for processing (2)
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (3 hunks)
  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx
🧰 Additional context used
📓 Path-based instructions (6)
apps/web/client/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/app/**/*.tsx: Default to Server Components; add 'use client' when using events, state/effects, browser APIs, or client‑only libraries
Do not use process.env in client code; import env from @/env instead

Avoid hardcoded user-facing text; use next-intl messages/hooks

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*
Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Use path aliases @/* and ~/* for imports mapping to src/*

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
apps/web/client/src/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable references across renders
Keep the active MobX store in a useRef and perform async cleanup with setTimeout(() => storeRef.current?.clear(), 0) to avoid route-change races
Avoid useMemo for creating MobX store instances
Avoid putting the MobX store instance in effect dependency arrays if it causes loops; split concerns by domain

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable identities across renders
Keep the active MobX store in a useRef and clean up asynchronously with setTimeout(() => storeRef.current?.clear(), 0)
Do not use useMemo to create MobX stores
Avoid placing MobX store instances in effect dependency arrays if it causes loops; split concerns instead
observer components must be client components; place a single client boundary at the feature entry; child observers need not repeat 'use client'

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
apps/web/client/src/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Default to Server Components; add 'use client' only when using events, state/effects, browser APIs, or client-only libs

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using the any type unless absolutely necessary

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
🧬 Code graph analysis (1)
apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (3)
apps/web/client/src/app/project/[id]/_components/canvas/hotkeys/index.tsx (1)
  • HotkeysArea (8-119)
apps/web/client/src/app/project/[id]/_components/canvas/frames.tsx (1)
  • Frames (8-19)
apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx (1)
  • DragSelectOverlay (14-37)
🔇 Additional comments (2)
apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (2)

74-121: Confirm: intersect vs contain semantics for multi-select.

Current logic selects frames that intersect the rect. If you intended “containment only,” swap the predicate as below.

-            // Check if frame intersects with selection rectangle
-            return !(
-                frameLeft > canvasSelectionRect.right ||
-                frameRight < canvasSelectionRect.left ||
-                frameTop > canvasSelectionRect.bottom ||
-                frameBottom < canvasSelectionRect.top
-            );
+            // Check if frame is fully contained within selection rectangle
+            return (
+                frameLeft   >= canvasSelectionRect.left  &&
+                frameRight  <= canvasSelectionRect.right &&
+                frameTop    >= canvasSelectionRect.top   &&
+                frameBottom <= canvasSelectionRect.bottom
+            );

252-266: LGTM: onMouseLeave no longer aborts active drags.

Good guard with e.buttons === 0 to prevent premature termination.

- Add real-time teal outline feedback for frames during drag selection
- Prevent element hover outlines from showing while drag selecting
- Add isDragSelecting state to prevent hover events during drag
- Show frames with semi-transparent teal outline as they enter selection area
- Frames now show visual feedback immediately during drag, not just after
- Use Onlook's teal color tokens for consistent styling
- Selected frames show teal-400 outline and text (unchanged)
- Frames in drag selection show teal-500 outline and text (one tier darker)
- Remove opacity change, use solid colors for clarity
- Pass isInDragSelection prop through to TopBar for text color changes
- Pass isInDragSelection prop through FrameView to FrameComponent
- Add teal-500 outline to iframe when frame is in drag selection
- Ensures the gray outer border changes to teal when dragged over
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: 0

♻️ Duplicate comments (4)
apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (4)

108-123: Mouse‑move handler: throttling and early return look good.

Runs at ~60fps only during drag; avoids unnecessary work otherwise. Nice.


311-318: Good fix: don’t end drag on mouseleave while pressed.

This matches expected canvas behavior when dragging outside bounds.


35-63: Drag‑select rarely starts: broaden target check and prevent default.

Strict event.target !== containerRef.current blocks most clicks (children/overlays). Also prevent default to avoid text selection. This was flagged earlier and still applies.

-const handleCanvasMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
-  if (event.target !== containerRef.current) {
-      return;
-  }
+const handleCanvasMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
+  const target = event.target as HTMLElement | null;
+  const isBackground =
+    target === event.currentTarget ||
+    (target instanceof HTMLElement && target.id === EditorAttributes.CANVAS_CONTAINER_ID);
+  if (!isBackground) return;
 
   // Start drag selection only in design mode and left mouse button
   if (editorEngine.state.editorMode === EditorMode.DESIGN && event.button === 0) {
+      event.preventDefault();
       const rect = containerRef.current.getBoundingClientRect();
       const x = event.clientX - rect.left;
       const y = event.clientY - rect.top;

125-175: End‑of‑drag can get stuck if mouseup happens outside the canvas. Add a global mouseup fallback.

Releasing the mouse outside the container leaves isDragSelecting and the overlay active.

Apply this diff near the existing effect setup to register a temporary global listener while dragging:

 useEffect(() => {
   const div = containerRef.current;
   if (div) {
     div.addEventListener('wheel', handleWheel, { passive: false });
     div.addEventListener('mousedown', middleMouseButtonDown);
     div.addEventListener('mouseup', middleMouseButtonUp);
     return () => {
       div.removeEventListener('wheel', handleWheel);
       div.removeEventListener('mousedown', middleMouseButtonDown);
       div.removeEventListener('mouseup', middleMouseButtonUp);
       handleCanvasMouseMove.cancel?.(); // Clean up throttled function
     };
   }
-}, [handleWheel, middleMouseButtonDown, middleMouseButtonUp, handleCanvasMouseMove]);
+}, [handleWheel, middleMouseButtonDown, middleMouseButtonUp, handleCanvasMouseMove]);
+
+useEffect(() => {
+  if (!isDragSelecting) return;
+  const onGlobalMouseUp = () => {
+    setIsDragSelecting(false);
+    setFramesInSelection(new Set());
+    editorEngine.state.isDragSelecting = false;
+  };
+  window.addEventListener('mouseup', onGlobalMouseUp);
+  return () => window.removeEventListener('mouseup', onGlobalMouseUp);
+}, [isDragSelecting, editorEngine.state]);
🧹 Nitpick comments (4)
apps/web/client/src/components/store/editor/state/index.ts (2)

18-18: Add reset of drag‑select flag on store clear.

isDragSelecting is global UI state; not clearing it can leave hover suppression and visuals stuck after resets/navigation.

Apply this diff to clear it:

   clear() {
     this.hotkeysOpen = false;
     this.publishOpen = false;
     this.branchTab = null;
     this.manageBranchId = null;
     this.resetCanvasScrollingDebounced.cancel();
+    this.isDragSelecting = false;
   }

38-40: Consider hiding overlay while drag‑selecting.

If overlays should be hidden during drag selection (similar to panning/scrolling), fold isDragSelecting into shouldHideOverlay. If not desired, ignore.

-  return this._canvasScrolling || this.canvasPanning
+  return this._canvasScrolling || this.canvasPanning || this.isDragSelecting
apps/web/client/src/app/project/[id]/_components/canvas/frame/view.tsx (1)

303-307: Avoid double outlines (wrapper + iframe) during drag‑select.

FrameView wrapper already draws a teal outline; the iframe adds another when isInDragSelection, causing a thicker/doubled border. Drop the iframe’s drag‑select outline and keep wrapper as the single source of truth.

 className={cn(
   'backdrop-blur-sm transition outline outline-4',
   isActiveBranch && 'outline-teal-400',
   isActiveBranch && !isSelected && 'outline-dashed',
-  !isActiveBranch && isInDragSelection && 'outline-teal-500',
 )}
apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (1)

6-6: Import throttle from the path module to reduce bundle size.

Avoid lodash barrel import in client bundles.

-import { throttle } from 'lodash';
+import throttle from 'lodash/throttle';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0497fed and a41bcab.

📒 Files selected for processing (7)
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/gesture.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx (3 hunks)
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx (2 hunks)
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/view.tsx (2 hunks)
  • apps/web/client/src/app/project/[id]/_components/canvas/frames.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (3 hunks)
  • apps/web/client/src/components/store/editor/state/index.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
apps/web/client/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/app/**/*.tsx: Default to Server Components; add 'use client' when using events, state/effects, browser APIs, or client‑only libraries
Do not use process.env in client code; import env from @/env instead

Avoid hardcoded user-facing text; use next-intl messages/hooks

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/frame/gesture.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/view.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frames.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*
Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Use path aliases @/* and ~/* for imports mapping to src/*

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/frame/gesture.tsx
  • apps/web/client/src/components/store/editor/state/index.ts
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/view.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frames.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx
apps/web/client/src/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable references across renders
Keep the active MobX store in a useRef and perform async cleanup with setTimeout(() => storeRef.current?.clear(), 0) to avoid route-change races
Avoid useMemo for creating MobX store instances
Avoid putting the MobX store instance in effect dependency arrays if it causes loops; split concerns by domain

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable identities across renders
Keep the active MobX store in a useRef and clean up asynchronously with setTimeout(() => storeRef.current?.clear(), 0)
Do not use useMemo to create MobX stores
Avoid placing MobX store instances in effect dependency arrays if it causes loops; split concerns instead
observer components must be client components; place a single client boundary at the feature entry; child observers need not repeat 'use client'

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/frame/gesture.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/view.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frames.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/frame/gesture.tsx
  • apps/web/client/src/components/store/editor/state/index.ts
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/view.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frames.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx
apps/web/client/src/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Default to Server Components; add 'use client' only when using events, state/effects, browser APIs, or client-only libs

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/frame/gesture.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/view.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frames.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using the any type unless absolutely necessary

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/frame/gesture.tsx
  • apps/web/client/src/components/store/editor/state/index.ts
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/view.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frames.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx
🧬 Code graph analysis (4)
apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx (2)
packages/models/src/project/frame.ts (1)
  • Frame (4-16)
apps/web/client/src/components/store/editor/frames/manager.ts (1)
  • isSelected (77-79)
apps/web/client/src/app/project/[id]/_components/canvas/frames.tsx (3)
apps/web/client/src/components/store/editor/index.tsx (1)
  • useEditorEngine (10-14)
apps/web/client/src/components/store/editor/frames/manager.ts (1)
  • FrameData (13-17)
apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx (1)
  • FrameView (15-86)
apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (3)
apps/web/client/src/app/project/[id]/_components/canvas/hotkeys/index.tsx (1)
  • HotkeysArea (8-119)
apps/web/client/src/app/project/[id]/_components/canvas/frames.tsx (1)
  • Frames (8-23)
apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx (1)
  • DragSelectOverlay (14-37)
apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx (6)
packages/models/src/project/frame.ts (1)
  • Frame (4-16)
apps/web/client/src/components/store/editor/index.tsx (1)
  • useEditorEngine (10-14)
apps/web/client/src/app/project/[id]/_components/canvas/frame/view.tsx (2)
  • IFrameView (24-29)
  • FrameComponent (60-317)
apps/web/client/src/components/store/editor/frames/manager.ts (1)
  • isSelected (77-79)
apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx (1)
  • TopBar (13-226)
packages/ui/tokens.ts (1)
  • colors (1-112)
🔇 Additional comments (4)
apps/web/client/src/app/project/[id]/_components/canvas/frame/gesture.tsx (1)

96-99: Good guard to suppress hover during drag‑select.

Early‑returning when state.isDragSelecting avoids noisy hover and unnecessary IPC to frames. LGTM.

apps/web/client/src/app/project/[id]/_components/canvas/frames.tsx (1)

8-19: Prop wiring looks correct.

Passing isInDragSelection={framesInDragSelection.has(id)} is straightforward and keeps FrameView decoupled from selection set internals.

apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx (1)

132-137: Consistent highlight for drag‑selected but unselected frames.

Using teal styling when !isSelected && isInDragSelection aligns with the iframe/frame wrapper visuals.

apps/web/client/src/app/project/[id]/_components/canvas/frame/index.tsx (1)

65-71: Single place to render drag‑select outline is good; ensure no conflicting inner outlines.

Wrapper outline uses design tokens and looks good. After removing the iframe’s drag‑select outline (see view.tsx suggestion), visuals will be consistent and avoid double borders.

@vercel vercel bot temporarily deployed to Preview – docs September 17, 2025 17:20 Inactive
- Add window-level mouseup event listener when drag selecting starts
- Ensures drag selection properly terminates even when mouse released outside canvas
- Prevents application from getting stuck in drag selecting state
- Addresses Graphite review feedback about edge case handling
@vercel vercel bot temporarily deployed to Preview – docs September 17, 2025 17:22 Inactive
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: 0

♻️ Duplicate comments (1)
apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (1)

35-63: Fix drag-start target check; allow clicks on canvas descendants and prevent text selection

Strict event.target !== containerRef.current blocks drag-start when clicking the inner canvas or descendants. Use a background containment check and call preventDefault() when starting a drag. This was flagged earlier and remains unfixed.

-    if (event.target !== containerRef.current) {
-        return;
-    }
+    const target = event.target as HTMLElement | null;
+    const isBackground =
+        target === event.currentTarget ||
+        (target instanceof HTMLElement && target.id === EditorAttributes.CANVAS_CONTAINER_ID);
+    if (!isBackground) return;

-    // Start drag selection only in design mode and left mouse button
-    if (editorEngine.state.editorMode === EditorMode.DESIGN && event.button === 0) {
+    // Start drag selection only in design mode and left mouse button
+    if (editorEngine.state.editorMode === EditorMode.DESIGN && event.button === 0) {
+        event.preventDefault(); // avoid native text selection during drag
         const rect = containerRef.current.getBoundingClientRect();
         const x = event.clientX - rect.left;
         const y = event.clientY - rect.top;
🧹 Nitpick comments (5)
apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (5)

6-6: Import throttle in a tree‑shakable way to avoid pulling all of lodash

Switch to the per‑method import to reduce bundle size.

-import { throttle } from 'lodash';
+import throttle from 'lodash/throttle';

27-33: Cache container rect during drag to cut repeated layouts; clear on drag end

getBoundingClientRect() on every mousemove is costly. Cache it at drag start and reuse during the drag; reset on mouseup. Keeps ~60fps smoother on large canvases.

@@
-    const containerRef = useRef<HTMLDivElement>(null);
+    const containerRef = useRef<HTMLDivElement>(null);
+    const containerRectRef = useRef<DOMRect | null>(null);
@@
-        if (editorEngine.state.editorMode === EditorMode.DESIGN && event.button === 0) {
-            const rect = containerRef.current.getBoundingClientRect();
+        if (editorEngine.state.editorMode === EditorMode.DESIGN && event.button === 0) {
+            const rect = (containerRectRef.current = containerRef.current!.getBoundingClientRect());
             const x = event.clientX - rect.left;
             const y = event.clientY - rect.top;
@@
-            const rect = containerRef.current.getBoundingClientRect();
+            const rect = containerRectRef.current ?? containerRef.current.getBoundingClientRect();
             const x = event.clientX - rect.left;
             const y = event.clientY - rect.top;
@@
-            const handleGlobalMouseUp = (event: MouseEvent) => {
+            const handleGlobalMouseUp = (event: MouseEvent) => {
                 // Calculate which frames are within the selection rectangle
@@
                 setIsDragSelecting(false);
                 setFramesInSelection(new Set());
                 editorEngine.state.isDragSelecting = false;
+                containerRectRef.current = null;
             };

Also applies to: 35-63, 108-123, 258-313


258-313: Add a small movement threshold to avoid accidental selections on click

Prevents zero/1‑px drags from triggering selection scans.

-            const handleGlobalMouseUp = (event: MouseEvent) => {
+            const handleGlobalMouseUp = (event: MouseEvent) => {
+                const dx = Math.abs(dragSelectStart.x - dragSelectEnd.x);
+                const dy = Math.abs(dragSelectStart.y - dragSelectEnd.y);
+                const MIN_DRAG = 3; // px
+                if (dx < MIN_DRAG && dy < MIN_DRAG) {
+                    setIsDragSelecting(false);
+                    setFramesInSelection(new Set());
+                    editorEngine.state.isDragSelecting = false;
+                    return;
+                }
                 // Calculate which frames are within the selection rectangle

65-106: DRY up intersection logic (compute once; reuse for live preview and final select)

The AABB intersection is duplicated. Extract a helper to reduce bugs and keep behavior consistent.

@@
-    const updateFramesInSelection = useCallback((start: { x: number; y: number }, end: { x: number; y: number }) => {
+    const getIntersectingFrameIds = useCallback((canvasSelectionRect: {
+        left: number; top: number; right: number; bottom: number;
+    }) => {
+        const ids = new Set<string>();
+        editorEngine.frames.getAll().forEach(fd => {
+            const f = fd.frame;
+            const frameRect = {
+                left: f.position.x,
+                top:  f.position.y,
+                right: f.position.x + f.dimension.width,
+                bottom: f.position.y + f.dimension.height,
+            };
+            const intersects = !(
+                frameRect.left > canvasSelectionRect.right ||
+                frameRect.right < canvasSelectionRect.left ||
+                frameRect.top > canvasSelectionRect.bottom ||
+                frameRect.bottom < canvasSelectionRect.top
+            );
+            if (intersects) ids.add(f.id);
+        });
+        return ids;
+    }, [editorEngine.frames]);
+
+    const updateFramesInSelection = useCallback((start: { x: number; y: number }, end: { x: number; y: number }) => {
         const selectionRect = {
             left: Math.min(start.x, end.x),
             top: Math.min(start.y, end.y),
             right: Math.max(start.x, end.x),
             bottom: Math.max(start.y, end.y),
         };
         
         // Convert selection rect to canvas coordinates
-        const canvasSelectionRect = {
+        const canvasSelectionRect = {
             left: (selectionRect.left - position.x) / scale,
             top: (selectionRect.top - position.y) / scale,
             right: (selectionRect.right - position.x) / scale,
             bottom: (selectionRect.bottom - position.y) / scale,
         };
-        // Find all frames that intersect with the selection rectangle
-        const allFrames = editorEngine.frames.getAll();
-        const intersectingFrameIds = new Set<string>();
-        
-        allFrames.forEach(frameData => {
-            const frame = frameData.frame;
-            const frameLeft = frame.position.x;
-            const frameTop = frame.position.y;
-            const frameRight = frame.position.x + frame.dimension.width;
-            const frameBottom = frame.position.y + frame.dimension.height;
-            
-            // Check if frame intersects with selection rectangle
-            const intersects = !(
-                frameLeft > canvasSelectionRect.right ||
-                frameRight < canvasSelectionRect.left ||
-                frameTop > canvasSelectionRect.bottom ||
-                frameBottom < canvasSelectionRect.top
-            );
-            
-            if (intersects) {
-                intersectingFrameIds.add(frame.id);
-            }
-        });
-        
-        setFramesInSelection(intersectingFrameIds);
+        setFramesInSelection(getIntersectingFrameIds(canvasSelectionRect));
-    }, [position, scale, editorEngine.frames]);
+    }, [position, scale, getIntersectingFrameIds]);
@@
-                // Find all frames that intersect with the selection rectangle
-                const allFrames = editorEngine.frames.getAll();
-                const selectedFrames = allFrames.filter(frameData => {
-                    const frame = frameData.frame;
-                    const frameLeft = frame.position.x;
-                    const frameTop = frame.position.y;
-                    const frameRight = frame.position.x + frame.dimension.width;
-                    const frameBottom = frame.position.y + frame.dimension.height;
-                    
-                    // Check if frame intersects with selection rectangle
-                    return !(
-                        frameLeft > canvasSelectionRect.right ||
-                        frameRight < canvasSelectionRect.left ||
-                        frameTop > canvasSelectionRect.bottom ||
-                        frameBottom < canvasSelectionRect.top
-                    );
-                });
+                const ids = getIntersectingFrameIds(canvasSelectionRect);
+                const selectedFrames = editorEngine.frames.getAll()
+                  .filter(fd => ids.has(fd.frame.id));

Also applies to: 270-294


125-128: Remove no‑op mouseup handler and redundant mouseleave block

Mouseup is handled globally; these are now dead code and can confuse future maintenance.

-    const handleCanvasMouseUp = (event: React.MouseEvent<HTMLDivElement>) => {
-        // Mouse up is now handled by the global listener in useEffect
-        // This function is kept for consistency but the logic is in the global handler
-    };
@@
-                onMouseUp={handleCanvasMouseUp}
-                onMouseLeave={(e) => {
-                    // Only terminate drag if no mouse button is pressed
-                    if (e.buttons === 0) {
-                        handleCanvasMouseUp(e);
-                    }
-                }}
+                // onMouseUp handled globally via window 'mouseup'
+                // onMouseLeave not needed with global handler

Also applies to: 321-327

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a41bcab and 990fffa.

📒 Files selected for processing (2)
  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (3 hunks)
  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx
🧰 Additional context used
📓 Path-based instructions (6)
apps/web/client/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/app/**/*.tsx: Default to Server Components; add 'use client' when using events, state/effects, browser APIs, or client‑only libraries
Do not use process.env in client code; import env from @/env instead

Avoid hardcoded user-facing text; use next-intl messages/hooks

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*
Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Use path aliases @/* and ~/* for imports mapping to src/*

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
apps/web/client/src/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable references across renders
Keep the active MobX store in a useRef and perform async cleanup with setTimeout(() => storeRef.current?.clear(), 0) to avoid route-change races
Avoid useMemo for creating MobX store instances
Avoid putting the MobX store instance in effect dependency arrays if it causes loops; split concerns by domain

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable identities across renders
Keep the active MobX store in a useRef and clean up asynchronously with setTimeout(() => storeRef.current?.clear(), 0)
Do not use useMemo to create MobX stores
Avoid placing MobX store instances in effect dependency arrays if it causes loops; split concerns instead
observer components must be client components; place a single client boundary at the feature entry; child observers need not repeat 'use client'

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
apps/web/client/src/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Default to Server Components; add 'use client' only when using events, state/effects, browser APIs, or client-only libs

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using the any type unless absolutely necessary

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
🧬 Code graph analysis (1)
apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (2)
apps/web/client/src/app/project/[id]/_components/canvas/frames.tsx (1)
  • Frames (8-23)
apps/web/client/src/app/project/[id]/_components/canvas/overlay/drag-select.tsx (1)
  • DragSelectOverlay (14-37)
🔇 Additional comments (1)
apps/web/client/src/app/project/[id]/_components/canvas/index.tsx (1)

258-313: Global mouseup handler is a solid addition

Correctly resolves the “mouseup outside canvas” edge case. Nice.

@drfarrell
Copy link
Collaborator Author

Adds the ability to click and drag to select multiple frames. What is missing is the ability to add a webview's branch as context to the chat.

I tried building that capability (where when you select a webview topbar it adds that branch into the chat context) in #2840 but the AI was giving me a really hard time so I scrapped it.

@Kitenite up to you if you think this is the right PR to add this kind of functionality, but at the very least this "drag" rectangle behavior could be the start of allowing people to arrange the frames on a canvas or add multiple frames in-context at the same time.

  1. Drag to select frames functionality (this PR)
  2. Add selected frames as context into the AI
  3. Allow dragging of multiple frames – at the same time (so if you have multiple frames selected and you click to drag them, you don't select the elements within them, but rather drag the "group" of the frames together.

@drfarrell drfarrell requested a review from Kitenite September 17, 2025 17:32
@vercel vercel bot temporarily deployed to Preview – docs September 17, 2025 21:33 Inactive
Comment on lines 261 to 311
const handleGlobalMouseUp = (event: MouseEvent) => {
// Calculate which frames are within the selection rectangle
const selectionRect = {
left: Math.min(dragSelectStart.x, dragSelectEnd.x),
top: Math.min(dragSelectStart.y, dragSelectEnd.y),
right: Math.max(dragSelectStart.x, dragSelectEnd.x),
bottom: Math.max(dragSelectStart.y, dragSelectEnd.y),
};

// Convert selection rect to canvas coordinates
const canvasSelectionRect = {
left: (selectionRect.left - position.x) / scale,
top: (selectionRect.top - position.y) / scale,
right: (selectionRect.right - position.x) / scale,
bottom: (selectionRect.bottom - position.y) / scale,
};

// Find all frames that intersect with the selection rectangle
const allFrames = editorEngine.frames.getAll();
const selectedFrames = allFrames.filter(frameData => {
const frame = frameData.frame;
const frameLeft = frame.position.x;
const frameTop = frame.position.y;
const frameRight = frame.position.x + frame.dimension.width;
const frameBottom = frame.position.y + frame.dimension.height;

// Check if frame intersects with selection rectangle
return !(
frameLeft > canvasSelectionRect.right ||
frameRight < canvasSelectionRect.left ||
frameTop > canvasSelectionRect.bottom ||
frameBottom < canvasSelectionRect.top
);
});

// Select the frames if any were found in the selection
if (selectedFrames.length > 0) {
editorEngine.frames.select(
selectedFrames.map(fd => fd.frame),
event.shiftKey // multiselect if shift is held
);
}

setIsDragSelecting(false);
setFramesInSelection(new Set());
editorEngine.state.isDragSelecting = false;
};

window.addEventListener('mouseup', handleGlobalMouseUp);
return () => window.removeEventListener('mouseup', handleGlobalMouseUp);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Logic consistency error: The frame intersection calculation is duplicated in both updateFramesInSelection (lines 65-106) and the global mouseup handler (lines 261-311). These two implementations could diverge over time, leading to inconsistent selection behavior where the visual feedback during drag doesn't match the final selection result.

Suggested change
const handleGlobalMouseUp = (event: MouseEvent) => {
// Calculate which frames are within the selection rectangle
const selectionRect = {
left: Math.min(dragSelectStart.x, dragSelectEnd.x),
top: Math.min(dragSelectStart.y, dragSelectEnd.y),
right: Math.max(dragSelectStart.x, dragSelectEnd.x),
bottom: Math.max(dragSelectStart.y, dragSelectEnd.y),
};
// Convert selection rect to canvas coordinates
const canvasSelectionRect = {
left: (selectionRect.left - position.x) / scale,
top: (selectionRect.top - position.y) / scale,
right: (selectionRect.right - position.x) / scale,
bottom: (selectionRect.bottom - position.y) / scale,
};
// Find all frames that intersect with the selection rectangle
const allFrames = editorEngine.frames.getAll();
const selectedFrames = allFrames.filter(frameData => {
const frame = frameData.frame;
const frameLeft = frame.position.x;
const frameTop = frame.position.y;
const frameRight = frame.position.x + frame.dimension.width;
const frameBottom = frame.position.y + frame.dimension.height;
// Check if frame intersects with selection rectangle
return !(
frameLeft > canvasSelectionRect.right ||
frameRight < canvasSelectionRect.left ||
frameTop > canvasSelectionRect.bottom ||
frameBottom < canvasSelectionRect.top
);
});
// Select the frames if any were found in the selection
if (selectedFrames.length > 0) {
editorEngine.frames.select(
selectedFrames.map(fd => fd.frame),
event.shiftKey // multiselect if shift is held
);
}
setIsDragSelecting(false);
setFramesInSelection(new Set());
editorEngine.state.isDragSelecting = false;
};
window.addEventListener('mouseup', handleGlobalMouseUp);
return () => window.removeEventListener('mouseup', handleGlobalMouseUp);
}
const handleGlobalMouseUp = (event: MouseEvent) => {
// Calculate which frames are within the selection rectangle
const selectionRect = {
left: Math.min(dragSelectStart.x, dragSelectEnd.x),
top: Math.min(dragSelectStart.y, dragSelectEnd.y),
right: Math.max(dragSelectStart.x, dragSelectEnd.x),
bottom: Math.max(dragSelectStart.y, dragSelectEnd.y),
};
// Convert selection rect to canvas coordinates
const canvasSelectionRect = {
left: (selectionRect.left - position.x) / scale,
top: (selectionRect.top - position.y) / scale,
right: (selectionRect.right - position.x) / scale,
bottom: (selectionRect.bottom - position.y) / scale,
};
// Use the same function that updateFramesInSelection uses to find intersecting frames
const selectedFrames = getFramesInSelectionRectangle(canvasSelectionRect);
// Select the frames if any were found in the selection
if (selectedFrames.length > 0) {
editorEngine.frames.select(
selectedFrames.map(fd => fd.frame),
event.shiftKey // multiselect if shift is held
);
}
setIsDragSelecting(false);
setFramesInSelection(new Set());
editorEngine.state.isDragSelecting = false;
};
window.addEventListener('mouseup', handleGlobalMouseUp);
return () => window.removeEventListener('mouseup', handleGlobalMouseUp);
}

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

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

Caution

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

⚠️ Outside diff range comments (2)
apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx (2)

1-1: Missing 'use client' — this component uses hooks and events

This file is in app/ and uses hooks; it must be a client component. Without this, Next.js will error at build/runtime.

+'use client';
 import { useEditorEngine } from '@/components/store/editor';

193-199: Security: add rel when using target="_blank"

Prevent window.opener hijacking by adding rel="noopener noreferrer".

-                        target="_blank"
+                        target="_blank"
+                        rel="noopener noreferrer"
🧹 Nitpick comments (5)
apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/helpers.ts (3)

61-74: Avoid persisting to storage on every mousemove

updateAndSaveToStorage inside a per‑move loop can thrash storage/history and hurt perf when multi‑selecting. Prefer in‑memory updates during drag and a single save on mouseup, or throttle/RAF‑batch writes.

Is updateAndSaveToStorage internally debounced? If not, I can propose a RAF‑batched approach that commits once in endMove.


10-13: Type import for React event typings

Explicitly import the React types to make React.MouseEvent resilient to TS config changes.

+import type React from 'react';
 export function createMouseMoveHandler(
     startEvent: React.MouseEvent<HTMLDivElement, MouseEvent>,
     options: MouseMoveHandlerOptions
 ) {

85-87: Prefer Pointer Events + pointer capture for robust drag (touch, pen, iframe edges)

Mouse events miss touch/pen and can lose moves over iframes. Switching to pointerdown/move/up with e.currentTarget.setPointerCapture(e.pointerId) improves reliability across devices.

I can draft a pointer‑events version if desired.

apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx (2)

145-176: Internationalize user‑facing strings per guidelines

Hardcoded labels (“Go back”, “Go forward”, “Refresh Page”, “Preview in new tab”) should use next‑intl.

+import { useTranslations } from 'next-intl';
 …
 export const TopBar = observer(
     ({ frame, isInDragSelection = false }: { frame: Frame; isInDragSelection?: boolean }) => {
+        const t = useTranslations('TopBar');-                    <HoverOnlyTooltip content="Go back" side="top" className="mb-1" hideArrow>
+                    <HoverOnlyTooltip content={t('goBack')} side="top" className="mb-1" hideArrow>-                    <HoverOnlyTooltip content="Go forward" side="top" className="mb-1" hideArrow>
+                    <HoverOnlyTooltip content={t('goForward')} side="top" className="mb-1" hideArrow>-                    <HoverOnlyTooltip content="Refresh Page" side="top" className="mb-1" hideArrow>
+                    <HoverOnlyTooltip content={t('refreshPage')} side="top" className="mb-1" hideArrow>-                <HoverOnlyTooltip content="Preview in new tab" side="top" hideArrow className="mb-1">
+                <HoverOnlyTooltip content={t('previewInNewTab')} side="top" hideArrow className="mb-1">

Also applies to: 192-193


75-78: Minor: define clearElements before usage to avoid TDZ gotchas

It’s safe due to closure timing, but defining clearElements above handleMouseDown improves readability and avoids TDZ confusion.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 041dac4 and 4087cb6.

📒 Files selected for processing (2)
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/helpers.ts (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx (4 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*
Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Use path aliases @/* and ~/* for imports mapping to src/*

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/helpers.ts
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/helpers.ts
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx
apps/web/client/src/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Default to Server Components; add 'use client' only when using events, state/effects, browser APIs, or client-only libs

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/helpers.ts
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using the any type unless absolutely necessary

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/helpers.ts
  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx
apps/web/client/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/app/**/*.tsx: Default to Server Components; add 'use client' when using events, state/effects, browser APIs, or client‑only libraries
Do not use process.env in client code; import env from @/env instead

Avoid hardcoded user-facing text; use next-intl messages/hooks

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx
apps/web/client/src/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable references across renders
Keep the active MobX store in a useRef and perform async cleanup with setTimeout(() => storeRef.current?.clear(), 0) to avoid route-change races
Avoid useMemo for creating MobX store instances
Avoid putting the MobX store instance in effect dependency arrays if it causes loops; split concerns by domain

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable identities across renders
Keep the active MobX store in a useRef and clean up asynchronously with setTimeout(() => storeRef.current?.clear(), 0)
Do not use useMemo to create MobX stores
Avoid placing MobX store instances in effect dependency arrays if it causes loops; split concerns instead
observer components must be client components; place a single client boundary at the feature entry; child observers need not repeat 'use client'

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx
🧬 Code graph analysis (2)
apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/helpers.ts (2)
apps/web/client/src/components/store/editor/engine.ts (1)
  • EditorEngine (33-138)
packages/models/src/project/frame.ts (1)
  • Frame (4-16)
apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/index.tsx (4)
packages/models/src/project/frame.ts (1)
  • Frame (4-16)
apps/web/client/src/components/store/editor/index.tsx (1)
  • useEditorEngine (10-14)
apps/web/client/src/components/store/editor/frames/manager.ts (1)
  • isSelected (77-79)
apps/web/client/src/app/project/[id]/_components/canvas/frame/top-bar/helpers.ts (1)
  • createMouseMoveHandler (10-87)

@vercel vercel bot temporarily deployed to Preview – docs September 17, 2025 22:51 Inactive
@Kitenite Kitenite merged commit f7092a4 into main Sep 17, 2025
4 of 6 checks passed
@Kitenite Kitenite deleted the drag-to-select-on-canvas branch September 17, 2025 22:52

const allFrames = editorEngine.frames.getAll();
return allFrames.filter(frameData =>
intersectingFrameIds.includes(frameData.frame.id)
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider using a Set for frame ID lookups in getSelectedFrameData to improve performance, especially if the frame list grows large.

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.

3 participants