From b7e2d470a9672e485f8c30d82cc4d1df634326b6 Mon Sep 17 00:00:00 2001 From: elpatron Date: Sun, 31 May 2026 13:08:31 +0200 Subject: [PATCH] fix: Tour-Tooltip auf feste Breite begrenzen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entfernt left+right-Stretching in CSS, positioniert das Tooltip horizontal am Spotlight und misst Ziele nach Navigation mit verzögerten Retries. Co-authored-by: Cursor --- client/src/App.css | 10 +++--- client/src/components/AppTourOverlay.tsx | 42 ++++++++++++++++++------ client/src/context/AppTourContext.tsx | 6 ++++ 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/client/src/App.css b/client/src/App.css index 40888ca..df63536 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -4276,10 +4276,12 @@ body.app-tour-active .app-tour-target-active { } .app-tour-tooltip:not(.centered) { - left: max(16px, env(safe-area-inset-left, 0px)); - right: max(16px, env(safe-area-inset-right, 0px)); - width: auto; - max-width: none; + left: 50%; + transform: translateX(-50%); +} + +.app-tour-tooltip:not(.centered).app-tour-tooltip--anchored { + transform: none; } .app-tour-tooltip.centered { diff --git a/client/src/components/AppTourOverlay.tsx b/client/src/components/AppTourOverlay.tsx index 8e06b04..ce07736 100644 --- a/client/src/components/AppTourOverlay.tsx +++ b/client/src/components/AppTourOverlay.tsx @@ -18,6 +18,14 @@ interface SpotlightRect { const TOOLTIP_EDGE_MARGIN = 16 const TOOLTIP_ESTIMATED_HEIGHT = 240 +const TOOLTIP_WIDTH = 420 + +function computeTooltipLeft(spotlight: SpotlightRect): number { + const tooltipWidth = Math.min(TOOLTIP_WIDTH, window.innerWidth - TOOLTIP_EDGE_MARGIN * 2) + const ideal = spotlight.left + spotlight.width / 2 - tooltipWidth / 2 + const maxLeft = window.innerWidth - TOOLTIP_EDGE_MARGIN - tooltipWidth + return Math.max(TOOLTIP_EDGE_MARGIN, Math.min(ideal, maxLeft)) +} function buildCutoutClipPath(rect: SpotlightRect): string { const right = rect.left + rect.width @@ -51,6 +59,7 @@ export default function AppTourOverlay() { currentStepId, currentStepIndex, totalSteps, + layoutTick, nextStep, prevStep, skipTour @@ -66,7 +75,10 @@ export default function AppTourOverlay() { return } + let cancelled = false + const updateSpotlight = () => { + if (cancelled) return const selector = getTourTargetSelector(currentStepId) if (!selector) { setSpotlight(null) @@ -78,6 +90,10 @@ export default function AppTourOverlay() { return } const rect = el.getBoundingClientRect() + if (rect.width <= 0 || rect.height <= 0) { + setSpotlight(null) + return + } const padding = 8 setSpotlight({ top: Math.max(8, rect.top - padding), @@ -90,19 +106,17 @@ export default function AppTourOverlay() { updateSpotlight() window.addEventListener('resize', updateSpotlight) window.addEventListener('scroll', updateSpotlight, true) - const retryDelay = getTourTargetRetryDelay(currentStepId) - const timer = window.setTimeout(updateSpotlight, retryDelay) - const retryTimer = retryDelay > 120 - ? window.setTimeout(updateSpotlight, retryDelay + 180) - : undefined + + const retryDelays = [getTourTargetRetryDelay(currentStepId), 120, 280, 480] + const timers = retryDelays.map((delay) => window.setTimeout(updateSpotlight, delay)) return () => { - window.clearTimeout(timer) - if (retryTimer !== undefined) window.clearTimeout(retryTimer) + cancelled = true + for (const timer of timers) window.clearTimeout(timer) window.removeEventListener('resize', updateSpotlight) window.removeEventListener('scroll', updateSpotlight, true) } - }, [currentStepId, isActive]) + }, [currentStepId, isActive, layoutTick]) useEffect(() => { if (!isActive) return @@ -138,9 +152,17 @@ export default function AppTourOverlay() { const tooltipStyle = centered ? undefined : spotlight - ? { top: computeTooltipTop(spotlight) } + ? { top: computeTooltipTop(spotlight), left: computeTooltipLeft(spotlight) } : { top: '20%' } + const tooltipClassName = [ + 'app-tour-tooltip', + centered ? 'centered' : '', + !centered && spotlight ? 'app-tour-tooltip--anchored' : '' + ] + .filter(Boolean) + .join(' ') + const backdropStyle = spotlight && !centered ? { clipPath: buildCutoutClipPath(spotlight) } : undefined @@ -165,7 +187,7 @@ export default function AppTourOverlay() { /> )} -
+
diff --git a/client/src/context/AppTourContext.tsx b/client/src/context/AppTourContext.tsx index 414a278..9dc8fe9 100644 --- a/client/src/context/AppTourContext.tsx +++ b/client/src/context/AppTourContext.tsx @@ -51,6 +51,7 @@ interface AppTourContextValue { currentStepId: TourStepId | null currentStepIndex: number totalSteps: number + layoutTick: number startTour: (options?: { force?: boolean; demoMode?: boolean }) => void stopTour: () => void restartTour: () => void @@ -135,6 +136,7 @@ export function AppTourProvider({ children }: { children: ReactNode }) { const [stepIndex, setStepIndex] = useState(0) const [pendingAfterLogin, setPendingAfterLogin] = useState(false) const [isDemoTour, setIsDemoTour] = useState(false) + const [layoutTick, setLayoutTick] = useState(0) const navigationRef = useRef(null) const demoContextRef = useRef(null) const tourModeRef = useRef<{ demoMode: boolean }>({ demoMode: false }) @@ -276,6 +278,8 @@ export function AppTourProvider({ children }: { children: ReactNode }) { if (!stepId) return applyStepSideEffects(stepId) scrollToCurrentTarget(stepId) + const timer = window.setTimeout(() => setLayoutTick((tick) => tick + 1), 0) + return () => window.clearTimeout(timer) }, [isActive, isDemoTour, stepIndex, applyStepSideEffects, scrollToCurrentTarget]) const restartTour = useCallback(() => { @@ -318,6 +322,7 @@ export function AppTourProvider({ children }: { children: ReactNode }) { currentStepId, currentStepIndex: stepIndex, totalSteps: stepOrder.length, + layoutTick, startTour, stopTour, restartTour, @@ -342,6 +347,7 @@ export function AppTourProvider({ children }: { children: ReactNode }) { startTour, stepIndex, stepOrder.length, + layoutTick, stopTour ] )