/* Maitha Tech — Vagas: componentes compartilhados pela família de páginas
(listagem, detalhe, candidatura, indicação, confirmação).
Depende de mt-primitives.jsx (Button, Accent, Reveal, Section, Eyebrow)
e de mt-vagas-api.jsx. Carregado antes dos arquivos de página. */
/* —— Estilos da família (injetados uma vez, padrão de RD_STYLE) ———— */
const VAGAS_STYLE = `
.mt-vagas-orn { position: absolute; top: 0; right: -7%; width: clamp(360px, 34vw, 560px); height: auto; pointer-events: none; user-select: none; z-index: 0; opacity: 0.9; -webkit-mask-image: linear-gradient(to bottom left, #000 55%, transparent 100%); mask-image: linear-gradient(to bottom left, #000 55%, transparent 100%); }
@media (max-width: 760px) { .mt-vagas-orn { width: 240px; opacity: 0.7; } }
.mt-vagas-orn-detail { position: absolute; top: 0; right: -3%; width: clamp(300px, 28vw, 460px); height: auto; pointer-events: none; user-select: none; z-index: 0; opacity: 0.9; -webkit-mask-image: linear-gradient(to bottom left, #000 55%, transparent 100%); mask-image: linear-gradient(to bottom left, #000 55%, transparent 100%); }
@media (max-width: 760px) { .mt-vagas-orn-detail { width: 220px; opacity: 0.7; } }
.mt-vagas-orn-success { position: absolute; bottom: -70px; left: -7%; width: clamp(300px, 28vw, 460px); height: auto; pointer-events: none; user-select: none; z-index: 0; opacity: 0.9; transform: rotate(180deg); -webkit-mask-image: linear-gradient(to top right, #000 55%, transparent 100%); mask-image: linear-gradient(to top right, #000 55%, transparent 100%); }
@media (max-width: 760px) { .mt-vagas-orn-success { width: 220px; opacity: 0.7; } }
.mt-vagas-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
.mt-vagas-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px 22px; }
.mt-vagas-span-2 { grid-column: 1 / -1; }
.mt-vagas-toolbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.mt-vagas-dd { position: relative; }
.mt-vagas-dd-panel { position: absolute; top: calc(100% + 8px); left: 0; min-width: 260px; z-index: 20; }
.mt-vagas-desc p { margin: 0 0 14px; }
.mt-vagas-desc p:last-child { margin-bottom: 0; }
.mt-vagas-desc ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.mt-vagas-field select, .mt-vagas-field input { color-scheme: dark; }
.mt-vagas-field input[type=checkbox], .mt-vagas-field input[type=radio] { accent-color: var(--accent); }
.mt-vagas-field select option { background: var(--mt-near-black); color: var(--text-strong); }
.mt-vagas-sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
@keyframes mt-vagas-shimmer { from { opacity: .45; } to { opacity: .9; } }
.mt-vagas-skel { background: var(--mt-graphite); border-radius: var(--radius-md); animation: mt-vagas-shimmer .9s ease-in-out infinite alternate; }
@media (prefers-reduced-motion: reduce) { .mt-vagas-skel { animation: none; } }
@media (max-width: 760px) {
.mt-vagas-grid, .mt-vagas-form-grid { grid-template-columns: 1fr !important; }
.mt-vagas-dd-panel { position: static; min-width: 0; margin-top: 8px; }
}
`;
function vagasInjectStyle() {
if (document.getElementById("mt-vagas-style")) return;
const el = document.createElement("style");
el.id = "mt-vagas-style";
el.textContent = VAGAS_STYLE;
document.head.appendChild(el);
}
vagasInjectStyle();
/* —— Ícones (paths Lucide, traço currentColor) ————————————— */
const VAGAS_ICON_PATHS = {
"map-pin": ' ',
briefcase: ' ',
layers: ' ',
tag: ' ',
monitor: ' ',
search: ' ',
"chevron-down": ' ',
"chevron-left": ' ',
"chevron-right": ' ',
"arrow-left": ' ',
"arrow-right": ' ',
check: ' ',
plus: ' ',
paperclip: ' ',
"circle-check": ' ',
alert: ' ',
x: ' ',
house: ' ',
linkedin: ' ',
instagram: ' ',
youtube: ' ',
};
function VagasIcon({ name, size = 16, stroke = 1.75, style = {}, className = "" }) {
const inner = VAGAS_ICON_PATHS[name] || "";
return (
);
}
/* —— Hero compacto (sem imagem) ——————————————————————————— */
function VagasBackLink({ href, children }) {
return (
{children}
);
}
function VagasHero({ eyebrow, title, lead, back, size = "lg", children }) {
const fs = size === "lg" ? "clamp(2.6rem, 5.4vw, 4.8rem)" : "clamp(2rem, 3.6vw, 3rem)";
return (
{back ? {back.label} : null}
{eyebrow ? (
{eyebrow}
) : null}
{title}
{lead ? {lead}
: null}
{children}
);
}
/* —— Tags / meta —————————————————————————————————————— */
function VagasTag({ icon, children, tone = "neutral" }) {
return (
{icon ? : null}
{children}
);
}
/* Localização + senioridade (+ modelo de trabalho no detalhe). O regime de
contratação (hiringType) não é exibido por decisão de produto. */
function VagasMetaRow({ location, profile, model, tone = "warm", style = {} }) {
const loc = vagasNormalizeLocation(location).label;
return (
{loc ? {loc} : null}
{model ? {model} : null}
{profile ? {profile} : null}
);
}
/* —— Card de vaga ————————————————————————————————————— */
function VagasJobCard({ job, delay = 0 }) {
const [h, setH] = React.useState(false);
const skills = (job.skills || []).slice(0, 3).join(" · ");
return (
setH(true)} onMouseLeave={() => setH(false)} onFocus={() => setH(true)} onBlur={() => setH(false)}
style={{ display: "flex", flexDirection: "column", gap: 16, height: "100%", boxSizing: "border-box", background: "var(--surface-card)", border: `1px solid ${h ? "var(--accent)" : "var(--border-default)"}`, borderRadius: "var(--radius-lg)", padding: "clamp(22px, 2.6vw, 30px)", color: "inherit", textDecoration: "none", transform: h ? "translateY(-4px)" : "none", transition: "border-color var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out)" }}>
{skills ? {skills} : null}
{job.name}
Ver vaga
);
}
/* —— Busca ————————————————————————————————————————— */
function VagasSearchBar({ value, onChange, count, placeholder = "Buscar por vagas" }) {
const [focus, setFocus] = React.useState(false);
return (
onChange(e.target.value)} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
type="search" placeholder={placeholder} aria-label="Buscar vagas"
style={{ flex: 1, minWidth: 0, padding: "0.8rem 0", fontFamily: "var(--font-sans)", fontSize: "var(--fs-body)", color: "var(--text-strong)", background: "transparent", border: 0, outline: "none" }} />
{typeof count === "number" ? {count} {count === 1 ? "vaga" : "vagas"} : null}
);
}
/* —— Chip de filtro com dropdown de checkboxes ———————————— */
function VagasFilterChip({ label, icon, options, selected, onToggle, onClear }) {
const [open, setOpen] = React.useState(false);
const [h, setH] = React.useState(false);
const ref = React.useRef(null);
const n = selected.size;
React.useEffect(() => {
if (!open) return;
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
document.addEventListener("mousedown", onDoc);
document.addEventListener("keydown", onKey);
return () => { document.removeEventListener("mousedown", onDoc); document.removeEventListener("keydown", onKey); };
}, [open]);
const active = n > 0;
return (
setOpen((o) => !o)} aria-expanded={open} aria-haspopup="listbox" onMouseEnter={() => setH(true)} onMouseLeave={() => setH(false)}
style={{ display: "inline-flex", alignItems: "center", gap: 8, padding: "9px 16px", borderRadius: "var(--radius-pill)", cursor: "pointer", fontFamily: "var(--font-sans)", fontSize: "var(--fs-sub)", fontWeight: "var(--fw-medium)", letterSpacing: "0.02em", border: `1px solid ${active ? "var(--mt-white)" : h || open ? "var(--border-strong)" : "var(--border-default)"}`, background: active ? "var(--mt-white)" : "transparent", color: active ? "var(--mt-black)" : "var(--text-body)", transition: "background var(--dur-base), color var(--dur-base), border-color var(--dur-base)" }}>
{icon ? : null}
{label}
{active ? {n} : null}
{open ? (
{options.length === 0 ?
Sem opções
: null}
{options.map((o) => {
const on = selected.has(o.key);
return (
onToggle(o.key)} style={{ width: 15, height: 15, margin: 0 }} />
{o.label}
{typeof o.count === "number" ? {o.count} : null}
);
})}
{n > 0 && onClear ? (
{ onClear(); setOpen(false); }} style={{ marginTop: 6, width: "100%", padding: "9px 12px", background: "none", border: 0, borderTop: "1px solid var(--border-faint)", cursor: "pointer", fontFamily: "var(--font-sans)", fontSize: "var(--fs-sub)", color: "var(--text-muted)", textAlign: "left" }}>Limpar
) : null}
) : null}
);
}
/* —— Paginação ———————————————————————————————————— */
function VagasPageButton({ children, active, disabled, onClick, label }) {
const [h, setH] = React.useState(false);
return (
setH(true)} onMouseLeave={() => setH(false)}
style={{ width: 38, height: 38, display: "inline-flex", alignItems: "center", justifyContent: "center", borderRadius: "50%", cursor: disabled ? "default" : "pointer", fontFamily: "var(--font-sans)", fontSize: "var(--fs-sub)", fontWeight: "var(--fw-medium)", border: `1px solid ${active ? "var(--mt-white)" : h && !disabled ? "var(--border-strong)" : "var(--border-default)"}`, background: active ? "var(--mt-white)" : "transparent", color: active ? "var(--mt-black)" : "var(--text-body)", opacity: disabled ? 0.35 : 1, transition: "background var(--dur-base), border-color var(--dur-base), color var(--dur-base)" }}>
{children}
);
}
function vagasPageList(page, total) {
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
const set = new Set([1, total, page - 1, page, page + 1].filter((p) => p >= 1 && p <= total));
const arr = Array.from(set).sort((a, b) => a - b);
const out = [];
arr.forEach((p, i) => { if (i && p - arr[i - 1] > 1) out.push("…"); out.push(p); });
return out;
}
function VagasPagination({ page, pageSize, total, onPage }) {
const pages = Math.max(1, Math.ceil(total / pageSize));
if (total <= pageSize) return null;
const shown = Math.min(total, page * pageSize) - (page - 1) * pageSize;
return (
{shown} de {total} vagas
onPage(page - 1)} label="Página anterior">
{vagasPageList(page, pages).map((p, i) => p === "…"
? …
: onPage(p)} label={`Página ${p}`}>{p} )}
= pages} onClick={() => onPage(page + 1)} label="Próxima página">
);
}
/* —— Controles de formulário ————————————————————————— */
function vagasControlStyle(focused, error, extra = {}) {
return {
width: "100%", boxSizing: "border-box", minHeight: "3rem", padding: "0.75rem 1rem",
fontFamily: "var(--font-sans)", fontSize: "var(--fs-body)", lineHeight: 1.5, color: "var(--text-strong)",
background: "var(--mt-near-black)", borderRadius: "var(--radius-md)", outline: "none",
border: `1px solid ${error ? "var(--mt-danger)" : focused ? "var(--mt-white)" : "var(--border-strong)"}`,
boxShadow: focused ? "0 0 0 4px var(--focus-ring)" : "none",
transition: "border-color var(--dur-base), box-shadow var(--dur-base)",
...extra,
};
}
function VagasField({ label, required, error, hint, htmlFor, span2 = false, children }) {
return (
{label ? (
{label}{required ? * : null}
) : null}
{children}
{error ? {error}
: hint ? {hint} : null}
);
}
function VagasInput({ error, style = {}, ...rest }) {
const [focus, setFocus] = React.useState(false);
return (
{ setFocus(true); rest.onFocus && rest.onFocus(e); }} onBlur={(e) => { setFocus(false); rest.onBlur && rest.onBlur(e); }}
aria-invalid={error ? "true" : undefined} style={vagasControlStyle(focus, error, style)} />
);
}
/* options: [{value,label}] ou strings */
function VagasSelect({ options = [], placeholder = "Selecione", value, onChange, error, loading = false, disabled = false, id, ...rest }) {
const [focus, setFocus] = React.useState(false);
const seen = new Set();
const opts = options.map((o) => (typeof o === "string" ? { value: o, label: o } : o)).filter((o) => { if (seen.has(o.value)) return false; seen.add(o.value); return true; });
return (
onChange(e.target.value)} disabled={disabled || loading} aria-invalid={error ? "true" : undefined}
onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
style={vagasControlStyle(focus, error, { appearance: "none", WebkitAppearance: "none", paddingRight: "2.6rem", cursor: disabled || loading ? "not-allowed" : "pointer", color: value ? "var(--text-strong)" : "var(--text-faint)", opacity: disabled || loading ? 0.6 : 1 })}>
{loading ? "Carregando…" : placeholder}
{opts.map((o) => {o.label} )}
);
}
const VAGAS_CV_ACCEPT = ".pdf,.docx,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document";
function VagasFileInput({ id, file, onChange, error, accept = VAGAS_CV_ACCEPT }) {
const [focus, setFocus] = React.useState(false);
const [over, setOver] = React.useState(false);
const lit = focus || over;
return (
onChange(e.target.files && e.target.files[0] ? e.target.files[0] : null)} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)} />
{ e.preventDefault(); setOver(true); }} onDragLeave={() => setOver(false)}
onDrop={(e) => { e.preventDefault(); setOver(false); const f = e.dataTransfer.files && e.dataTransfer.files[0]; if (f) onChange(f); }}
style={{ display: "flex", alignItems: "center", gap: 12, minHeight: "3rem", padding: "0.9rem 1rem", boxSizing: "border-box", cursor: "pointer", borderRadius: "var(--radius-md)", background: "var(--mt-near-black)", border: `1px dashed ${error ? "var(--mt-danger)" : lit ? "var(--mt-white)" : "var(--border-strong)"}`, boxShadow: focus ? "0 0 0 4px var(--focus-ring)" : "none", color: file ? "var(--text-strong)" : "var(--text-muted)", fontSize: "var(--fs-body)", transition: "border-color var(--dur-base), box-shadow var(--dur-base)" }}>
{file ? file.name : "Clique aqui para anexar"}
{file ? Trocar : null}
);
}
/* options: [{value,label}] — pills exclusivas */
function VagasRadioGroup({ name, options, value, onChange }) {
return (
{options.map((o) => {
const on = value === o.value;
return (
onChange(o.value)} className="mt-vagas-sr" />
{on ? : null}
{o.label}
);
})}
);
}
function VagasCheckbox({ id, checked, onChange, error, children }) {
return (
onChange(e.target.checked)} aria-invalid={error ? "true" : undefined} style={{ width: 16, height: 16, margin: "2px 0 0", flex: "none" }} />
{children}
);
}
function VagasSubmitBar({ loading, label = "Enviar", error }) {
return (
}>
{loading ? "Enviando…" : label}
{error ? (
{error}
) : null}
);
}
/* —— Descrição da vaga ————————————————————————————— */
function VagasBullet({ kind = "check", children }) {
const box = { flex: "none", marginTop: 2, width: 22, height: 22, borderRadius: "50%", display: "inline-flex", alignItems: "center", justifyContent: "center" };
let mark;
if (kind === "check") mark = ;
else if (kind === "plus") mark = ;
else mark = ;
return (
{mark}
{children}
);
}
function VagasDetailSection({ title, blocks, delay = 0 }) {
return (
{title ? {title} : null}
{blocks.map((b, i) => b.type === "ul"
? {b.items.map((it, j) => {it} )}
: {b.text}
)}
);
}
function VagasDescription({ text }) {
const sections = React.useMemo(() => vagasParseDescription(text), [text]);
return (
{sections.map((s, i) => )}
);
}
/* —— Estados ————————————————————————————————————— */
function VagasSuccessPanel({ title, children, cta, extra }) {
return (
{title}
{children ? {children}
: null}
{cta ? {cta}
: null}
{extra ? {extra}
: null}
);
}
function VagasSkeleton({ variant = "cards", count = 6 }) {
if (variant === "cards") {
return (
{Array.from({ length: count }, (_, i) => (
))}
);
}
if (variant === "form") {
return (
{Array.from({ length: count }, (_, i) =>
)}
);
}
return (
);
}
function VagasEmptyState({ query, onClear }) {
return (
Nenhuma vaga encontrada{query ? ` para “${query}”` : ""}.
{onClear ?
Limpar filtros
: null}
);
}
function VagasErrorState({ title = "Não foi possível carregar", message, onRetry, cta }) {
return (
{title}
{message ?
{message}
: null}
{onRetry ? Tentar novamente : null}
{cta}
);
}
Object.assign(window, {
VAGAS_STYLE, vagasInjectStyle, VAGAS_ICON_PATHS, VagasIcon, VAGAS_CV_ACCEPT,
VagasBackLink, VagasHero, VagasTag, VagasMetaRow, VagasJobCard, VagasSearchBar, VagasFilterChip, VagasPagination,
vagasControlStyle, VagasField, VagasInput, VagasSelect, VagasFileInput, VagasRadioGroup, VagasCheckbox, VagasSubmitBar,
VagasBullet, VagasDetailSection, VagasDescription, VagasSuccessPanel, VagasSkeleton, VagasEmptyState, VagasErrorState,
});