From 80f2f65a156a9817aec523cb281ec587de412a36 Mon Sep 17 00:00:00 2001 From: Daniel R Farrell Date: Tue, 30 Sep 2025 18:45:37 -0700 Subject: [PATCH 1/2] initial onboarding flow with screen disableing and "blinking" --- .../[id]/_components/bottom-bar/index.tsx | 9 +- .../[id]/_components/left-panel/index.tsx | 2 +- .../src/app/project/[id]/_components/main.tsx | 9 + .../[id]/_components/members/index.tsx | 13 +- .../[id]/_components/onboarding-trigger.tsx | 23 ++ .../right-panel/chat-tab/chat-input/index.tsx | 45 ++- .../[id]/_components/right-panel/index.tsx | 2 +- .../[id]/_components/top-bar/index.tsx | 4 +- .../[id]/_components/top-bar/mode-toggle.tsx | 2 +- .../top-bar/publish/trigger-button.tsx | 5 + .../client/src/app/project/[id]/providers.tsx | 5 +- .../onboarding/onboarding-context.tsx | 133 +++++++ .../onboarding/onboarding-overlay.tsx | 344 ++++++++++++++++++ .../src/components/onboarding/red-glow.tsx | 127 +++++++ 14 files changed, 695 insertions(+), 28 deletions(-) create mode 100644 apps/web/client/src/app/project/[id]/_components/onboarding-trigger.tsx create mode 100644 apps/web/client/src/components/onboarding/onboarding-context.tsx create mode 100644 apps/web/client/src/components/onboarding/onboarding-overlay.tsx create mode 100644 apps/web/client/src/components/onboarding/red-glow.tsx diff --git a/apps/web/client/src/app/project/[id]/_components/bottom-bar/index.tsx b/apps/web/client/src/app/project/[id]/_components/bottom-bar/index.tsx index accafb390c..3d92b6ec61 100644 --- a/apps/web/client/src/app/project/[id]/_components/bottom-bar/index.tsx +++ b/apps/web/client/src/app/project/[id]/_components/bottom-bar/index.tsx @@ -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'; @@ -57,7 +58,9 @@ const TOOLBAR_ITEMS = ({ t }: { t: ReturnType }) => [ 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(() => { @@ -69,12 +72,16 @@ export const BottomBar = observer(() => { return ( { onMouseLeave={handleMouseLeave} > {/* Left sidebar with tabs */} -
+
{tabs.map((tab) => (
+ {/* Onboarding Trigger */} + + {/* Left Panel */}
{
+ + {/* Onboarding Overlay - positioned at top level to break panel constraints */} + + diff --git a/apps/web/client/src/app/project/[id]/_components/members/index.tsx b/apps/web/client/src/app/project/[id]/_components/members/index.tsx index 372803953f..c386e8923f 100644 --- a/apps/web/client/src/app/project/[id]/_components/members/index.tsx +++ b/apps/web/client/src/app/project/[id]/_components/members/index.tsx @@ -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'; @@ -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); @@ -24,7 +28,14 @@ export const Members = ({ onPopoverOpenChange }: MembersProps) => { - diff --git a/apps/web/client/src/app/project/[id]/_components/onboarding-trigger.tsx b/apps/web/client/src/app/project/[id]/_components/onboarding-trigger.tsx new file mode 100644 index 0000000000..46bf7079fc --- /dev/null +++ b/apps/web/client/src/app/project/[id]/_components/onboarding-trigger.tsx @@ -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'; + +export const OnboardingTrigger = observer(() => { + const { startOnboarding, isActive } = useOnboarding(); + + return ( +
+ +
+ ); +}); diff --git a/apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx b/apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx index a72fa675eb..2a95b739e9 100644 --- a/apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx +++ b/apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx @@ -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'; @@ -41,6 +42,7 @@ export const ChatInput = observer(({ onSendMessage, }: ChatInputProps) => { const editorEngine = useEditorEngine(); + const { isActive, currentStep } = useOnboarding(); const t = useTranslations(); const textareaRef = useRef(null); const [isComposing, setIsComposing] = useState(false); @@ -48,6 +50,7 @@ export const ChatInput = observer(({ const [isDragging, setIsDragging] = useState(false); 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 = () => { @@ -311,26 +314,28 @@ export const ChatInput = observer(({ return ( -
{ - handleDrop(e); - setIsDragging(false); - }} - onDragOver={handleDragOver} - onDragEnter={(e) => { - e.preventDefault(); - handleDragStateChange(true, e); - }} - onDragLeave={(e) => { - if (!e.currentTarget.contains(e.relatedTarget as Node)) { - handleDragStateChange(false, e); - } - }} - > +
{ + handleDrop(e); + setIsDragging(false); + }} + onDragOver={handleDragOver} + onDragEnter={(e) => { + e.preventDefault(); + handleDragStateChange(true, e); + }} + onDragLeave={(e) => { + if (!e.currentTarget.contains(e.relatedTarget as Node)) { + handleDragStateChange(false, e); + } + }} + > { return (
diff --git a/apps/web/client/src/app/project/[id]/_components/top-bar/index.tsx b/apps/web/client/src/app/project/[id]/_components/top-bar/index.tsx index ccc31fbd23..a6db438e90 100644 --- a/apps/web/client/src/app/project/[id]/_components/top-bar/index.tsx +++ b/apps/web/client/src/app/project/[id]/_components/top-bar/index.tsx @@ -42,13 +42,13 @@ export const TopBar = observer(() => { ]; return ( -
+
-
+
diff --git a/apps/web/client/src/app/project/[id]/_components/top-bar/mode-toggle.tsx b/apps/web/client/src/app/project/[id]/_components/top-bar/mode-toggle.tsx index 9a3db0e02c..3b13502d84 100644 --- a/apps/web/client/src/app/project/[id]/_components/top-bar/mode-toggle.tsx +++ b/apps/web/client/src/app/project/[id]/_components/top-bar/mode-toggle.tsx @@ -36,7 +36,7 @@ export const ModeToggle = observer(() => { } return ( -
+
{ + const { isActive, currentStep } = useOnboarding(); const editorEngine = useEditorEngine(); const { deployment: previewDeployment, isDeploying: isPreviewDeploying } = useHostingType(DeploymentType.PREVIEW); const { deployment: customDeployment, isDeploying: isCustomDeploying } = useHostingType(DeploymentType.CUSTOM); @@ -15,6 +17,8 @@ export const TriggerButton = observer(() => { 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; @@ -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} diff --git a/apps/web/client/src/app/project/[id]/providers.tsx b/apps/web/client/src/app/project/[id]/providers.tsx index c8d57925ec..511c1f2c60 100644 --- a/apps/web/client/src/app/project/[id]/providers.tsx +++ b/apps/web/client/src/app/project/[id]/providers.tsx @@ -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 = ({ @@ -16,7 +17,9 @@ export const ProjectProviders = ({ return ( - {children} + + {children} + ); diff --git a/apps/web/client/src/components/onboarding/onboarding-context.tsx b/apps/web/client/src/components/onboarding/onboarding-context.tsx new file mode 100644 index 0000000000..a5cca71cb1 --- /dev/null +++ b/apps/web/client/src/components/onboarding/onboarding-context.tsx @@ -0,0 +1,133 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { createContext, useContext, useState, useEffect } from 'react'; + +export interface OnboardingStep { + id: string; + title: string; + description: string; + target: string; // CSS selector or identifier for the element to highlight +} + +export const ONBOARDING_STEPS: OnboardingStep[] = [ + { + id: 'chat-input', + title: 'Welcome to Onlook!', + description: 'Start by typing your first message here to begin designing your project.', + target: 'chat-input' + }, + { + id: 'toolbar', + title: 'Restart & Reset', + description: 'Use the toolbar to restart your sandbox if you run into any issues.', + target: 'editor-toolbar' + }, + { + id: 'left-panel', + title: 'Assets & Resources', + description: 'Access your assets, components, and project files in the left panel.', + target: 'left-panel' + }, + { + id: 'design-mode', + title: 'Preview Mode', + description: 'Switch to design mode to see how your project looks and feels.', + target: 'design-mode-toggle' + }, + { + id: 'publish', + title: 'Share Your Work', + description: 'Publish and share your amazing creations with the world!', + target: 'publish-button' + } +]; + +interface OnboardingContextType { + currentStep: number; + isActive: boolean; + isFadingOut: boolean; + nextStep: () => void; + previousStep: () => void; + startOnboarding: () => void; + completeOnboarding: () => void; + skipStep: () => void; + setFadingOut: (value: boolean) => void; +} + +const OnboardingContext = createContext(undefined); + +export const useOnboarding = () => { + const context = useContext(OnboardingContext); + if (!context) { + throw new Error('useOnboarding must be used within an OnboardingProvider'); + } + return context; +}; + +interface OnboardingProviderProps { + children: ReactNode; +} + +export const OnboardingProvider = ({ children }: OnboardingProviderProps) => { + const [currentStep, setCurrentStep] = useState(0); + const [isActive, setIsActive] = useState(false); + const [isFadingOut, setIsFadingOut] = useState(false); + + const startOnboarding = () => { + setIsActive(true); + setCurrentStep(0); + setIsFadingOut(false); + }; + + const nextStep = () => { + if (currentStep < ONBOARDING_STEPS.length - 1) { + setCurrentStep(currentStep + 1); + } else { + completeOnboarding(); + } + }; + + const previousStep = () => { + if (currentStep > 0) { + setCurrentStep(currentStep - 1); + } + }; + + const skipStep = () => { + nextStep(); + }; + + // Listen for skip events from overlay + useEffect(() => { + const handleSkip = () => { + skipStep(); + }; + + window.addEventListener('onboarding-skip', handleSkip); + return () => window.removeEventListener('onboarding-skip', handleSkip); + }, [skipStep]); + + const completeOnboarding = () => { + setIsActive(false); + setCurrentStep(0); + // Store in localStorage that onboarding has been completed + localStorage.setItem('onlook-onboarding-completed', 'true'); + }; + + return ( + + {children} + + ); +}; diff --git a/apps/web/client/src/components/onboarding/onboarding-overlay.tsx b/apps/web/client/src/components/onboarding/onboarding-overlay.tsx new file mode 100644 index 0000000000..3ed58bc81a --- /dev/null +++ b/apps/web/client/src/components/onboarding/onboarding-overlay.tsx @@ -0,0 +1,344 @@ +'use client'; + +import { useOnboarding } from '@/components/onboarding/onboarding-context'; +import { useEditorEngine } from '@/components/store/editor'; +import { cn } from '@onlook/ui/utils'; +import { useEffect, useState } from 'react'; + +export const OnboardingOverlay = () => { + const { currentStep, isActive, nextStep, completeOnboarding } = useOnboarding(); + const editorEngine = useEditorEngine(); + const [showGlow, setShowGlow] = useState(false); + const [showText, setShowText] = useState(false); + const [targetElement, setTargetElement] = useState(null); + const [isFadingOut, setIsFadingOut] = useState(false); + const [isFadingIn, setIsFadingIn] = useState(false); + const [blinkTrigger, setBlinkTrigger] = useState(0); + + useEffect(() => { + if (isActive && currentStep === 0) { + // Find the chat input element + const chatInput = document.querySelector('[data-onboarding-target="chat-input"]'); + if (chatInput) { + setTargetElement(chatInput as HTMLElement); + + // Delay the glow appearance + const glowTimer = setTimeout(() => setShowGlow(true), 300); + const textTimer = setTimeout(() => setShowText(true), 800); + + return () => { + clearTimeout(glowTimer); + clearTimeout(textTimer); + }; + } + } else if (isActive && currentStep === 1) { + // Find the project toolbar element + const toolbar = document.querySelector('[data-onboarding-target="project-toolbar"]'); + if (toolbar) { + setTargetElement(toolbar as HTMLElement); + + // Delay the glow appearance + const glowTimer = setTimeout(() => setShowGlow(true), 300); + const textTimer = setTimeout(() => setShowText(true), 800); + + return () => { + clearTimeout(glowTimer); + clearTimeout(textTimer); + }; + } + } else if (isActive && currentStep === 2) { + // Find the left panel element + const leftPanel = document.querySelector('[data-onboarding-target="left-panel"]'); + if (leftPanel) { + setTargetElement(leftPanel as HTMLElement); + + // Delay the glow appearance + const glowTimer = setTimeout(() => setShowGlow(true), 300); + const textTimer = setTimeout(() => setShowText(true), 800); + + return () => { + clearTimeout(glowTimer); + clearTimeout(textTimer); + }; + } + } else if (isActive && currentStep === 3) { + // Find the mode toggle element + const modeToggle = document.querySelector('[data-onboarding-target="mode-toggle"]'); + if (modeToggle) { + setTargetElement(modeToggle as HTMLElement); + + // Delay the glow appearance + const glowTimer = setTimeout(() => setShowGlow(true), 300); + const textTimer = setTimeout(() => setShowText(true), 800); + + return () => { + clearTimeout(glowTimer); + clearTimeout(textTimer); + }; + } + } else if (isActive && currentStep === 4) { + // Find the top-right actions element + const topRightActions = document.querySelector('[data-onboarding-target="top-right-actions"]'); + if (topRightActions) { + setTargetElement(topRightActions as HTMLElement); + + // Delay the glow appearance + const glowTimer = setTimeout(() => setShowGlow(true), 300); + const textTimer = setTimeout(() => setShowText(true), 800); + + return () => { + clearTimeout(glowTimer); + clearTimeout(textTimer); + }; + } + } else { + setShowGlow(false); + setShowText(false); + setTargetElement(null); + } + }, [isActive, currentStep]); + + // Clear any selected elements and frames when onboarding becomes active + useEffect(() => { + if (isActive) { + editorEngine.elements.clear(); + editorEngine.frames.deselectAll(); + setIsFadingOut(false); + setIsFadingIn(true); + // Set to false after animation completes + const timer = setTimeout(() => setIsFadingIn(false), 100); + return () => clearTimeout(timer); + } + }, [isActive, editorEngine]); + + // Handle fade out and completion + useEffect(() => { + if (isFadingOut) { + const timer = setTimeout(() => { + completeOnboarding(); + setIsFadingOut(false); + }, 700); // Match the fade-in duration + + return () => clearTimeout(timer); + } + }, [isFadingOut, completeOnboarding]); + + const handleFinish = () => { + setShowGlow(false); + setShowText(false); + setIsFadingOut(true); + }; + + if (!isActive || (currentStep !== 0 && currentStep !== 1 && currentStep !== 2 && currentStep !== 3 && currentStep !== 4) || !targetElement) { + return null; + } + + const rect = targetElement.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + // For step 1 (toolbar), position the glow below the toolbar + const isToolbarStep = currentStep === 1; + // For step 2 (left panel), position the glow on the left side + const isLeftPanelStep = currentStep === 2; + // For step 3 (mode toggle), position the glow around the toggle + const isModeToggleStep = currentStep === 3; + // For step 4 (top-right actions), position the glow around the actions + const isTopRightStep = currentStep === 4; + + const glowTop = isToolbarStep + ? rect.bottom - 80 + : isLeftPanelStep + ? rect.top - 50 + : isModeToggleStep + ? rect.top - 30 + : isTopRightStep + ? rect.top - 30 + : rect.top - 50; + const glowLeft = isToolbarStep + ? rect.left - 100 + : isLeftPanelStep + ? rect.left - 100 + : isModeToggleStep + ? rect.left - 50 + : isTopRightStep + ? rect.left - 50 + : rect.left - 100; + + return ( + <> + {/* Dark overlay over the rest of the interface - blocks all interactions */} +
+ + {/* High z-index transparent overlay to block ALL interactions above the dark overlay */} +
setBlinkTrigger(prev => prev + 1)} + /> + + {/* Large radial glow - positioned around target element */} +
+ + {/* Additional outer glow layer */} +
+ + {/* Extra large ambient glow */} +
+ + {/* Floating Text */} + {showText && ( +
+
+ {/* Main text */} +

+ {isToolbarStep + ? "Use these tools to design and interact with your project." + : isLeftPanelStep + ? "Access your layers, branding, pages, and more from here." + : isModeToggleStep + ? "Switch between Design and Preview modes to edit or view your project." + : isTopRightStep + ? "Invite colleagues or publish your work to share it with the world." + : "Type your first message here to begin designing your project." + } +

+ + {/* Buttons */} +
+ {!isTopRightStep && ( + + )} + + +
+ +
+
+ )} + + + + ); +}; diff --git a/apps/web/client/src/components/onboarding/red-glow.tsx b/apps/web/client/src/components/onboarding/red-glow.tsx new file mode 100644 index 0000000000..c4e4d34bba --- /dev/null +++ b/apps/web/client/src/components/onboarding/red-glow.tsx @@ -0,0 +1,127 @@ +'use client'; + +import { cn } from '@onlook/ui/utils'; +import { useEffect, useState } from 'react'; + +interface RedGlowProps { + children: React.ReactNode; + isActive?: boolean; + text?: string; + className?: string; + onComplete?: () => void; +} + +export const RedGlow = ({ + children, + isActive = false, + text = "Welcome to Onlook! Start by typing your first message here.", + className, + onComplete +}: RedGlowProps) => { + const [showGlow, setShowGlow] = useState(false); + const [showText, setShowText] = useState(false); + + useEffect(() => { + if (isActive) { + // Delay the glow appearance for a smooth entrance + const glowTimer = setTimeout(() => setShowGlow(true), 300); + // Delay the text appearance slightly after the glow + const textTimer = setTimeout(() => setShowText(true), 800); + + return () => { + clearTimeout(glowTimer); + clearTimeout(textTimer); + }; + } else { + setShowGlow(false); + setShowText(false); + } + }, [isActive]); + + const handleComplete = () => { + setShowGlow(false); + setShowText(false); + onComplete?.(); + }; + + if (!isActive) { + return <>{children}; + } + + return ( +
+ {/* Large Red Glow Effect - extends far beyond boundaries */} +
+ + {/* Even larger outer glow layer */} +
+ + {/* Extra large ambient glow */} +
+ + {/* Floating Text */} + {showText && ( +
+
+ {/* Text background with subtle glow */} +
+
+ + {/* Main text */} +

+ {text} +

+ + {/* Subtle animation for the text */} +
+
+
+ )} + + {/* Skip button */} + {showText && ( + + )} + + {/* The actual content */} +
+ {children} +
+
+ ); +}; From 47439da26abf98526d850917831a458c271863b4 Mon Sep 17 00:00:00 2001 From: Daniel R Farrell Date: Tue, 30 Sep 2025 21:12:49 -0700 Subject: [PATCH 2/2] Tweaks and improvements to the onboarding overlay --- .../right-panel/chat-tab/chat-input/index.tsx | 91 +++++-- .../onboarding/onboarding-overlay.tsx | 236 +++++++++--------- 2 files changed, 182 insertions(+), 145 deletions(-) diff --git a/apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx b/apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx index 2a95b739e9..079e2f9196 100644 --- a/apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx +++ b/apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx @@ -19,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'; @@ -45,9 +46,11 @@ export const ChatInput = observer(({ const { isActive, currentStep } = useOnboarding(); const t = useTranslations(); const textareaRef = useRef(null); + const containerRef = useRef(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; @@ -82,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]); + useEffect(() => { const handleGlobalKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter' && suggestionRef.current?.handleEnterSelection()) { @@ -313,29 +340,37 @@ export const ChatInput = observer(({ }; - return ( -
{ - handleDrop(e); - setIsDragging(false); - }} - onDragOver={handleDragOver} - onDragEnter={(e) => { - e.preventDefault(); - handleDragStateChange(true, e); - }} - onDragLeave={(e) => { - if (!e.currentTarget.contains(e.relatedTarget as Node)) { - handleDragStateChange(false, e); - } - }} - > + const chatInputContent = (isPortal = false) => ( +
{ + handleDrop(e); + setIsDragging(false); + }} + onDragOver={handleDragOver} + onDragEnter={(e) => { + e.preventDefault(); + handleDragStateChange(true, e); + }} + onDragLeave={(e) => { + if (!e.currentTarget.contains(e.relatedTarget as Node)) { + handleDragStateChange(false, e); + } + }} + >
); + + return ( + <> + {chatInputContent(false)} + {isOnboardingChatStep && portalPosition && typeof window !== 'undefined' && createPortal( + chatInputContent(true), + document.body + )} + + ); }); diff --git a/apps/web/client/src/components/onboarding/onboarding-overlay.tsx b/apps/web/client/src/components/onboarding/onboarding-overlay.tsx index 3ed58bc81a..8b0ec758bb 100644 --- a/apps/web/client/src/components/onboarding/onboarding-overlay.tsx +++ b/apps/web/client/src/components/onboarding/onboarding-overlay.tsx @@ -16,85 +16,41 @@ export const OnboardingOverlay = () => { const [blinkTrigger, setBlinkTrigger] = useState(0); useEffect(() => { - if (isActive && currentStep === 0) { - // Find the chat input element - const chatInput = document.querySelector('[data-onboarding-target="chat-input"]'); - if (chatInput) { - setTargetElement(chatInput as HTMLElement); - - // Delay the glow appearance - const glowTimer = setTimeout(() => setShowGlow(true), 300); - const textTimer = setTimeout(() => setShowText(true), 800); - - return () => { - clearTimeout(glowTimer); - clearTimeout(textTimer); - }; - } - } else if (isActive && currentStep === 1) { - // Find the project toolbar element - const toolbar = document.querySelector('[data-onboarding-target="project-toolbar"]'); - if (toolbar) { - setTargetElement(toolbar as HTMLElement); - - // Delay the glow appearance - const glowTimer = setTimeout(() => setShowGlow(true), 300); - const textTimer = setTimeout(() => setShowText(true), 800); - - return () => { - clearTimeout(glowTimer); - clearTimeout(textTimer); - }; - } - } else if (isActive && currentStep === 2) { - // Find the left panel element - const leftPanel = document.querySelector('[data-onboarding-target="left-panel"]'); - if (leftPanel) { - setTargetElement(leftPanel as HTMLElement); - - // Delay the glow appearance - const glowTimer = setTimeout(() => setShowGlow(true), 300); - const textTimer = setTimeout(() => setShowText(true), 800); - - return () => { - clearTimeout(glowTimer); - clearTimeout(textTimer); - }; - } - } else if (isActive && currentStep === 3) { - // Find the mode toggle element - const modeToggle = document.querySelector('[data-onboarding-target="mode-toggle"]'); - if (modeToggle) { - setTargetElement(modeToggle as HTMLElement); - - // Delay the glow appearance - const glowTimer = setTimeout(() => setShowGlow(true), 300); - const textTimer = setTimeout(() => setShowText(true), 800); - - return () => { - clearTimeout(glowTimer); - clearTimeout(textTimer); - }; - } - } else if (isActive && currentStep === 4) { - // Find the top-right actions element - const topRightActions = document.querySelector('[data-onboarding-target="top-right-actions"]'); - if (topRightActions) { - setTargetElement(topRightActions as HTMLElement); - - // Delay the glow appearance - const glowTimer = setTimeout(() => setShowGlow(true), 300); - const textTimer = setTimeout(() => setShowText(true), 800); - - return () => { - clearTimeout(glowTimer); - clearTimeout(textTimer); - }; - } - } else { + const stepTargets: Record = { + 0: 'chat-input', + 1: 'project-toolbar', + 2: 'left-panel', + 3: 'mode-toggle', + 4: 'top-right-actions', + }; + + if (!isActive) { + setShowGlow(false); + setShowText(false); + setTargetElement(null); + return; + } + + const targetSelector = stepTargets[currentStep]; + if (!targetSelector) { setShowGlow(false); setShowText(false); setTargetElement(null); + return; + } + + const element = document.querySelector(`[data-onboarding-target="${targetSelector}"]`); + if (element) { + setTargetElement(element as HTMLElement); + const glowTimer = setTimeout(() => setShowGlow(true), 300); + const textTimer = setTimeout(() => setShowText(true), 800); + + return () => { + clearTimeout(glowTimer); + clearTimeout(textTimer); + }; + } else { + setTargetElement(null); } }, [isActive, currentStep]); @@ -129,7 +85,7 @@ export const OnboardingOverlay = () => { setIsFadingOut(true); }; - if (!isActive || (currentStep !== 0 && currentStep !== 1 && currentStep !== 2 && currentStep !== 3 && currentStep !== 4) || !targetElement) { + if (!isActive || !targetElement) { return null; } @@ -137,40 +93,85 @@ export const OnboardingOverlay = () => { const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; - // For step 1 (toolbar), position the glow below the toolbar const isToolbarStep = currentStep === 1; - // For step 2 (left panel), position the glow on the left side const isLeftPanelStep = currentStep === 2; - // For step 3 (mode toggle), position the glow around the toggle const isModeToggleStep = currentStep === 3; - // For step 4 (top-right actions), position the glow around the actions const isTopRightStep = currentStep === 4; + + const stepContent: Record = { + 0: "Type your first message here to begin designing your project.", + 1: "Use these tools to design and interact with your project.", + 2: "Access your layers, branding, pages, support, and more from here.", + 3: "Switch between Design and Preview modes to edit or view your project.", + 4: "Invite colleagues or publish your work to share it with the world.", + }; + + const glowConfig: Record = { + 0: { + top: rect.top - 50, + left: rect.left - 100, + zIndex: "z-[60]", + width: { primary: rect.width + 100, outer: rect.width + 300, ambient: rect.width + 400 }, + height: { primary: rect.height + 100, outer: rect.height + 200, ambient: rect.height + 300 }, + textLeft: centerX - 150, + textTop: centerY - 200, + }, + 1: { + top: rect.bottom - 80, + left: rect.left - 100, + zIndex: "z-10", + width: { primary: rect.width + 100, outer: rect.width + 300, ambient: rect.width + 400 }, + height: { primary: rect.height + 100, outer: rect.height + 200, ambient: rect.height + 300 }, + textLeft: centerX - 150, + textTop: rect.top - 120, + }, + 2: { + top: rect.top - 50, + left: rect.left - 100, + zIndex: "z-10", + width: { primary: rect.width + 100, outer: rect.width + 300, ambient: rect.width + 400 }, + height: { primary: rect.height + 100, outer: rect.height + 200, ambient: rect.height + 300 }, + textLeft: rect.right + 20, + textTop: rect.top + 80, + }, + 3: { + top: rect.top - 30, + left: rect.left - 50, + zIndex: "z-10", + width: { primary: rect.width + 100, outer: rect.width + 200, ambient: rect.width + 300 }, + height: { primary: rect.height + 60, outer: rect.height + 160, ambient: rect.height + 260 }, + textLeft: centerX - 150, + textTop: rect.bottom + 30, + }, + 4: { + top: rect.top - 30, + left: rect.left - 50, + zIndex: "z-[60]", + width: { primary: rect.width, outer: rect.width + 100, ambient: rect.width + 200 }, + height: { primary: rect.height + 60, outer: rect.height + 160, ambient: rect.height + 260 }, + textLeft: rect.right - 350, + textTop: rect.bottom + 50, + }, + }; - const glowTop = isToolbarStep - ? rect.bottom - 80 - : isLeftPanelStep - ? rect.top - 50 - : isModeToggleStep - ? rect.top - 30 - : isTopRightStep - ? rect.top - 30 - : rect.top - 50; - const glowLeft = isToolbarStep - ? rect.left - 100 - : isLeftPanelStep - ? rect.left - 100 - : isModeToggleStep - ? rect.left - 50 - : isTopRightStep - ? rect.left - 50 - : rect.left - 100; + const config = glowConfig[currentStep] || glowConfig[0]!; + const glowTop = config.top; + const glowLeft = config.left; return ( <> {/* Dark overlay over the rest of the interface - blocks all interactions */}
{ "blur-md will-change-[opacity,transform]", "transition-[opacity,left,top,width,height] duration-700 ease-in-out", showGlow && !isFadingOut ? "opacity-100" : "opacity-0", - isTopRightStep ? "z-[60]" : (isToolbarStep || isLeftPanelStep || isModeToggleStep) ? "z-10" : "z-[60]" + config.zIndex )} style={{ left: isTopRightStep ? glowLeft + 50 : glowLeft, top: glowTop, - width: isModeToggleStep ? rect.width + 100 : isTopRightStep ? rect.width : rect.width + 100, - height: (isModeToggleStep || isTopRightStep) ? rect.height + 60 : rect.height + 100, + width: config.width.primary, + height: config.height.primary, transform: 'scale(1.1) translateZ(0)', }} /> @@ -207,18 +208,18 @@ export const OnboardingOverlay = () => {
@@ -232,13 +233,13 @@ export const OnboardingOverlay = () => { "blur-xl will-change-[opacity,transform]", "transition-[opacity,left,top,width,height] duration-1000 ease-in-out", showGlow && !isFadingOut ? "opacity-60" : "opacity-0", - isTopRightStep ? "z-[60]" : (isToolbarStep || isLeftPanelStep || isModeToggleStep) ? "z-10" : "z-[60]" + config.zIndex )} style={{ left: isTopRightStep ? glowLeft - 50 : glowLeft - 100, top: glowTop - 100, - width: isModeToggleStep ? rect.width + 300 : isTopRightStep ? rect.width + 200 : rect.width + 400, - height: (isModeToggleStep || isTopRightStep) ? rect.height + 260 : rect.height + 300, + width: config.width.ambient, + height: config.height.ambient, transform: 'scale(1.3) translateZ(0)', }} /> @@ -251,8 +252,8 @@ export const OnboardingOverlay = () => { "transition-opacity duration-500 ease-in-out opacity-100" )} style={{ - left: isLeftPanelStep ? rect.right + 20 : isTopRightStep ? rect.right - 350 : isModeToggleStep ? centerX - 150 : centerX - 150, - top: isToolbarStep ? rect.top - 120 : isLeftPanelStep ? rect.top + 80 : isModeToggleStep ? rect.bottom + 30 : isTopRightStep ? rect.bottom + 50 : centerY - 200, + left: config.textLeft, + top: config.textTop, width: 300, pointerEvents: 'auto', }} @@ -267,16 +268,7 @@ export const OnboardingOverlay = () => { isLeftPanelStep ? "text-left" : "text-center", "animate-in fade-in slide-in-from-bottom-2 duration-500" )}> - {isToolbarStep - ? "Use these tools to design and interact with your project." - : isLeftPanelStep - ? "Access your layers, branding, pages, and more from here." - : isModeToggleStep - ? "Switch between Design and Preview modes to edit or view your project." - : isTopRightStep - ? "Invite colleagues or publish your work to share it with the world." - : "Type your first message here to begin designing your project." - } + {stepContent[currentStep]}

{/* Buttons */}