diff --git a/front/src/components/tutorial/TutorialOverlay.js b/front/src/components/tutorial/TutorialOverlay.js index 91a7f0d..b34cf25 100644 --- a/front/src/components/tutorial/TutorialOverlay.js +++ b/front/src/components/tutorial/TutorialOverlay.js @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useCallback, useState } from 'react'; import { View, Text, StyleSheet, Modal, TouchableOpacity, - Animated, useWindowDimensions, Platform, + Animated, useWindowDimensions, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useTutorial } from '../../context/TutorialContext'; @@ -87,11 +87,16 @@ export default function TutorialOverlay({ navigation }) { isLastStep, targets, nextStep, dismiss, } = useTutorial(); - const { width: W, height: H } = useWindowDimensions(); - // Sur web desktop, le portail Modal est recadré à 430 px centré (CSS dans index.js). - // Les coordonnées renvoyées par measure() sont en coords viewport, donc on soustrait - // l'offset gauche du conteneur pour obtenir des coordonnées locales au portail. - const modalLeft = Platform.OS === 'web' && W > 430 ? (W - 430) / 2 : 0; + const { height: H } = useWindowDimensions(); + // Sur web, le de react-native-web portale dans un
en + // position:fixed attaché à document.body — INDÉPENDANT du conteneur #root + // (recentré à 430 px sur desktop via web/index.html). measure() renvoie déjà + // des coordonnées relatives au viewport complet, le même repère que celui + // utilisé par ce Modal plein-viewport : aucune correction d'offset n'est à + // appliquer. Un ancien code soustrayait un offset "modalLeft" en supposant + // le Modal lui-même recentré à 430 px — ce qui décalait le spotlight de + // plusieurs centaines de pixels sur une fenêtre desktop large (le cadre se + // retrouvait loin de la carte réellement visible, recentrée dans #root). const slideY = useRef(new Animated.Value(24)).current; const opacity = useRef(new Animated.Value(0)).current; @@ -120,7 +125,7 @@ export default function TutorialOverlay({ navigation }) { const hasSpot = !!targetRect; const sp = hasSpot ? { - x: (targetRect.x - modalLeft) - SPOTLIGHT_PADDING, + x: targetRect.x - SPOTLIGHT_PADDING, y: targetRect.y - SPOTLIGHT_PADDING, w: targetRect.width + SPOTLIGHT_PADDING * 2, h: targetRect.height + SPOTLIGHT_PADDING * 2, diff --git a/front/src/context/TutorialContext.js b/front/src/context/TutorialContext.js index a6384b4..87bd0b8 100644 --- a/front/src/context/TutorialContext.js +++ b/front/src/context/TutorialContext.js @@ -225,8 +225,17 @@ export function useTutorialTarget(key) { }); }, [key, registerTarget]); + // Plusieurs mesures échelonnées sur ~1s : sur un écran atteint via une + // transition de stack (slide horizontal, @react-navigation/stack), la + // position mesurée juste après le premier layout peut encore refléter un + // état transitoire (mi-glissement, décalé vers la droite) plutôt que la + // position de repos finale — measure() renvoie la position réellement + // rendue à l'écran, transform en cours inclus. Chaque mesure écrase la + // précédente (registerTarget), donc la dernière — une fois la transition + // calmée — corrige automatiquement le spotlight, quel que soit le + // mécanisme ou la durée exacte de la transition. const onLayout = useCallback(() => { - setTimeout(measure, 80); + [80, 250, 450, 700, 1000].forEach((delay) => setTimeout(measure, delay)); }, [measure]); return { ref, onLayout, remeasure: measure }; diff --git a/front/src/screens/Profile/InventoryScreen.js b/front/src/screens/Profile/InventoryScreen.js index 43cc559..6796bdc 100644 --- a/front/src/screens/Profile/InventoryScreen.js +++ b/front/src/screens/Profile/InventoryScreen.js @@ -46,17 +46,30 @@ export default function InventoryScreen({ navigation }) { // ─── Tutorial (chapitre Inventaire) ────────────────────────────────────────── const { pendingChapterId, activeChapterId, startChapter } = useTutorial(); - const { ref: chestRef, onLayout: onChestLayout } = useTutorialTarget('inventory_chest'); + const { ref: chestRef, onLayout: onChestLayout, remeasure: rChest } = useTutorialTarget('inventory_chest'); useFocusEffect(useCallback(() => { refetch(); }, [refetch])); useFocusEffect( useCallback(() => { - if (pendingChapterId === 'inventory') { - const t = setTimeout(() => startChapter('inventory'), 400); - return () => clearTimeout(t); - } - }, [pendingChapterId, startChapter]), + if (pendingChapterId !== 'inventory') return; + // ProfileStack (@react-navigation/stack) glisse cet écran depuis la + // droite : mesurer la cible pendant la transition capture sa position + // mi-glissement → spotlight décalé à droite, hors-cadre. On attend la + // fin RÉELLE de la transition (transitionEnd) avant de re-mesurer et de + // démarrer le chapitre ; le setTimeout reste un filet de sécurité si + // l'event ne se déclenche pas (écran déjà stable, pas de transition). + let started = false; + const launch = () => { + if (started) return; + started = true; + rChest(); + setTimeout(() => startChapter('inventory'), 50); + }; + const unsub = navigation.addListener('transitionEnd', launch); + const t = setTimeout(launch, 500); + return () => { unsub(); clearTimeout(t); }; + }, [pendingChapterId, startChapter, navigation, rChest]), ); const inventory = user?.inventory ?? [];