fix: Tour-Tooltip auf feste Breite begrenzen

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 <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 13:08:31 +02:00
parent 520ba766a3
commit b7e2d470a9
3 changed files with 44 additions and 14 deletions
+6 -4
View File
@@ -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 {
+32 -10
View File
@@ -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() {
/>
)}
<div className={`app-tour-tooltip${centered ? ' centered' : ''}`} style={tooltipStyle}>
<div className={tooltipClassName} style={tooltipStyle}>
<button type="button" className="app-tour-close" onClick={skipTour} aria-label={t('tour.skip')}>
<X size={18} />
</button>
+6
View File
@@ -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<TourNavigation | null>(null)
const demoContextRef = useRef<DemoTourContext | null>(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
]
)