diff --git a/src/components/chatbot/ChatbotNodeDialogs.tsx b/src/components/chatbot/ChatbotNodeDialogs.tsx index fe2d9e051..97438267e 100644 --- a/src/components/chatbot/ChatbotNodeDialogs.tsx +++ b/src/components/chatbot/ChatbotNodeDialogs.tsx @@ -11,6 +11,7 @@ import { } from 'lucide-react'; import { cn } from '@/lib/utils'; +// eslint-disable-next-line react-refresh/only-export-components export const nodeTypes: Record; color: string }> = { start: { label: 'Início', icon: Zap, color: 'border-success bg-success/10' }, message: { label: 'Mensagem', icon: MessageSquare, color: 'border-info bg-info/10' }, diff --git a/src/components/cognitive/ProgressiveDisclosure.tsx b/src/components/cognitive/ProgressiveDisclosure.tsx index 0fed98195..749ba495d 100644 --- a/src/components/cognitive/ProgressiveDisclosure.tsx +++ b/src/components/cognitive/ProgressiveDisclosure.tsx @@ -15,6 +15,7 @@ interface DisclosureContextType { const DisclosureContext = createContext({ level: 'basic', setLevel: () => {} }); +// eslint-disable-next-line react-refresh/only-export-components export function useDisclosureLevel() { return useContext(DisclosureContext); } export function DisclosureProvider({ children }: { children: ReactNode }) { diff --git a/src/components/contacts/ContactEngagementScore.tsx b/src/components/contacts/ContactEngagementScore.tsx index 0b35a7661..c2b3b0339 100644 --- a/src/components/contacts/ContactEngagementScore.tsx +++ b/src/components/contacts/ContactEngagementScore.tsx @@ -128,4 +128,5 @@ export function ContactEngagementScore({ ); } +// eslint-disable-next-line react-refresh/only-export-components export { calculateEngagement }; diff --git a/src/components/contacts/ContactErrorBoundary.tsx b/src/components/contacts/ContactErrorBoundary.tsx index bf51e4894..736cddb71 100644 --- a/src/components/contacts/ContactErrorBoundary.tsx +++ b/src/components/contacts/ContactErrorBoundary.tsx @@ -118,6 +118,7 @@ export class ContactErrorBoundary extends Component { * Wrap any contacts component with an error boundary. * Usage: const SafeContactsView = withContactErrorBoundary(ContactsViewV3); */ +// eslint-disable-next-line react-refresh/only-export-components export function withContactErrorBoundary( Component: React.ComponentType, onReset?: () => void diff --git a/src/components/contacts/ContactsTable.tsx b/src/components/contacts/ContactsTable.tsx index 657abb7b5..4b5c551c4 100644 --- a/src/components/contacts/ContactsTable.tsx +++ b/src/components/contacts/ContactsTable.tsx @@ -33,6 +33,7 @@ const CONTACT_TYPE_ICONS: Record = { outros: , }; +// eslint-disable-next-line react-refresh/only-export-components export { CONTACT_TYPE_ICONS }; type SortField = 'name' | 'type' | 'phone' | 'email' | 'company' | 'job_title' | 'created_at'; diff --git a/src/components/dashboard/DashboardFilters.tsx b/src/components/dashboard/DashboardFilters.tsx index 320f1b6f3..691c74586 100644 --- a/src/components/dashboard/DashboardFilters.tsx +++ b/src/components/dashboard/DashboardFilters.tsx @@ -55,6 +55,7 @@ const PERIOD_OPTIONS = [ { value: 'custom', label: 'Personalizado' }, ] as const; +// eslint-disable-next-line react-refresh/only-export-components export const getDefaultFilters = (): DashboardFiltersState => ({ dateRange: { from: startOfDay(new Date()), diff --git a/src/components/dashboard/DashboardWidgetRenderer.tsx b/src/components/dashboard/DashboardWidgetRenderer.tsx index 24b428d16..e30911a13 100644 --- a/src/components/dashboard/DashboardWidgetRenderer.tsx +++ b/src/components/dashboard/DashboardWidgetRenderer.tsx @@ -43,6 +43,7 @@ interface DashboardStats { }>; } +// eslint-disable-next-line react-refresh/only-export-components export function buildStatsCards(stats: DashboardStats) { const openRate = stats.totalConversations > 0 ? Math.round((stats.openConversations / stats.totalConversations) * 100) diff --git a/src/components/dashboard/SentimentHelpers.tsx b/src/components/dashboard/SentimentHelpers.tsx index 46504e7f5..c8ac1f6d3 100644 --- a/src/components/dashboard/SentimentHelpers.tsx +++ b/src/components/dashboard/SentimentHelpers.tsx @@ -17,6 +17,7 @@ export interface SentimentData { alerts_count: number; } +// eslint-disable-next-line react-refresh/only-export-components export function useRealSentimentData(days: number): SentimentData[] | null { const { data } = useQuery({ queryKey: ['sentiment-trend', days], diff --git a/src/components/dashboard/TrendIndicator.tsx b/src/components/dashboard/TrendIndicator.tsx index 6dc201194..fd4392fed 100644 --- a/src/components/dashboard/TrendIndicator.tsx +++ b/src/components/dashboard/TrendIndicator.tsx @@ -20,6 +20,7 @@ interface TrendIndicatorProps { animated?: boolean; } +// eslint-disable-next-line react-refresh/only-export-components export function calculateTrend(current: number, previous: number): { percentage: number; direction: 'up' | 'down' | 'neutral'; diff --git a/src/components/dashboard/WidgetConfigSheet.tsx b/src/components/dashboard/WidgetConfigSheet.tsx index fe2baa846..51695b44e 100644 --- a/src/components/dashboard/WidgetConfigSheet.tsx +++ b/src/components/dashboard/WidgetConfigSheet.tsx @@ -131,4 +131,5 @@ export function WidgetConfigSheet({ widgets, isEditMode, setIsEditMode, onToggle ); } +// eslint-disable-next-line react-refresh/only-export-components export { sizeLabels, sizeIcons }; diff --git a/src/components/effects/Confetti.tsx b/src/components/effects/Confetti.tsx index b565f32a7..918a81d53 100644 --- a/src/components/effects/Confetti.tsx +++ b/src/components/effects/Confetti.tsx @@ -265,6 +265,7 @@ export function CelebrationOverlay({ } // Hook for triggering celebrations +// eslint-disable-next-line react-refresh/only-export-components export function useCelebration() { const [celebrating, setCelebrating] = useState(false); const [celebrationData, setCelebrationData] = useState<{ diff --git a/src/components/effects/EasterEggs.tsx b/src/components/effects/EasterEggs.tsx index d7f5e9cef..6fb2d800a 100644 --- a/src/components/effects/EasterEggs.tsx +++ b/src/components/effects/EasterEggs.tsx @@ -283,6 +283,7 @@ export const EasterEggsProvider = forwardRef { } // HOC para envolver componentes com Error Boundary +// eslint-disable-next-line react-refresh/only-export-components export function withErrorBoundary

( WrappedComponent: React.ComponentType

, fallback?: ReactNode diff --git a/src/components/keyboard/GlobalKeyboardProvider.tsx b/src/components/keyboard/GlobalKeyboardProvider.tsx index 63e97cb39..618fe1a73 100644 --- a/src/components/keyboard/GlobalKeyboardProvider.tsx +++ b/src/components/keyboard/GlobalKeyboardProvider.tsx @@ -14,6 +14,7 @@ interface GlobalKeyboardContextType { const GlobalKeyboardContext = createContext(null); +// eslint-disable-next-line react-refresh/only-export-components export const useGlobalKeyboard = () => { const context = useContext(GlobalKeyboardContext); if (!context) { diff --git a/src/components/mobile/InAppNotificationProvider.tsx b/src/components/mobile/InAppNotificationProvider.tsx index e44c899ab..28b17ef29 100644 --- a/src/components/mobile/InAppNotificationProvider.tsx +++ b/src/components/mobile/InAppNotificationProvider.tsx @@ -9,6 +9,7 @@ const InAppNotificationContext = createContext({ showNotification: () => {}, }); +// eslint-disable-next-line react-refresh/only-export-components export function useInAppNotification() { return useContext(InAppNotificationContext); } diff --git a/src/components/mobile/MobileNavigation.tsx b/src/components/mobile/MobileNavigation.tsx index ad509ab6c..58df4ae64 100644 --- a/src/components/mobile/MobileNavigation.tsx +++ b/src/components/mobile/MobileNavigation.tsx @@ -60,6 +60,7 @@ export function MobileTabBar({ items, activeId, onChange, className, variant = ' } // Default tab presets +// eslint-disable-next-line react-refresh/only-export-components export const defaultMobileTabItems: TabItem[] = [ { id: 'home', icon: , label: 'Início' }, { id: 'inbox', icon: , label: 'Inbox', badge: 3 }, diff --git a/src/components/mobile/SwipeGestures.tsx b/src/components/mobile/SwipeGestures.tsx index c30908fa0..bf4cb23e5 100644 --- a/src/components/mobile/SwipeGestures.tsx +++ b/src/components/mobile/SwipeGestures.tsx @@ -3,6 +3,7 @@ import { motion, useMotionValue, useTransform, PanInfo, AnimatePresence } from ' import { cn } from '@/lib/utils'; // Re-export utilities and types from extracted modules +// eslint-disable-next-line react-refresh/only-export-components export { haptics, defaultSwipeActions } from './swipeUtils'; export type { SwipeAction } from './swipeUtils'; export { TouchRipple } from './TouchRipple'; diff --git a/src/components/onboarding/OnboardingTour.tsx b/src/components/onboarding/OnboardingTour.tsx index 77afd1933..f24990cdd 100644 --- a/src/components/onboarding/OnboardingTour.tsx +++ b/src/components/onboarding/OnboardingTour.tsx @@ -23,6 +23,7 @@ interface TourContextType { const TourContext = createContext(null); +// eslint-disable-next-line react-refresh/only-export-components export function useTour() { const context = useContext(TourContext); if (!context) { @@ -83,4 +84,5 @@ export function TourProvider({ children, onComplete }: TourProviderProps) { } // Re-export for backward compatibility +// eslint-disable-next-line react-refresh/only-export-components export { DEFAULT_ONBOARDING_STEPS } from './defaultTourSteps'; diff --git a/src/components/performance/LazyRoutes.tsx b/src/components/performance/LazyRoutes.tsx index b84686f68..6574926e4 100644 --- a/src/components/performance/LazyRoutes.tsx +++ b/src/components/performance/LazyRoutes.tsx @@ -146,7 +146,7 @@ export function LazyView({ children, fallbackType = 'default' }: LazyViewProps) } // HOC for creating lazy components with custom fallback -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic HOC requires any for ComponentType inference +// eslint-disable-next-line @typescript-eslint/no-explicit-any, react-refresh/only-export-components -- generic HOC requires any for ComponentType inference export function withLazyLoading>( importFn: () => Promise<{ default: T }>, fallbackType: LazyLoadFallbackProps['type'] = 'default' diff --git a/src/components/performance/Prefetcher.tsx b/src/components/performance/Prefetcher.tsx index 3f6800885..01be5bc7b 100644 --- a/src/components/performance/Prefetcher.tsx +++ b/src/components/performance/Prefetcher.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-refresh/only-export-components -- componente exporta múltiplos símbolos não-componente (hooks, constants, helpers); split em arquivos próprios fica em backlog */ import { useEffect, useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; @@ -74,14 +75,30 @@ export function useIntersectionPrefetch(routes: string[]) { const { prefetch } = usePrefetchRoute(); useEffect(() => { - // Prefetch routes when user is idle - const idleCallback = window.requestIdleCallback || ((cb) => setTimeout(cb, 1)); + let idleHandle: number | undefined; + let timeoutHandle: ReturnType | undefined; + const hasIdleCallback = typeof window !== 'undefined' && 'requestIdleCallback' in window; - idleCallback(() => { + const run = () => { routes.forEach((route) => { prefetch(route); }); - }); + }; + + if (hasIdleCallback) { + idleHandle = window.requestIdleCallback(run); + } else { + timeoutHandle = setTimeout(run, 1); + } + + return () => { + if (idleHandle !== undefined && hasIdleCallback && 'cancelIdleCallback' in window) { + window.cancelIdleCallback(idleHandle); + } + if (timeoutHandle !== undefined) { + clearTimeout(timeoutHandle); + } + }; }, [routes, prefetch]); } @@ -115,31 +132,57 @@ export function useNetworkAwarePrefetch() { // Prefetch critical routes on app load export function CriticalRoutePrefetcher() { useEffect(() => { + let cancelled = false; + let timeoutHandle: ReturnType | undefined; + let idleHandle: number | undefined; + + const wait = (ms: number) => new Promise((resolve) => { + timeoutHandle = setTimeout(() => { + timeoutHandle = undefined; + resolve(); + }, ms); + }); + const prefetchCritical = async () => { // Wait for main content to load - await new Promise((resolve) => setTimeout(resolve, 2000)); + await wait(2000); + if (cancelled) return; // Prefetch critical routes const criticalRoutes = ['dashboard', 'contacts', 'settings']; - + for (const route of criticalRoutes) { + if (cancelled) break; const prefetchFn = routePrefetchConfig[route]; if (prefetchFn) { try { await prefetchFn(); + if (cancelled) break; // Small delay between prefetches to not block main thread - await new Promise((resolve) => setTimeout(resolve, 100)); + await wait(100); } catch (err) { log.error('Unexpected error in Prefetcher:', err); } } } }; - // Use requestIdleCallback if available - if ('requestIdleCallback' in window) { - window.requestIdleCallback(() => prefetchCritical()); + const hasIdleCallback = typeof window !== 'undefined' && 'requestIdleCallback' in window; + if (hasIdleCallback) { + idleHandle = window.requestIdleCallback(() => { + void prefetchCritical(); + }); } else { - prefetchCritical(); + void prefetchCritical(); } + + return () => { + cancelled = true; + if (timeoutHandle !== undefined) { + clearTimeout(timeoutHandle); + } + if (idleHandle !== undefined && hasIdleCallback && 'cancelIdleCallback' in window) { + window.cancelIdleCallback(idleHandle); + } + }; }, []); return null; diff --git a/src/components/team-chat/MessageReactions.tsx b/src/components/team-chat/MessageReactions.tsx index 26b2aef38..910331eb1 100644 --- a/src/components/team-chat/MessageReactions.tsx +++ b/src/components/team-chat/MessageReactions.tsx @@ -6,7 +6,9 @@ import { cn } from '@/lib/utils'; import { motion } from 'framer-motion'; import type { AggregatedReaction } from '@/features/inbox/hooks/team-chat/useTeamMessageReactions'; +// eslint-disable-next-line react-refresh/only-export-components export const QUICK_EMOJIS = ['👍', '❤️', '😂', '😮', '😢', '🙏']; +// eslint-disable-next-line react-refresh/only-export-components export const EXTENDED_EMOJIS = [ ...QUICK_EMOJIS, '🔥', '🎉', '👏', '💯', '✅', '❌', '👀', '🤔', '😍', '😎', diff --git a/src/components/team-chat/TeamMemberProfileHeader.tsx b/src/components/team-chat/TeamMemberProfileHeader.tsx index 6201ac913..87c32a908 100644 --- a/src/components/team-chat/TeamMemberProfileHeader.tsx +++ b/src/components/team-chat/TeamMemberProfileHeader.tsx @@ -23,6 +23,7 @@ interface MemberProfile { export type { MemberProfile }; +// eslint-disable-next-line react-refresh/only-export-components export function getBirthdayInfo(birthday: string | null) { if (!birthday) return null; const date = new Date(birthday); @@ -35,6 +36,7 @@ export function getBirthdayInfo(birthday: string | null) { return { date, age, isToday, daysUntil }; } +// eslint-disable-next-line react-refresh/only-export-components export function getRoleBadge(role: string | null) { const map: Record = { admin: { label: 'Admin', className: 'bg-destructive/10 text-destructive border-destructive/20' }, diff --git a/src/components/theme/HighContrastToggle.tsx b/src/components/theme/HighContrastToggle.tsx index b80cbf469..648890d5e 100644 --- a/src/components/theme/HighContrastToggle.tsx +++ b/src/components/theme/HighContrastToggle.tsx @@ -96,6 +96,7 @@ export function HighContrastProvider({ children }: { children: React.ReactNode } ); } +// eslint-disable-next-line react-refresh/only-export-components export function useHighContrast() { const context = useContext(HighContrastContext); if (!context) { diff --git a/src/components/ui/accessible-toast.tsx b/src/components/ui/accessible-toast.tsx index b3401a741..92188749a 100644 --- a/src/components/ui/accessible-toast.tsx +++ b/src/components/ui/accessible-toast.tsx @@ -27,6 +27,7 @@ interface ToastContextValue { const ToastContext = createContext(null); +// eslint-disable-next-line react-refresh/only-export-components export function useAccessibleToast() { const context = useContext(ToastContext); if (!context) { diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index 65daeec06..1f152c889 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -196,6 +196,7 @@ export { AvatarWithStatus, AvatarGroup, StatusIndicator, + // eslint-disable-next-line react-refresh/only-export-components avatarVariants, type StatusType, }; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index 1741d4b45..b7ccbc215 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -34,4 +34,5 @@ const Badge = React.forwardRef(({ className, variant Badge.displayName = "Badge"; +// eslint-disable-next-line react-refresh/only-export-components export { Badge, badgeVariants }; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 2b53f018d..0e0ee5e1b 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -110,4 +110,5 @@ const MotionButton = React.forwardRef( ); MotionButton.displayName = "MotionButton"; +// eslint-disable-next-line react-refresh/only-export-components export { Button, MotionButton, buttonVariants }; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 5db1c7adf..2ff50d336 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -101,4 +101,5 @@ const CardFooter = React.forwardRef { diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 1317951e7..707a9379f 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -115,5 +115,6 @@ export { DialogFooter, DialogTitle, DialogDescription, + // eslint-disable-next-line react-refresh/only-export-components dialogContentVariants, }; diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx index 439029fcf..4d9300c6e 100644 --- a/src/components/ui/form.tsx +++ b/src/components/ui/form.tsx @@ -126,4 +126,5 @@ const FormMessage = React.forwardRef( ); Input.displayName = "Input"; +// eslint-disable-next-line react-refresh/only-export-components export { Input, inputVariants }; diff --git a/src/components/ui/motion.ts b/src/components/ui/motion.ts new file mode 100644 index 000000000..4a507c59b --- /dev/null +++ b/src/components/ui/motion.ts @@ -0,0 +1,45 @@ +/** + * Motion barrel file. + * Re-exports all sub-modules for backward compatibility. + */ +export { + fadeInUp, + fadeIn, + scaleIn, + slideInRight, + slideInLeft, + staggerContainer, + staggerItem, + neonReveal, + staggeredNeonContainer, + staggeredNeonItem, +} from './motion/variants'; + +export { + PageTransition, + NeonPageReveal, + MotionCard, + MotionButton, + StaggeredList, + StaggeredItem, + MotionFadeIn, + MotionSlideUp, + MotionScale, + MotionInteractive, + SkeletonShimmer, +} from './motion/components'; + +export { + AnimatedCounter, + AnimatedProgress, + Presence, + StaggerContainerEnhanced, + SlideTransition, + HoverScale, + AnimatedList, + AnimatedListItem, + Typewriter, +} from './motion/effects'; + +// Re-exports from framer-motion for convenience +export { AnimatePresence, motion } from 'framer-motion'; diff --git a/src/components/ui/motion.tsx b/src/components/ui/motion.tsx deleted file mode 100644 index bb8bbeebb..000000000 --- a/src/components/ui/motion.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Motion barrel file. - * Re-exports all sub-modules for backward compatibility. - */ -export { - fadeInUp, fadeIn, scaleIn, slideInRight, slideInLeft, - staggerContainer, staggerItem, neonReveal, staggeredNeonContainer, staggeredNeonItem, -} from './motion/variants'; - -export { - PageTransition, NeonPageReveal, MotionCard, MotionButton, - StaggeredList, StaggeredItem, MotionFadeIn, MotionSlideUp, - MotionScale, MotionInteractive, SkeletonShimmer, -} from './motion/components'; - -export { - AnimatedCounter, AnimatedProgress, Presence, StaggerContainerEnhanced, - SlideTransition, HoverScale, AnimatedList, AnimatedListItem, Typewriter, -} from './motion/effects'; - -// Re-exports from framer-motion for convenience -export { AnimatePresence, motion } from 'framer-motion'; diff --git a/src/components/ui/offline-indicator.tsx b/src/components/ui/offline-indicator.tsx index 136369d7b..a9677b6c0 100644 --- a/src/components/ui/offline-indicator.tsx +++ b/src/components/ui/offline-indicator.tsx @@ -80,15 +80,18 @@ export function OfflineIndicator({ className }: OfflineIndicatorProps) { } // Hook for offline detection +// eslint-disable-next-line react-refresh/only-export-components export function useOfflineStatus() { const [isOnline, setIsOnline] = useState(navigator.onLine); const [wasOffline, setWasOffline] = useState(false); useEffect(() => { + let timeoutId: ReturnType | null = null; + const handleOnline = () => { if (!isOnline) { setWasOffline(true); - setTimeout(() => setWasOffline(false), 3000); + timeoutId = setTimeout(() => setWasOffline(false), 3000); } setIsOnline(true); }; @@ -101,6 +104,7 @@ export function useOfflineStatus() { window.addEventListener('offline', handleOffline); return () => { + if (timeoutId !== null) clearTimeout(timeoutId); window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; diff --git a/src/components/ui/scroll-to-top.tsx b/src/components/ui/scroll-to-top.tsx index d3ef88ad1..4e39fe3da 100644 --- a/src/components/ui/scroll-to-top.tsx +++ b/src/components/ui/scroll-to-top.tsx @@ -59,6 +59,7 @@ export function ScrollToTopButton({ scrollRef, threshold = 400, className }: Scr /** * Hook version for custom implementations */ +// eslint-disable-next-line react-refresh/only-export-components export function useScrollToTop(scrollRef: RefObject, threshold = 400) { const [visible, setVisible] = useState(false); diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.ts similarity index 95% rename from src/components/ui/sidebar.tsx rename to src/components/ui/sidebar.ts index 64d9cb579..8313bedd3 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.ts @@ -24,4 +24,4 @@ export { SidebarSeparator, SidebarTrigger, useSidebar, -} from "./sidebar/index"; +} from './sidebar/index'; diff --git a/src/components/ui/sidebar/sidebar-context.tsx b/src/components/ui/sidebar/sidebar-context.tsx index 3d0375181..e8d615468 100644 --- a/src/components/ui/sidebar/sidebar-context.tsx +++ b/src/components/ui/sidebar/sidebar-context.tsx @@ -20,8 +20,10 @@ export type SidebarContextType = { toggleSidebar: () => void; }; +// eslint-disable-next-line react-refresh/only-export-components export const SidebarContext = React.createContext(null); +// eslint-disable-next-line react-refresh/only-export-components export function useSidebar() { const context = React.useContext(SidebarContext); if (!context) { diff --git a/src/components/ui/sidebar/sidebar-menu.tsx b/src/components/ui/sidebar/sidebar-menu.tsx index 73cafc484..11bb09d9b 100644 --- a/src/components/ui/sidebar/sidebar-menu.tsx +++ b/src/components/ui/sidebar/sidebar-menu.tsx @@ -16,6 +16,7 @@ export const SidebarMenuItem = React.forwardRefspan:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", { diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx index 8fa496e78..314b46065 100644 --- a/src/components/ui/skeleton.tsx +++ b/src/components/ui/skeleton.tsx @@ -135,5 +135,6 @@ export { SkeletonText, SkeletonAvatar, SkeletonButton, + // eslint-disable-next-line react-refresh/only-export-components skeletonVariants, }; diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index f0aee12d8..15cb6b7fb 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -35,4 +35,5 @@ const Toaster = ({ ...props }: ToasterProps) => { ); }; +// eslint-disable-next-line react-refresh/only-export-components export { Toaster, toast }; diff --git a/src/components/ui/toggle.tsx b/src/components/ui/toggle.tsx index de5dfc5ad..04cf582f0 100644 --- a/src/components/ui/toggle.tsx +++ b/src/components/ui/toggle.tsx @@ -34,4 +34,5 @@ const Toggle = React.forwardRef< Toggle.displayName = TogglePrimitive.Root.displayName; +// eslint-disable-next-line react-refresh/only-export-components export { Toggle, toggleVariants }; diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx index 7d5ae4311..981ba1e0f 100644 --- a/src/components/ui/tooltip.tsx +++ b/src/components/ui/tooltip.tsx @@ -126,5 +126,6 @@ export { TooltipContentEnhanced, TooltipProvider, SimpleTooltip, + // eslint-disable-next-line react-refresh/only-export-components tooltipVariants, }; diff --git a/src/components/ui/visually-hidden.tsx b/src/components/ui/visually-hidden.tsx index 1856ab6ea..561c71cc5 100644 --- a/src/components/ui/visually-hidden.tsx +++ b/src/components/ui/visually-hidden.tsx @@ -28,13 +28,26 @@ export function VisuallyHidden({ /** * Hook to announce content to screen readers */ +// eslint-disable-next-line react-refresh/only-export-components export function useAnnounce() { const [announcement, setAnnouncement] = React.useState(''); + const timeoutRef = React.useRef | null>(null); + + React.useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); const announce = React.useCallback((message: string, _politeness: 'polite' | 'assertive' = 'polite') => { // Clear first to ensure re-announcement of same message setAnnouncement(''); - setTimeout(() => setAnnouncement(message), 100); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => setAnnouncement(message), 100); }, []); const Announcer = React.useMemo(() => { diff --git a/src/features/auth/components/ProtectedRoute.tsx b/src/features/auth/components/ProtectedRoute.tsx index c2c0add70..6d43e0889 100644 --- a/src/features/auth/components/ProtectedRoute.tsx +++ b/src/features/auth/components/ProtectedRoute.tsx @@ -38,21 +38,38 @@ export function ProtectedRoute({ const loading = authLoading || rolesLoading; useEffect(() => { + let isMounted = true; if (!loading && user && requiredPermission) { - supabase.rpc('user_has_permission', { - _user_id: user.id, - _permission_name: requiredPermission - }).then(({ data, error }) => { - if (error) { - log.error('Permission check failed:', error.message); + // Reset state antes de iniciar a checagem para evitar render transiente + // com valor stale (allow/deny da checagem anterior) quando user/permission mudam + setHasPermission(null); + // Wrap PromiseLike in Promise.resolve to enable .catch() (supabase.rpc returns PromiseLike) + Promise.resolve( + supabase.rpc('user_has_permission', { + _user_id: user.id, + _permission_name: requiredPermission, + }) + ) + .then(({ data, error }) => { + if (!isMounted) return; + if (error) { + log.error('Permission check failed:', error.message); + setHasPermission(false); + return; + } + setHasPermission(data === true); + }) + .catch((err: unknown) => { + if (!isMounted) return; + log.error('Permission check failed:', err instanceof Error ? err.message : String(err)); setHasPermission(false); - return; - } - setHasPermission(data === true); - }); + }); } else if (!requiredPermission) { setHasPermission(true); } + return () => { + isMounted = false; + }; }, [loading, user, requiredPermission]); if (loading || (requiredPermission && hasPermission === null)) { @@ -96,6 +113,7 @@ export function ProtectedRoute({ } // Higher-order component for permission-based rendering +// eslint-disable-next-line react-refresh/only-export-components export function withPermission

( WrappedComponent: React.ComponentType

, permission: string diff --git a/src/features/inbox/components/ContactTypeFilter.tsx b/src/features/inbox/components/ContactTypeFilter.tsx index 6854605ac..67cd0d9b5 100644 --- a/src/features/inbox/components/ContactTypeFilter.tsx +++ b/src/features/inbox/components/ContactTypeFilter.tsx @@ -26,6 +26,7 @@ const isGroup = (phone: string | null | undefined): boolean => { return /^\d+-\d+$/.test(phone.replace(/\D/g, '')); }; +// eslint-disable-next-line react-refresh/only-export-components export const FILTER_OPTIONS: FilterOption[] = [ { value: 'all', label: 'Todos os tipos', icon: Users, iconColor: 'text-muted-foreground', match: () => true }, { value: 'individual', label: 'Chats Individuais', icon: MessageSquare, iconColor: 'text-primary', @@ -194,6 +195,7 @@ export function ContactTypeFilter({ value, onChange, conversations }: ContactTyp // ---------- helper for filtering ---------- +// eslint-disable-next-line react-refresh/only-export-components export function filterByContactType( conversations: ConversationWithMessages[], contactType: string | null, diff --git a/src/features/inbox/components/LinkPreview.tsx b/src/features/inbox/components/LinkPreview.tsx index e52ad26d3..9e888d399 100644 --- a/src/features/inbox/components/LinkPreview.tsx +++ b/src/features/inbox/components/LinkPreview.tsx @@ -9,6 +9,7 @@ import { type LinkMetadata, } from './linkPreviewUtils'; +// eslint-disable-next-line react-refresh/only-export-components export { extractLinks }; interface LinkPreviewProps { diff --git a/src/features/inbox/components/MessagePreview.tsx b/src/features/inbox/components/MessagePreview.tsx index 49bd469a5..c983b4a71 100644 --- a/src/features/inbox/components/MessagePreview.tsx +++ b/src/features/inbox/components/MessagePreview.tsx @@ -160,6 +160,7 @@ export function MessagePreview({ content, className }: MessagePreviewProps) { } // Hook to detect if content has formattable elements +// eslint-disable-next-line react-refresh/only-export-components export function useHasFormattableContent(content: string): boolean { return useMemo(() => { if (!content) return false; diff --git a/src/features/inbox/components/SentimentIndicator.tsx b/src/features/inbox/components/SentimentIndicator.tsx index 2e0ed9cec..0f8fdb388 100644 --- a/src/features/inbox/components/SentimentIndicator.tsx +++ b/src/features/inbox/components/SentimentIndicator.tsx @@ -56,6 +56,7 @@ const sentimentConfig: Record= 70) return 'positive'; if (score >= 45) return 'neutral'; diff --git a/src/features/inbox/components/SlashCommands.tsx b/src/features/inbox/components/SlashCommands.tsx index b7475a5a9..2d1292148 100644 --- a/src/features/inbox/components/SlashCommands.tsx +++ b/src/features/inbox/components/SlashCommands.tsx @@ -147,5 +147,6 @@ export function SlashCommands({ inputValue, onSelectCommand, onClose, isOpen }: ); } +// eslint-disable-next-line react-refresh/only-export-components export { SLASH_COMMANDS }; export type { SlashCommand }; diff --git a/src/features/inbox/components/SwipeableListItem.tsx b/src/features/inbox/components/SwipeableListItem.tsx index 2847424b3..3190ec347 100644 --- a/src/features/inbox/components/SwipeableListItem.tsx +++ b/src/features/inbox/components/SwipeableListItem.tsx @@ -6,6 +6,7 @@ import type { SwipeAction } from './swipeActions'; // Re-export for consumers export type { SwipeAction } from './swipeActions'; +// eslint-disable-next-line react-refresh/only-export-components export { SWIPE_ACTIONS } from './swipeActions'; interface SwipeableListItemProps { diff --git a/src/features/inbox/components/TemplatesWithVariables.tsx b/src/features/inbox/components/TemplatesWithVariables.tsx index dcb616bad..712872c7f 100644 --- a/src/features/inbox/components/TemplatesWithVariables.tsx +++ b/src/features/inbox/components/TemplatesWithVariables.tsx @@ -11,6 +11,9 @@ import { toast } from 'sonner'; import { AVAILABLE_VARIABLES, replaceVariables } from './template-utils'; import { TemplateEditorDialog, VariableInserter, TemplatePreview } from './templates/TemplateEditorDialog'; import { useMessageTemplates, type Template } from '../hooks/useMessageTemplates'; +import { getLogger } from '@/lib/logger'; + +const log = getLogger('TemplatesWithVariables'); // Variable highlighter function VariableHighlighter({ text, className }: { text: string; className?: string }) { @@ -40,7 +43,9 @@ export function TemplatesWithVariables({ onUseTemplate, contactData }: TemplateW const [editingTemplate, setEditingTemplate] = useState