This commit is contained in:
2026-03-29 10:26:38 -05:00
parent 05d4f6e78b
commit b1c99893a6
1628 changed files with 67782 additions and 60143 deletions

View File

@@ -6,7 +6,6 @@ import {
StyleSheet,
Dimensions,
Animated,
Platform,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useCoachMarks } from '../context/CoachMarksContext';
@@ -19,14 +18,13 @@ const TOOLTIP_VERTICAL_OFFSET = 32;
export const CoachMarksOverlay: React.FC = () => {
const { isActive, currentStep, steps, layouts, next, skip } = useCoachMarks();
const { isDarkMode, colorPalette } = useApp();
const { isDarkMode, colorPalette, t } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const fadeAnim = useRef(new Animated.Value(0)).current;
const scaleAnim = useRef(new Animated.Value(0.88)).current;
const pulseAnim = useRef(new Animated.Value(1)).current;
// Fade in when tour starts or step changes
useEffect(() => {
if (isActive) {
fadeAnim.setValue(0);
@@ -36,24 +34,22 @@ export const CoachMarksOverlay: React.FC = () => {
Animated.spring(scaleAnim, { toValue: 1, tension: 80, friction: 9, useNativeDriver: true }),
]).start();
// Pulse animation on highlight
Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, { toValue: 1.06, duration: 900, useNativeDriver: true }),
Animated.timing(pulseAnim, { toValue: 1, duration: 900, useNativeDriver: true }),
])
]),
).start();
} else {
pulseAnim.stopAnimation();
}
}, [isActive, currentStep]);
}, [currentStep, fadeAnim, isActive, pulseAnim, scaleAnim]);
if (!isActive || steps.length === 0) return null;
const step = steps[currentStep];
const layout = layouts[step.elementKey];
// Fallback wenn Element noch nicht gemessen
const highlight = layout
? {
x: layout.x - HIGHLIGHT_PADDING,
@@ -64,10 +60,13 @@ export const CoachMarksOverlay: React.FC = () => {
}
: { x: SCREEN_W / 2 - 40, y: SCREEN_H / 2 - 40, w: 80, h: 80, r: 40 };
// Tooltip-Position berechnen
const tooltipW = 260;
const tooltipMaxH = 140;
let tooltipX = Math.max(12, Math.min(SCREEN_W - tooltipW - 12, highlight.x + highlight.w / 2 - tooltipW / 2));
const tooltipX = Math.max(
12,
Math.min(SCREEN_W - tooltipW - 12, highlight.x + highlight.w / 2 - tooltipW / 2),
);
let tooltipY: number;
const spaceBelow = SCREEN_H - (highlight.y + highlight.h);
const spaceAbove = highlight.y;
@@ -80,39 +79,43 @@ export const CoachMarksOverlay: React.FC = () => {
if (tooltipY + tooltipMaxH > SCREEN_H - 60) tooltipY = highlight.y - tooltipMaxH - 24;
}
// Keep coachmark bubbles slightly higher to avoid overlap with bright focus circles.
tooltipY -= TOOLTIP_VERTICAL_OFFSET;
const minTooltipY = 24;
const maxTooltipY = SCREEN_H - tooltipMaxH - 24;
tooltipY = Math.max(minTooltipY, Math.min(maxTooltipY, tooltipY));
tooltipY = Math.max(24, Math.min(SCREEN_H - tooltipMaxH - 24, tooltipY));
const arrowPointsUp = tooltipY > highlight.y;
return (
<Animated.View style={[StyleSheet.absoluteFill, styles.root, { opacity: fadeAnim }]} pointerEvents="box-none">
{/* Dark overlay — 4 Rechtecke um die Aussparung */}
{/* Oben */}
<View style={[styles.overlay, { top: 0, left: 0, right: 0, height: Math.max(0, highlight.y) }]} />
{/* Links */}
<View style={[styles.overlay, {
top: highlight.y, left: 0, width: Math.max(0, highlight.x), height: highlight.h,
}]} />
{/* Rechts */}
<View style={[styles.overlay, {
top: highlight.y,
left: highlight.x + highlight.w,
right: 0,
height: highlight.h,
}]} />
{/* Unten */}
<View style={[styles.overlay, {
top: highlight.y + highlight.h,
left: 0,
right: 0,
bottom: 0,
}]} />
<View
style={[
styles.overlay,
{ top: highlight.y, left: 0, width: Math.max(0, highlight.x), height: highlight.h },
]}
/>
<View
style={[
styles.overlay,
{
top: highlight.y,
left: highlight.x + highlight.w,
right: 0,
height: highlight.h,
},
]}
/>
<View
style={[
styles.overlay,
{
top: highlight.y + highlight.h,
left: 0,
right: 0,
bottom: 0,
},
]}
/>
{/* Pulsierender Ring um das Highlight */}
<Animated.View
style={[
styles.highlightRing,
@@ -129,7 +132,6 @@ export const CoachMarksOverlay: React.FC = () => {
pointerEvents="none"
/>
{/* Tooltip-Karte */}
<Animated.View
style={[
styles.tooltip,
@@ -144,7 +146,6 @@ export const CoachMarksOverlay: React.FC = () => {
},
]}
>
{/* Arrow */}
<View
style={[
styles.arrow,
@@ -157,7 +158,6 @@ export const CoachMarksOverlay: React.FC = () => {
]}
/>
{/* Schritt-Indikator */}
<View style={styles.stepRow}>
{steps.map((_, i) => (
<View
@@ -176,7 +176,7 @@ export const CoachMarksOverlay: React.FC = () => {
<View style={styles.tooltipFooter}>
<TouchableOpacity onPress={skip} style={styles.skipBtn}>
<Text style={[styles.skipText, { color: colors.textMuted }]}>Überspringen</Text>
<Text style={[styles.skipText, { color: colors.textMuted }]}>{t.coachSkip}</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={next}
@@ -184,7 +184,7 @@ export const CoachMarksOverlay: React.FC = () => {
activeOpacity={0.82}
>
<Text style={[styles.nextText, { color: colors.onPrimary }]}>
{currentStep === steps.length - 1 ? 'Fertig' : 'Weiter'}
{currentStep === steps.length - 1 ? t.coachDone : t.coachNext}
</Text>
<Ionicons
name={currentStep === steps.length - 1 ? 'checkmark' : 'arrow-forward'}