sapiens-mcp 1.46.5 → 1.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/registry.js +14 -5
- package/dist/resume/template.js +1044 -0
- package/dist/skills.js +42 -0
- package/dist/tools/resume.js +154 -0
- package/package.json +3 -2
package/dist/registry.js
CHANGED
|
@@ -33,6 +33,7 @@ import { trilhas, trilhasSchema } from "./tools/trilhas.js";
|
|
|
33
33
|
import { semana, semanaSchema } from "./tools/semana.js";
|
|
34
34
|
import { shareDrop, shareDropSchema } from "./tools/shareDrop.js";
|
|
35
35
|
import { skill, skillSchema } from "./tools/skill.js";
|
|
36
|
+
import { resume, resumeSchema } from "./tools/resume.js";
|
|
36
37
|
import { describeConvexError } from "./convexClient.js";
|
|
37
38
|
import { skillMenuLine } from "./skills.js";
|
|
38
39
|
/**
|
|
@@ -208,6 +209,11 @@ export const TOOLS = {
|
|
|
208
209
|
schema: shareDropSchema,
|
|
209
210
|
handler: shareDrop,
|
|
210
211
|
},
|
|
212
|
+
sapiens_resume: {
|
|
213
|
+
description: "Currículo PRO — o gerador de currículo da casa, DE GRAÇA e sem login (mundo fechado: não fala com o backend, não cobra Sinapse). VOCÊ conduz a entrevista (ou lê o texto de LinkedIn/currículo antigo que a pessoa colar), monta o JSON no contrato e recebe o HTML pronto pra imprimir em PDF (A4). Sub-actions: 'schema' (o contrato de dados + os 4 modelos; leia ANTES do primeiro render), 'render' (data -> HTML completo). Modelos (arg theme): 'pro' (default, duas colunas com sidebar verde-jade: foto/monograma, contato, chips de competências, barras de idioma, timeline de experiência), 'executivo' (duas colunas grafite + bronze, nome em serifa), 'moderno' (coluna única, roxo da casa), 'minimal' (coluna única preto e branco); accentColor troca a cor de destaque. FOTO DE PERFIL: opcional e NUNCA persistida em banco. No MCP instalado (stdio), photoPath lê a foto local e embute como data URI direto no HTML, sem sair da máquina; no remoto, data.photo aceita URL https. No stdio, outPath salva o .html direto no disco e a resposta volta enxuta (sem trafegar o HTML inteiro). Regra de conteúdo: melhore a redação, jamais invente fato; lang ('pt'|'en') casa os títulos de seção com o idioma do conteúdo. É o MESMO template do Lab /experimentos/resume-builder.",
|
|
214
|
+
schema: resumeSchema,
|
|
215
|
+
handler: resume,
|
|
216
|
+
},
|
|
211
217
|
};
|
|
212
218
|
// Guia de uso server-level: o protocolo MCP devolve isto no handshake (initialize)
|
|
213
219
|
// e TODO cliente (Helen no Hermes, Gemini CLI, Cursor, Antigravity) injeta no modelo.
|
|
@@ -221,7 +227,7 @@ O passo a passo travado de cada fluxo mora na tool sapiens_skill, servida por es
|
|
|
221
227
|
- sapiens_skill action=list -> o índice (slug + quando usar cada uma).
|
|
222
228
|
- sapiens_skill action=get name=<slug> -> a skill inteira.
|
|
223
229
|
Slugs: ${skillMenuLine()}
|
|
224
|
-
Puxe a skill ANTES de: gerar música ou efeito sonoro (musica), escolher modelo de vídeo (video), gerar imagem (imagem), criar na identidade do usuário (studio), montar tirinha (tirinha), postar no Fórum ou no chat (forum-comunidade), redigir qualquer texto publicável (voz-da-casa), incorporar o Sintético (companhia), missão de Desafio (trilhas). Perdido no começo: primeiros-passos.
|
|
230
|
+
Puxe a skill ANTES de: gerar música ou efeito sonoro (musica), escolher modelo de vídeo (video), gerar imagem (imagem), criar na identidade do usuário (studio), montar tirinha (tirinha), postar no Fórum ou no chat (forum-comunidade), redigir qualquer texto publicável (voz-da-casa), incorporar o Sintético (companhia), missão de Desafio (trilhas), montar currículo (curriculo). Perdido no começo: primeiros-passos.
|
|
225
231
|
Cliente que lê resources MCP acha o MESMO conteúdo em skill://sapiens/<slug>/SKILL.md.
|
|
226
232
|
|
|
227
233
|
REGRA DE OURO:
|
|
@@ -249,10 +255,12 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
249
255
|
"sapiens_reference",
|
|
250
256
|
"sapiens_skill",
|
|
251
257
|
]);
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
|
|
258
|
+
// As tools que NÃO falam com o backend: skills são conteúdo estático do pacote
|
|
259
|
+
// e o currículo é render puro (data -> HTML). openWorldHint=false é honesto e
|
|
260
|
+
// ajuda o client a auto-aprovar sem nem pensar (nenhuma ação na conta de
|
|
261
|
+
// ninguém). sapiens_resume fica FORA do READ_ONLY_TOOLS: outPath escreve um
|
|
262
|
+
// .html no disco da pessoa (no stdio), então read-only seria mentira.
|
|
263
|
+
const CLOSED_WORLD_TOOLS = new Set(["sapiens_skill", "sapiens_resume"]);
|
|
256
264
|
const TOOL_TITLES = {
|
|
257
265
|
sapiens_pipeline: "Pipeline de Conteúdo",
|
|
258
266
|
sapiens_image: "Gerar Imagem",
|
|
@@ -286,6 +294,7 @@ const TOOL_TITLES = {
|
|
|
286
294
|
sapiens_semana: "Minha Semana",
|
|
287
295
|
sapiens_instagram: "Auto-DM Instagram",
|
|
288
296
|
sapiens_share: "Share Drop",
|
|
297
|
+
sapiens_resume: "Currículo PRO",
|
|
289
298
|
};
|
|
290
299
|
// Tools ADMIN-ONLY de ponta a ponta (o Convex recusa user comum em toda action):
|
|
291
300
|
// escondidas do tools/list quando o servidor SABE que o tier é user. São as
|
|
@@ -0,0 +1,1044 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Currículo PRO — o template da casa, fonte ÚNICA.
|
|
3
|
+
*
|
|
4
|
+
* Mora no pacote sapiens-mcp de propósito: o Lab web (apps/sapiens, rota
|
|
5
|
+
* /experimentos/resume-builder) importa daqui via `sapiens-mcp/resume`, e a
|
|
6
|
+
* tool sapiens_resume serve o MESMO gerador pra qualquer client MCP. Um
|
|
7
|
+
* template, duas portas, zero drift.
|
|
8
|
+
*
|
|
9
|
+
* É código PURO (data -> string HTML): sem rede, sem backend, sem persistência.
|
|
10
|
+
* A foto (`photo`) entra como data URI ou URL e vai DIRETO pro HTML gerado,
|
|
11
|
+
* nunca é gravada em banco nenhum. No stdio ela nem sai da máquina da pessoa.
|
|
12
|
+
*
|
|
13
|
+
* Quatro modelos (`theme`): 'pro' e 'executivo' são a folha de duas colunas
|
|
14
|
+
* (sidebar escura com foto, contato, chips e barras de idioma; coluna principal
|
|
15
|
+
* com timeline de experiência), 'moderno' e 'minimal' são a folha clássica de
|
|
16
|
+
* coluna única.
|
|
17
|
+
*/
|
|
18
|
+
export const RESUME_THEMES = {
|
|
19
|
+
pro: {
|
|
20
|
+
label: "PRO",
|
|
21
|
+
description: "Duas colunas: sidebar verde-jade escura com foto, contato, chips e idiomas; principal com timeline de experiência. O visual de consultoria.",
|
|
22
|
+
layout: "sidebar",
|
|
23
|
+
fontsQuery: "family=Inter:wght@400;500;600;700;800",
|
|
24
|
+
displayFamily: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
|
|
25
|
+
bodyFamily: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
|
|
26
|
+
accent: "#3E9C86",
|
|
27
|
+
sidebarBg: "#17453C",
|
|
28
|
+
sidebarText: "#F2F7F5",
|
|
29
|
+
sidebarMuted: "#B9CCC6",
|
|
30
|
+
pageBg: "#EFF2F1",
|
|
31
|
+
photoRadius: "50%",
|
|
32
|
+
},
|
|
33
|
+
executivo: {
|
|
34
|
+
label: "Executivo",
|
|
35
|
+
description: "Duas colunas em grafite com detalhe bronze e nome em serifa. Sóbrio, direção e C-level.",
|
|
36
|
+
layout: "sidebar",
|
|
37
|
+
fontsQuery: "family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600;700",
|
|
38
|
+
displayFamily: "'Fraunces', Georgia, 'Times New Roman', serif",
|
|
39
|
+
bodyFamily: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
|
|
40
|
+
accent: "#B08D57",
|
|
41
|
+
sidebarBg: "#23262B",
|
|
42
|
+
sidebarText: "#F4F2EE",
|
|
43
|
+
sidebarMuted: "#B7B3AA",
|
|
44
|
+
pageBg: "#F1F0ED",
|
|
45
|
+
photoRadius: "50%",
|
|
46
|
+
},
|
|
47
|
+
moderno: {
|
|
48
|
+
label: "Moderno",
|
|
49
|
+
description: "Coluna única, foto no cabeçalho, roxo da casa. Direto e contemporâneo.",
|
|
50
|
+
layout: "classico",
|
|
51
|
+
fontsQuery: "family=Inter:wght@400;500;600;700&family=Sora:wght@600;700;800",
|
|
52
|
+
displayFamily: "'Sora', 'Inter', -apple-system, sans-serif",
|
|
53
|
+
bodyFamily: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
|
|
54
|
+
accent: "#6C63FF",
|
|
55
|
+
sidebarBg: "#1A1A2E",
|
|
56
|
+
sidebarText: "#F2F2F7",
|
|
57
|
+
sidebarMuted: "#B9B9C9",
|
|
58
|
+
pageBg: "#F6F6F9",
|
|
59
|
+
photoRadius: "22%",
|
|
60
|
+
},
|
|
61
|
+
minimal: {
|
|
62
|
+
label: "Minimal",
|
|
63
|
+
description: "Coluna única, preto no branco, filetes finos. Máximo de respiro, zero enfeite.",
|
|
64
|
+
layout: "classico",
|
|
65
|
+
fontsQuery: "family=Inter:wght@400;500;600;700;800",
|
|
66
|
+
displayFamily: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
|
|
67
|
+
bodyFamily: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
|
|
68
|
+
accent: "#111111",
|
|
69
|
+
sidebarBg: "#161616",
|
|
70
|
+
sidebarText: "#F5F5F5",
|
|
71
|
+
sidebarMuted: "#BBBBBB",
|
|
72
|
+
pageBg: "#FFFFFF",
|
|
73
|
+
photoRadius: "8px",
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const LABELS = {
|
|
77
|
+
summary: { pt: "Resumo Profissional", en: "Professional Summary" },
|
|
78
|
+
experience: { pt: "Experiência Profissional", en: "Professional Experience" },
|
|
79
|
+
contact: { pt: "Contacto", en: "Contact" },
|
|
80
|
+
languages: { pt: "Idiomas", en: "Languages" },
|
|
81
|
+
clients: { pt: "Clientes Selecionados", en: "Selected Clients" },
|
|
82
|
+
education: { pt: "Educação", en: "Education" },
|
|
83
|
+
skills: { pt: "Competências", en: "Skills" },
|
|
84
|
+
grants: { pt: "Bolsas & Fomentos", en: "Grants" },
|
|
85
|
+
awards: { pt: "Prémios & Honrarias", en: "Honors & Awards" },
|
|
86
|
+
publications: { pt: "Publicações", en: "Publications" },
|
|
87
|
+
};
|
|
88
|
+
function hexToRgba(hex, alpha) {
|
|
89
|
+
const m = hex.trim().match(/^#?([0-9a-f]{6})$/i);
|
|
90
|
+
if (!m)
|
|
91
|
+
return `rgba(62, 156, 134, ${alpha})`;
|
|
92
|
+
const n = parseInt(m[1], 16);
|
|
93
|
+
return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;
|
|
94
|
+
}
|
|
95
|
+
function escapeHtml(text) {
|
|
96
|
+
if (text === null || text === undefined)
|
|
97
|
+
return "";
|
|
98
|
+
return String(text)
|
|
99
|
+
.replace(/&/g, "&")
|
|
100
|
+
.replace(/</g, "<")
|
|
101
|
+
.replace(/>/g, ">")
|
|
102
|
+
.replace(/"/g, """);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Texto corrido (summary, bullets) aceita SÓ <em> e <strong>. O resto escapa.
|
|
106
|
+
*/
|
|
107
|
+
function sanitizeRichText(text) {
|
|
108
|
+
if (text === null || text === undefined)
|
|
109
|
+
return "";
|
|
110
|
+
return escapeHtml(text).replace(/<(\/?)(em|strong)>/gi, "<$1$2>");
|
|
111
|
+
}
|
|
112
|
+
/** Foto: data URI de imagem ou URL http(s). Fora disso, ignora em silêncio. */
|
|
113
|
+
function safePhotoSrc(photo) {
|
|
114
|
+
if (typeof photo !== "string")
|
|
115
|
+
return null;
|
|
116
|
+
const p = photo.trim();
|
|
117
|
+
if (/^data:image\/(png|jpe?g|webp|gif);base64,[a-z0-9+/=\s]+$/i.test(p))
|
|
118
|
+
return p;
|
|
119
|
+
if (/^https?:\/\/[^\s"'<>]+$/i.test(p))
|
|
120
|
+
return p;
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/** Iniciais pro monograma quando não há foto (o "AF" no círculo). */
|
|
124
|
+
function monogram(name) {
|
|
125
|
+
const parts = String(name ?? "")
|
|
126
|
+
.trim()
|
|
127
|
+
.split(/\s+/)
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
if (parts.length === 0)
|
|
130
|
+
return "CV";
|
|
131
|
+
const first = parts[0][0] ?? "";
|
|
132
|
+
const last = parts.length > 1 ? (parts[parts.length - 1][0] ?? "") : "";
|
|
133
|
+
return (first + last).toUpperCase();
|
|
134
|
+
}
|
|
135
|
+
const ICONS = {
|
|
136
|
+
location: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>`,
|
|
137
|
+
phone: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>`,
|
|
138
|
+
email: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>`,
|
|
139
|
+
linkedin: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-4 0v7h-4v-7a6 6 0 0 1 6-6z"/><rect x="2" y="9" width="4" height="12"/><circle cx="4" cy="4" r="2"/></svg>`,
|
|
140
|
+
portfolio: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="18" rx="2"/><path d="M8 7h8M8 12h8M8 17h8"/></svg>`,
|
|
141
|
+
website: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>`,
|
|
142
|
+
};
|
|
143
|
+
function collectContacts(data) {
|
|
144
|
+
const out = [];
|
|
145
|
+
if (data.location)
|
|
146
|
+
out.push({ icon: "location", content: data.location });
|
|
147
|
+
if (data.phone)
|
|
148
|
+
out.push({ icon: "phone", content: data.phone });
|
|
149
|
+
if (data.email)
|
|
150
|
+
out.push({ icon: "email", content: data.email, url: `mailto:${data.email}` });
|
|
151
|
+
if (data.linkedin?.url)
|
|
152
|
+
out.push({ icon: "linkedin", content: data.linkedin.label || data.linkedin.url, url: data.linkedin.url });
|
|
153
|
+
if (data.portfolio?.url)
|
|
154
|
+
out.push({ icon: "portfolio", content: data.portfolio.label || data.portfolio.url, url: data.portfolio.url });
|
|
155
|
+
if (data.website?.url)
|
|
156
|
+
out.push({ icon: "website", content: data.website.label || data.website.url, url: data.website.url });
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
function renderContact(entry) {
|
|
160
|
+
const iconSvg = ICONS[entry.icon] || ICONS.website;
|
|
161
|
+
const inner = entry.url
|
|
162
|
+
? `<a href="${escapeHtml(entry.url)}" target="_blank" rel="noopener">${escapeHtml(entry.content)}</a>`
|
|
163
|
+
: escapeHtml(entry.content);
|
|
164
|
+
return `<span class="contact-item">${iconSvg} ${inner}</span>`;
|
|
165
|
+
}
|
|
166
|
+
/* ============================================================
|
|
167
|
+
CSS
|
|
168
|
+
============================================================ */
|
|
169
|
+
function buildCss(spec, accent) {
|
|
170
|
+
const isSidebar = spec.layout === "sidebar";
|
|
171
|
+
const accentSoft = hexToRgba(accent, 0.09);
|
|
172
|
+
const accentBorder = hexToRgba(accent, 0.18);
|
|
173
|
+
const sidebarChipBg = "rgba(255, 255, 255, 0.09)";
|
|
174
|
+
const sidebarChipBorder = "rgba(255, 255, 255, 0.13)";
|
|
175
|
+
const sidebarRule = "rgba(255, 255, 255, 0.14)";
|
|
176
|
+
// Na sidebar escura o accent puro pode ficar apagado; clareia levemente.
|
|
177
|
+
const accentOnDark = hexToRgba(accent, 1).replace("rgba(", "rgba(").replace(", 1)", ", 1)");
|
|
178
|
+
const base = `
|
|
179
|
+
:root {
|
|
180
|
+
--bg: ${spec.pageBg};
|
|
181
|
+
--surface: #FFFFFF;
|
|
182
|
+
--ink: #1B1E23;
|
|
183
|
+
--ink-2: #4A4E57;
|
|
184
|
+
--ink-3: #82868E;
|
|
185
|
+
--border: #E5E6E8;
|
|
186
|
+
--border-light: #F0F0F2;
|
|
187
|
+
--accent: ${accent};
|
|
188
|
+
--accent-soft: ${accentSoft};
|
|
189
|
+
--accent-border: ${accentBorder};
|
|
190
|
+
--tag-bg: #F3F4F4;
|
|
191
|
+
--tag-text: #45484E;
|
|
192
|
+
--sidebar-bg: ${spec.sidebarBg};
|
|
193
|
+
--sidebar-text: ${spec.sidebarText};
|
|
194
|
+
--sidebar-muted: ${spec.sidebarMuted};
|
|
195
|
+
--sidebar-accent: ${accentOnDark};
|
|
196
|
+
--font-display: ${spec.displayFamily};
|
|
197
|
+
--font-body: ${spec.bodyFamily};
|
|
198
|
+
--photo-radius: ${spec.photoRadius};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
202
|
+
|
|
203
|
+
html {
|
|
204
|
+
font-size: 15.5px;
|
|
205
|
+
-webkit-font-smoothing: antialiased;
|
|
206
|
+
-moz-osx-font-smoothing: grayscale;
|
|
207
|
+
text-rendering: optimizeLegibility;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
body {
|
|
211
|
+
font-family: var(--font-body);
|
|
212
|
+
color: var(--ink);
|
|
213
|
+
background-color: var(--bg);
|
|
214
|
+
line-height: 1.6;
|
|
215
|
+
padding: 2.2rem 1rem;
|
|
216
|
+
font-feature-settings: "kern" 1, "liga" 1;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
a { color: var(--accent); text-decoration: none; }
|
|
220
|
+
a:hover { opacity: .72; }
|
|
221
|
+
|
|
222
|
+
.sheet {
|
|
223
|
+
max-width: 880px;
|
|
224
|
+
margin: 0 auto;
|
|
225
|
+
background: var(--surface);
|
|
226
|
+
border-radius: 12px;
|
|
227
|
+
overflow: hidden;
|
|
228
|
+
box-shadow: 0 1px 2px rgba(20, 22, 27, .05), 0 14px 44px rgba(20, 22, 27, .09);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ---- Rótulo de seção: filete curto + caixa alta espaçada ---- */
|
|
232
|
+
.sec-label {
|
|
233
|
+
display: flex;
|
|
234
|
+
align-items: center;
|
|
235
|
+
gap: .65rem;
|
|
236
|
+
font-size: .72rem;
|
|
237
|
+
font-weight: 700;
|
|
238
|
+
text-transform: uppercase;
|
|
239
|
+
letter-spacing: .16em;
|
|
240
|
+
color: var(--ink);
|
|
241
|
+
margin-bottom: 1rem;
|
|
242
|
+
}
|
|
243
|
+
.sec-label::before {
|
|
244
|
+
content: '';
|
|
245
|
+
width: 22px;
|
|
246
|
+
height: 3px;
|
|
247
|
+
border-radius: 2px;
|
|
248
|
+
background: var(--accent);
|
|
249
|
+
flex-shrink: 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.main-section { margin-bottom: 1.9rem; }
|
|
253
|
+
.main-section:last-child { margin-bottom: 0; }
|
|
254
|
+
|
|
255
|
+
.summary-text {
|
|
256
|
+
font-size: .86rem;
|
|
257
|
+
color: var(--ink-2);
|
|
258
|
+
line-height: 1.75;
|
|
259
|
+
max-width: 70ch;
|
|
260
|
+
}
|
|
261
|
+
.summary-text em { font-style: italic; color: var(--ink); font-weight: 500; }
|
|
262
|
+
.summary-text strong { font-weight: 600; color: var(--ink); }
|
|
263
|
+
|
|
264
|
+
/* ---- Experiência: timeline com pino + pílula de período ---- */
|
|
265
|
+
.xp { position: relative; padding-left: 1.35rem; }
|
|
266
|
+
.xp::before {
|
|
267
|
+
content: '';
|
|
268
|
+
position: absolute;
|
|
269
|
+
left: 5px; top: 10px; bottom: 6px;
|
|
270
|
+
width: 1px;
|
|
271
|
+
background: var(--border);
|
|
272
|
+
}
|
|
273
|
+
.xp-item { position: relative; margin-bottom: 1.5rem; }
|
|
274
|
+
.xp-item:last-child { margin-bottom: 0; }
|
|
275
|
+
.xp-item::before {
|
|
276
|
+
content: '';
|
|
277
|
+
position: absolute;
|
|
278
|
+
left: -1.35rem; top: 5px;
|
|
279
|
+
width: 9px; height: 9px;
|
|
280
|
+
border-radius: 50%;
|
|
281
|
+
background: var(--surface);
|
|
282
|
+
border: 2px solid var(--accent);
|
|
283
|
+
}
|
|
284
|
+
.xp-head {
|
|
285
|
+
display: flex;
|
|
286
|
+
justify-content: space-between;
|
|
287
|
+
align-items: flex-start;
|
|
288
|
+
gap: .5rem 1rem;
|
|
289
|
+
margin-bottom: .15rem;
|
|
290
|
+
}
|
|
291
|
+
.xp-role { font-size: .95rem; font-weight: 700; letter-spacing: -0.01em; line-height: 1.3; }
|
|
292
|
+
.xp-company { display: block; font-size: .8rem; font-weight: 600; color: var(--accent); margin-top: 1px; }
|
|
293
|
+
.xp-company a { color: var(--accent); }
|
|
294
|
+
.xp-period {
|
|
295
|
+
font-size: .7rem;
|
|
296
|
+
font-weight: 600;
|
|
297
|
+
color: var(--ink-3);
|
|
298
|
+
background: var(--tag-bg);
|
|
299
|
+
padding: 3px 10px;
|
|
300
|
+
border-radius: 5px;
|
|
301
|
+
white-space: nowrap;
|
|
302
|
+
font-variant-numeric: tabular-nums;
|
|
303
|
+
flex-shrink: 0;
|
|
304
|
+
margin-top: 2px;
|
|
305
|
+
}
|
|
306
|
+
.xp-location { font-size: .74rem; color: var(--ink-3); margin-bottom: .35rem; }
|
|
307
|
+
.xp-note {
|
|
308
|
+
font-size: .72rem;
|
|
309
|
+
font-style: italic;
|
|
310
|
+
color: var(--ink-3);
|
|
311
|
+
margin: .2rem 0 .35rem;
|
|
312
|
+
padding: 4px 10px;
|
|
313
|
+
background: var(--tag-bg);
|
|
314
|
+
border-radius: 5px;
|
|
315
|
+
display: inline-block;
|
|
316
|
+
}
|
|
317
|
+
.xp-bullets { list-style: none; margin-top: .45rem; }
|
|
318
|
+
.xp-bullets li {
|
|
319
|
+
font-size: .8rem;
|
|
320
|
+
color: var(--ink-2);
|
|
321
|
+
line-height: 1.6;
|
|
322
|
+
padding-left: 1.05rem;
|
|
323
|
+
position: relative;
|
|
324
|
+
margin-bottom: .4rem;
|
|
325
|
+
}
|
|
326
|
+
.xp-bullets li::before {
|
|
327
|
+
content: '';
|
|
328
|
+
position: absolute;
|
|
329
|
+
left: 1px; top: 8px;
|
|
330
|
+
width: 5px; height: 5px;
|
|
331
|
+
border-radius: 50%;
|
|
332
|
+
background: ${hexToRgba(accent, 0.55)};
|
|
333
|
+
}
|
|
334
|
+
.xp-bullets li strong { font-weight: 600; color: var(--ink); }
|
|
335
|
+
.xp-bullets li em { font-style: italic; }
|
|
336
|
+
|
|
337
|
+
.xp-divider { display: flex; align-items: center; gap: 1rem; margin: 1.4rem 0; }
|
|
338
|
+
.xp-divider .line { flex: 1; height: 1px; background: var(--border); }
|
|
339
|
+
.xp-divider .label {
|
|
340
|
+
font-size: .66rem;
|
|
341
|
+
font-weight: 600;
|
|
342
|
+
text-transform: uppercase;
|
|
343
|
+
letter-spacing: .1em;
|
|
344
|
+
color: var(--ink-3);
|
|
345
|
+
white-space: nowrap;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* ---- Educação ---- */
|
|
349
|
+
.edu-list { list-style: none; }
|
|
350
|
+
.edu-list li {
|
|
351
|
+
padding: .55rem 0;
|
|
352
|
+
border-bottom: 1px solid var(--border-light);
|
|
353
|
+
display: flex;
|
|
354
|
+
justify-content: space-between;
|
|
355
|
+
align-items: baseline;
|
|
356
|
+
gap: .6rem 1rem;
|
|
357
|
+
flex-wrap: wrap;
|
|
358
|
+
}
|
|
359
|
+
.edu-list li:last-child { border-bottom: none; }
|
|
360
|
+
.edu-main { font-size: .84rem; color: var(--ink-2); min-width: 0; }
|
|
361
|
+
.edu-main strong { font-weight: 600; color: var(--ink); display: block; line-height: 1.35; }
|
|
362
|
+
.edu-sub { font-size: .76rem; color: var(--ink-3); }
|
|
363
|
+
.edu-year {
|
|
364
|
+
font-size: .7rem;
|
|
365
|
+
font-weight: 600;
|
|
366
|
+
color: var(--ink-3);
|
|
367
|
+
background: var(--tag-bg);
|
|
368
|
+
padding: 3px 10px;
|
|
369
|
+
border-radius: 5px;
|
|
370
|
+
white-space: nowrap;
|
|
371
|
+
flex-shrink: 0;
|
|
372
|
+
}
|
|
373
|
+
.edu-badge {
|
|
374
|
+
font-size: .66rem;
|
|
375
|
+
font-weight: 600;
|
|
376
|
+
color: var(--accent);
|
|
377
|
+
background: var(--accent-soft);
|
|
378
|
+
padding: 2px 9px;
|
|
379
|
+
border-radius: 100px;
|
|
380
|
+
white-space: nowrap;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* ---- Chips de skill (coluna principal, layout clássico) ---- */
|
|
384
|
+
.skills-group { margin-bottom: .95rem; }
|
|
385
|
+
.skills-group:last-child { margin-bottom: 0; }
|
|
386
|
+
.skills-group-title {
|
|
387
|
+
font-size: .68rem;
|
|
388
|
+
font-weight: 600;
|
|
389
|
+
text-transform: uppercase;
|
|
390
|
+
letter-spacing: .09em;
|
|
391
|
+
color: var(--ink-3);
|
|
392
|
+
margin-bottom: .45rem;
|
|
393
|
+
}
|
|
394
|
+
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
395
|
+
.chip {
|
|
396
|
+
font-size: .72rem;
|
|
397
|
+
font-weight: 500;
|
|
398
|
+
color: var(--tag-text);
|
|
399
|
+
background: var(--tag-bg);
|
|
400
|
+
padding: 4px 12px;
|
|
401
|
+
border-radius: 6px;
|
|
402
|
+
white-space: nowrap;
|
|
403
|
+
}
|
|
404
|
+
.chip--accent {
|
|
405
|
+
background: var(--accent-soft);
|
|
406
|
+
color: var(--accent);
|
|
407
|
+
border: 1px solid var(--accent-border);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* ---- Idiomas com barra ---- */
|
|
411
|
+
.langs { list-style: none; }
|
|
412
|
+
.langs li { margin-bottom: .7rem; }
|
|
413
|
+
.langs li:last-child { margin-bottom: 0; }
|
|
414
|
+
.lang-row {
|
|
415
|
+
display: flex;
|
|
416
|
+
justify-content: space-between;
|
|
417
|
+
align-items: baseline;
|
|
418
|
+
gap: 1rem;
|
|
419
|
+
font-size: .78rem;
|
|
420
|
+
margin-bottom: .3rem;
|
|
421
|
+
}
|
|
422
|
+
.lang-name { font-weight: 600; }
|
|
423
|
+
.lang-level { font-size: .72rem; color: var(--ink-3); }
|
|
424
|
+
.lang-bar { height: 4px; border-radius: 3px; background: var(--border-light); overflow: hidden; }
|
|
425
|
+
.lang-bar > span { display: block; height: 100%; border-radius: 3px; background: var(--accent); }
|
|
426
|
+
|
|
427
|
+
/* ---- Listas auxiliares ---- */
|
|
428
|
+
.plain-list { list-style: none; }
|
|
429
|
+
.plain-list li {
|
|
430
|
+
font-size: .8rem;
|
|
431
|
+
color: var(--ink-2);
|
|
432
|
+
padding: .22rem 0 .22rem 1.1rem;
|
|
433
|
+
position: relative;
|
|
434
|
+
line-height: 1.6;
|
|
435
|
+
}
|
|
436
|
+
.plain-list li strong { font-weight: 600; color: var(--ink); }
|
|
437
|
+
.plain-list.diamonds li::before {
|
|
438
|
+
content: '◆';
|
|
439
|
+
position: absolute; left: 0; top: 7px;
|
|
440
|
+
color: var(--accent);
|
|
441
|
+
font-size: 7px;
|
|
442
|
+
opacity: .65;
|
|
443
|
+
}
|
|
444
|
+
.plain-list.arrows li::before {
|
|
445
|
+
content: '→';
|
|
446
|
+
position: absolute; left: 0;
|
|
447
|
+
color: var(--ink-3);
|
|
448
|
+
font-size: .78rem;
|
|
449
|
+
}
|
|
450
|
+
.plain-list li em { font-style: italic; color: var(--ink-3); }
|
|
451
|
+
|
|
452
|
+
.clients-line { font-size: .84rem; color: var(--ink-2); line-height: 1.8; letter-spacing: .02em; }
|
|
453
|
+
|
|
454
|
+
.closing-quote { text-align: center; padding-top: 1.4rem; border-top: 1px solid var(--border-light); }
|
|
455
|
+
.closing-quote p { font-style: italic; font-size: .86rem; color: var(--ink-2); }
|
|
456
|
+
.closing-quote span {
|
|
457
|
+
display: block;
|
|
458
|
+
margin-top: .3rem;
|
|
459
|
+
font-style: normal;
|
|
460
|
+
font-size: .66rem;
|
|
461
|
+
color: var(--ink-3);
|
|
462
|
+
letter-spacing: .06em;
|
|
463
|
+
text-transform: uppercase;
|
|
464
|
+
}
|
|
465
|
+
.sheet-footer {
|
|
466
|
+
padding: 1.1rem 2.4rem;
|
|
467
|
+
text-align: center;
|
|
468
|
+
font-size: .7rem;
|
|
469
|
+
color: var(--ink-3);
|
|
470
|
+
background: var(--bg);
|
|
471
|
+
}
|
|
472
|
+
.sheet-footer a { font-weight: 500; }
|
|
473
|
+
`;
|
|
474
|
+
const sidebarCss = `
|
|
475
|
+
/* ============ LAYOUT DUAS COLUNAS ============ */
|
|
476
|
+
.sheet { display: grid; grid-template-columns: 300px 1fr; }
|
|
477
|
+
.side {
|
|
478
|
+
background: var(--sidebar-bg);
|
|
479
|
+
color: var(--sidebar-text);
|
|
480
|
+
padding: 2.6rem 1.9rem 2.2rem;
|
|
481
|
+
}
|
|
482
|
+
.main { padding: 2.6rem 2.5rem 2.4rem; min-width: 0; }
|
|
483
|
+
|
|
484
|
+
.side a { color: var(--sidebar-text); }
|
|
485
|
+
.side a:hover { opacity: .8; }
|
|
486
|
+
|
|
487
|
+
.side-photo-wrap { display: flex; justify-content: center; margin-bottom: 1.5rem; }
|
|
488
|
+
.side-photo {
|
|
489
|
+
width: 128px; height: 128px;
|
|
490
|
+
object-fit: cover;
|
|
491
|
+
border-radius: var(--photo-radius);
|
|
492
|
+
border: 2px solid rgba(255, 255, 255, .35);
|
|
493
|
+
outline: 6px solid rgba(255, 255, 255, .06);
|
|
494
|
+
}
|
|
495
|
+
.side-monogram {
|
|
496
|
+
width: 128px; height: 128px;
|
|
497
|
+
border-radius: var(--photo-radius);
|
|
498
|
+
border: 2px solid ${hexToRgba(accent, 0.75)};
|
|
499
|
+
display: flex;
|
|
500
|
+
align-items: center;
|
|
501
|
+
justify-content: center;
|
|
502
|
+
font-family: var(--font-display);
|
|
503
|
+
font-size: 2rem;
|
|
504
|
+
font-weight: 500;
|
|
505
|
+
letter-spacing: .12em;
|
|
506
|
+
color: var(--sidebar-text);
|
|
507
|
+
}
|
|
508
|
+
.side-name {
|
|
509
|
+
font-family: var(--font-display);
|
|
510
|
+
text-align: center;
|
|
511
|
+
font-size: 1.55rem;
|
|
512
|
+
font-weight: 700;
|
|
513
|
+
letter-spacing: -0.01em;
|
|
514
|
+
line-height: 1.2;
|
|
515
|
+
margin-bottom: .45rem;
|
|
516
|
+
}
|
|
517
|
+
.side-title {
|
|
518
|
+
text-align: center;
|
|
519
|
+
font-size: .72rem;
|
|
520
|
+
font-weight: 700;
|
|
521
|
+
letter-spacing: .18em;
|
|
522
|
+
text-transform: uppercase;
|
|
523
|
+
color: var(--sidebar-accent);
|
|
524
|
+
line-height: 1.6;
|
|
525
|
+
margin-bottom: 1.9rem;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.side-label {
|
|
529
|
+
font-size: .68rem;
|
|
530
|
+
font-weight: 700;
|
|
531
|
+
text-transform: uppercase;
|
|
532
|
+
letter-spacing: .16em;
|
|
533
|
+
color: var(--sidebar-accent);
|
|
534
|
+
padding-bottom: .45rem;
|
|
535
|
+
border-bottom: 1px solid ${sidebarRule};
|
|
536
|
+
margin-bottom: .8rem;
|
|
537
|
+
}
|
|
538
|
+
.side-section { margin-bottom: 1.7rem; }
|
|
539
|
+
.side-section:last-child { margin-bottom: 0; }
|
|
540
|
+
|
|
541
|
+
.side .contact-item {
|
|
542
|
+
display: flex;
|
|
543
|
+
align-items: flex-start;
|
|
544
|
+
gap: .55rem;
|
|
545
|
+
font-size: .76rem;
|
|
546
|
+
color: var(--sidebar-text);
|
|
547
|
+
margin-bottom: .55rem;
|
|
548
|
+
line-height: 1.45;
|
|
549
|
+
word-break: break-word;
|
|
550
|
+
}
|
|
551
|
+
.side .contact-item svg { width: 13px; height: 13px; flex-shrink: 0; margin-top: 3px; opacity: .75; }
|
|
552
|
+
|
|
553
|
+
.side-highlight {
|
|
554
|
+
background: rgba(255, 255, 255, .07);
|
|
555
|
+
border: 1px solid ${sidebarChipBorder};
|
|
556
|
+
border-left: 3px solid var(--sidebar-accent);
|
|
557
|
+
border-radius: 7px;
|
|
558
|
+
padding: .65rem .8rem;
|
|
559
|
+
margin-top: .9rem;
|
|
560
|
+
}
|
|
561
|
+
.side-highlight strong { display: block; font-size: .74rem; font-weight: 600; margin-bottom: .15rem; }
|
|
562
|
+
.side-highlight p { font-size: .7rem; color: var(--sidebar-muted); line-height: 1.5; }
|
|
563
|
+
|
|
564
|
+
.side .chips { gap: 6px; }
|
|
565
|
+
.side .chip {
|
|
566
|
+
background: ${sidebarChipBg};
|
|
567
|
+
border: 1px solid ${sidebarChipBorder};
|
|
568
|
+
color: var(--sidebar-text);
|
|
569
|
+
font-weight: 500;
|
|
570
|
+
border-radius: 6px;
|
|
571
|
+
}
|
|
572
|
+
.side .skills-group-title { color: var(--sidebar-muted); }
|
|
573
|
+
|
|
574
|
+
.side .langs .lang-name { color: var(--sidebar-text); }
|
|
575
|
+
.side .langs .lang-level { color: var(--sidebar-muted); }
|
|
576
|
+
.side .lang-bar { background: rgba(255, 255, 255, .14); }
|
|
577
|
+
.side .lang-bar > span { background: var(--sidebar-accent); }
|
|
578
|
+
|
|
579
|
+
.side-lab {
|
|
580
|
+
margin-top: 1rem;
|
|
581
|
+
display: inline-flex;
|
|
582
|
+
align-items: center;
|
|
583
|
+
gap: .5rem;
|
|
584
|
+
padding: 5px 12px;
|
|
585
|
+
background: rgba(255, 255, 255, .07);
|
|
586
|
+
border: 1px solid ${sidebarChipBorder};
|
|
587
|
+
border-radius: 100px;
|
|
588
|
+
font-size: .62rem;
|
|
589
|
+
font-weight: 600;
|
|
590
|
+
letter-spacing: .05em;
|
|
591
|
+
text-transform: uppercase;
|
|
592
|
+
color: var(--sidebar-muted);
|
|
593
|
+
}
|
|
594
|
+
.side-lab i { width: 6px; height: 6px; border-radius: 50%; background: var(--sidebar-accent); }
|
|
595
|
+
|
|
596
|
+
@media (max-width: 680px) {
|
|
597
|
+
body { padding: .5rem; }
|
|
598
|
+
.sheet { grid-template-columns: 1fr; border-radius: 0; }
|
|
599
|
+
.side { padding: 2rem 1.4rem; }
|
|
600
|
+
.main { padding: 1.8rem 1.4rem; }
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
@media print {
|
|
604
|
+
@page { margin: 0; size: A4; }
|
|
605
|
+
html { font-size: 12.5px; }
|
|
606
|
+
body { padding: 0; background: white; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
607
|
+
.sheet { max-width: 100%; border-radius: 0; box-shadow: none; grid-template-columns: 33% 1fr; }
|
|
608
|
+
.side { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
609
|
+
.main { padding: 2rem 1.8rem; }
|
|
610
|
+
.xp-item, .edu-list li, .skills-group, .langs li { break-inside: avoid; }
|
|
611
|
+
.sec-label { break-after: avoid; }
|
|
612
|
+
a[href]::after { content: none; }
|
|
613
|
+
.sheet-footer { display: none; }
|
|
614
|
+
.no-print { display: none !important; }
|
|
615
|
+
}
|
|
616
|
+
`;
|
|
617
|
+
const classicCss = `
|
|
618
|
+
/* ============ LAYOUT CLÁSSICO (coluna única) ============ */
|
|
619
|
+
.head {
|
|
620
|
+
padding: 2.8rem 2.6rem 1.8rem;
|
|
621
|
+
border-bottom: 1px solid var(--border);
|
|
622
|
+
position: relative;
|
|
623
|
+
}
|
|
624
|
+
.head::before {
|
|
625
|
+
content: '';
|
|
626
|
+
position: absolute;
|
|
627
|
+
top: 0; left: 0; right: 0;
|
|
628
|
+
height: 3px;
|
|
629
|
+
background: linear-gradient(90deg, var(--accent), ${hexToRgba(accent, 0.45)}, var(--accent));
|
|
630
|
+
}
|
|
631
|
+
.head-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 1.5rem; }
|
|
632
|
+
.head-identity { min-width: 0; }
|
|
633
|
+
.head-photo {
|
|
634
|
+
width: 96px; height: 96px;
|
|
635
|
+
flex-shrink: 0;
|
|
636
|
+
object-fit: cover;
|
|
637
|
+
border-radius: var(--photo-radius);
|
|
638
|
+
border: 1px solid var(--border);
|
|
639
|
+
box-shadow: 0 2px 10px rgba(20, 22, 27, .08);
|
|
640
|
+
}
|
|
641
|
+
.head-name {
|
|
642
|
+
font-family: var(--font-display);
|
|
643
|
+
font-size: 2.3rem;
|
|
644
|
+
font-weight: 700;
|
|
645
|
+
letter-spacing: -0.025em;
|
|
646
|
+
line-height: 1.08;
|
|
647
|
+
margin-bottom: .35rem;
|
|
648
|
+
}
|
|
649
|
+
.head-title { font-size: 1.05rem; color: var(--ink-2); letter-spacing: -0.01em; margin-bottom: 1rem; }
|
|
650
|
+
.head-title strong { font-weight: 600; color: var(--accent); }
|
|
651
|
+
.head-meta { display: flex; flex-wrap: wrap; gap: .45rem 1.3rem; font-size: .78rem; color: var(--ink-3); }
|
|
652
|
+
.head-meta .contact-item { display: flex; align-items: center; gap: .35rem; }
|
|
653
|
+
.head-meta .contact-item svg { width: 13px; height: 13px; flex-shrink: 0; opacity: .8; }
|
|
654
|
+
.head-highlight {
|
|
655
|
+
margin-top: 1rem;
|
|
656
|
+
background: var(--accent-soft);
|
|
657
|
+
border: 1px solid var(--accent-border);
|
|
658
|
+
border-radius: 7px;
|
|
659
|
+
padding: .55rem .85rem;
|
|
660
|
+
max-width: 46ch;
|
|
661
|
+
}
|
|
662
|
+
.head-highlight strong { display: block; font-size: .74rem; font-weight: 600; }
|
|
663
|
+
.head-highlight p { font-size: .7rem; color: var(--ink-3); line-height: 1.5; }
|
|
664
|
+
.head-lab {
|
|
665
|
+
margin-top: 1rem;
|
|
666
|
+
display: inline-flex;
|
|
667
|
+
align-items: center;
|
|
668
|
+
gap: .5rem;
|
|
669
|
+
padding: 5px 14px;
|
|
670
|
+
background: var(--accent-soft);
|
|
671
|
+
border: 1px solid var(--accent-border);
|
|
672
|
+
border-radius: 100px;
|
|
673
|
+
font-size: .66rem;
|
|
674
|
+
font-weight: 600;
|
|
675
|
+
color: var(--accent);
|
|
676
|
+
letter-spacing: .05em;
|
|
677
|
+
text-transform: uppercase;
|
|
678
|
+
}
|
|
679
|
+
.head-lab i { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); opacity: .6; }
|
|
680
|
+
|
|
681
|
+
.main { padding: 2rem 2.6rem 2.2rem; }
|
|
682
|
+
.langs { max-width: 380px; }
|
|
683
|
+
|
|
684
|
+
@media (max-width: 640px) {
|
|
685
|
+
body { padding: .5rem; }
|
|
686
|
+
.sheet { border-radius: 0; }
|
|
687
|
+
.head, .main { padding-left: 1.4rem; padding-right: 1.4rem; }
|
|
688
|
+
.head-name { font-size: 1.8rem; }
|
|
689
|
+
.head-row { flex-direction: column-reverse; }
|
|
690
|
+
.head-photo { width: 76px; height: 76px; }
|
|
691
|
+
.xp-head { flex-direction: column; gap: .15rem; }
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
@media print {
|
|
695
|
+
@page { margin: 0.55in; size: A4; }
|
|
696
|
+
html { font-size: 13px; }
|
|
697
|
+
body { padding: 0; background: white; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
698
|
+
.sheet { max-width: 100%; border-radius: 0; box-shadow: none; }
|
|
699
|
+
.head, .main { padding-left: 0; padding-right: 0; }
|
|
700
|
+
.head { padding-top: .4rem; }
|
|
701
|
+
.xp-item, .edu-list li, .skills-group, .langs li { break-inside: avoid; }
|
|
702
|
+
.sec-label { break-after: avoid; }
|
|
703
|
+
a { color: var(--accent) !important; }
|
|
704
|
+
a[href]::after { content: none; }
|
|
705
|
+
.sheet-footer { display: none; }
|
|
706
|
+
.no-print { display: none !important; }
|
|
707
|
+
}
|
|
708
|
+
`;
|
|
709
|
+
return base + (isSidebar ? sidebarCss : classicCss);
|
|
710
|
+
}
|
|
711
|
+
/* ============================================================
|
|
712
|
+
Renderers de seção (compartilhados entre os dois layouts)
|
|
713
|
+
============================================================ */
|
|
714
|
+
function renderExperience(data, L) {
|
|
715
|
+
if (!Array.isArray(data.experience) || data.experience.length === 0)
|
|
716
|
+
return "";
|
|
717
|
+
let inner = "";
|
|
718
|
+
for (const era of data.experience) {
|
|
719
|
+
if (era.era) {
|
|
720
|
+
inner += `
|
|
721
|
+
<div class="xp-divider"><span class="line"></span><span class="label">${escapeHtml(era.era)}</span><span class="line"></span></div>`;
|
|
722
|
+
}
|
|
723
|
+
for (const item of era.items || []) {
|
|
724
|
+
const companyInner = item.companyUrl
|
|
725
|
+
? `<a href="${escapeHtml(item.companyUrl)}" target="_blank" rel="noopener">${escapeHtml(item.company)}</a>`
|
|
726
|
+
: escapeHtml(item.company);
|
|
727
|
+
const bullets = (item.bullets || [])
|
|
728
|
+
.map((b) => `<li>${sanitizeRichText(typeof b === "string" ? b : b?.text)}</li>`)
|
|
729
|
+
.join("\n");
|
|
730
|
+
inner += `
|
|
731
|
+
<div class="xp-item">
|
|
732
|
+
<div class="xp-head">
|
|
733
|
+
<div>
|
|
734
|
+
<h3 class="xp-role">${escapeHtml(item.role)}</h3>
|
|
735
|
+
<span class="xp-company">${companyInner}</span>
|
|
736
|
+
</div>
|
|
737
|
+
${item.period ? `<span class="xp-period">${escapeHtml(item.period)}</span>` : ""}
|
|
738
|
+
</div>
|
|
739
|
+
${item.location ? `<p class="xp-location">${escapeHtml(item.location)}</p>` : ""}
|
|
740
|
+
${item.note ? `<span class="xp-note">${escapeHtml(item.note)}</span>` : ""}
|
|
741
|
+
${bullets ? `<ul class="xp-bullets">${bullets}</ul>` : ""}
|
|
742
|
+
</div>`;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return `
|
|
746
|
+
<section class="main-section">
|
|
747
|
+
<p class="sec-label">${L("experience")}</p>
|
|
748
|
+
<div class="xp">${inner}</div>
|
|
749
|
+
</section>`;
|
|
750
|
+
}
|
|
751
|
+
function renderEducation(data, L) {
|
|
752
|
+
if (!Array.isArray(data.education) || data.education.length === 0)
|
|
753
|
+
return "";
|
|
754
|
+
const items = data.education
|
|
755
|
+
.map((edu) => {
|
|
756
|
+
const badge = edu.badge ? ` <span class="edu-badge">${escapeHtml(edu.badge)}</span>` : "";
|
|
757
|
+
const subtitle = edu.subtitle ? `<span class="edu-sub">${escapeHtml(edu.subtitle)}</span>` : "";
|
|
758
|
+
return `<li>
|
|
759
|
+
<div class="edu-main"><strong>${escapeHtml(edu.title)}${badge}</strong>${escapeHtml(edu.institution)}${subtitle ? "<br>" : ""}${subtitle}</div>
|
|
760
|
+
${edu.year ? `<span class="edu-year">${escapeHtml(edu.year)}</span>` : ""}
|
|
761
|
+
</li>`;
|
|
762
|
+
})
|
|
763
|
+
.join("\n");
|
|
764
|
+
return `
|
|
765
|
+
<section class="main-section">
|
|
766
|
+
<p class="sec-label">${L("education")}</p>
|
|
767
|
+
<ul class="edu-list">${items}</ul>
|
|
768
|
+
</section>`;
|
|
769
|
+
}
|
|
770
|
+
function renderSkillsGroups(data) {
|
|
771
|
+
if (!Array.isArray(data.skills) || data.skills.length === 0)
|
|
772
|
+
return "";
|
|
773
|
+
return data.skills
|
|
774
|
+
.map((group) => {
|
|
775
|
+
const tags = (group.tags || [])
|
|
776
|
+
.map((t) => `<span class="chip${t?.accent ? " chip--accent" : ""}">${escapeHtml(t?.name ?? t)}</span>`)
|
|
777
|
+
.join("\n");
|
|
778
|
+
return `
|
|
779
|
+
<div class="skills-group">
|
|
780
|
+
<p class="skills-group-title">${escapeHtml(group.group)}</p>
|
|
781
|
+
<div class="chips">${tags}</div>
|
|
782
|
+
</div>`;
|
|
783
|
+
})
|
|
784
|
+
.join("\n");
|
|
785
|
+
}
|
|
786
|
+
function renderLanguages(data) {
|
|
787
|
+
if (!Array.isArray(data.languages) || data.languages.length === 0)
|
|
788
|
+
return "";
|
|
789
|
+
return `<ul class="langs">${data.languages
|
|
790
|
+
.map((l) => {
|
|
791
|
+
const pct = Math.max(8, Math.min(100, Number(l?.pct) || 65));
|
|
792
|
+
return `<li>
|
|
793
|
+
<div class="lang-row"><span class="lang-name">${escapeHtml(l?.name)}</span><span class="lang-level">${escapeHtml(l?.level ?? "")}</span></div>
|
|
794
|
+
<div class="lang-bar"><span style="width: ${pct}%"></span></div>
|
|
795
|
+
</li>`;
|
|
796
|
+
})
|
|
797
|
+
.join("\n")}</ul>`;
|
|
798
|
+
}
|
|
799
|
+
function renderTail(data, L) {
|
|
800
|
+
let out = "";
|
|
801
|
+
if (data.clients) {
|
|
802
|
+
out += `
|
|
803
|
+
<section class="main-section">
|
|
804
|
+
<p class="sec-label">${L("clients")}</p>
|
|
805
|
+
<p class="clients-line">${escapeHtml(data.clients)}</p>
|
|
806
|
+
</section>`;
|
|
807
|
+
}
|
|
808
|
+
const simple = (key) => {
|
|
809
|
+
const arr = data[key];
|
|
810
|
+
if (!Array.isArray(arr) || arr.length === 0)
|
|
811
|
+
return;
|
|
812
|
+
const items = arr
|
|
813
|
+
.map((a) => {
|
|
814
|
+
const name = a.url
|
|
815
|
+
? `<a href="${escapeHtml(a.url)}" target="_blank" rel="noopener">${escapeHtml(a.name)}</a>`
|
|
816
|
+
: `<strong>${escapeHtml(a.name)}</strong>`;
|
|
817
|
+
return `<li>${name}${a.detail ? ` · ${escapeHtml(a.detail)}` : ""}</li>`;
|
|
818
|
+
})
|
|
819
|
+
.join("\n");
|
|
820
|
+
out += `
|
|
821
|
+
<section class="main-section">
|
|
822
|
+
<p class="sec-label">${L(key)}</p>
|
|
823
|
+
<ul class="plain-list diamonds">${items}</ul>
|
|
824
|
+
</section>`;
|
|
825
|
+
};
|
|
826
|
+
simple("grants");
|
|
827
|
+
simple("awards");
|
|
828
|
+
if (Array.isArray(data.publications) && data.publications.length > 0) {
|
|
829
|
+
const items = data.publications
|
|
830
|
+
.map((p) => {
|
|
831
|
+
const title = p.url
|
|
832
|
+
? `<a href="${escapeHtml(p.url)}" target="_blank" rel="noopener">${escapeHtml(p.title)}</a>`
|
|
833
|
+
: escapeHtml(p.title);
|
|
834
|
+
return `<li>${title}${p.note ? ` <em>· ${escapeHtml(p.note)}</em>` : ""}</li>`;
|
|
835
|
+
})
|
|
836
|
+
.join("\n");
|
|
837
|
+
out += `
|
|
838
|
+
<section class="main-section">
|
|
839
|
+
<p class="sec-label">${L("publications")}</p>
|
|
840
|
+
<ul class="plain-list arrows">${items}</ul>
|
|
841
|
+
</section>`;
|
|
842
|
+
}
|
|
843
|
+
if (data.closingQuote) {
|
|
844
|
+
out += `
|
|
845
|
+
<div class="closing-quote">
|
|
846
|
+
<p>"${escapeHtml(data.closingQuote.text)}"<span>${escapeHtml(data.closingQuote.author)}</span></p>
|
|
847
|
+
</div>`;
|
|
848
|
+
}
|
|
849
|
+
return out;
|
|
850
|
+
}
|
|
851
|
+
function renderFooter(data) {
|
|
852
|
+
if (!data.footer)
|
|
853
|
+
return "";
|
|
854
|
+
const links = data.footer.links
|
|
855
|
+
? " · " + data.footer.links.map((l) => `<a href="${escapeHtml(l.url)}" target="_blank" rel="noopener">${escapeHtml(l.label)}</a>`).join(" · ")
|
|
856
|
+
: "";
|
|
857
|
+
return `<footer class="sheet-footer">${escapeHtml(data.footer.name)}${links}</footer>`;
|
|
858
|
+
}
|
|
859
|
+
/* ============================================================
|
|
860
|
+
Documento
|
|
861
|
+
============================================================ */
|
|
862
|
+
/**
|
|
863
|
+
* Gera o documento HTML completo do currículo a partir dos dados estruturados.
|
|
864
|
+
* `data` segue o shape documentado em RESUME_SCHEMA_DESCRIPTION.
|
|
865
|
+
*/
|
|
866
|
+
export function generateResumeHTML(data, options = {}) {
|
|
867
|
+
const theme = options.theme && RESUME_THEMES[options.theme] ? options.theme : "pro";
|
|
868
|
+
const spec = RESUME_THEMES[theme];
|
|
869
|
+
const rawAccent = options.accentColor?.trim();
|
|
870
|
+
const accent = rawAccent && /^#?[0-9a-f]{6}$/i.test(rawAccent)
|
|
871
|
+
? rawAccent.startsWith("#")
|
|
872
|
+
? rawAccent
|
|
873
|
+
: `#${rawAccent}`
|
|
874
|
+
: spec.accent;
|
|
875
|
+
const lang = data?.lang === "pt" ? "pt" : "en";
|
|
876
|
+
const L = (key) => LABELS[key]?.[lang] ?? LABELS[key]?.en ?? key;
|
|
877
|
+
const contacts = collectContacts(data);
|
|
878
|
+
const photoSrc = safePhotoSrc(data.photo);
|
|
879
|
+
const titleHtml = data.titleHighlight
|
|
880
|
+
? `${escapeHtml(data.title)} · <strong>${escapeHtml(data.titleHighlight)}</strong>`
|
|
881
|
+
: escapeHtml(data.title);
|
|
882
|
+
const summaryHtml = data.summary
|
|
883
|
+
? `
|
|
884
|
+
<section class="main-section">
|
|
885
|
+
<p class="sec-label">${L("summary")}</p>
|
|
886
|
+
<p class="summary-text">${sanitizeRichText(data.summary)}</p>
|
|
887
|
+
</section>`
|
|
888
|
+
: "";
|
|
889
|
+
const mainCore = summaryHtml +
|
|
890
|
+
renderExperience(data, L) +
|
|
891
|
+
renderEducation(data, L) +
|
|
892
|
+
renderTail(data, L);
|
|
893
|
+
let bodyHtml = "";
|
|
894
|
+
if (spec.layout === "sidebar") {
|
|
895
|
+
const photoHtml = photoSrc
|
|
896
|
+
? `<img class="side-photo" src="${escapeHtml(photoSrc)}" alt="${escapeHtml(data.name)}" />`
|
|
897
|
+
: `<div class="side-monogram">${escapeHtml(monogram(data.name))}</div>`;
|
|
898
|
+
const highlightHtml = data.highlight
|
|
899
|
+
? `<div class="side-highlight"><strong>${escapeHtml(data.highlight.title)}</strong><p>${escapeHtml(data.highlight.text)}</p></div>`
|
|
900
|
+
: "";
|
|
901
|
+
const labHtml = data.labBadge
|
|
902
|
+
? `<div class="side-lab"><i></i>${escapeHtml(data.labBadge.text)}${data.labBadge.url ? ` <a href="${escapeHtml(data.labBadge.url)}" target="_blank" rel="noopener">${escapeHtml(data.labBadge.urlLabel || data.labBadge.url)}</a>` : ""}</div>`
|
|
903
|
+
: "";
|
|
904
|
+
const skillsSide = renderSkillsGroups(data);
|
|
905
|
+
const langsSide = renderLanguages(data);
|
|
906
|
+
bodyHtml = `
|
|
907
|
+
<div class="sheet">
|
|
908
|
+
<aside class="side">
|
|
909
|
+
<div class="side-photo-wrap">${photoHtml}</div>
|
|
910
|
+
<h1 class="side-name">${escapeHtml(data.name)}</h1>
|
|
911
|
+
<p class="side-title">${titleHtml}</p>
|
|
912
|
+
${contacts.length > 0
|
|
913
|
+
? `<div class="side-section"><p class="side-label">${L("contact")}</p>${contacts.map(renderContact).join("\n")}${highlightHtml}</div>`
|
|
914
|
+
: highlightHtml}
|
|
915
|
+
${skillsSide ? `<div class="side-section"><p class="side-label">${L("skills")}</p>${skillsSide}</div>` : ""}
|
|
916
|
+
${langsSide ? `<div class="side-section"><p class="side-label">${L("languages")}</p>${langsSide}</div>` : ""}
|
|
917
|
+
${labHtml}
|
|
918
|
+
</aside>
|
|
919
|
+
<div class="main">
|
|
920
|
+
${mainCore}
|
|
921
|
+
</div>
|
|
922
|
+
</div>
|
|
923
|
+
${renderFooter(data)}`;
|
|
924
|
+
}
|
|
925
|
+
else {
|
|
926
|
+
const photoHtml = photoSrc
|
|
927
|
+
? `<img class="head-photo" src="${escapeHtml(photoSrc)}" alt="${escapeHtml(data.name)}" />`
|
|
928
|
+
: "";
|
|
929
|
+
const highlightHtml = data.highlight
|
|
930
|
+
? `<div class="head-highlight"><strong>${escapeHtml(data.highlight.title)}</strong><p>${escapeHtml(data.highlight.text)}</p></div>`
|
|
931
|
+
: "";
|
|
932
|
+
const labHtml = data.labBadge
|
|
933
|
+
? `<div class="head-lab"><i></i>${data.labBadge.url
|
|
934
|
+
? `${escapeHtml(data.labBadge.text)}: <a href="${escapeHtml(data.labBadge.url)}" target="_blank" rel="noopener">${escapeHtml(data.labBadge.urlLabel || data.labBadge.url)}</a>`
|
|
935
|
+
: escapeHtml(data.labBadge.text)}</div>`
|
|
936
|
+
: "";
|
|
937
|
+
const skillsMain = renderSkillsGroups(data);
|
|
938
|
+
const langsMain = renderLanguages(data);
|
|
939
|
+
bodyHtml = `
|
|
940
|
+
<div class="sheet">
|
|
941
|
+
<header class="head">
|
|
942
|
+
<div class="head-row">
|
|
943
|
+
<div class="head-identity">
|
|
944
|
+
<h1 class="head-name">${escapeHtml(data.name)}</h1>
|
|
945
|
+
<p class="head-title">${titleHtml}</p>
|
|
946
|
+
<div class="head-meta">${contacts.map(renderContact).join("\n")}</div>
|
|
947
|
+
${highlightHtml}
|
|
948
|
+
${labHtml}
|
|
949
|
+
</div>
|
|
950
|
+
${photoHtml}
|
|
951
|
+
</div>
|
|
952
|
+
</header>
|
|
953
|
+
<div class="main">
|
|
954
|
+
${summaryHtml}
|
|
955
|
+
${renderExperience(data, L)}
|
|
956
|
+
${renderEducation(data, L)}
|
|
957
|
+
${skillsMain ? `<section class="main-section"><p class="sec-label">${L("skills")}</p>${skillsMain}</section>` : ""}
|
|
958
|
+
${langsMain ? `<section class="main-section"><p class="sec-label">${L("languages")}</p>${langsMain}</section>` : ""}
|
|
959
|
+
${renderTail(data, L)}
|
|
960
|
+
</div>
|
|
961
|
+
</div>
|
|
962
|
+
${renderFooter(data)}`;
|
|
963
|
+
}
|
|
964
|
+
return `<!DOCTYPE html>
|
|
965
|
+
<html lang="${lang === "pt" ? "pt-BR" : "en"}">
|
|
966
|
+
<head>
|
|
967
|
+
<meta charset="UTF-8">
|
|
968
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
969
|
+
<title>${escapeHtml(data.name)} · ${escapeHtml(data.title)}</title>
|
|
970
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
971
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
972
|
+
<link href="https://fonts.googleapis.com/css2?${spec.fontsQuery}&display=swap" rel="stylesheet">
|
|
973
|
+
<style>${buildCss(spec, accent)}</style>
|
|
974
|
+
</head>
|
|
975
|
+
<body class="theme-${theme}">
|
|
976
|
+
${bodyHtml}
|
|
977
|
+
</body>
|
|
978
|
+
</html>`;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* O contrato de dados, em texto, pra quem monta o JSON (o Gemini do Lab web e
|
|
982
|
+
* qualquer agente via MCP leem a MESMA descrição).
|
|
983
|
+
*/
|
|
984
|
+
export const RESUME_SCHEMA_DESCRIPTION = `
|
|
985
|
+
{
|
|
986
|
+
"lang": "pt" ou "en" (idioma dos títulos de seção; use o idioma do conteúdo),
|
|
987
|
+
"name": "Full Name",
|
|
988
|
+
"title": "Professional Title (e.g., Software Engineer)",
|
|
989
|
+
"titleHighlight": "Specialty highlight (e.g., AI & Machine Learning)",
|
|
990
|
+
"location": "City, Country",
|
|
991
|
+
"phone": "+1 555 123 4567",
|
|
992
|
+
"email": "user@email.com",
|
|
993
|
+
"linkedin": { "url": "https://linkedin.com/in/user", "label": "linkedin.com/in/user" },
|
|
994
|
+
"portfolio": { "url": "https://portfolio.com", "label": "portfolio.com" },
|
|
995
|
+
"website": { "url": "https://site.com", "label": "site.com" },
|
|
996
|
+
"summary": "Professional summary with <em>italic</em> and <strong>bold</strong> for emphasis.",
|
|
997
|
+
"highlight": { "title": "Work Permit", "text": "Short callout shown near the contact info (visa, availability, relocation...)" },
|
|
998
|
+
"experience": [
|
|
999
|
+
{
|
|
1000
|
+
"era": "Current Phase (optional divider label)",
|
|
1001
|
+
"items": [
|
|
1002
|
+
{
|
|
1003
|
+
"role": "Job Title",
|
|
1004
|
+
"company": "Company Name",
|
|
1005
|
+
"companyUrl": "https://company.com",
|
|
1006
|
+
"period": "2022 - Present",
|
|
1007
|
+
"location": "Remote",
|
|
1008
|
+
"note": "Optional context note",
|
|
1009
|
+
"bullets": [
|
|
1010
|
+
"<strong>Achievement:</strong> Description of what was accomplished.",
|
|
1011
|
+
"<strong>Project:</strong> Details about a key project."
|
|
1012
|
+
]
|
|
1013
|
+
}
|
|
1014
|
+
]
|
|
1015
|
+
}
|
|
1016
|
+
],
|
|
1017
|
+
"clients": "Client A · Client B · Client C",
|
|
1018
|
+
"education": [
|
|
1019
|
+
{ "title": "B.S. Computer Science", "institution": "MIT", "year": "2020", "badge": "Honors", "subtitle": "Focus: AI" }
|
|
1020
|
+
],
|
|
1021
|
+
"skills": [
|
|
1022
|
+
{
|
|
1023
|
+
"group": "Core",
|
|
1024
|
+
"tags": [
|
|
1025
|
+
{ "name": "React", "accent": true },
|
|
1026
|
+
{ "name": "Node.js", "accent": true },
|
|
1027
|
+
{ "name": "Python" }
|
|
1028
|
+
]
|
|
1029
|
+
}
|
|
1030
|
+
],
|
|
1031
|
+
"languages": [
|
|
1032
|
+
{ "name": "Português", "level": "Nativo", "pct": 100 },
|
|
1033
|
+
{ "name": "Inglês", "level": "Fluente", "pct": 88 }
|
|
1034
|
+
],
|
|
1035
|
+
"grants": [{ "name": "Google Cloud Credits", "url": "https://cloud.google.com" }],
|
|
1036
|
+
"awards": [{ "name": "Best Project 2023", "detail": "University competition", "url": "" }],
|
|
1037
|
+
"publications": [{ "title": "My Article", "url": "https://medium.com/article", "note": "featured" }],
|
|
1038
|
+
"closingQuote": { "text": "An inspiring quote", "author": "Author Name" },
|
|
1039
|
+
"footer": { "name": "Full Name", "links": [{ "label": "Portfolio", "url": "https://portfolio.com" }] }
|
|
1040
|
+
}
|
|
1041
|
+
Nota: "photo" (data URI ou URL https) é aceito no render, mas NUNCA é coletado
|
|
1042
|
+
pelo chat nem gravado em banco: quem anexa é o cliente, localmente. "pct" dos
|
|
1043
|
+
idiomas é 0-100 (barra). Modelos: pro | executivo (duas colunas com sidebar),
|
|
1044
|
+
moderno | minimal (coluna única).`;
|
package/dist/skills.js
CHANGED
|
@@ -470,6 +470,48 @@ O retrato envelhece. Quando a pessoa pedir, rode a action de novo e sobrescreva
|
|
|
470
470
|
|
|
471
471
|
A soul é derivada e já sai filtrada da casa: saldo, transações, e-mail, contatos, conversas e criações privadas não viajam. Não tente completar o retrato com dado de outra tool.`,
|
|
472
472
|
},
|
|
473
|
+
{
|
|
474
|
+
name: "curriculo",
|
|
475
|
+
title: "Currículo PRO",
|
|
476
|
+
description: "Montar um currículo profissional com a tool sapiens_resume: a entrevista, o contrato de dados, os 4 modelos e a foto que nunca sai da máquina. Puxe quando a pessoa pedir currículo, CV ou resume.",
|
|
477
|
+
body: `## O que é
|
|
478
|
+
|
|
479
|
+
O gerador de currículo da casa, de graça: \`sapiens_resume\` não fala com o backend, não exige login e não cobra Sinapse. VOCÊ é o entrevistador e o redator; a tool é a gráfica. O mesmo template do Lab /experimentos/resume-builder.
|
|
480
|
+
|
|
481
|
+
## O fluxo
|
|
482
|
+
|
|
483
|
+
1. \`sapiens_resume action=schema\` uma vez: volta o contrato de dados e os 4 modelos.
|
|
484
|
+
2. Colete o conteúdo por UMA de duas portas:
|
|
485
|
+
- **Entrevista**: pergunte em blocos curtos (nome e título -> contato -> resumo -> experiências, uma por vez -> formação -> competências -> idiomas -> extras). Uma pergunta por vez, não um formulário inteiro.
|
|
486
|
+
- **Texto colado**: a pessoa cola o perfil do LinkedIn ou um currículo antigo; você extrai e CONFIRMA o que entendeu antes de renderizar.
|
|
487
|
+
3. Monte o JSON no contrato e chame \`action=render\`. No MCP instalado, passe \`outPath\` (ex: a pasta do projeto ou o Desktop) pra salvar o .html direto, sem trafegar o HTML na conversa.
|
|
488
|
+
4. Entregue: o arquivo abre no navegador e vira PDF com Ctrl+P (A4, margens padrão).
|
|
489
|
+
|
|
490
|
+
## Os 4 modelos (arg theme)
|
|
491
|
+
|
|
492
|
+
- **pro** (default): duas colunas, sidebar verde-jade escura com foto, contato, chips de competências e barras de idioma; principal com timeline de experiência. O visual de consultoria.
|
|
493
|
+
- **executivo**: duas colunas em grafite com detalhe bronze e nome em serifa. Direção e C-level.
|
|
494
|
+
- **moderno**: coluna única, roxo da casa. Direto e contemporâneo.
|
|
495
|
+
- **minimal**: coluna única, preto no branco. Máximo respiro.
|
|
496
|
+
|
|
497
|
+
\`accentColor\` (hex) troca a cor de destaque de qualquer modelo.
|
|
498
|
+
|
|
499
|
+
## A foto (regra de privacidade)
|
|
500
|
+
|
|
501
|
+
A foto NUNCA vai pra banco nenhum, nem da casa nem de terceiro:
|
|
502
|
+
|
|
503
|
+
- **MCP instalado (stdio)**: \`photoPath\` com o caminho local; ela vira data URI embutido no HTML, sem sair da máquina.
|
|
504
|
+
- **Conexão remota**: path local é recusado. Use \`data.photo\` com URL https, ou mande a pessoa anexar a foto no Lab web, onde ela fica só no navegador.
|
|
505
|
+
- Sem foto, o modelo pro/executivo desenha um monograma com as iniciais. Também fica bonito.
|
|
506
|
+
|
|
507
|
+
## Regras de conteúdo
|
|
508
|
+
|
|
509
|
+
- Melhore a redação, JAMAIS invente fato: cargo, empresa, período e número são da pessoa. Bullet forte começa com \`<strong>Rótulo:</strong>\` (Estratégia, Redação, Resultado...).
|
|
510
|
+
- \`lang\` ('pt' | 'en') casa os títulos de seção com o idioma do conteúdo.
|
|
511
|
+
- Competências em grupos (3 a 5), com \`accent: true\` só nas que definem a pessoa.
|
|
512
|
+
- Idiomas com \`pct\` honesto (nativo ~100, fluente ~85, intermédio ~55).
|
|
513
|
+
- Visto/disponibilidade/mudança entram em \`highlight\` (o box perto do contato), não no resumo.`,
|
|
514
|
+
},
|
|
473
515
|
];
|
|
474
516
|
export const SKILLS = Object.fromEntries(SKILL_LIST.map((s) => [s.name, s]));
|
|
475
517
|
/** Monta o SKILL.md completo (frontmatter do formato aberto + corpo). */
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { generateResumeHTML, RESUME_SCHEMA_DESCRIPTION, RESUME_THEMES, } from "../resume/template.js";
|
|
3
|
+
import { isRemoteContext } from "../convexClient.js";
|
|
4
|
+
/**
|
|
5
|
+
* sapiens_resume: o Currículo PRO da casa via MCP.
|
|
6
|
+
*
|
|
7
|
+
* Junto com sapiens_skill, é tool de MUNDO FECHADO: não fala com o backend,
|
|
8
|
+
* não exige login, não cobra Sinapse. O agente conduz a entrevista (ou lê o
|
|
9
|
+
* LinkedIn colado), monta o JSON no contrato e chama render; o HTML volta
|
|
10
|
+
* pronto pra salvar e imprimir em PDF. É o MESMO template do Lab
|
|
11
|
+
* /experimentos/resume-builder (fonte única em ../resume/template.ts).
|
|
12
|
+
*
|
|
13
|
+
* FOTO: nunca toca banco nenhum. No stdio, photoPath lê o arquivo AQUI na
|
|
14
|
+
* máquina da pessoa e embute como data URI direto no HTML. No remoto, path
|
|
15
|
+
* local é recusado (seria ler arquivo do servidor); a pessoa passa uma URL
|
|
16
|
+
* https ou anexa a foto depois, no Lab web.
|
|
17
|
+
*/
|
|
18
|
+
const THEME_IDS = Object.keys(RESUME_THEMES);
|
|
19
|
+
const MAX_PHOTO_BYTES = 4 * 1024 * 1024;
|
|
20
|
+
export const resumeSchema = z.object({
|
|
21
|
+
action: z
|
|
22
|
+
.enum(["schema", "render"])
|
|
23
|
+
.default("schema")
|
|
24
|
+
.describe("'schema' devolve o contrato de dados + os modelos disponíveis (leia antes do primeiro render). 'render' recebe os dados e devolve o HTML pronto."),
|
|
25
|
+
data: z
|
|
26
|
+
.union([z.record(z.unknown()), z.string()])
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Os dados do currículo no shape do contrato (objeto, ou string JSON). Obrigatório no render. Mínimo: name + title."),
|
|
29
|
+
theme: z
|
|
30
|
+
.enum(THEME_IDS)
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Modelo visual: 'pro' (default, duas colunas com sidebar jade), 'executivo' (sidebar grafite + serifa), 'moderno' (coluna única roxa), 'minimal' (coluna única preto e branco)."),
|
|
33
|
+
accentColor: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Hex 6 dígitos pra trocar a cor de destaque do modelo (ex '#0E7C66')."),
|
|
37
|
+
photoPath: z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Caminho absoluto de uma foto local (PNG/JPEG/WebP até 4MB) pra embutir como foto de perfil. SÓ no MCP instalado (stdio): a foto vira data URI dentro do HTML e não sai da máquina. No remoto use data.photo com URL https."),
|
|
41
|
+
outPath: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Caminho absoluto de um .html pra salvar o resultado direto no disco (SÓ stdio). Com outPath a resposta volta enxuta ({savedTo, bytes}) em vez do HTML inteiro."),
|
|
45
|
+
});
|
|
46
|
+
function detectImageMime(buf) {
|
|
47
|
+
if (buf.length > 8 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47)
|
|
48
|
+
return "image/png";
|
|
49
|
+
if (buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
|
|
50
|
+
return "image/jpeg";
|
|
51
|
+
if (buf.length > 12 &&
|
|
52
|
+
buf.toString("ascii", 0, 4) === "RIFF" &&
|
|
53
|
+
buf.toString("ascii", 8, 12) === "WEBP")
|
|
54
|
+
return "image/webp";
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
async function readLocalPhotoAsDataUri(filePath) {
|
|
58
|
+
if (isRemoteContext()) {
|
|
59
|
+
throw new Error("photoPath (arquivo local) só funciona no MCP instalado na sua máquina (stdio). " +
|
|
60
|
+
"Na conexão remota, passe data.photo com uma URL https, ou anexe a foto no Lab web (/experimentos/resume-builder), onde ela fica só no navegador.");
|
|
61
|
+
}
|
|
62
|
+
const [{ default: fs }, { default: path }] = await Promise.all([
|
|
63
|
+
import("node:fs/promises"),
|
|
64
|
+
import("node:path"),
|
|
65
|
+
]);
|
|
66
|
+
const abs = path.resolve(filePath);
|
|
67
|
+
let buf;
|
|
68
|
+
try {
|
|
69
|
+
buf = await fs.readFile(abs);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new Error(`Não achei/li a foto em photoPath: "${filePath}". Use o caminho absoluto (ex: C:\\Users\\voce\\foto.jpg).`);
|
|
73
|
+
}
|
|
74
|
+
if (buf.length > MAX_PHOTO_BYTES) {
|
|
75
|
+
const mb = (buf.length / (1024 * 1024)).toFixed(1);
|
|
76
|
+
throw new Error(`Foto muito grande (${mb}MB; teto 4MB). Exporte menor (um recorte quadrado de ~800px resolve) e tente de novo.`);
|
|
77
|
+
}
|
|
78
|
+
const mime = detectImageMime(buf);
|
|
79
|
+
if (!mime) {
|
|
80
|
+
throw new Error(`O arquivo em photoPath não parece PNG/JPEG/WebP ("${filePath}").`);
|
|
81
|
+
}
|
|
82
|
+
return `data:${mime};base64,${buf.toString("base64")}`;
|
|
83
|
+
}
|
|
84
|
+
async function saveHtmlLocally(outPath, html) {
|
|
85
|
+
if (isRemoteContext()) {
|
|
86
|
+
throw new Error("outPath (salvar em disco) só funciona no MCP instalado na sua máquina (stdio). Na conexão remota, pegue o campo html da resposta e salve você.");
|
|
87
|
+
}
|
|
88
|
+
if (!/\.html?$/i.test(outPath)) {
|
|
89
|
+
throw new Error(`outPath precisa terminar em .html (recebi "${outPath}").`);
|
|
90
|
+
}
|
|
91
|
+
const [{ default: fs }, { default: path }] = await Promise.all([
|
|
92
|
+
import("node:fs/promises"),
|
|
93
|
+
import("node:path"),
|
|
94
|
+
]);
|
|
95
|
+
const abs = path.resolve(outPath);
|
|
96
|
+
await fs.writeFile(abs, html, "utf8");
|
|
97
|
+
return abs;
|
|
98
|
+
}
|
|
99
|
+
export async function resume(args) {
|
|
100
|
+
const action = args.action ?? "schema";
|
|
101
|
+
if (action === "schema") {
|
|
102
|
+
return {
|
|
103
|
+
themes: Object.entries(RESUME_THEMES).map(([id, t]) => ({
|
|
104
|
+
id,
|
|
105
|
+
label: t.label,
|
|
106
|
+
layout: t.layout,
|
|
107
|
+
description: t.description,
|
|
108
|
+
})),
|
|
109
|
+
contract: RESUME_SCHEMA_DESCRIPTION,
|
|
110
|
+
cost: 0,
|
|
111
|
+
notes: [
|
|
112
|
+
"Entreviste a pessoa (ou leia o texto do LinkedIn que ela colar), monte o JSON e chame action=render. Não invente fato: melhore a redação, preserve a verdade.",
|
|
113
|
+
"lang define o idioma dos títulos de seção ('pt' ou 'en'); case com o idioma do conteúdo.",
|
|
114
|
+
"Foto é opcional e NUNCA vai pra banco: no stdio use photoPath (vira data URI local); no remoto, data.photo com URL https.",
|
|
115
|
+
"O HTML sai pronto pra imprimir em PDF (abrir no navegador -> Ctrl+P -> Salvar como PDF, A4).",
|
|
116
|
+
"No stdio, passe outPath pra salvar direto no disco sem trafegar o HTML inteiro na conversa.",
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// action === "render"
|
|
121
|
+
let data = args.data;
|
|
122
|
+
if (typeof data === "string") {
|
|
123
|
+
try {
|
|
124
|
+
data = JSON.parse(data);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
throw new Error("data veio como string mas não é JSON válido. Mande o objeto do contrato (veja action=schema).");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!data || typeof data !== "object") {
|
|
131
|
+
throw new Error("render exige 'data' com os dados do currículo. Rode action=schema pra ver o contrato.");
|
|
132
|
+
}
|
|
133
|
+
if (!data.name || !data.title) {
|
|
134
|
+
throw new Error("data precisa de pelo menos 'name' e 'title'. Complete e chame de novo.");
|
|
135
|
+
}
|
|
136
|
+
if (args.photoPath) {
|
|
137
|
+
data = { ...data, photo: await readLocalPhotoAsDataUri(args.photoPath) };
|
|
138
|
+
}
|
|
139
|
+
const theme = args.theme ?? "pro";
|
|
140
|
+
const html = generateResumeHTML(data, {
|
|
141
|
+
theme,
|
|
142
|
+
accentColor: args.accentColor,
|
|
143
|
+
});
|
|
144
|
+
const base = {
|
|
145
|
+
theme,
|
|
146
|
+
bytes: Buffer.byteLength(html, "utf8"),
|
|
147
|
+
tip: "Abra o .html no navegador e imprima em PDF (Ctrl+P, A4, margens padrão). A foto, se houver, já está embutida no arquivo.",
|
|
148
|
+
};
|
|
149
|
+
if (args.outPath) {
|
|
150
|
+
const savedTo = await saveHtmlLocally(args.outPath, html);
|
|
151
|
+
return { ...base, savedTo };
|
|
152
|
+
}
|
|
153
|
+
return { ...base, html };
|
|
154
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sapiens-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.47.0",
|
|
4
4
|
"mcpName": "com.sapiensinteticos/sapiens",
|
|
5
5
|
"description": "MCP server pra operar o Sapiens Sintéticos (sapiensinteticos.com) pelo Claude Code: gerar imagem, escrever artigo, voz, música e mais, na sua conta. Login pelo código de sapiensinteticos.com/conectar-claude.",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
11
|
"./remote": "./dist/remote.js",
|
|
12
|
-
"./registry": "./dist/registry.js"
|
|
12
|
+
"./registry": "./dist/registry.js",
|
|
13
|
+
"./resume": "./dist/resume/template.js"
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
15
16
|
"dist",
|