From 470be869b77ee76eca3c675c966e6a31484ae1ca Mon Sep 17 00:00:00 2001 From: Varsha Date: Wed, 3 Jun 2026 21:00:54 +0000 Subject: [PATCH 1/2] fix: fix onboarding tooltip misalignment and cut-off on viewport edge --- src/components/OnboardingTour.tsx | 59 ++++++++++++++++++++----------- src/components/VideoEditor.tsx | 2 +- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/src/components/OnboardingTour.tsx b/src/components/OnboardingTour.tsx index c1cef066..cdcba79f 100644 --- a/src/components/OnboardingTour.tsx +++ b/src/components/OnboardingTour.tsx @@ -26,7 +26,7 @@ const TOUR_STEPS: TourStep[] = [ title: "Pick an output format", description: "Choose a preset optimised for your platform — Instagram, YouTube, TikTok and more.", - position: "left", + position: "bottom", }, { targetId: "trim", @@ -34,6 +34,7 @@ const TOUR_STEPS: TourStep[] = [ description: "After uploading, set in/out points and tweak colour in the controls that appear on the left.", position: "left", + requiresFile: true, }, { targetId: "export-button", @@ -41,10 +42,11 @@ const TOUR_STEPS: TourStep[] = [ description: "Click Export (or press ⌘↵) to process your video locally — nothing ever leaves your device.", position: "top", + requiresFile: true, }, ]; -const PADDING = 12; // spotlight padding around target element +const PADDING = 12; const TOOLTIP_OFFSET = 16; interface Rect { @@ -70,29 +72,48 @@ function getTooltipStyle( height: rect.height + PADDING * 2, }; + let style: React.CSSProperties; + switch (position) { case "top": - return { + style = { top: sr.top - th - TOOLTIP_OFFSET, left: sr.left + sr.width / 2 - tw / 2, }; + break; case "left": - return { + style = { top: sr.top + sr.height / 2 - th / 2, left: sr.left - tw - TOOLTIP_OFFSET, }; + break; case "right": - return { + style = { top: sr.top + sr.height / 2 - th / 2, left: sr.left + sr.width + TOOLTIP_OFFSET, }; + break; case "bottom": default: - return { + style = { top: sr.top + sr.height + TOOLTIP_OFFSET, left: sr.left + sr.width / 2 - tw / 2, }; + break; + } + + const vw = window.innerWidth; + const vh = window.innerHeight; + const margin = 8; + + if (typeof style.left === "number") { + style.left = Math.max(margin, Math.min(style.left, vw - tw - margin)); } + if (typeof style.top === "number") { + style.top = Math.max(margin, Math.min(style.top, vh - th - margin)); + } + + return style; } interface SpotlightProps { @@ -132,7 +153,6 @@ function Spotlight({ rect }: SpotlightProps) { fill="rgba(0,0,0,0.65)" mask="url(#spotlight-mask)" /> - {/* Highlight ring */} - {/* Progress bar */}
- {/* Step counter */}

Step {stepIndex + 1} of {totalSteps}

@@ -227,7 +245,7 @@ function Tooltip({ ); } -export default function OnboardingTour() { +export default function OnboardingTour({ hasFile = false }: { hasFile?: boolean }) { const [stepIndex, setStepIndex] = useState(0); const [visible, setVisible] = useState(false); const [targetRect, setTargetRect] = useState(null); @@ -254,7 +272,7 @@ export default function OnboardingTour() { width: r.width, height: r.height, }); - }, 400); // wait for scroll to finish + }, 400); return; } if (tries <= 0) { @@ -267,7 +285,6 @@ export default function OnboardingTour() { }); }, []); - // Initialise on mount useEffect(() => { if (localStorage.getItem(TOUR_KEY)) return; const t = setTimeout(async () => { @@ -280,7 +297,6 @@ export default function OnboardingTour() { return () => clearTimeout(t); }, [measureTarget]); - // Measure target whenever step changes (skip on first render — init effect handles that) useEffect(() => { if (!visible) return; if (isFirstRender.current) { @@ -292,8 +308,14 @@ export default function OnboardingTour() { return; } + if (currentStep.requiresFile && !hasFile) { + if (stepIndex < TOUR_STEPS.length - 1) setStepIndex((i) => i + 1); + else dismiss(); + return; + } + let retryCount = 0; - const maxRetries = 10; // Retry up to ~5s with 500ms delays + const maxRetries = 10; let retryTimer: number | null = null; let cancelled = false; @@ -309,7 +331,6 @@ export default function OnboardingTour() { retryCount++; retryTimer = window.setTimeout(tryMeasure, 500); } else { - // If we've retried enough, fallback to advancing or dismissing if (stepIndex < TOUR_STEPS.length - 1) setStepIndex((i) => i + 1); else dismiss(); } @@ -326,10 +347,8 @@ export default function OnboardingTour() { cancelled = true; if (retryTimer !== null) clearTimeout(retryTimer); }; - }, [stepIndex, visible, measureTarget, dismiss, currentStep]); + }, [stepIndex, visible, measureTarget, dismiss, currentStep, hasFile]); - // Re-measure on resize or scroll so spotlight stays anchored to target. - // requestAnimationFrame prevents layout thrashing on rapid scroll/resize events. useEffect(() => { if (!visible) return; let rafId: number; @@ -348,7 +367,6 @@ export default function OnboardingTour() { }; }, [visible, stepIndex, measureTarget]); - // Keyboard support useEffect(() => { if (!visible) return; const onKey = (e: KeyboardEvent) => { @@ -366,8 +384,7 @@ export default function OnboardingTour() { return createPortal( <> - {/* Clickable backdrop to skip */} -