/* global React */ const { useState, useEffect, useRef, useCallback, useMemo } = React; // ===================================================================== // Ícones (stroke SVG, inline) // ===================================================================== const Ico = ({ d, size = 14, className = 'ico' }) => ( ); const IcoCart = (p) => ; const IcoBox = (p) => ; const IcoClock = (p) => ; const IcoStack = (p) => ; const IcoSliders = (p) => ; const IcoMap = (p) => ; const IcoPlus = (p) => ; const IcoX = (p) => ; const IcoSave = (p) => ; const IcoDownload = (p) => ; const IcoUpload = (p) => ; const IcoRefresh = (p) => ; const IcoTrash = (p) => ; const IcoEye = (p) => ; const IcoFile = (p) => ; const IcoSparkle = (p) => ; const IcoLogout = (p) => ; window.Icons = { IcoCart, IcoBox, IcoClock, IcoStack, IcoSliders, IcoMap, IcoPlus, IcoX, IcoSave, IcoDownload, IcoUpload, IcoRefresh, IcoTrash, IcoEye, IcoFile, IcoSparkle, IcoLogout }; // ===================================================================== // Tooltip (hover sobre .tip) // ===================================================================== function Tooltip({ children, text }) { const [show, setShow] = useState(false); const [pos, setPos] = useState({ x: 0, y: 0 }); const ref = useRef(); const onEnter = (e) => { const r = e.currentTarget.getBoundingClientRect(); setPos({ x: r.left + r.width/2, y: r.top }); setShow(true); }; return ( <> setShow(false)} tabIndex={0}>? {show && (
{text}
)} ); } window.Tooltip = Tooltip; // ===================================================================== // Segmented (Sim/Não toggle moderno) // ===================================================================== function Segmented({ value, options, onChange, full = true }) { return (
{options.map(o => ( ))}
); } window.Segmented = Segmented; // ===================================================================== // Toast // ===================================================================== function ToastHost({ toast }) { if (!toast) return null; return (
{toast.kind === 'ok' && } {toast.msg}
); } window.ToastHost = ToastHost; // ===================================================================== // NumInput: input numérico tolerante (aceita vírgula, ponto e milhar BR) // // Correção importante: // - O texto digitado fica em estado local enquanto o campo está em edição. // - O componente só sincroniza com o valor externo quando o usuário NÃO está // digitando. Isso evita o efeito de “digitei, saí do campo e voltou para 0”. // - No blur, o valor confirmado é mantido visualmente e enviado ao pai sem // depender da ordem de renderização do React. // ===================================================================== const OBJ_STR_RE = /\[object[^\]]*\]/g; const isCorrupt = (s) => typeof s === 'string' && s.includes('[object'); function parseLocaleNumber(raw) { if (raw === null || raw === undefined) return null; if (typeof raw === 'number') return isFinite(raw) ? raw : null; let s = String(raw) .replace(OBJ_STR_RE, '') .replace(/R\$|%/g, '') .replace(/\s+/g, '') .trim(); if (!s || s === '-' || s === ',' || s === '.' || s === '-,' || s === '-.') return null; const hasComma = s.includes(','); const hasDot = s.includes('.'); if (hasComma && hasDot) { // O último separador digitado é tratado como decimal. if (s.lastIndexOf(',') > s.lastIndexOf('.')) { s = s.replace(/\./g, '').replace(',', '.'); // 19.813,24 -> 19813.24 } else { s = s.replace(/,/g, ''); // 19,813.24 -> 19813.24 } } else if (hasComma) { s = s.replace(',', '.'); } else if (hasDot) { // 19.813 deve ser aceito como milhar BR; 19.8 como decimal. const sign = s.startsWith('-') ? '-' : ''; const body = sign ? s.slice(1) : s; if (/^\d{1,3}(\.\d{3})+$/.test(body)) s = sign + body.replace(/\./g, ''); } const n = Number(s); return Number.isFinite(n) ? n : null; } function numToDraft(value) { if (value === null || value === undefined || value === '') return ''; const n = parseLocaleNumber(value); return n === null ? '' : String(n); } function NumInput({ value, onChange, ...rest }) { const [draft, setDraft] = useState(() => numToDraft(value)); const [editing, setEditing] = useState(false); const committedRef = useRef(null); const emit = useCallback((n) => { if (typeof onChange === 'function') onChange(n); }, [onChange]); useEffect(() => { if (editing) return; const external = parseLocaleNumber(value); const committed = committedRef.current; // Após blur, pode existir um render intermediário com o valor antigo do pai. // Nesse caso, preserva o texto confirmado em vez de voltar ao valor anterior. if (committed !== null) { if (external === null || Math.abs(external - committed) > 0.0000001) return; committedRef.current = null; } setDraft(numToDraft(value)); }, [value, editing]); const safeDraft = (typeof draft === 'string' ? draft : '').replace(OBJ_STR_RE, ''); return ( setEditing(true)} onBlur={() => { const num = parseLocaleNumber(safeDraft); setEditing(false); if (num === null) { committedRef.current = 0; setDraft(numToDraft(0)); emit(0); return; } committedRef.current = num; setDraft(numToDraft(num)); emit(num); }} onChange={(ev) => { const text = ev.target.value; if (isCorrupt(text)) { setDraft(''); return; } setDraft(text); const num = parseLocaleNumber(text); // Atualiza o estado do pai sempre que o texto já representa um número. // O draft local preserva formatos intermediários como "12," enquanto digita. if (num !== null) emit(num); }} /> ); } window.NumInput = NumInput; window.__isCorrupt = isCorrupt; window.__parseLocaleNumber = parseLocaleNumber;