From 8ee29a86dc83ce26a3abb3572ef10e0cbdd0ddee Mon Sep 17 00:00:00 2001 From: ria gracy Date: Wed, 27 May 2026 12:30:05 +0530 Subject: [PATCH 1/2] Improve onboarding tour responsiveness and stability --- src/components/OnboardingTour.tsx | 186 +++++++++++++++++++----------- src/components/PresetSelector.tsx | 2 +- src/components/VideoEditor.tsx | 14 +-- 3 files changed, 127 insertions(+), 75 deletions(-) diff --git a/src/components/OnboardingTour.tsx b/src/components/OnboardingTour.tsx index c1cef066..db63fbbd 100644 --- a/src/components/OnboardingTour.tsx +++ b/src/components/OnboardingTour.tsx @@ -1,6 +1,11 @@ "use client"; -import { useEffect, useRef, useState, useCallback } from "react"; +import { + useEffect, + useRef, + useState, + useCallback, +} from "react"; import { createPortal } from "react-dom"; const TOUR_KEY = "reframe_onboarding_complete"; @@ -40,7 +45,7 @@ const TOUR_STEPS: TourStep[] = [ title: "Export your video", description: "Click Export (or press ⌘↵) to process your video locally — nothing ever leaves your device.", - position: "top", + position: "bottom", }, ]; @@ -69,30 +74,48 @@ function getTooltipStyle( width: rect.width + PADDING * 2, height: rect.height + PADDING * 2, }; +const viewportWidth = + typeof window !== "undefined" ? window.innerWidth : 1024; + const isMobile = viewportWidth < 1024; - switch (position) { - case "top": - return { - top: sr.top - th - TOOLTIP_OFFSET, - left: sr.left + sr.width / 2 - tw / 2, - }; - case "left": - return { - top: sr.top + sr.height / 2 - th / 2, - left: sr.left - tw - TOOLTIP_OFFSET, - }; - case "right": - return { - top: sr.top + sr.height / 2 - th / 2, - left: sr.left + sr.width + TOOLTIP_OFFSET, - }; - case "bottom": - default: - return { - top: sr.top + sr.height + TOOLTIP_OFFSET, - left: sr.left + sr.width / 2 - tw / 2, - }; - } +const viewportHeight = + typeof window !== "undefined" ? window.innerHeight : 768; + +const resolvedPosition = isMobile ? "bottom" : position; + +let top = 0; +let left = 0; + +switch (resolvedPosition) { + case "top": + top = sr.top - th - TOOLTIP_OFFSET; + left = sr.left + sr.width / 2 - tw / 2; + break; + + case "left": + top = sr.top + sr.height / 2 - th / 2; + left = sr.left - tw - TOOLTIP_OFFSET; + break; + + case "right": + top = sr.top + sr.height / 2 - th / 2; + left = sr.left + sr.width + TOOLTIP_OFFSET; + break; + + case "top": + default: + top = sr.top + sr.height + TOOLTIP_OFFSET; + left = sr.left + sr.width / 2 - tw / 2; + break; +} + +top = Math.max( + 16, + Math.min(top, viewportHeight - th - 16) +); +left = Math.max(16, Math.min(left, viewportWidth - tw - 16)); + +return { top, left }; } interface SpotlightProps { @@ -166,7 +189,15 @@ function Tooltip({ onSkip, tooltipRef, }: TooltipProps) { - const style = getTooltipStyle(rect, step.position, tooltipRef); + const [ready, setReady] = useState(false); + +useEffect(() => { + setReady(true); +}, []); + +const style = ready + ? getTooltipStyle(rect, step.position, tooltipRef) + : { opacity: 0 }; const isLast = stepIndex === totalSteps - 1; return ( @@ -175,7 +206,7 @@ function Tooltip({ role="dialog" aria-modal="true" aria-label={`Onboarding step ${stepIndex + 1} of ${totalSteps}: ${step.title}`} - className="fixed z-[9999] w-80 rounded-xl shadow-2xl border + className="fixed z-[9999] w-[min(20rem,calc(100vw-2rem))] rounded-xl shadow-2xl border bg-[var(--surface)] border-[var(--border)] text-[var(--text)] @@ -209,16 +240,13 @@ function Tooltip({ > Skip tour - @@ -243,20 +271,29 @@ export default function OnboardingTour() { const measureTarget = useCallback((id: string): Promise => { return new Promise((resolve) => { const attempt = (tries: number) => { - const el = document.getElementById(id); - if (el) { - el.scrollIntoView({ behavior: "smooth", block: "center" }); - setTimeout(() => { - const r = el.getBoundingClientRect(); - resolve({ - top: r.top, - left: r.left, - width: r.width, - height: r.height, - }); - }, 400); // wait for scroll to finish - return; - } + const el = document.getElementById(id); + +if (el) { + if (tries === 5) { + el.scrollIntoView({ + block: "center", + behavior: "smooth", + }); + } + + setTimeout(() => { + const r = el.getBoundingClientRect(); + + resolve({ + top: r.top, + left: r.left, + width: r.width, + height: r.height, + }); + }, 400); + + return; +} if (tries <= 0) { resolve(null); return; @@ -303,7 +340,6 @@ export default function OnboardingTour() { if (cancelled) return; if (rect) { setTargetRect(rect); - setTimeout(() => tooltipRef.current?.focus(), 50); retryCount = 0; } else if (retryCount < maxRetries) { retryCount++; @@ -331,22 +367,38 @@ export default function OnboardingTour() { // 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; - const remeasure = () => { - cancelAnimationFrame(rafId); - rafId = requestAnimationFrame(() => { - measureTarget(TOUR_STEPS[stepIndex]?.targetId ?? "").then(setTargetRect); + if (!visible) return; + + let resizeTimer: number; + + const remeasure = () => { + clearTimeout(resizeTimer); + + resizeTimer = window.setTimeout(() => { + const el = document.getElementById( + TOUR_STEPS[stepIndex]?.targetId ?? "" + ); + + if (!el) return; + + const r = el.getBoundingClientRect(); + + setTargetRect({ + top: r.top, + left: r.left, + width: r.width, + height: r.height, }); - }; - window.addEventListener("resize", remeasure); - window.addEventListener("scroll", remeasure, true); - return () => { - cancelAnimationFrame(rafId); - window.removeEventListener("resize", remeasure); - window.removeEventListener("scroll", remeasure, true); - }; - }, [visible, stepIndex, measureTarget]); + }, 100); + }; + + window.addEventListener("resize", remeasure); + + return () => { + clearTimeout(resizeTimer); + window.removeEventListener("resize", remeasure); + }; +}, [visible, stepIndex]); // Keyboard support useEffect(() => { diff --git a/src/components/PresetSelector.tsx b/src/components/PresetSelector.tsx index fd129ab2..dcb65280 100644 --- a/src/components/PresetSelector.tsx +++ b/src/components/PresetSelector.tsx @@ -139,7 +139,7 @@ export default function PresetSelector({ recipe, onChange }: Props) { ); return ( -
+
{/* Quick-action row */}
{QUICK_ACTIONS.map(({ preset, label, platform, icon }) => { diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx index 1e4e9f0d..37a2566b 100644 --- a/src/components/VideoEditor.tsx +++ b/src/components/VideoEditor.tsx @@ -77,7 +77,7 @@ function AccordionSection({ aria-expanded={isOpen} aria-controls={`${id}-panel`} onClick={onToggle} - className="w-full flex items-center justify-between px-3 py-2 text-left hover:bg-[var(--border)] transition-colors duration-150" + className="w-full flex items-center justify-between px-4 py-3 text-left hover:bg-[var(--border)] transition-colors duration-150" >
{icon} @@ -228,7 +228,7 @@ export default function VideoEditor() { const [selectedTextId, setSelectedTextId] = useState(null); const [openSections, setOpenSections] = useState({ resize: true, - trim: false, + trim: true, rotation: false, text: false, audio: false, @@ -324,8 +324,8 @@ export default function VideoEditor() { {status === "error" && `Export failed: ${error}`}
-
-
+
+
-
+
} @@ -627,7 +627,7 @@ export default function VideoEditor() {
{!file && ( @@ -699,7 +699,7 @@ export default function VideoEditor() { title={!file ? "Upload a video to enable export" : undefined} className={cn( "w-full flex items-center justify-center gap-3 py-5 min-h-[44px] rounded-xl", - "font-display text-2xl tracking-widest transition-all duration-200", + "font-display text-xl md:text-2xl tracking-widest transition-all duration-200", file && !isProcessing ? "bg-[var(--accent)] hover:bg-[var(--accent-hover)] hover:scale-[1.02] text-white shadow-[var(--shadow)] active:scale-[0.98] cursor-pointer" : "bg-[var(--border)] text-[var(--muted)] cursor-not-allowed" From 598d7e7b368e045533bbca1c71b71f9b6fd04228 Mon Sep 17 00:00:00 2001 From: ria gracy Date: Thu, 4 Jun 2026 13:02:21 +0530 Subject: [PATCH 2/2] Add pulse animation to export button when ready --- src/components/VideoEditor.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx index 28771a40..616cd5b9 100644 --- a/src/components/VideoEditor.tsx +++ b/src/components/VideoEditor.tsx @@ -760,12 +760,13 @@ export default function VideoEditor() { className={cn( "w-full flex items-center justify-center gap-3 py-5 min-h-[44px] rounded-xl", "font-display text-xl md:text-2xl tracking-widest transition-all duration-200", + file && status === "idle" && "motion-safe:animate-pulse", file && !isProcessing ? "bg-[var(--accent)] hover:bg-[var(--accent-hover)] hover:scale-[1.02] text-white shadow-[var(--shadow)] active:scale-[0.98] cursor-pointer" : "bg-[var(--border)] text-[var(--muted)] cursor-not-allowed" )} > - + {isProcessing ? "PROCESSING" : "EXPORT"}