sapiens-mcp 1.70.0 → 1.71.1
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 +22 -22
- package/dist/resume/coverLetter.js +106 -106
- package/dist/resume/template.js +690 -690
- package/dist/tools/gallery.js +60 -5
- package/dist/tools/image.js +44 -5
- package/package.json +1 -1
package/dist/registry.js
CHANGED
|
@@ -76,7 +76,7 @@ export const TOOLS = {
|
|
|
76
76
|
handler: repertorio,
|
|
77
77
|
},
|
|
78
78
|
sapiens_gallery: {
|
|
79
|
-
description: "Browse, publicação e upload das imagens do user. Sub-actions: list (últimas N peças, com prompt/model/url + isPublic + acervoUrl; kind=image por default, kind=video traz os takes com posterUrl, a capa, e kind=all mistura. Depois de ingerir ou gerar vídeo, é o list kind=video que dá a capa pra mostrar miniatura + player em vez de link solto), get (1 imagem com metadados, opcionalmente base64), publish (torna a PRÓPRIA imagem pública: entra na galeria pública + feed Pinterest, e ganha página indexável /imagem/<id> se o modelo não for degen devolve publicPageUrl; publicado NÃO despublica, não existe unpublish), upload (traz pra galeria uma peça gerada FORA da casa, por sourceUrl https, filePath local ou base64; ADMIN por ora), ingest (A PONTE, qualquer conta logada: peça que a PESSOA dirigiu com a ficha do personagem daqui e um motor de fora renderizou, no crédito dela (Kie, fal, Magnific, Sogni, a própria placa): entra NATIVA na timeline e nasce PRIVADA (isPublic false), publicável DEPOIS por ela como qualquer geração da casa, nunca sozinha, com custo 0 em Sinapses e o custo real guardado em externalCost; carrega o prompt VERBATIM, as referências da casa (referenceImageIds), o personagem (characterId) e o motor (externalEngine 'provedor-motor'); a peça chega por filePath (local, só no MCP instalado), sourceUrl (o render, baixado pelo MCP na máquina da pessoa, nunca pelo servidor) ou base64; o carimbo +18 nasce na entrada pela CLASSE do provedor (peso aberto como Sogni, Replicate, HF, fal, WaveSpeed, Civitai e a própria placa entram carimbados, só a curadoria tira; serviço com moderação como Kie, Magnific, Freepik, Krea entra sem) ou por unfiltered=true dito pela pessoa (carimbo dela, ela tira depois); idempotente por sourceUrl ou pelo hash do arquivo, então re-ingerir corrige a ficha em vez de duplicar; tetos 12 MB imagem, 60 MB vídeo, 40 por dia. A chave do provedor NUNCA passa por aqui: quem gerou foi o harness da pessoa. Passo a passo na skill 'pontes'), refs (acopla peças da casa como REFERÊNCIA numa peça que já existe, de upload ou de ingest; ADMIN, e só na própria peça), cast (QUEM ESTÁ EM CENA numa peça que já existe, quando é mais de uma criatura: characterIds em ordem, o protagonista primeiro, e a peça passa a contar na ficha de todos eles em vez de sumir da segunda; qualquer membro, na própria peça). Use list/get pra reusar imagem como referência (passe o imageId em sapiens_image mode=edit ou mode=variation) ou pra mostrar pro user o que ele já tem; publish quando o user quer divulgar a imagem dele. IMPORTANTE sobre upload: a peça entra PRIVADA e continua privada: o servidor recusa publicar e recusa compartilhar na comunidade, porque ela é do usuário e a responsabilidade é dele. O caminho pra ela virar coisa pública é usar como referência numa geração daqui e publicar o resultado. O refs escreve só proveniência (de que ficha/folha a peça nasceu): não promove nada, e um upload continua sem publicar depois de ganhar referência. COMO A PEÇA FOI FEITA: list aceita skill=<slug> e devolve só o que saiu daquela skill (o mesmo slug que sapiens_image gravou em skills), com a busca varrendo as últimas 200 peças antes de cortar no limit; cada item volta com o campo skills, e null ali quer dizer que ninguém marcou, não que foi feita sem skill.",
|
|
79
|
+
description: "Browse, publicação e upload das imagens do user. Sub-actions: list (últimas N peças, com prompt/model/url + isPublic + acervoUrl; kind=image por default, kind=video traz os takes com posterUrl, a capa, e kind=all mistura. Depois de ingerir ou gerar vídeo, é o list kind=video que dá a capa pra mostrar miniatura + player em vez de link solto), get (1 imagem com metadados, opcionalmente base64), publish (torna a PRÓPRIA imagem pública: entra na galeria pública + feed Pinterest, e ganha página indexável /imagem/<id> se o modelo não for degen devolve publicPageUrl; publicado NÃO despublica, não existe unpublish), upload (traz pra galeria uma peça gerada FORA da casa, por sourceUrl https, filePath local ou base64; ADMIN por ora), ingest (A PONTE, qualquer conta logada: peça que a PESSOA dirigiu com a ficha do personagem daqui e um motor de fora renderizou, no crédito dela (Kie, fal, Magnific, Sogni, a própria placa): entra NATIVA na timeline e nasce PRIVADA (isPublic false), publicável DEPOIS por ela como qualquer geração da casa, nunca sozinha, com custo 0 em Sinapses e o custo real guardado em externalCost; carrega o prompt VERBATIM, as referências da casa (referenceImageIds), o personagem (characterId) e o motor (externalEngine 'provedor-motor'); a peça chega por filePath (local, só no MCP instalado), sourceUrl (o render, baixado pelo MCP na máquina da pessoa, nunca pelo servidor) ou base64; o carimbo +18 nasce na entrada pela CLASSE do provedor (peso aberto como Sogni, Replicate, HF, fal, WaveSpeed, Civitai e a própria placa entram carimbados, só a curadoria tira; serviço com moderação como Kie, Magnific, Freepik, Krea entra sem) ou por unfiltered=true dito pela pessoa (carimbo dela, ela tira depois); idempotente por sourceUrl ou pelo hash do arquivo, então re-ingerir corrige a ficha em vez de duplicar; tetos 12 MB imagem, 60 MB vídeo, 40 por dia. A chave do provedor NUNCA passa por aqui: quem gerou foi o harness da pessoa. Passo a passo na skill 'pontes'), refs (acopla peças da casa como REFERÊNCIA numa peça que já existe, de upload ou de ingest; ADMIN, e só na própria peça), processo (O COMO JUNTO DO QUE: anexa a uma peça as peças que mostram como ela foi dirigida, o mapa de rota desenhado sobre o still, a prancha de frames com o tempo de cada marco, o antes e depois de duas rodadas; elas viram a seção 'Como foi feito' na página pública da peça, na ordem que você mandar e com a sua legenda por peça; qualquer membro, só na própria peça, teto de 12. É IRMÃ do refs e a diferença importa: refs é o INSUMO que entrou na geração, processo é o REGISTRO de como ela foi dirigida, e uma lista não mexe na outra. Peça de processo privada não aparece pra quem está de fora nem em miniatura: pra aparecer, publique ela também), cast (QUEM ESTÁ EM CENA numa peça que já existe, quando é mais de uma criatura: characterIds em ordem, o protagonista primeiro, e a peça passa a contar na ficha de todos eles em vez de sumir da segunda; qualquer membro, na própria peça). Use list/get pra reusar imagem como referência (passe o imageId em sapiens_image mode=edit ou mode=variation) ou pra mostrar pro user o que ele já tem; publish quando o user quer divulgar a imagem dele. IMPORTANTE sobre upload: a peça entra PRIVADA e continua privada: o servidor recusa publicar e recusa compartilhar na comunidade, porque ela é do usuário e a responsabilidade é dele. O caminho pra ela virar coisa pública é usar como referência numa geração daqui e publicar o resultado. O refs escreve só proveniência (de que ficha/folha a peça nasceu): não promove nada, e um upload continua sem publicar depois de ganhar referência. COMO A PEÇA FOI FEITA: list aceita skill=<slug> e devolve só o que saiu daquela skill (o mesmo slug que sapiens_image gravou em skills), com a busca varrendo as últimas 200 peças antes de cortar no limit; cada item volta com o campo skills, e null ali quer dizer que ninguém marcou, não que foi feita sem skill.",
|
|
80
80
|
schema: gallerySchema,
|
|
81
81
|
handler: gallery,
|
|
82
82
|
},
|
|
@@ -236,27 +236,27 @@ export const TOOLS = {
|
|
|
236
236
|
// É a "skill que anda junto com o pacote": escrevo uma vez, vale pra todos os clients,
|
|
237
237
|
// sem instalar nada. Cobre os tropeços reais (fluxo do musicator, model no vídeo, o
|
|
238
238
|
// disjuntor anti-loop). Mantém curto de propósito: viaja em todo handshake.
|
|
239
|
-
export const SAPIENS_INSTRUCTIONS = `Você opera o Sapiens Sintéticos (sapiensinteticos.com) NA CONTA de um usuário logado. Cada tool age de verdade na conta dele e muitas COBRAM Sinapses (o crédito da casa). Aja como operador, não no chute.
|
|
240
|
-
|
|
241
|
-
AS SKILLS DA CASA (leia ANTES de operar, não improvise o fluxo):
|
|
242
|
-
O passo a passo travado de cada fluxo mora na tool sapiens_skill, servida por este mesmo servidor: sem login, sem custo, sem rede. Ler a skill é mais barato que errar uma geração que cobra.
|
|
243
|
-
- sapiens_skill action=list -> o índice (slug + quando usar cada uma).
|
|
244
|
-
- sapiens_skill action=get name=<slug> -> a skill inteira.
|
|
245
|
-
Slugs: ${skillMenuLine()}
|
|
246
|
-
Puxe a skill ANTES de: gerar música ou efeito sonoro (musica), escolher modelo de vídeo (video), gerar imagem (imagem), criar personagem ou prompt de Midjourney (personagem), criar na identidade do usuário (studio), 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), guardar obra ou ferramenta no acervo (repertorio), gerar em motor de fora com crédito próprio e trazer a peça pro acervo (pontes). Perdido no começo: primeiros-passos.
|
|
247
|
-
Cliente que lê resources MCP acha o MESMO conteúdo em skill://sapiens/<slug>/SKILL.md.
|
|
248
|
-
|
|
249
|
-
REGRA DE OURO:
|
|
250
|
-
- PRIMEIRO CONTATO ou "o que você faz?"/"como começo?"/"o que dá pra fazer?": chame sapiens_meta action=start e MOSTRE o resultado na sua voz. Sem login, ele ensina a conectar; logado, traz saldo + primeiros poderes com exemplos. É a porta de entrada: não despeje a lista inteira de tools, deixe o start guiar.
|
|
251
|
-
- LOGO APÓS UM LOGIN BEM-SUCEDIDO (action=login retornou ok): chame action=start na sequência e mostre a porta de entrada. O recém-chegado não sabe o que pedir; não o deixe na tela em branco, guie a primeira jogada sem ele precisar perguntar.
|
|
252
|
-
- Antes de gerar algo caro (imagem/música/vídeo), cheque saldo: sapiens_meta action=credits (ou subscription). Saldo baixo, avise o usuário antes. Vídeo é o mais caro da casa: confirme com ele antes de disparar.
|
|
253
|
-
- SEM SALDO, ou motor que a casa não tem: a casa é ponte, não muro. Se a pessoa tem crédito na Kie, na fal, na Magnific, na Sogni ou na própria placa, ela gera LÁ com a ficha do personagem daqui (sapiens_character action=get) e a peça volta pro acervo por sapiens_gallery action=ingest. A chave dela NUNCA passa pelo Sapiens (nem em arg, nem no chat). sapiens_meta action=pontes diz o que ela já tem configurado; skill 'pontes' tem o passo a passo.
|
|
254
|
-
- REGRA DO TIMEOUT: geração SÍNCRONA pode estourar o teto de ~120s do cliente e voltar 'Timeout' MESMO tendo gerado e COBRADO. Nunca repita às cegas: confira antes onde o resultado cairia (a tabela de onde conferir está na skill 'primeiros-passos').
|
|
255
|
-
- "sessionToken expirado" = refaça login: sapiens_meta action=login com o código de sapiensinteticos.com/conectar-claude.
|
|
256
|
-
- sapiens_meta action=formats devolve os schemas por formato; action=whoami diz tier (user/admin) + saldo. sapiens_image e sapiens_video action=models trazem o catálogo com o preço ATUAL: consulte em vez de chutar custo.
|
|
257
|
-
|
|
258
|
-
MODO COMPANHIA: sapiens_meta action=start e action=whoami podem trazer um bloco 'companion'. Quando vier companion.active=true, você INCORPORA aquele Sintético (a voz dele, o avatar, o oi dele) continuando a operar na conta e nas Sinapses do USUÁRIO. Puxe a skill 'companhia' antes de fazer isso. Se vier 'characterOffer', siga o que ele manda: são personagens que o usuário escreveu e que podem entrar em cena, e quem OFERECE é você, na hora em que a conversa encostar num deles. Só personagem de autoria dele; a alma de personagem alheio não sai por aqui.
|
|
259
|
-
|
|
239
|
+
export const SAPIENS_INSTRUCTIONS = `Você opera o Sapiens Sintéticos (sapiensinteticos.com) NA CONTA de um usuário logado. Cada tool age de verdade na conta dele e muitas COBRAM Sinapses (o crédito da casa). Aja como operador, não no chute.
|
|
240
|
+
|
|
241
|
+
AS SKILLS DA CASA (leia ANTES de operar, não improvise o fluxo):
|
|
242
|
+
O passo a passo travado de cada fluxo mora na tool sapiens_skill, servida por este mesmo servidor: sem login, sem custo, sem rede. Ler a skill é mais barato que errar uma geração que cobra.
|
|
243
|
+
- sapiens_skill action=list -> o índice (slug + quando usar cada uma).
|
|
244
|
+
- sapiens_skill action=get name=<slug> -> a skill inteira.
|
|
245
|
+
Slugs: ${skillMenuLine()}
|
|
246
|
+
Puxe a skill ANTES de: gerar música ou efeito sonoro (musica), escolher modelo de vídeo (video), gerar imagem (imagem), criar personagem ou prompt de Midjourney (personagem), criar na identidade do usuário (studio), 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), guardar obra ou ferramenta no acervo (repertorio), gerar em motor de fora com crédito próprio e trazer a peça pro acervo (pontes). Perdido no começo: primeiros-passos.
|
|
247
|
+
Cliente que lê resources MCP acha o MESMO conteúdo em skill://sapiens/<slug>/SKILL.md.
|
|
248
|
+
|
|
249
|
+
REGRA DE OURO:
|
|
250
|
+
- PRIMEIRO CONTATO ou "o que você faz?"/"como começo?"/"o que dá pra fazer?": chame sapiens_meta action=start e MOSTRE o resultado na sua voz. Sem login, ele ensina a conectar; logado, traz saldo + primeiros poderes com exemplos. É a porta de entrada: não despeje a lista inteira de tools, deixe o start guiar.
|
|
251
|
+
- LOGO APÓS UM LOGIN BEM-SUCEDIDO (action=login retornou ok): chame action=start na sequência e mostre a porta de entrada. O recém-chegado não sabe o que pedir; não o deixe na tela em branco, guie a primeira jogada sem ele precisar perguntar.
|
|
252
|
+
- Antes de gerar algo caro (imagem/música/vídeo), cheque saldo: sapiens_meta action=credits (ou subscription). Saldo baixo, avise o usuário antes. Vídeo é o mais caro da casa: confirme com ele antes de disparar.
|
|
253
|
+
- SEM SALDO, ou motor que a casa não tem: a casa é ponte, não muro. Se a pessoa tem crédito na Kie, na fal, na Magnific, na Sogni ou na própria placa, ela gera LÁ com a ficha do personagem daqui (sapiens_character action=get) e a peça volta pro acervo por sapiens_gallery action=ingest. A chave dela NUNCA passa pelo Sapiens (nem em arg, nem no chat). sapiens_meta action=pontes diz o que ela já tem configurado; skill 'pontes' tem o passo a passo.
|
|
254
|
+
- REGRA DO TIMEOUT: geração SÍNCRONA pode estourar o teto de ~120s do cliente e voltar 'Timeout' MESMO tendo gerado e COBRADO. Nunca repita às cegas: confira antes onde o resultado cairia (a tabela de onde conferir está na skill 'primeiros-passos').
|
|
255
|
+
- "sessionToken expirado" = refaça login: sapiens_meta action=login com o código de sapiensinteticos.com/conectar-claude.
|
|
256
|
+
- sapiens_meta action=formats devolve os schemas por formato; action=whoami diz tier (user/admin) + saldo. sapiens_image e sapiens_video action=models trazem o catálogo com o preço ATUAL: consulte em vez de chutar custo.
|
|
257
|
+
|
|
258
|
+
MODO COMPANHIA: sapiens_meta action=start e action=whoami podem trazer um bloco 'companion'. Quando vier companion.active=true, você INCORPORA aquele Sintético (a voz dele, o avatar, o oi dele) continuando a operar na conta e nas Sinapses do USUÁRIO. Puxe a skill 'companhia' antes de fazer isso. Se vier 'characterOffer', siga o que ele manda: são personagens que o usuário escreveu e que podem entrar em cena, e quem OFERECE é você, na hora em que a conversa encostar num deles. Só personagem de autoria dele; a alma de personagem alheio não sai por aqui.
|
|
259
|
+
|
|
260
260
|
Voz da casa: 1ª pessoa, direto, anti-corporate, sem travessão. Pra bom entendedor, meia palavra basta. Texto que vai ser publicado pede a skill 'voz-da-casa' antes.`;
|
|
261
261
|
// Annotations MCP: título humano + dica read-only. São HINTS (não-confiáveis por
|
|
262
262
|
// spec): quem gateia de verdade continua o servidor (saldo, gate de admin,
|
|
@@ -36,111 +36,111 @@ export function generateCoverLetterHTML(cover, resume, options = {}) {
|
|
|
36
36
|
const paragraphs = (cover.paragraphs || [])
|
|
37
37
|
.map((p) => `<p class="body-p">${escapeHtml(p)}</p>`)
|
|
38
38
|
.join("\n");
|
|
39
|
-
return `<!DOCTYPE html>
|
|
40
|
-
<html lang="${lang === "en" ? "en" : "pt-BR"}">
|
|
41
|
-
<head>
|
|
42
|
-
<meta charset="UTF-8">
|
|
43
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
44
|
-
<title>${escapeHtml(resume.name)} · ${title}</title>
|
|
45
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
46
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
47
|
-
<link href="https://fonts.googleapis.com/css2?${spec.fontsQuery}&display=swap" rel="stylesheet">
|
|
48
|
-
<style>
|
|
49
|
-
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
50
|
-
html { font-size: 15.5px; -webkit-font-smoothing: antialiased; }
|
|
51
|
-
body {
|
|
52
|
-
font-family: ${spec.bodyFamily};
|
|
53
|
-
color: #1B1E23;
|
|
54
|
-
background: ${spec.pageBg};
|
|
55
|
-
line-height: 1.6;
|
|
56
|
-
padding: 2.2rem 1rem;
|
|
57
|
-
}
|
|
58
|
-
.sheet {
|
|
59
|
-
max-width: 760px;
|
|
60
|
-
margin: 0 auto;
|
|
61
|
-
background: #fff;
|
|
62
|
-
border-radius: 12px;
|
|
63
|
-
overflow: hidden;
|
|
64
|
-
box-shadow: 0 1px 2px rgba(20,22,27,.05), 0 14px 44px rgba(20,22,27,.09);
|
|
65
|
-
padding: 3rem 3.2rem 2.6rem;
|
|
66
|
-
position: relative;
|
|
67
|
-
}
|
|
68
|
-
.sheet::before {
|
|
69
|
-
content: '';
|
|
70
|
-
position: absolute;
|
|
71
|
-
top: 0; left: 0; right: 0;
|
|
72
|
-
height: 3px;
|
|
73
|
-
background: ${spec.accent};
|
|
74
|
-
}
|
|
75
|
-
.head { border-bottom: 1px solid #E5E6E8; padding-bottom: 1.2rem; margin-bottom: 1.6rem; }
|
|
76
|
-
.head-name {
|
|
77
|
-
font-family: ${spec.displayFamily};
|
|
78
|
-
font-size: 1.7rem;
|
|
79
|
-
font-weight: 700;
|
|
80
|
-
letter-spacing: -0.02em;
|
|
81
|
-
margin-bottom: .15rem;
|
|
82
|
-
}
|
|
83
|
-
.head-title {
|
|
84
|
-
font-size: .78rem;
|
|
85
|
-
font-weight: 700;
|
|
86
|
-
text-transform: uppercase;
|
|
87
|
-
letter-spacing: .14em;
|
|
88
|
-
color: ${spec.accent};
|
|
89
|
-
margin-bottom: .55rem;
|
|
90
|
-
}
|
|
91
|
-
.head-contact { font-size: .76rem; color: #82868E; }
|
|
92
|
-
.doc-label {
|
|
93
|
-
font-size: .68rem;
|
|
94
|
-
font-weight: 700;
|
|
95
|
-
text-transform: uppercase;
|
|
96
|
-
letter-spacing: .16em;
|
|
97
|
-
color: #82868E;
|
|
98
|
-
display: flex;
|
|
99
|
-
align-items: center;
|
|
100
|
-
gap: .65rem;
|
|
101
|
-
margin-bottom: 1.2rem;
|
|
102
|
-
}
|
|
103
|
-
.doc-label::before { content: ''; width: 22px; height: 3px; border-radius: 2px; background: ${spec.accent}; }
|
|
104
|
-
.date-line { font-size: .8rem; color: #82868E; margin-bottom: 1.4rem; }
|
|
105
|
-
.subject { font-size: .92rem; font-weight: 700; margin-bottom: 1.2rem; }
|
|
106
|
-
.greeting { font-size: .9rem; margin-bottom: 1rem; }
|
|
107
|
-
.body-p { font-size: .88rem; color: #4A4E57; line-height: 1.8; margin-bottom: 1rem; text-align: justify; }
|
|
108
|
-
.closing { margin-top: 1.6rem; font-size: .9rem; }
|
|
109
|
-
.signature {
|
|
110
|
-
margin-top: .4rem;
|
|
111
|
-
font-family: ${spec.displayFamily};
|
|
112
|
-
font-size: 1.05rem;
|
|
113
|
-
font-weight: 700;
|
|
114
|
-
}
|
|
115
|
-
@media (max-width: 640px) {
|
|
116
|
-
body { padding: .5rem; }
|
|
117
|
-
.sheet { border-radius: 0; padding: 1.8rem 1.4rem; }
|
|
118
|
-
}
|
|
119
|
-
@media print {
|
|
120
|
-
@page { margin: 0.7in; size: A4; }
|
|
121
|
-
html { font-size: 13px; }
|
|
122
|
-
body { padding: 0; background: white; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
123
|
-
.sheet { max-width: 100%; border-radius: 0; box-shadow: none; padding: 0; }
|
|
124
|
-
.sheet::before { display: none; }
|
|
125
|
-
.body-p { break-inside: avoid; }
|
|
126
|
-
}
|
|
127
|
-
</style>
|
|
128
|
-
</head>
|
|
129
|
-
<body>
|
|
130
|
-
<div class="sheet">
|
|
131
|
-
<header class="head">
|
|
132
|
-
<h1 class="head-name">${escapeHtml(resume.name)}</h1>
|
|
133
|
-
${resume.title ? `<p class="head-title">${escapeHtml(resume.title)}</p>` : ""}
|
|
134
|
-
<p class="head-contact">${contactLine(resume)}</p>
|
|
135
|
-
</header>
|
|
136
|
-
<p class="doc-label">${title}</p>
|
|
137
|
-
${cover.dateLine ? `<p class="date-line">${escapeHtml(cover.dateLine)}</p>` : ""}
|
|
138
|
-
${cover.subject ? `<p class="subject">${escapeHtml(cover.subject)}</p>` : ""}
|
|
139
|
-
<p class="greeting">${escapeHtml(cover.greeting)}</p>
|
|
140
|
-
${paragraphs}
|
|
141
|
-
<p class="closing">${escapeHtml(cover.closing)}</p>
|
|
142
|
-
<p class="signature">${escapeHtml(resume.name)}</p>
|
|
143
|
-
</div>
|
|
144
|
-
</body>
|
|
39
|
+
return `<!DOCTYPE html>
|
|
40
|
+
<html lang="${lang === "en" ? "en" : "pt-BR"}">
|
|
41
|
+
<head>
|
|
42
|
+
<meta charset="UTF-8">
|
|
43
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
44
|
+
<title>${escapeHtml(resume.name)} · ${title}</title>
|
|
45
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
46
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
47
|
+
<link href="https://fonts.googleapis.com/css2?${spec.fontsQuery}&display=swap" rel="stylesheet">
|
|
48
|
+
<style>
|
|
49
|
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
50
|
+
html { font-size: 15.5px; -webkit-font-smoothing: antialiased; }
|
|
51
|
+
body {
|
|
52
|
+
font-family: ${spec.bodyFamily};
|
|
53
|
+
color: #1B1E23;
|
|
54
|
+
background: ${spec.pageBg};
|
|
55
|
+
line-height: 1.6;
|
|
56
|
+
padding: 2.2rem 1rem;
|
|
57
|
+
}
|
|
58
|
+
.sheet {
|
|
59
|
+
max-width: 760px;
|
|
60
|
+
margin: 0 auto;
|
|
61
|
+
background: #fff;
|
|
62
|
+
border-radius: 12px;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
box-shadow: 0 1px 2px rgba(20,22,27,.05), 0 14px 44px rgba(20,22,27,.09);
|
|
65
|
+
padding: 3rem 3.2rem 2.6rem;
|
|
66
|
+
position: relative;
|
|
67
|
+
}
|
|
68
|
+
.sheet::before {
|
|
69
|
+
content: '';
|
|
70
|
+
position: absolute;
|
|
71
|
+
top: 0; left: 0; right: 0;
|
|
72
|
+
height: 3px;
|
|
73
|
+
background: ${spec.accent};
|
|
74
|
+
}
|
|
75
|
+
.head { border-bottom: 1px solid #E5E6E8; padding-bottom: 1.2rem; margin-bottom: 1.6rem; }
|
|
76
|
+
.head-name {
|
|
77
|
+
font-family: ${spec.displayFamily};
|
|
78
|
+
font-size: 1.7rem;
|
|
79
|
+
font-weight: 700;
|
|
80
|
+
letter-spacing: -0.02em;
|
|
81
|
+
margin-bottom: .15rem;
|
|
82
|
+
}
|
|
83
|
+
.head-title {
|
|
84
|
+
font-size: .78rem;
|
|
85
|
+
font-weight: 700;
|
|
86
|
+
text-transform: uppercase;
|
|
87
|
+
letter-spacing: .14em;
|
|
88
|
+
color: ${spec.accent};
|
|
89
|
+
margin-bottom: .55rem;
|
|
90
|
+
}
|
|
91
|
+
.head-contact { font-size: .76rem; color: #82868E; }
|
|
92
|
+
.doc-label {
|
|
93
|
+
font-size: .68rem;
|
|
94
|
+
font-weight: 700;
|
|
95
|
+
text-transform: uppercase;
|
|
96
|
+
letter-spacing: .16em;
|
|
97
|
+
color: #82868E;
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: .65rem;
|
|
101
|
+
margin-bottom: 1.2rem;
|
|
102
|
+
}
|
|
103
|
+
.doc-label::before { content: ''; width: 22px; height: 3px; border-radius: 2px; background: ${spec.accent}; }
|
|
104
|
+
.date-line { font-size: .8rem; color: #82868E; margin-bottom: 1.4rem; }
|
|
105
|
+
.subject { font-size: .92rem; font-weight: 700; margin-bottom: 1.2rem; }
|
|
106
|
+
.greeting { font-size: .9rem; margin-bottom: 1rem; }
|
|
107
|
+
.body-p { font-size: .88rem; color: #4A4E57; line-height: 1.8; margin-bottom: 1rem; text-align: justify; }
|
|
108
|
+
.closing { margin-top: 1.6rem; font-size: .9rem; }
|
|
109
|
+
.signature {
|
|
110
|
+
margin-top: .4rem;
|
|
111
|
+
font-family: ${spec.displayFamily};
|
|
112
|
+
font-size: 1.05rem;
|
|
113
|
+
font-weight: 700;
|
|
114
|
+
}
|
|
115
|
+
@media (max-width: 640px) {
|
|
116
|
+
body { padding: .5rem; }
|
|
117
|
+
.sheet { border-radius: 0; padding: 1.8rem 1.4rem; }
|
|
118
|
+
}
|
|
119
|
+
@media print {
|
|
120
|
+
@page { margin: 0.7in; size: A4; }
|
|
121
|
+
html { font-size: 13px; }
|
|
122
|
+
body { padding: 0; background: white; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
123
|
+
.sheet { max-width: 100%; border-radius: 0; box-shadow: none; padding: 0; }
|
|
124
|
+
.sheet::before { display: none; }
|
|
125
|
+
.body-p { break-inside: avoid; }
|
|
126
|
+
}
|
|
127
|
+
</style>
|
|
128
|
+
</head>
|
|
129
|
+
<body>
|
|
130
|
+
<div class="sheet">
|
|
131
|
+
<header class="head">
|
|
132
|
+
<h1 class="head-name">${escapeHtml(resume.name)}</h1>
|
|
133
|
+
${resume.title ? `<p class="head-title">${escapeHtml(resume.title)}</p>` : ""}
|
|
134
|
+
<p class="head-contact">${contactLine(resume)}</p>
|
|
135
|
+
</header>
|
|
136
|
+
<p class="doc-label">${title}</p>
|
|
137
|
+
${cover.dateLine ? `<p class="date-line">${escapeHtml(cover.dateLine)}</p>` : ""}
|
|
138
|
+
${cover.subject ? `<p class="subject">${escapeHtml(cover.subject)}</p>` : ""}
|
|
139
|
+
<p class="greeting">${escapeHtml(cover.greeting)}</p>
|
|
140
|
+
${paragraphs}
|
|
141
|
+
<p class="closing">${escapeHtml(cover.closing)}</p>
|
|
142
|
+
<p class="signature">${escapeHtml(resume.name)}</p>
|
|
143
|
+
</div>
|
|
144
|
+
</body>
|
|
145
145
|
</html>`;
|
|
146
146
|
}
|