/* =========================================================
Dr. Boechat — Shared form components (React, Babel)
Exposes components on window for the three form apps.
========================================================= */
const { useState, useRef, useEffect, useCallback } = React;
/* ---------------- Masks ---------------- */
const onlyDigits = (s) => (s || "").replace(/\D/g, "");
const maskCPF = (v) => {
const d = onlyDigits(v).slice(0, 11);
return d
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d{1,2})$/, "$1-$2");
};
const maskPhone = (v) => {
const d = onlyDigits(v).slice(0, 11);
if (d.length <= 10)
return d.replace(/(\d{2})(\d)/, "($1) $2").replace(/(\d{4})(\d)/, "$1-$2");
return d.replace(/(\d{2})(\d)/, "($1) $2").replace(/(\d{5})(\d)/, "$1-$2");
};
const maskCEP = (v) => onlyDigits(v).slice(0, 8).replace(/(\d{5})(\d)/, "$1-$2");
const maskDate = (v) => {
const d = onlyDigits(v).slice(0, 8);
return d.replace(/(\d{2})(\d)/, "$1/$2").replace(/(\d{2})(\d)/, "$1/$2");
};
const MASKS = { cpf: maskCPF, phone: maskPhone, cep: maskCEP, date: maskDate };
/* ---------------- Validators ---------------- */
const isCPFComplete = (v) => onlyDigits(v).length === 11;
const isEmail = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim());
const isPhoneComplete = (v) => onlyDigits(v).length >= 10;
const isDateComplete = (v) => onlyDigits(v).length === 8;
/* ---------------- Icons ---------------- */
const Ico = {
back: ,
alert: ,
arrowR: ,
arrowL: ,
check: ,
send: ,
eraser: ,
};
const Svg = ({ d, w = 18, sw = 1.8, fill = "none" }) => (
);
/* ---------------- AppTop ---------------- */
function AppTop({ logo = "../assets/logo-purple.png" }) {
return (
);
}
/* ---------------- Field wrapper ---------------- */
function Field({ label, required, error, hint, full, children }) {
return (
{label && (
)}
{children}
{error ? (
{error}
) : hint ? (
{hint}
) : null}
);
}
/* ---------------- TextField (with optional mask) ---------------- */
function TextField({
label, value, onChange, mask, type = "text", required, error, hint,
placeholder, full, inputMode, maxLength,
}) {
const filled = value && String(value).length > 0 && !error;
const handle = (e) => {
let v = e.target.value;
if (mask && MASKS[mask]) v = MASKS[mask](v);
onChange(v);
};
return (
{label && }
{error ? (
{error}
) : hint ? ({hint}) : null}
);
}
/* ---------------- SelectField ---------------- */
function SelectField({ label, value, onChange, options, required, error, hint, full, placeholder = "Selecione…" }) {
const filled = value && value.length > 0 && !error;
return (
{label && }
{error ? (
{error}
) : hint ? ({hint}) : null}
);
}
/* ---------------- TextArea ---------------- */
function TextArea({ label, value, onChange, required, error, hint, placeholder, rows }) {
const filled = value && value.length > 0;
return (
{label && }
);
}
/* ---------------- ChoiceGroup (radio/checkbox chips) ---------------- */
function ChoiceGroup({ options, value, onChange, multi }) {
const isOn = (o) => multi ? (value || []).includes(o) : value === o;
const toggle = (o) => {
if (multi) {
const set = new Set(value || []);
set.has(o) ? set.delete(o) : set.add(o);
onChange([...set]);
} else onChange(o);
};
return (
{options.map((o) => (
))}
);
}
/* ---------------- Yes/No row (anamnese) ---------------- */
function YesNo({ q, value, onChange }) {
return (
{q}
);
}
/* ---------------- Stepper ---------------- */
function Stepper({ steps, current }) {
const pct = ((current) / (steps.length - 1)) * 100;
return (
{steps.map((s, i) => (
{i < current ? : i + 1}
{s}
))}
);
}
/* ---------------- Section head ---------------- */
function SectionHead({ num, title }) {
return (
{num && {num}}
{title}
);
}
/* ---------------- Signature pad ---------------- */
function SignaturePad({ onChange }) {
const ref = useRef(null);
const [signed, setSigned] = useState(false);
const drawing = useRef(false);
const last = useRef(null);
const setup = useCallback(() => {
const c = ref.current; if (!c) return;
const r = c.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
c.width = r.width * dpr; c.height = r.height * dpr;
const ctx = c.getContext("2d");
ctx.scale(dpr, dpr);
ctx.lineWidth = 2.2; ctx.lineCap = "round"; ctx.lineJoin = "round";
ctx.strokeStyle = "#3F0C63";
}, []);
useEffect(() => { setup(); window.addEventListener("resize", setup); return () => window.removeEventListener("resize", setup); }, [setup]);
const pos = (e) => {
const c = ref.current; const r = c.getBoundingClientRect();
const t = e.touches ? e.touches[0] : e;
return { x: t.clientX - r.left, y: t.clientY - r.top };
};
const start = (e) => { e.preventDefault(); drawing.current = true; last.current = pos(e); };
const move = (e) => {
if (!drawing.current) return; e.preventDefault();
const ctx = ref.current.getContext("2d");
const p = pos(e);
ctx.beginPath(); ctx.moveTo(last.current.x, last.current.y); ctx.lineTo(p.x, p.y); ctx.stroke();
last.current = p;
if (!signed) { setSigned(true); onChange && onChange(true); }
};
const end = () => { drawing.current = false; };
const clear = () => {
const c = ref.current; const ctx = c.getContext("2d");
ctx.clearRect(0, 0, c.width, c.height);
setSigned(false); onChange && onChange(false);
};
return (
{signed ? "Assinatura registrada." : "Assine no quadro acima com o dedo ou mouse."}
);
}
/* ---------------- Scale row (0–N selectable) ---------------- */
function ScaleRow({ value, onChange, min = 0, max = 10, leftLabel, rightLabel }) {
const items = [];
for (let i = min; i <= max; i++) items.push(i);
return (
{items.map((i) => (
))}
{(leftLabel || rightLabel) && (
{leftLabel}{rightLabel}
)}
);
}
/* ---------------- Question block (number + text + control) ---------------- */
function QBlock({ num, text, children }) {
return (
{num != null && {num}}
{text}
{children}
);
}
/* ---------------- Legend (key for scale points) ---------------- */
function ScaleLegend({ items }) {
return (
{items.map((it) => (
{it.v} {it.l}
))}
);
}
/* ---------------- Result panel (score + band) ---------------- */
function ResultPanel({ score, max, band, bandColor, caption, children }) {
return (
{score}/{max}
{band &&
{band}
}
{caption &&
{caption}
}
{children}
);
}
/* ---------------- Sticky progress (answered / total) ---------------- */
function AnsweredBar({ answered, total }) {
const pct = total ? Math.round((answered / total) * 100) : 0;
return (
{answered} de {total} respondidas
);
}
/* ---------------- Success screen ---------------- */
function DoneScreen({ title, children }) {
return (
{title}
{children}
Atende pessoas, não doenças.
);
}
Object.assign(window, {
onlyDigits, MASKS, isCPFComplete, isEmail, isPhoneComplete, isDateComplete,
Ico, Svg, AppTop, Field, TextField, SelectField, TextArea, ChoiceGroup,
YesNo, Stepper, SectionHead, SignaturePad, DoneScreen,
ScaleRow, QBlock, ScaleLegend, ResultPanel, AnsweredBar,
});