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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { toast } from '@onlook/ui/sonner';
import { cn } from '@onlook/ui/utils';
import throttle from 'lodash/throttle';
import { observer } from 'mobx-react-lite';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { RightClickMenu } from './right-click';

export const GestureScreen = observer(({ frame, isResizing }: { frame: Frame, isResizing: boolean }) => {
Expand Down Expand Up @@ -75,11 +75,15 @@ export const GestureScreen = observer(({ frame, isResizing }: { frame: Frame, is
editorEngine.elements.shiftClick(el);
} else {
editorEngine.elements.click([el]);
editorEngine.move.startDragPreparation(el, pos, frameData);
}
break;
case MouseAction.DOUBLE_CLICK:
editorEngine.text.start(el, frameData.view);
if (el.oid) {
editorEngine.ide.openCodeBlock(el.oid);
} else {
toast.error('Cannot find element in code panel');
return;
}
break;
}
} catch (error) {
Expand All @@ -90,29 +94,25 @@ export const GestureScreen = observer(({ frame, isResizing }: { frame: Frame, is
[getRelativeMousePosition, editorEngine],
);

const throttledMouseMove = useMemo(
() =>
throttle(async (e: React.MouseEvent<HTMLDivElement>) => {
// Skip hover events during drag selection
if (editorEngine.state.isDragSelecting) {
return;
}

if (editorEngine.move.shouldDrag) {
await editorEngine.move.drag(e, getRelativeMousePosition);
} else if (
editorEngine.state.editorMode === EditorMode.DESIGN ||
((editorEngine.state.editorMode === EditorMode.INSERT_DIV ||
editorEngine.state.editorMode === EditorMode.INSERT_TEXT ||
editorEngine.state.editorMode === EditorMode.INSERT_IMAGE) &&
!editorEngine.insert.isDrawing)
) {
await handleMouseEvent(e, MouseAction.MOVE);
} else if (editorEngine.insert.isDrawing) {
editorEngine.insert.draw(e);
}
}, 16),
[editorEngine, getRelativeMousePosition, handleMouseEvent],
const throttledMouseMove = useMemo(() =>
throttle(async (e: React.MouseEvent<HTMLDivElement>) => {
// Skip hover events during drag selection
if (editorEngine.state.isDragSelecting) {
return;
}
if (
editorEngine.state.editorMode === EditorMode.DESIGN ||
((editorEngine.state.editorMode === EditorMode.INSERT_DIV ||
editorEngine.state.editorMode === EditorMode.INSERT_TEXT ||
editorEngine.state.editorMode === EditorMode.INSERT_IMAGE) &&
!editorEngine.insert.isDrawing)
) {
await handleMouseEvent(e, MouseAction.MOVE);
} else if (editorEngine.insert.isDrawing) {
editorEngine.insert.draw(e);
}
}, 16),
[editorEngine.state.isDragSelecting, editorEngine.state.editorMode, editorEngine.insert.isDrawing, getRelativeMousePosition, handleMouseEvent],
);

useEffect(() => {
Expand All @@ -121,59 +121,6 @@ export const GestureScreen = observer(({ frame, isResizing }: { frame: Frame, is
};
}, [throttledMouseMove]);

// Global event listeners for comprehensive drag termination
useEffect(() => {
const handleGlobalMouseUp = (e: MouseEvent) => {
if (editorEngine.move.shouldDrag || editorEngine.move.isPreparing) {
editorEngine.move.cancelDragPreparation();
// Create a synthetic React event for consistency
const syntheticEvent = {
...e,
currentTarget: e.target,
} as unknown as React.MouseEvent<HTMLDivElement>;
void editorEngine.move.end(syntheticEvent);
}
};

const handleKeyDown = (e: KeyboardEvent) => {
// Terminate drag on Escape key
if (e.key === 'Escape' && (editorEngine.move.shouldDrag || editorEngine.move.isPreparing)) {
editorEngine.move.cancelDragPreparation();
void editorEngine.move.endAllDrag();
}
};

const handleVisibilityChange = () => {
// Terminate drag when page becomes hidden (e.g., tab switch, minimize)
if (document.hidden && (editorEngine.move.shouldDrag || editorEngine.move.isPreparing)) {
editorEngine.move.cancelDragPreparation();
void editorEngine.move.endAllDrag();
}
};

const handleBlur = () => {
// Terminate drag when window loses focus
if (editorEngine.move.shouldDrag || editorEngine.move.isPreparing) {
editorEngine.move.cancelDragPreparation();
void editorEngine.move.endAllDrag();
}
};

// Add global event listeners
document.addEventListener('mouseup', handleGlobalMouseUp, true);
document.addEventListener('keydown', handleKeyDown, true);
document.addEventListener('visibilitychange', handleVisibilityChange);
window.addEventListener('blur', handleBlur);

return () => {
// Clean up event listeners
document.removeEventListener('mouseup', handleGlobalMouseUp, true);
document.removeEventListener('keydown', handleKeyDown, true);
document.removeEventListener('visibilitychange', handleVisibilityChange);
window.removeEventListener('blur', handleBlur);
};
}, [editorEngine.move]);

const handleClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
editorEngine.frames.select([frame]);
Expand Down Expand Up @@ -206,9 +153,6 @@ export const GestureScreen = observer(({ frame, isResizing }: { frame: Frame, is
return;
}

editorEngine.move.cancelDragPreparation();

await editorEngine.move.end(e);
await editorEngine.insert.end(e, frameData.view);
}

Expand Down Expand Up @@ -278,13 +222,6 @@ export const GestureScreen = observer(({ frame, isResizing }: { frame: Frame, is
onClick={handleClick}
onMouseOut={handleMouseOut}
onMouseLeave={handleMouseUp}
onContextMenu={(e) => {
// Terminate drag on right-click
if (editorEngine.move.shouldDrag || editorEngine.move.isPreparing) {
editorEngine.move.cancelDragPreparation();
void editorEngine.move.end(e);
}
}}
onMouseMove={throttledMouseMove}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
Expand Down
14 changes: 11 additions & 3 deletions apps/web/client/src/app/project/[id]/_components/canvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useEditorEngine } from '@/components/store/editor';
import { EditorAttributes } from '@onlook/constants';
import { EditorMode } from '@onlook/models';
import { EditorMode, EditorTabValue } from '@onlook/models';
import { throttle } from 'lodash';
import { observer } from 'mobx-react-lite';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
Expand Down Expand Up @@ -39,7 +39,10 @@ export const Canvas = observer(() => {
}

// Start drag selection only in design mode and left mouse button
if (editorEngine.state.editorMode === EditorMode.DESIGN && event.button === 0) {
if (event.button !== 0) {
return;
}
if (editorEngine.state.editorMode === EditorMode.DESIGN) {
const rect = containerRef.current.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
Expand All @@ -57,7 +60,12 @@ export const Canvas = observer(() => {
editorEngine.clearUI();
editorEngine.frames.deselectAll();
}
} else if (event.button === 0) {

// Switch to chat mode when clicking on empty canvas space during code editing
if (editorEngine.state.rightPanelTab === EditorTabValue.DEV) {
editorEngine.state.rightPanelTab = EditorTabValue.CHAT;
}
} else {
// Only clear UI for left clicks that don't start drag selection
editorEngine.clearUI();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const CodeControls = observer(() => {

return (
<>
<div className="flex flex-row opacity-50 transition-opacity duration-200 group-hover/panel:opacity-100">
<div className="flex flex-row items-center transition-opacity duration-200">
<Tooltip>
<DropdownMenu>
<TooltipTrigger asChild>
Expand Down Expand Up @@ -66,7 +66,6 @@ export const CodeControls = observer(() => {
</DropdownMenu>
<TooltipContent side="bottom" hideArrow>
<p>Create or Upload File</p>
<TooltipArrow className="fill-foreground" />
</TooltipContent>
</Tooltip>
<Tooltip>
Expand All @@ -82,32 +81,31 @@ export const CodeControls = observer(() => {
</TooltipTrigger>
<TooltipContent side="bottom" hideArrow>
<p>New Folder</p>
<TooltipArrow className="fill-foreground" />
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
variant="secondary"
size="icon"
onClick={saveFile}
disabled={!isDirty}
className={cn(
"p-2 w-fit h-fit cursor-pointer",
"px-1.5 py-0.75 w-fit h-fit cursor-pointer mr-0.5 ml-1",
isDirty
? "text-teal-200 hover:text-teal-100 hover:bg-teal-500"
? "text-background-primary hover:text-teal-100 hover:bg-teal-500 bg-foreground-primary"
: "hover:bg-background-onlook hover:text-teal-200"
)}
>
<Icons.Save className={cn(
"h-4 w-4",
isDirty && "text-teal-200 group-hover:text-teal-100"
)} />
<span className="text-small">Save</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" hideArrow>
<p>Save changes</p>
<TooltipArrow className="fill-foreground" />
</TooltipContent>
</Tooltip>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,13 @@ export const CodeTab = observer(() => {

// Create the selection and apply it in a single transaction
const selection = EditorSelection.create([EditorSelection.range(startPos, endPos)]);

editorView.dispatch({
selection,
effects: [
EditorView.scrollIntoView(selection.main, {
y: 'start'
EditorView.scrollIntoView(startPos, {
y: 'start',
yMargin: 48
})
],
userEvent: 'select.element'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,12 @@ export const RightPanel = observer(() => {
{selectedTab === EditorTabValue.DEV && <CodeControls />}
</TabsList>
<ChatHistory isOpen={isChatHistoryOpen} onOpenChange={setIsChatHistoryOpen} />
<TabsContent className="h-full overflow-y-auto" value={EditorTabValue.CHAT}>
<TabsContent
forceMount
className={cn(
"h-full overflow-y-auto",
editorEngine.state.rightPanelTab !== EditorTabValue.CHAT && 'hidden',
)} value={EditorTabValue.CHAT}>
Comment on lines +82 to +87
Copy link
Contributor

Choose a reason for hiding this comment

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

Potential rendering bug: Using forceMount with conditional visibility via 'hidden' class can cause issues with component lifecycle and accessibility. The TabsContent will always be mounted but hidden with CSS, which may cause the ChatTab component to initialize and run effects even when not visible, potentially leading to unnecessary API calls or state updates. Consider using conditional rendering instead of forceMount + hidden class.

Suggested change
<TabsContent
forceMount
className={cn(
"h-full overflow-y-auto",
editorEngine.state.rightPanelTab !== EditorTabValue.CHAT && 'hidden',
)} value={EditorTabValue.CHAT}>
<TabsContent
className="h-full overflow-y-auto"
value={EditorTabValue.CHAT}>

Spotted by Diamond

Fix in Graphite


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

{currentConversation && (
<ChatTab
conversationId={currentConversation.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export class InsertManager {
const origin = getRelativeMousePositionToFrameView(e, frameView);
await this.insertElement(frameView, newRect, origin);
this.drawOrigin = undefined;
this.editorEngine.state.editorMode = EditorMode.DESIGN;
}

private updateInsertRect(pos: ElementPosition) {
Expand Down