,
document.body
);
}
/* ---------- Drawer ---------- */
function Drawer({ children, onClose, head, footer, className }) {
useEffect(() => {
const h = (e) => e.key === 'Escape' && onClose();
window.addEventListener('keydown', h);
return () => window.removeEventListener('keydown', h);
}, [onClose]);
// Portal para document.body: garante que o drawer ocupe a viewport inteira
// mesmo quando aberto a partir de uma tela com animação/transform.
return ReactDOM.createPortal(
e.target === e.currentTarget && onClose()}>
{head}
{children}
{footer &&
{footer}
}
,
document.body
);
}
/* ---------- Field ---------- */
function Field({ label, optional, children, full }) {
return (
{label && }
{children}
);
}
function Input(props) { return ; }
function Textarea(props) { return ; }
function Select({ children, ...props }) { return ; }
function ColorPicker({ value, onChange, usedColors = [], currentColor = '', label = 'Cor da agenda', optional = true }) {
const DB = window.DB || {};
const normalize = DB.normalizeColor || ((c) => String(c || '').trim().toLowerCase() || 'gray');
const swatches = DB.personColorSwatches || [];
const selected = normalize(value);
const current = normalize(currentColor || value);
const used = new Set((usedColors || []).map(normalize).filter((c) => c && c !== 'gray' && c !== current));
const taken = selected !== 'gray' && used.has(selected);
const customValue = /^#[0-9a-f]{6}$/i.test(selected) ? selected : '#d1d5db';
return (
{swatches.map((item) => {
const color = normalize(item.value);
const disabled = used.has(color);
const active = selected === color;
return (
{taken ? 'Esta cor ja esta sendo usada por outra pessoa.' : 'Se nenhuma cor for escolhida, sera usado cinza claro.'}
);
}
/* ---------- Input com máscara (telefone, CPF, CEP) ----------
`mask` recebe o texto e devolve já formatado. O onChange recebe um
evento com o valor mascarado — compatível com os handlers `set('campo')`. */
function MaskInput({ mask, value, onChange, ...props }) {
const apply = mask ? (v) => window.DB.mask[mask](v) : (v) => v;
return (
onChange && onChange({ target: { value: apply(e.target.value) } })}
{...props} />
);
}
/* ---------- Input monetário (R$) ----------
Mantém o valor como número; exibe formatado. onChange recebe o número. */
function MoneyInput({ value, onChange, ...props }) {
return (
onChange && onChange(window.DB.moneyFromDigits(e.target.value))}
{...props} />
);
}
/* ---------- Toggle ---------- */
function Switch({ on, onClick, disabled = false }) {
return ;
}
/* ---------- Segmented ---------- */
function Seg({ options, value, onChange }) {
return (
{options.map(o => (
))}
);
}
/* ---------- Checkbox ---------- */
function Checkbox({ on, onClick }) {
return ;
}
/* ---------- Dropdown menu ---------- */
function Menu({ trigger, children, align = 'right', width }) {
const [open, setOpen] = useState(false);
const [pos, setPos] = useState(null);
const ref = useRef(null);
const menuRef = useRef(null);
useEffect(() => {
if (!open) return;
const h = (e) => {
if (ref.current && !ref.current.contains(e.target) && (!menuRef.current || !menuRef.current.contains(e.target))) setOpen(false);
};
const close = () => setOpen(false);
document.addEventListener('mousedown', h);
window.addEventListener('scroll', close, true);
window.addEventListener('resize', close);
return () => {
document.removeEventListener('mousedown', h);
window.removeEventListener('scroll', close, true);
window.removeEventListener('resize', close);
};
}, [open]);
const toggle = () => {
if (!open && ref.current) {
const r = ref.current.getBoundingClientRect();
const up = window.innerHeight - r.bottom < 280 && r.top > 280; // perto do rodapé, abre para cima
setPos({
top: up ? 'auto' : r.bottom + 6,
bottom: up ? window.innerHeight - r.top + 6 : 'auto',
left: align === 'left' ? r.left : 'auto',
right: align === 'left' ? 'auto' : window.innerWidth - r.right,
});
}
setOpen(o => !o);
};
// Portal + position:fixed: o menu nunca é cortado por containers
// com overflow (cards de tabela, drawers).
return (