diff --git a/.eslintrc.tailwind.js b/.eslintrc.tailwind.js deleted file mode 100644 index fa951213a..000000000 --- a/.eslintrc.tailwind.js +++ /dev/null @@ -1,20 +0,0 @@ -import type { Linter } from "eslint"; - -const config: Linter.Config = { - plugins: ["tailwindcss"], - rules: { - "tailwindcss/no-arbitrary-value": "error", - "tailwindcss/no-custom-classname": "off", - "tailwindcss/classnames-order": "warn", - "tailwindcss/enforce-shorthand": "warn", - }, - settings: { - tailwindcss: { - callees: ["cn", "cva", "clsx"], - config: "tailwind.config.ts", - whitelist: [], // Adicione classes personalizadas que devem ser permitidas aqui - }, - }, -}; - -export default config; diff --git a/scripts/ds-config.ts b/scripts/ds-config.ts index 64bba258e..5d0445c91 100644 --- a/scripts/ds-config.ts +++ b/scripts/ds-config.ts @@ -54,7 +54,7 @@ export const DS_CONFIG = { ], FORBIDDEN_PATTERNS: [ { pattern: /(?:bg|text|border)-(?:\[(?:#(?:[0-9a-fA-F]{3,6})|rgb|hsl|rgba|hsla)\])/, label: 'Arbitrary Color' }, - { pattern: /(?:bg|text|border)-(?:white|black(?:(?!\-[0-9]))|(?:red|blue|green|yellow|slate|gray|zinc|neutral|stone|orange|amber|lime|emerald|teal|cyan|sky|indigo|violet|purple|fuchsia|pink|rose)-[0-9]+)\b/, label: 'Literal Color' }, + { pattern: /(?:bg|text|border)-(?:white|black(?:(?!-[0-9]))|(?:red|blue|green|yellow|slate|gray|zinc|neutral|stone|orange|amber|lime|emerald|teal|cyan|sky|indigo|violet|purple|fuchsia|pink|rose)-[0-9]+)\b/, label: 'Literal Color' }, { pattern: /font-(?:inter|sans|mono|serif)\b/, label: 'Literal Font' }, { pattern: /(? = () => { setIsAddDialogOpen(true); toast.info("Atalho: Novo Registro", { duration: 1000 }); break; - case 'f': + case 'f': { e.preventDefault(); const searchInput = document.querySelector('input[placeholder*="Buscar"]') as HTMLInputElement; if (searchInput) { @@ -128,6 +128,7 @@ export const ContactsRichView: React.FC = () => { toast.info("Atalho: Focar Busca", { duration: 1000 }); } break; + } case 'g': e.preventDefault(); state.setViewMode('grid'); diff --git a/src/components/security/RateLimitRealtimeAlerts.tsx b/src/components/security/RateLimitRealtimeAlerts.tsx index cffe2eeec..c7491bab4 100644 --- a/src/components/security/RateLimitRealtimeAlerts.tsx +++ b/src/components/security/RateLimitRealtimeAlerts.tsx @@ -80,7 +80,7 @@ export function RateLimitRealtimeAlerts() { const audio = new Audio('/notification.mp3'); audio.volume = 0.5; audio.play().catch(() => {}); - } catch (e) {} + } catch { /* intentionally empty */ } }; const handleDismiss = async (alertId: string) => { diff --git a/src/features/auth/components/PasswordStrengthMeter.tsx b/src/features/auth/components/PasswordStrengthMeter.tsx index bee4560e0..11f188c42 100644 --- a/src/features/auth/components/PasswordStrengthMeter.tsx +++ b/src/features/auth/components/PasswordStrengthMeter.tsx @@ -19,7 +19,7 @@ const requirements: PasswordRequirement[] = [ { label: 'Letra maiúscula (A-Z)', test: (p) => /[A-Z]/.test(p), weight: 1 }, { label: 'Letra minúscula (a-z)', test: (p) => /[a-z]/.test(p), weight: 1 }, { label: 'Número (0-9)', test: (p) => /\d/.test(p), weight: 1 }, - { label: 'Caractere especial (!@#$%)', test: (p) => /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(p), weight: 1 }, + { label: 'Caractere especial (!@#$%)', test: (p) => /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(p), weight: 1 }, ]; // Simple hash function for k-anonymity check diff --git a/src/features/connections/hooks/useConnectionsManager.ts b/src/features/connections/hooks/useConnectionsManager.ts index 3cd3ed199..d021da436 100644 --- a/src/features/connections/hooks/useConnectionsManager.ts +++ b/src/features/connections/hooks/useConnectionsManager.ts @@ -161,7 +161,7 @@ export function useConnectionsManager() { } else { sessionStorage.setItem(QR_STORAGE_KEY, JSON.stringify(qrCodeDialog)); } - } catch {} + } catch { /* intentionally empty */ } }, [qrCodeDialog]); useEffect(() => { diff --git a/src/features/inbox/components/chat/MarkdownPreview.tsx b/src/features/inbox/components/chat/MarkdownPreview.tsx index e6e6e1624..7a8df69df 100644 --- a/src/features/inbox/components/chat/MarkdownPreview.tsx +++ b/src/features/inbox/components/chat/MarkdownPreview.tsx @@ -16,7 +16,7 @@ export function formatWhatsAppText(text: string): string { // Code blocks (```...```) — must come before inline .replace(/```([\s\S]*?)```/g, '$1') // Bold (*...*) — not greedy, avoid matching ** - .replace(/\*([^\*]+)\*/g, '$1') + .replace(/\*([^*]+)\*/g, '$1') // Italic (_..._) .replace(/_((?!_)[^_]+)_/g, '$1') // Strikethrough (~...~) diff --git a/src/features/inbox/components/linkPreviewUtils.ts b/src/features/inbox/components/linkPreviewUtils.ts index fc9893696..24a607ae8 100644 --- a/src/features/inbox/components/linkPreviewUtils.ts +++ b/src/features/inbox/components/linkPreviewUtils.ts @@ -1,5 +1,5 @@ // URL regex pattern -export const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi; +export const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/gi; const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp']; const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.ogg', '.mov']; diff --git a/src/features/inbox/hooks/useScheduledMediaUpload.ts b/src/features/inbox/hooks/useScheduledMediaUpload.ts index d9ef99630..93d70dce3 100644 --- a/src/features/inbox/hooks/useScheduledMediaUpload.ts +++ b/src/features/inbox/hooks/useScheduledMediaUpload.ts @@ -26,7 +26,6 @@ export function useScheduledMediaUpload() { const uploadScheduledMedia = useCallback( async (attachment: File): Promise => { const fileName = `scheduled_${Date.now()}_${attachment.name}`; - let mediaUrl: string | undefined; let messageType = 'text'; const { error: uploadError } = await supabase.storage @@ -49,7 +48,7 @@ export function useScheduledMediaUpload() { .from('whatsapp-media') .createSignedUrl(fileName, 604800); - mediaUrl = signedData?.signedUrl; + const mediaUrl = signedData?.signedUrl; messageType = attachment.type.startsWith('audio') ? 'audio' diff --git a/src/features/inbox/hooks/useSipClient.ts b/src/features/inbox/hooks/useSipClient.ts index efddc94a4..b1e614785 100644 --- a/src/features/inbox/hooks/useSipClient.ts +++ b/src/features/inbox/hooks/useSipClient.ts @@ -41,7 +41,7 @@ export function useSipClient() { const findContactByPhone = useCallback(async (phone: string): Promise => { try { - const n = phone.replace(/[\s\-\(\)]/g, ''); + const n = phone.replace(/[\s\-()]/g, ''); const { data, error } = await supabase.from('contacts').select('id').or(`phone.eq.${n},phone.eq.+${n},phone.ilike.%${n.slice(-8)}%`).limit(1).maybeSingle(); return data?.id || null; } catch { return null; } diff --git a/src/hooks/useActionFeedback.ts b/src/hooks/useActionFeedback.ts index b26330aef..f4571f3b9 100644 --- a/src/hooks/useActionFeedback.ts +++ b/src/hooks/useActionFeedback.ts @@ -49,6 +49,7 @@ export function useActionFeedback() { return new Promise((resolve) => { const { description, undoDuration = 5000, onUndo, onConfirm } = options; let undone = false; + // eslint-disable-next-line prefer-const -- closure: timeoutId is referenced inside toast `action.onClick` (declared above) and only assigned later by setTimeout let timeoutId: NodeJS.Timeout; const toastResult = showFeedback('info', { description, duration: undoDuration, diff --git a/src/hooks/useCSAT.ts b/src/hooks/useCSAT.ts index 9e3ea824f..3e5b084f3 100644 --- a/src/hooks/useCSAT.ts +++ b/src/hooks/useCSAT.ts @@ -27,14 +27,16 @@ export function useCSAT(period: 'today' | 'week' | 'month' = 'month') { switch (period) { case 'today': return new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString(); - case 'week': + case 'week': { const weekAgo = new Date(now); weekAgo.setDate(weekAgo.getDate() - 7); return weekAgo.toISOString(); - case 'month': + } + case 'month': { const monthAgo = new Date(now); monthAgo.setMonth(monthAgo.getMonth() - 1); return monthAgo.toISOString(); + } } }; diff --git a/src/hooks/useThemeAudit.ts b/src/hooks/useThemeAudit.ts index b6b010d2b..f27eed3c6 100644 --- a/src/hooks/useThemeAudit.ts +++ b/src/hooks/useThemeAudit.ts @@ -56,7 +56,7 @@ export const useThemeAudit = () => { try { const parsed = JSON.parse(saved); presetId = parsed.preset || DEFAULT_PRESET_ID; - } catch (_e) {} + } catch { /* intentionally empty */ } } const preset = PRESETS.find(p => p.id === presetId); diff --git a/src/lib/csvUtils.ts b/src/lib/csvUtils.ts index 2c8676a68..d2708ce15 100644 --- a/src/lib/csvUtils.ts +++ b/src/lib/csvUtils.ts @@ -151,7 +151,7 @@ export function getCsvFilename(prefix: string, suffix?: string): string { const date = new Date().toISOString().slice(0, 10); const parts = [prefix, suffix, date].filter(Boolean).join('-'); // Strip unsafe filename chars - return parts.replace(/[^a-zA-Z0-9_\-\.]/g, '_') + '.csv'; + return parts.replace(/[^a-zA-Z0-9_.-]/g, '_') + '.csv'; } // ── Compat wrappers ────────────────────────────────────────────────────── diff --git a/src/lib/evolutionDirectClient.ts b/src/lib/evolutionDirectClient.ts index eec21eb8c..41cef57fe 100644 --- a/src/lib/evolutionDirectClient.ts +++ b/src/lib/evolutionDirectClient.ts @@ -15,7 +15,7 @@ export function getDirectConfig(): DirectEvoConfig | null { if (!raw) return null; const cfg = JSON.parse(raw) as DirectEvoConfig; if (cfg.evolution_api_url && cfg.evolution_api_key) return cfg; - } catch {} + } catch { /* intentionally empty */ } return null; } diff --git a/src/lib/security.ts b/src/lib/security.ts index 49569f9be..9a7ed1e3f 100644 --- a/src/lib/security.ts +++ b/src/lib/security.ts @@ -99,6 +99,7 @@ export function truncate(str: string, maxLength: number): string { export function sanitizeDisplayName(name: string, maxLength = 100): string { // Remove zero-width characters, control chars, and direction overrides const cleaned = name + // eslint-disable-next-line no-control-regex -- intentionally matches control chars (U+0000-001F, U+007F-009F) to strip them .replace(/[\u200B-\u200F\u202A-\u202E\uFEFF\u0000-\u001F\u007F-\u009F]/g, '') .trim(); @@ -112,7 +113,7 @@ export function sanitizeDisplayName(name: string, maxLength = 100): string { export function sanitizeJid(jid: string): string | null { // Individual: 5511999999999@s.whatsapp.net // Group: 120363XXXX@g.us - const jidPattern = /^[\d\-]+@(s\.whatsapp\.net|g\.us|lid|newsletter)$/; + const jidPattern = /^[\d-]+@(s\.whatsapp\.net|g\.us|lid|newsletter)$/; const trimmed = jid.trim(); if (jidPattern.test(trimmed)) { diff --git a/src/pages/admin/AdminStressTestPage.tsx b/src/pages/admin/AdminStressTestPage.tsx index a17972f63..6fe2b3e06 100644 --- a/src/pages/admin/AdminStressTestPage.tsx +++ b/src/pages/admin/AdminStressTestPage.tsx @@ -90,7 +90,6 @@ export default function AdminStressTestPage() { type: StressTaskType; idx: number; phone: string; instance: string; }) => { const sample = await sampleFor(type, idx); - let messageId: string | undefined; let result: any; let accessibility: StressResult['accessibility'] | undefined; @@ -162,7 +161,7 @@ export default function AdminStressTestPage() { } as any); break; } - messageId = result?.key?.id ?? result?.messageId ?? result?.id; + const messageId = result?.key?.id ?? result?.messageId ?? result?.id; return { messageId, detail: sample.detail, accessibility }; }, [evo]);