// One app-wide snackbar, mounted once in the root layout. `showToast` is a // plain function so stores and rows can call it without a context hop. A new // toast replaces the current one (last action wins) and the timer restarts. import { useEffect, useState } from "react"; import { Pressable, StyleSheet, Text } from "react-native"; import Animated, { FadeInDown, FadeOutDown } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { haptic } from "../lib/haptics"; import { C, radius, shadow, space, type } from "../lib/theme"; export interface ToastSpec { message: string; action?: { label: string; onPress: () => void }; } const DURATION_MS = 5000; // Sits on the home FAB's baseline (index.tsx `fab.bottom`) — above the // launcher bar, and the right padding leaves the FAB itself uncovered. const FAB_BOTTOM = 104; const FAB_CLEARANCE = space.xl + 56 + space.md; let setCurrent: ((t: ToastSpec | null) => void) | null = null; export function showToast(spec: ToastSpec) { setCurrent?.(spec); } export function ToastHost() { const [toast, setToast] = useState(null); const insets = useSafeAreaInsets(); useEffect(() => { setCurrent = setToast; return () => { if (setCurrent === setToast) setCurrent = null; }; }, []); useEffect(() => { if (!toast) return; const timer = setTimeout(() => setToast(null), DURATION_MS); return () => clearTimeout(timer); }, [toast]); if (!toast) return null; const { action } = toast; return ( setToast(null)}> {toast.message} {action && ( { haptic.tap(); setToast(null); action.onPress(); }} style={({ pressed }) => [s.action, pressed && { opacity: 0.6 }]} > {action.label} )} ); } const s = StyleSheet.create({ wrap: { position: "absolute", left: space.lg, right: FAB_CLEARANCE, alignItems: "flex-start", }, toast: { flexDirection: "row", alignItems: "center", gap: space.lg, maxWidth: 420, paddingVertical: space.md, paddingLeft: space.lg, paddingRight: space.md, borderRadius: radius.pill, backgroundColor: C.text, ...shadow.fab, }, message: { ...type.row, color: C.bg, flexShrink: 1 }, action: { paddingVertical: space.xs, paddingHorizontal: space.sm }, actionText: { ...type.row, color: C.accentSoft, fontWeight: "600" }, });