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
@@ -1,6 +1,7 @@
'use client';

import { Hotkey } from '@/components/hotkey';
import { useOnboarding } from '@/components/onboarding/onboarding-context';
import { useEditorEngine } from '@/components/store/editor';
import { transKeys } from '@/i18n/keys';
import { EditorMode } from '@onlook/models';
Expand Down Expand Up @@ -57,7 +58,9 @@ const TOOLBAR_ITEMS = ({ t }: { t: ReturnType<typeof useTranslations> }) => [
export const BottomBar = observer(() => {
const t = useTranslations();
const editorEngine = useEditorEngine();
const { isActive, currentStep } = useOnboarding();
const toolbarItems = TOOLBAR_ITEMS({ t });
const isOnboardingToolbarStep = isActive && currentStep === 1;

// Ensure default state is set
useEffect(() => {
Expand All @@ -69,12 +72,16 @@ export const BottomBar = observer(() => {
return (
<AnimatePresence mode="wait">
<motion.div
data-onboarding-target="project-toolbar"
initial={{ opacity: 0, y: 20 }}
animate={{
opacity: editorEngine.state.editorMode !== EditorMode.PREVIEW ? 1 : 0,
y: editorEngine.state.editorMode !== EditorMode.PREVIEW ? 0 : 20,
}}
className="absolute left-1/2 -translate-x-1/2 bottom-4 flex flex-col border-[0.5px] border-border p-1 px-1 bg-background rounded-lg backdrop-blur drop-shadow-xl overflow-hidden"
className={cn(
"absolute left-1/2 -translate-x-1/2 bottom-4 flex flex-col border-[0.5px] border-border p-1 px-1 bg-background rounded-lg backdrop-blur drop-shadow-xl overflow-hidden z-50 transition-all duration-300",
isOnboardingToolbarStep && "ring-1 ring-red-500 ring-offset-2 ring-offset-background shadow-[0_0_12px_rgba(239,68,68,0.8)]"
)}
transition={{
type: 'spring',
bounce: 0.1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export const LeftPanel = observer(() => {
onMouseLeave={handleMouseLeave}
>
{/* Left sidebar with tabs */}
<div className="w-20 bg-background-onlook/60 backdrop-blur-xl flex flex-col items-center py-0.5 gap-2">
<div data-onboarding-target="left-panel" className="w-20 bg-background-onlook/60 backdrop-blur-xl flex flex-col items-center py-0.5 gap-2">
{tabs.map((tab) => (
<button
key={tab.value}
Expand Down
9 changes: 9 additions & 0 deletions apps/web/client/src/app/project/[id]/_components/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { EditorBar } from './editor-bar';
import { LeftPanel } from './left-panel';
import { RightPanel } from './right-panel';
import { TopBar } from './top-bar';
import { OnboardingTrigger } from './onboarding-trigger';
import { OnboardingOverlay } from '@/components/onboarding/onboarding-overlay';

export const Main = observer(() => {
const router = useRouter();
Expand Down Expand Up @@ -84,6 +86,9 @@ export const Main = observer(() => {
<TopBar />
</div>

{/* Onboarding Trigger */}
<OnboardingTrigger />

{/* Left Panel */}
<div
ref={leftPanelRef}
Expand Down Expand Up @@ -120,6 +125,10 @@ export const Main = observer(() => {

<BottomBar />
</div>

{/* Onboarding Overlay - positioned at top level to break panel constraints */}
<OnboardingOverlay />

<SettingsModalWithProjects />
<SubscriptionModal />
</TooltipProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use client';

import { useOnboarding } from '@/components/onboarding/onboarding-context';
import { Button } from '@onlook/ui/button';
import { Icons } from '@onlook/ui/icons';
import { Popover, PopoverContent, PopoverTrigger } from '@onlook/ui/popover';
import { Tooltip, TooltipContent, TooltipTrigger } from '@onlook/ui/tooltip';
import { cn } from '@onlook/ui/utils';
import { useState, useRef, useEffect } from 'react';
import { MembersContent } from './members-content';

Expand All @@ -12,7 +14,9 @@ interface MembersProps {
}

export const Members = ({ onPopoverOpenChange }: MembersProps) => {
const { isActive, currentStep } = useOnboarding();
const [isOpen, setIsOpen] = useState(false);
const isOnboardingStep5 = isActive && currentStep === 4;

const handleOpenChange = (open: boolean) => {
setIsOpen(open);
Expand All @@ -24,7 +28,14 @@ export const Members = ({ onPopoverOpenChange }: MembersProps) => {
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button variant="outline" size="icon" className="rounded-full size-8 hover:border-border bg-background-secondary hover:bg-background-secondary/80 text-foreground-secondary hover:text-foreground-primary">
<Button
variant="outline"
size="icon"
className={cn(
"rounded-full size-8 hover:border-border bg-background-secondary hover:bg-background-secondary/80 text-foreground-secondary hover:text-foreground-primary transition-all duration-300",
isOnboardingStep5 && "ring-1 ring-red-500 ring-offset-2 ring-offset-background shadow-[0_0_12px_rgba(239,68,68,0.8)]"
)}
>
<Icons.Plus className="size-4" />
</Button>
</PopoverTrigger>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import { useOnboarding } from '@/components/onboarding/onboarding-context';
import { Button } from '@onlook/ui/button';
import { observer } from 'mobx-react-lite';
import { useEffect, useState } from 'react';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused imports.

useEffect and useState are imported but never used in this component.

Apply this diff:

-import { useEffect, useState } from 'react';
+import { } from 'react';

Or remove the line entirely if no React imports are needed:

-import { useEffect, useState } from 'react';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { useEffect, useState } from 'react';
🤖 Prompt for AI Agents
In apps/web/client/src/app/project/[id]/_components/onboarding-trigger.tsx
around line 6, the imports "useEffect" and "useState" are unused; remove them
from the import list (or remove the entire React import line if no other React
symbols are required) so the file no longer contains unused imports.


export const OnboardingTrigger = observer(() => {
const { startOnboarding, isActive } = useOnboarding();

return (
<div className="absolute top-20 left-1/2 transform -translate-x-1/2 z-[200]">
<Button
onClick={startOnboarding}
variant="outline"
size="sm"
className="bg-background/90 backdrop-blur-sm border-primary/30 hover:bg-primary/10 text-foreground-primary shadow-lg"
>
{isActive ? 'Restart Tour' : 'Start Tour'}
</Button>
</div>
);
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import type { SendMessage } from '@/app/project/[id]/_hooks/use-chat';
import { useOnboarding } from '@/components/onboarding/onboarding-context';
import { useEditorEngine } from '@/components/store/editor';
import { FOCUS_CHAT_INPUT_EVENT } from '@/components/store/editor/chat';
import { transKeys } from '@/i18n/keys';
Expand All @@ -18,6 +19,7 @@ import { compressImageInBrowser } from '@onlook/utility';
import { observer } from 'mobx-react-lite';
import { useTranslations } from 'next-intl';
import { useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { validateImageLimit } from '../context-pills/helpers';
import { InputContextPills } from '../context-pills/input-context-pills';
import { Suggestions, type SuggestionsRef } from '../suggestions';
Expand All @@ -41,13 +43,17 @@ export const ChatInput = observer(({
onSendMessage,
}: ChatInputProps) => {
const editorEngine = useEditorEngine();
const { isActive, currentStep } = useOnboarding();
const t = useTranslations();
const textareaRef = useRef<HTMLTextAreaElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [isComposing, setIsComposing] = useState(false);
const [actionTooltipOpen, setActionTooltipOpen] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [portalPosition, setPortalPosition] = useState<{ top: number; left: number; width: number; height: number } | null>(null);
const chatMode = editorEngine.state.chatMode;
const [inputValue, setInputValue] = useState('');
const isOnboardingChatStep = isActive && currentStep === 0;
const lastUsageMessage = useMemo(() => messages.findLast(msg => msg.metadata?.usage), [messages]);

const focusInput = () => {
Expand Down Expand Up @@ -79,6 +85,30 @@ export const ChatInput = observer(({
return () => window.removeEventListener(FOCUS_CHAT_INPUT_EVENT, focusHandler);
}, []);

// Track position for portal when in onboarding mode
useEffect(() => {
if (isOnboardingChatStep && containerRef.current) {
const updatePosition = () => {
const rect = containerRef.current?.getBoundingClientRect();
if (rect) {
setPortalPosition({
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
});
}
};

updatePosition();
window.addEventListener('resize', updatePosition);

return () => window.removeEventListener('resize', updatePosition);
} else {
setPortalPosition(null);
}
}, [isOnboardingChatStep]);
Comment on lines +90 to +110
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Portal clone never repositions with layout changes

portalPosition is sampled only once (and on resize), yet the clone is rendered with fixed top/left/width/height. As soon as the chat input grows (multi-line typing, suggestion pills) or the right panel scrolls/reflows, the hidden source moves but the fixed portal stays at the stale coordinates, so the visible chat input and the onboarding glow drift apart and drops land in the wrong place. Hook a ResizeObserver (and at least a scroll listener on the nearest scroll container) to keep portalPosition in sync while step 0 is active.

Apply something like:

 useEffect(() => {
-    if (isOnboardingChatStep && containerRef.current) {
-        const updatePosition = () => {
-            const rect = containerRef.current?.getBoundingClientRect();
-            if (rect) {
-                setPortalPosition({
-                    top: rect.top,
-                    left: rect.left,
-                    width: rect.width,
-                    height: rect.height,
-                });
-            }
-        };
-
-        updatePosition();
-        window.addEventListener('resize', updatePosition);
-
-        return () => window.removeEventListener('resize', updatePosition);
-    } else {
-        setPortalPosition(null);
-    }
-}, [isOnboardingChatStep]);
+    const node = containerRef.current;
+    if (!isOnboardingChatStep || !node) {
+        setPortalPosition(null);
+        return;
+    }
+
+    const updatePosition = () => {
+        const rect = node.getBoundingClientRect();
+        setPortalPosition({
+            top: rect.top,
+            left: rect.left,
+            width: rect.width,
+            height: rect.height,
+        });
+    };
+
+    const resizeObserver = new ResizeObserver(updatePosition);
+    resizeObserver.observe(node);
+    updatePosition();
+
+    const handleScroll = () => updatePosition();
+    window.addEventListener('scroll', handleScroll, true);
+
+    return () => {
+        resizeObserver.disconnect();
+        window.removeEventListener('scroll', handleScroll, true);
+    };
+}, [isOnboardingChatStep]);

Also applies to: 352-358


useEffect(() => {
const handleGlobalKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && suggestionRef.current?.handleEnterSelection()) {
Expand Down Expand Up @@ -310,12 +340,22 @@ export const ChatInput = observer(({
};


return (
const chatInputContent = (isPortal = false) => (
<div
ref={!isPortal ? containerRef : undefined}
data-onboarding-target={!isPortal ? "chat-input" : undefined}
className={cn(
'flex flex-col w-full text-foreground-tertiary border-t text-small transition-colors duration-200 [&[data-dragging-image=true]]:bg-teal-500/40',
'flex flex-col w-full text-foreground-tertiary border-t text-small transition-all duration-300 [&[data-dragging-image=true]]:bg-teal-500/40 bg-background',
isDragging && 'cursor-copy',
isPortal ? 'fixed z-[99999]' : 'relative z-[100]',
isOnboardingChatStep && !isPortal && 'invisible',
)}
style={isPortal && portalPosition ? {
top: portalPosition.top,
left: portalPosition.left,
width: portalPosition.width,
height: portalPosition.height,
} : undefined}
onDrop={(e) => {
handleDrop(e);
setIsDragging(false);
Expand Down Expand Up @@ -434,4 +474,14 @@ export const ChatInput = observer(({
</div>
</div>
);

return (
<>
{chatInputContent(false)}
{isOnboardingChatStep && portalPosition && typeof window !== 'undefined' && createPortal(
chatInputContent(true),
document.body
)}
</>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const RightPanel = 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-tl-xl',
'flex h-full w-full transition-width duration-300 bg-background/95 group/panel border-[0.5px] backdrop-blur-xl shadow rounded-tl-xl relative',
editorEngine.state.editorMode === EditorMode.PREVIEW && 'hidden',
)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ export const TopBar = observer(() => {
];

return (
<div className="bg-background-primary/20 backdrop-blur-md flex flex-row h-10 p-0 justify-center items-center">
<div className="bg-background-primary/20 backdrop-blur-md flex flex-row h-10 p-0 justify-center items-center relative z-[70]">
<div className="flex flex-row flex-grow basis-0 justify-start items-center">
<ProjectBreadcrumb />
<BranchDisplay />
</div>
<ModeToggle />
<div className="flex flex-grow basis-0 justify-end items-center gap-1.5 mr-2">
<div data-onboarding-target="top-right-actions" className="flex flex-grow basis-0 justify-end items-center gap-1.5 mr-2">
<div className="flex items-center group">
<div className={`transition-all duration-200 ${isMembersPopoverOpen ? 'mr-2' : '-mr-2 group-hover:mr-2'}`}>
<Members onPopoverOpenChange={setIsMembersPopoverOpen} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const ModeToggle = observer(() => {
}

return (
<div className="relative">
<div className="relative" data-onboarding-target="mode-toggle">
<ToggleGroup
className="font-normal h-7 mt-1"
type="single"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useOnboarding } from '@/components/onboarding/onboarding-context';
import { useEditorEngine } from '@/components/store/editor';
import { useHostingType } from '@/components/store/hosting';
import { DeploymentStatus, DeploymentType } from '@onlook/models';
Expand All @@ -8,13 +9,16 @@ import { cn } from '@onlook/ui/utils';
import { observer } from 'mobx-react-lite';

export const TriggerButton = observer(() => {
const { isActive, currentStep } = useOnboarding();
const editorEngine = useEditorEngine();
const { deployment: previewDeployment, isDeploying: isPreviewDeploying } = useHostingType(DeploymentType.PREVIEW);
const { deployment: customDeployment, isDeploying: isCustomDeploying } = useHostingType(DeploymentType.CUSTOM);
const isPreviewCompleted = previewDeployment?.status === DeploymentStatus.COMPLETED;
const isCustomCompleted = customDeployment?.status === DeploymentStatus.COMPLETED;
const isPreviewFailed = previewDeployment?.status === DeploymentStatus.FAILED;
const isCustomFailed = customDeployment?.status === DeploymentStatus.FAILED;

const isOnboardingStep5 = isActive && currentStep === 4;

const isCompleted = isPreviewCompleted || isCustomCompleted;
const isFailed = isPreviewFailed || isCustomFailed;
Expand Down Expand Up @@ -48,6 +52,7 @@ export const TriggerButton = observer(() => {
className={cn(
'px-3 flex items-center border-[0.5px] text-xs justify-center shadow-sm h-8 rounded-md transition-all duration-300 ease-in-out',
colorClasses,
isOnboardingStep5 && 'ring-1 ring-red-500 ring-offset-2 ring-offset-background shadow-[0_0_12px_rgba(239,68,68,0.6)]',
)}
>
{icon}
Expand Down
5 changes: 4 additions & 1 deletion apps/web/client/src/app/project/[id]/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { EditorEngineProvider } from '@/components/store/editor';
import { HostingProvider } from '@/components/store/hosting';
import { OnboardingProvider } from '@/components/onboarding/onboarding-context';
import type { Branch, Project } from '@onlook/models';

export const ProjectProviders = ({
Expand All @@ -16,7 +17,9 @@ export const ProjectProviders = ({
return (
<EditorEngineProvider project={project} branches={branches}>
<HostingProvider>
{children}
<OnboardingProvider>
{children}
</OnboardingProvider>
</HostingProvider>
</EditorEngineProvider>
);
Expand Down
Loading
Loading