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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { useHotkeys } from 'react-hotkeys-hook';

export const HotkeysArea = ({ children }: { children: ReactNode }) => {
const editorEngine = useEditorEngine();

// Check if we're in Code mode to disable certain hotkeys
const isCodeMode = editorEngine.state.editorMode === EditorMode.CODE;

// Zoom
useHotkeys(
Expand Down Expand Up @@ -50,39 +53,61 @@ export const HotkeysArea = ({ children }: { children: ReactNode }) => {
useHotkeys('alt', () => editorEngine.overlay.showMeasurement(), { keydown: true });
useHotkeys('alt', () => editorEngine.overlay.removeMeasurement(), { keyup: true });

// Actions
useHotkeys(Hotkey.UNDO.command, () => editorEngine.action.undo(), {
preventDefault: true,
// Actions - disabled in Code mode
useHotkeys(Hotkey.UNDO.command, () => !isCodeMode && editorEngine.action.undo(), {
preventDefault: !isCodeMode,
enabled: !isCodeMode,
});
useHotkeys(Hotkey.REDO.command, () => editorEngine.action.redo(), {
preventDefault: true,
useHotkeys(Hotkey.REDO.command, () => !isCodeMode && editorEngine.action.redo(), {
preventDefault: !isCodeMode,
enabled: !isCodeMode,
});
useHotkeys(Hotkey.ENTER.command, () => !isCodeMode && editorEngine.text.editSelectedElement(), {
preventDefault: !isCodeMode,
enabled: !isCodeMode
});
useHotkeys(Hotkey.ENTER.command, () => editorEngine.text.editSelectedElement(), { preventDefault: true });
useHotkeys([Hotkey.BACKSPACE.command, Hotkey.DELETE.command], () => {
if (editorEngine.elements.selected.length > 0) {
editorEngine.elements.delete();
if (!isCodeMode) {
if (editorEngine.elements.selected.length > 0) {
editorEngine.elements.delete();
}
else if (editorEngine.frames.selected.length > 0 && editorEngine.frames.canDelete()) {
editorEngine.frames.deleteSelected();
}
}
else if (editorEngine.frames.selected.length > 0 && editorEngine.frames.canDelete()) {
editorEngine.frames.deleteSelected();
}
}, { preventDefault: true });
}, { preventDefault: !isCodeMode, enabled: !isCodeMode });

// Group
useHotkeys(Hotkey.GROUP.command, () => editorEngine.group.groupSelectedElements());
useHotkeys(Hotkey.UNGROUP.command, () => editorEngine.group.ungroupSelectedElement());
// Group - disabled in Code mode
useHotkeys(Hotkey.GROUP.command, () => !isCodeMode && editorEngine.group.groupSelectedElements(), {
enabled: !isCodeMode
});
useHotkeys(Hotkey.UNGROUP.command, () => !isCodeMode && editorEngine.group.ungroupSelectedElement(), {
enabled: !isCodeMode
});

// Copy
useHotkeys(Hotkey.COPY.command, () => editorEngine.copy.copy(), { preventDefault: true });
useHotkeys(Hotkey.PASTE.command, () => editorEngine.copy.paste(), { preventDefault: true });
useHotkeys(Hotkey.CUT.command, () => editorEngine.copy.cut(), { preventDefault: true });
// Copy - disabled in Code mode
useHotkeys(Hotkey.COPY.command, () => !isCodeMode && editorEngine.copy.copy(), {
preventDefault: !isCodeMode,
enabled: !isCodeMode
});
useHotkeys(Hotkey.PASTE.command, () => !isCodeMode && editorEngine.copy.paste(), {
preventDefault: !isCodeMode,
enabled: !isCodeMode
});
useHotkeys(Hotkey.CUT.command, () => !isCodeMode && editorEngine.copy.cut(), {
preventDefault: !isCodeMode,
enabled: !isCodeMode
});
useHotkeys(Hotkey.DUPLICATE.command, () => {
if (editorEngine.elements.selected.length > 0) {
editorEngine.copy.duplicate();
}
else if (editorEngine.frames.selected.length > 0 && editorEngine.frames.canDuplicate()) {
editorEngine.frames.duplicateSelected();
if (!isCodeMode) {
if (editorEngine.elements.selected.length > 0) {
editorEngine.copy.duplicate();
}
else if (editorEngine.frames.selected.length > 0 && editorEngine.frames.canDuplicate()) {
editorEngine.frames.duplicateSelected();
}
}
}, { preventDefault: true });
}, { preventDefault: !isCodeMode, enabled: !isCodeMode });

// AI
useHotkeys(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import { ResizablePanel } from '@onlook/ui/resizable';
import { cn } from '@onlook/ui/utils';
import { observer } from 'mobx-react-lite';
import { CodeTab } from '../right-panel/code-tab';

export const CodePanel = observer(() => {
return (
<div
className={cn(
'flex h-full w-full transition-width duration-300 bg-background/95 group/panel border-[0.5px] backdrop-blur-xl shadow rounded-tr-xl',
)}
>
<ResizablePanel
side="left"
defaultWidth={700}
minWidth={400}
maxWidth={1200}
>
<div className="h-full">
<CodeTab />
</div>
</ResizablePanel>
</div>
);
});
69 changes: 44 additions & 25 deletions apps/web/client/src/app/project/[id]/_components/main.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
'use client';

import { useEditorEngine } from '@/components/store/editor';
import { SubscriptionModal } from '@/components/ui/pricing-modal';
import { SettingsModalWithProjects } from '@/components/ui/settings-modal/with-project';
import { EditorAttributes } from '@onlook/constants';
import { EditorMode } from '@onlook/models';
import { Button } from '@onlook/ui/button';
import { Icons } from '@onlook/ui/icons';
import { TooltipProvider } from '@onlook/ui/tooltip';
Expand All @@ -13,18 +15,21 @@ import { usePanelMeasurements } from '../_hooks/use-panel-measure';
import { useStartProject } from '../_hooks/use-start-project';
import { BottomBar } from './bottom-bar';
import { Canvas } from './canvas';
import { CodePanel } from './code-panel';
import { EditorBar } from './editor-bar';
import { LeftPanel } from './left-panel';
import { RightPanel } from './right-panel';
import { TopBar } from './top-bar';

export const Main = observer(() => {
const router = useRouter();
const editorEngine = useEditorEngine();
const { isProjectReady, error } = useStartProject();
const leftPanelRef = useRef<HTMLDivElement | null>(null);
const rightPanelRef = useRef<HTMLDivElement | null>(null);
const codePanelRef = useRef<HTMLDivElement | null>(null);
const { toolbarLeft, toolbarRight, editorBarAvailableWidth } = usePanelMeasurements(
leftPanelRef,
editorEngine.state.editorMode === EditorMode.CODE ? codePanelRef : leftPanelRef,
rightPanelRef,
);

Expand Down Expand Up @@ -84,31 +89,45 @@ export const Main = observer(() => {
<TopBar />
</div>

{/* Left Panel */}
<div
ref={leftPanelRef}
className="absolute top-10 left-0 h-[calc(100%-40px)] z-50"
>
<LeftPanel />
</div>
{/* EditorBar anchored between panels */}
<div
className="absolute top-10 z-49"
style={{
left: toolbarLeft,
right: toolbarRight,
overflow: 'hidden',
pointerEvents: 'none',
maxWidth: editorBarAvailableWidth,
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-start',
}}
>
<div style={{ pointerEvents: 'auto' }}>
<EditorBar availableWidth={editorBarAvailableWidth} />
{/* Left Panel - only in Design mode */}
{editorEngine.state.editorMode === EditorMode.DESIGN && (
<div
ref={leftPanelRef}
className="absolute top-10 left-0 h-[calc(100%-40px)] z-50"
>
<LeftPanel />
</div>
</div>
)}

{/* Code Panel - only in Code mode */}
{editorEngine.state.editorMode === EditorMode.CODE && (
<div
ref={codePanelRef}
className="absolute top-10 left-0 h-[calc(100%-40px)] z-50"
>
<CodePanel />
</div>
)}
{/* EditorBar anchored between panels - only in Design mode */}
{editorEngine.state.editorMode === EditorMode.DESIGN && (
<div
className="absolute top-10 z-49"
style={{
left: toolbarLeft,
right: toolbarRight,
overflow: 'hidden',
pointerEvents: 'none',
maxWidth: editorBarAvailableWidth,
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-start',
}}
>
<div style={{ pointerEvents: 'auto' }}>
<EditorBar availableWidth={editorBarAvailableWidth} />
</div>
</div>
)}

{/* Right Panel */}
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Hotkey } from '@/components/hotkey';
import { IDE } from '@/components/ide';
import { useEditorEngine } from '@/components/store/editor';
import { EditorTabValue } from '@onlook/models/editor';
import { EditorMode, EditorTabValue } from '@onlook/models/editor';
import type { DomElement } from '@onlook/models/element';
import { DEFAULT_IDE } from '@onlook/models/ide';
import {
Expand Down Expand Up @@ -158,6 +158,15 @@ export const RightClickMenu = observer(({ children }: RightClickMenuProps) => {
menuItems = [WINDOW_ITEMS];
} else {
const updatedToolItems = [
{
label: 'Open in Code',
action: () => {
editorEngine.state.editorMode = EditorMode.CODE;
viewSource(root);
},
icon: <Icons.Code className="mr-2 h-4 w-4" />,
disabled: !root,
},
instance !== null && {
label: 'View instance code',
action: () => viewSource(instance),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ export const RightPanel = observer(() => {
const [isChatHistoryOpen, setIsChatHistoryOpen] = useState(false);
const [inputValue, setInputValue] = useState('');

const selectedTab = editorEngine.state.rightPanelTab;
// Force chat tab when in code mode
const selectedTab = editorEngine.state.editorMode === EditorMode.CODE
? EditorTabValue.CHAT
: editorEngine.state.rightPanelTab;
const editPanelWidth = EDIT_PANEL_WIDTHS[selectedTab];

return (
Expand Down Expand Up @@ -61,13 +64,15 @@ export const RightPanel = observer(() => {
<Icons.ChevronDown className="ml-0.5 h-3 w-3 text-muted-foreground" />
</TabsTrigger>
</ChatPanelDropdown>
<TabsTrigger
className="bg-transparent py-2 px-1 text-small hover:text-foreground-hover cursor-pointer"
value={EditorTabValue.DEV}
>
<Icons.Code className="mr-1 h-4 w-4" />
Code
</TabsTrigger>
{editorEngine.state.editorMode !== EditorMode.CODE && (
<TabsTrigger
className="bg-transparent py-2 px-1 text-small hover:text-foreground-hover cursor-pointer"
value={EditorTabValue.DEV}
>
<Icons.Code className="mr-1 h-4 w-4" />
Code
</TabsTrigger>
)}
</div>
{selectedTab === EditorTabValue.CHAT && <ChatControls />}
{selectedTab === EditorTabValue.DEV && <CodeControls />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ import { useTranslations } from 'next-intl';

const MODE_TOGGLE_ITEMS: {
mode: EditorMode;
hotkey: Hotkey;
hotkey?: Hotkey;
}[] = [
{
mode: EditorMode.DESIGN,
hotkey: Hotkey.SELECT,
},
{
mode: EditorMode.CODE,
},
{
mode: EditorMode.PREVIEW,
hotkey: Hotkey.PREVIEW,
Expand All @@ -27,12 +30,14 @@ const MODE_TOGGLE_ITEMS: {
export const ModeToggle = observer(() => {
const t = useTranslations();
const editorEngine = useEditorEngine();
const mode: EditorMode.DESIGN | EditorMode.PREVIEW = getNormalizedMode(
const mode: EditorMode.DESIGN | EditorMode.CODE | EditorMode.PREVIEW = getNormalizedMode(
editorEngine.state.editorMode,
);

function getNormalizedMode(unnormalizedMode: EditorMode) {
return unnormalizedMode === EditorMode.PREVIEW ? EditorMode.PREVIEW : EditorMode.DESIGN;
if (unnormalizedMode === EditorMode.PREVIEW) return EditorMode.PREVIEW;
if (unnormalizedMode === EditorMode.CODE) return EditorMode.CODE;
return EditorMode.DESIGN;
}

return (
Expand All @@ -48,33 +53,49 @@ export const ModeToggle = observer(() => {
}}
>
{MODE_TOGGLE_ITEMS.map((item) => (
<Tooltip key={item.mode}>
<TooltipTrigger asChild>
<ToggleGroupItem
value={item.mode}
aria-label={item.hotkey.description}
className={cn(
'transition-all duration-150 ease-in-out px-4 py-2 whitespace-nowrap bg-transparent cursor-pointer text-sm',
mode === item.mode
? 'text-active text-sm hover:text-active hover:bg-transparent'
: 'text-foreground-secondary text-sm hover:text-foreground-hover hover:bg-transparent',
)}
>
{t(transKeys.editor.modes[item.mode.toLowerCase() as keyof typeof transKeys.editor.modes].name)}
</ToggleGroupItem>
</TooltipTrigger>
<TooltipContent side="bottom">
<HotkeyLabel hotkey={item.hotkey} />
</TooltipContent>
</Tooltip>
item.hotkey ? (
<Tooltip key={item.mode}>
<TooltipTrigger asChild>
<ToggleGroupItem
value={item.mode}
aria-label={item.hotkey.description}
className={cn(
'transition-all duration-150 ease-in-out px-4 py-2 whitespace-nowrap bg-transparent cursor-pointer text-sm',
mode === item.mode
? 'text-active text-sm hover:text-active hover:bg-transparent'
: 'text-foreground-secondary text-sm hover:text-foreground-hover hover:bg-transparent',
)}
>
{item.mode === EditorMode.CODE ? 'Code' : t(transKeys.editor.modes[item.mode.toLowerCase() as keyof typeof transKeys.editor.modes].name)}
</ToggleGroupItem>
</TooltipTrigger>
<TooltipContent side="bottom">
<HotkeyLabel hotkey={item.hotkey} />
</TooltipContent>
</Tooltip>
) : (
<ToggleGroupItem
key={item.mode}
value={item.mode}
aria-label={`${item.mode} mode`}
className={cn(
'transition-all duration-150 ease-in-out px-4 py-2 whitespace-nowrap bg-transparent cursor-pointer text-sm',
mode === item.mode
? 'text-active text-sm hover:text-active hover:bg-transparent'
: 'text-foreground-secondary text-sm hover:text-foreground-hover hover:bg-transparent',
)}
>
{item.mode === EditorMode.CODE ? 'Code' : t(transKeys.editor.modes[item.mode.toLowerCase() as keyof typeof transKeys.editor.modes].name)}
</ToggleGroupItem>
)
))}
</ToggleGroup>
<motion.div
className="absolute -top-1 h-0.5 bg-foreground"
initial={false}
animate={{
width: '50%',
x: mode === EditorMode.DESIGN ? '0%' : '100%',
width: '33.33%',
x: mode === EditorMode.DESIGN ? '0%' : mode === EditorMode.CODE ? '100%' : '200%',
}}
transition={{
type: 'tween',
Expand Down
Loading