From 8e431bd8769f0d5447f3d644a3ea552ecbb14eba Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Sat, 19 Sep 2026 23:56:25 -0300 Subject: [PATCH] =?UTF-8?q?feat(reels):=20navega=C3=A7=C3=A3o=20vis=C3=ADv?= =?UTF-8?q?el,=20trilho=20de=20comando=20e=20cena=20profile=20na=20skill?= =?UTF-8?q?=20de=20reels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - captura: swipe de toque real, scrollIntoView suave visível, upload de foto (fixture.photo) e cursor centralizado na origem - shot list: bloco graphics (hook/cta/profile/cover) como fonte da mensagem, badge de rótulo e distance do swipe validados - superfície: port do design revisado (trilho de comando + cap, hook com pattern/coração, CTA com punho, capa) e cena nova profile (avatar IG/WA) - reel foto-de-perfil draft (8 cenas, 30,2s) + fixture sintética sem PII - testes unit do schema e changelog da evolução --- .agents/skills/reels-tutoriais/SKILL.md | 30 +- .../2026-09-19-reels-tutoriais-evolucao.md | 1 + ...ls-tutoriais-foto-de-perfil-ui-design.html | 1544 +++++++++++++++++ scripts/build-reel.mjs | 63 +- scripts/lib/campaignKitAssets.mjs | 43 +- scripts/lib/reelCapture.mjs | 132 +- scripts/lib/reelShotList.mjs | 133 +- scripts/lib/reelTemplates.mjs | 307 ++-- scripts/reels/fixtures/profile-photo.png | Bin 0 -> 75680 bytes scripts/reels/shot-lists/cards.json | 23 + scripts/reels/shot-lists/foto-de-perfil.json | 179 ++ tests/unit/reelShotList.unit.spec.ts | 180 ++ 12 files changed, 2481 insertions(+), 154 deletions(-) create mode 100644 docs/changelog/2026-09-19-reels-tutoriais-evolucao.md create mode 100644 docs/plans/reels-tutoriais-foto-de-perfil-ui-design.html create mode 100644 scripts/reels/fixtures/profile-photo.png create mode 100644 scripts/reels/shot-lists/foto-de-perfil.json diff --git a/.agents/skills/reels-tutoriais/SKILL.md b/.agents/skills/reels-tutoriais/SKILL.md index c6499f9c..deb32c4d 100644 --- a/.agents/skills/reels-tutoriais/SKILL.md +++ b/.agents/skills/reels-tutoriais/SKILL.md @@ -57,7 +57,7 @@ pnpm reels:build [--audio] └─ captura: Chromium mobile (360×640 @3x = 1080×1920 nativo) ├─ cursor sintético + halo amarelo no alvo do clique └─ screencast com timestamps de parede (sem drift) → frames JPEG - └─ assets: legendas/etiqueta em PNG transparente + capa.png + └─ assets: trilho de comando/cap em PNG transparente + capa.png └─ clipes por cena: zoompan (do log de cliques) + overlay └─ concat H.264/yuv420p 30fps ├─ transcrição SEMPRE: narracao.srt + roteiro.md @@ -81,13 +81,24 @@ pnpm reels:tts:setup # venv do edge-tts (só para --audio) ## Contrato do shot list `scripts/reels/shot-lists/.json` é **versionado** e é a fonte única da -verdade. A primeira cena é o `hook`, a última é o `cta` (gráficas) e no meio -ficam as cenas de captura com `badge` (etiqueta de passo), `caption` (legenda -queimada), `setup` (preparo invisível, ex. `scrollIntoView`) e `steps` -(`click`, `fill`, `waitFor`, `download`). O `fixture.cardName` é fictício +verdade. A primeira cena é o `hook`, a última é o `cta` (gráficas); o meio +pode ter mais gráficas (ex. `profile`, a ponte para Instagram/WhatsApp) e as +cenas de captura com `badge` (passo `n/total` ou pílula de rótulo, ex. +`COMECE`), `caption` (o comando do trilho), `setup` (preparo invisível, ex. +`scrollIntoView`) e `steps` (`click`, `fill`, `waitFor`, `download`, `upload`, +`scrollIntoView`, `swipe`). O `upload` clica no alvo visível (ex. “Escolher +foto”) e envia o arquivo de `fixture.photo` (ou do `value` do passo) pelo +seletor de arquivo do navegador; o `swipe` arrasta o track mobile com toque +real (`distance` em px). `fixture.cardName` e `fixture.photo` são fictícios (zero PII de terceiros). Qualquer ajuste de roteiro, texto, ordem, ritmo ou alvo de clique é feito **aqui** — nunca no MP4. +A copy das cenas gráficas e da capa vive no bloco `graphics` do shot list +(`hook`, `cta`, `profile` e `cover`, cada um com `eyebrow`, `headlineLines`, +`accentIndex`, `sub`/`tag`/`actionLines`/`url`/`instruction`/`apps` conforme o +template): o template carrega a estrutura, o reel carrega a mensagem — nenhum +texto temático fica hardcoded no renderer. + A fala de cada cena vive **no shot list**, no campo `narration` (opcional, até 600 caracteres): as cenas de captura caem no texto da própria `caption` quando ele falta (os espaços autorais são preservados), e cenas gráficas sem @@ -148,9 +159,12 @@ sem) e então mostre os artefatos e o resumo (duração, cenas, hash). Depois: ## Design -A superfície gráfica (hook, legenda, etiqueta de passo, cursor/halo, capa, -safe zone) vem do artefato aprovado `docs/plans/reels-tutoriais-ui-design.html` -+ assets; o port é classe-a-classe em `scripts/lib/reelTemplates.mjs`. O +A superfície gráfica (hook, cap de marca, trilho de comando, cursor/halo, +capa, cena `profile`, safe zone) vem dos artefatos aprovados +`docs/plans/reels-tutoriais-ui-design.html` (família) e +`docs/plans/reels-tutoriais-foto-de-perfil-ui-design.html` (revisão: trilho no +topo em vez de legenda no centro, copy por reel, ilustrações do kit, ponte +para o perfil); o port é classe-a-classe em `scripts/lib/reelTemplates.mjs`. O `designer` é dono da estrutura visual — não improvise layout. ## Marca oficial (kit 1313) diff --git a/docs/changelog/2026-09-19-reels-tutoriais-evolucao.md b/docs/changelog/2026-09-19-reels-tutoriais-evolucao.md new file mode 100644 index 00000000..8869a15c --- /dev/null +++ b/docs/changelog/2026-09-19-reels-tutoriais-evolucao.md @@ -0,0 +1 @@ +**Skill `reels-tutoriais` — navegação visível, trilho de comando, cena `profile` e copy por reel (2026-09-19):** evolução pedida no gate do reel `foto-de-perfil` (rascunho, **não aprovado para publicação**). O shot list ganha `upload` (envia `fixture.photo` pelo seletor de arquivo clicando no alvo visível, ex. “Escolher foto”), `swipe` (arrasto de toque real no track mobile, com hold antes do `touchEnd` para não voar além do snap) e `scrollIntoView` visível com rolagem suave (o `setup` continua instantâneo e invisível — sem teletransporte na tela); `badge` aceita pílula de rótulo (`COMECE`) além de `n/total`; e todo o texto das cenas gráficas/capa migra para o bloco `graphics` do shot list (`hook`, `cta`, `profile`, `cover` — `eyebrow`, `headlineLines`, `accentIndex`, `sub`/`tag`/`actionLines`/`url`/`instruction`/`apps`), com a normalização fail-closed e cobertura unit correspondente. O capture agora centra o cursor sintético na origem e a superfície gráfica foi re-portada do design revisado (`docs/plans/reels-tutoriais-foto-de-perfil-ui-design.html`): sai a legenda preta central e entram o cap de marca (topo) + trilho de comando único em y88 (`1/4`…`4/4` + ação curta), o hook com pattern/coração oficiais, o CTA com o punho e a cena nova `profile` (~4s, animação CSS) que leva a arte baixada para o avatar do Instagram e do WhatsApp — nenhuma estrela solta como logo de canto e nenhum lockup recriado; o leitor de assets virou `readImageDataUris` (kit + moldura/fixture do reel). O `cards.json` recebeu o mesmo bloco `graphics` (o reel cards adota a superfície nova no próximo build). Verificação: `pnpm lint`, `pnpm typecheck` e `pnpm test:unit` verdes (4.202 unit; novos casos de `graphics`, `badge` de rótulo, `swipe` e cena `profile`). Sem mudança no site público/Payload, sem schema/migration/Consent; a mídia do pacote segue gitignored. diff --git a/docs/plans/reels-tutoriais-foto-de-perfil-ui-design.html b/docs/plans/reels-tutoriais-foto-de-perfil-ui-design.html new file mode 100644 index 00000000..186d02fd --- /dev/null +++ b/docs/plans/reels-tutoriais-foto-de-perfil-ui-design.html @@ -0,0 +1,1544 @@ + + + + + + + Design revisado — Reel foto de perfil + + + + +
+
+
+ REVISÃO DO GATE + foto-de-perfil + 360 × 640 CSS · @3x + 8 cenas · ~25s +
+

+ A tela não salta: ela conduz. +

+

+ Extensão do design aprovado da família em reels-tutoriais-ui-design.html. + Esta revisão troca a abertura genérica por copy de foto de perfil, mostra a chegada à + home, o scroll e o swipe da galeria, cria uma ponte visual até Instagram/WhatsApp e reduz + os comandos a um único trilho no topo. Nenhum lockup foi redesenhado: marca e ilustrações + vêm do kit oficial. +

+ +
+ +
+

01 · SUPERFÍCIE REVISADA

+

+ Quatro frames que definem o reel +

+

+ Hook e fechamento usam ilustrações oficiais em escala grande, nunca uma estrela solta como + selo. A captura mantém o produto visível. A cena profile conclui o trabalho que o site + começou. +

+
+ +
+
+
+

01 · Hook

+

0–2,6s

+
+ GRÁFICA +
+
+ + Ilustração oficial de coração com pulso +
+ TUTORIAL · FOTO DE PERFIL +

+ Sua foto
com a moldura
do Solla. +

+

+ Veja como fazer pelo celular e usar no Instagram ou WhatsApp. +

+
+ Jorge Solla 1313 — Mais Saúde, Mais Futuro +
+

+ Correção: o tema aparece na primeira frase. + Pattern + coração são ilustrações do kit; a única assinatura é a marca completa + oficial. +

+
+ + +
+
+
+

03 · Galeria

+

swipe → tile → drawer

+
+ CAPTURA +
+
+
+ +
+

+ FAÇA PARTE +

+

+ Mostre que você
está com Solla +

+

+ Escolha um modelo, coloque seu nome ou sua foto e baixe seu card. +

+
+ +
+
+ Jorge Solla — Deputado Federal +
+
+ 1/4 +

Deslize e toque em “Moldura quadrada”.

+
+
+

+ Correção: badge e comando viram um só trilho + a y=88. Sem caixa preta no centro; foto, moldura e botão permanecem descobertos. +

+
+ + +
+
+
+

07 · Profile

+

~4s · clique para repetir

+
+ +
+
+ Jorge Solla — Deputado Federal +
+

PASSO FINAL

+

Use a arte
no seu perfil.

+
+
+
+
Instagram
+
+
+ Avatar anterior +
+
+

@seuperfil

+
+
+ +
+ + + ✓ +

Escolha a imagem baixada como nova foto de perfil.

+
+

+ Leitura: a própria arte baixada viaja para os + dois avatares. Mock genérico, sem rede e sem imitar telas proprietárias em detalhe. +

+
+ + +
+
+
+

08 · CTA

+

últimos 3s

+
+ GRÁFICA +
+
+
+ Ilustração oficial de punho erguido +
+
+

AGORA É COM VOCÊ

+

+ Troque sua foto.
Mostre seu apoio. +

+
+
+

FAÇA A SUA

+

Acesse pelo
link na bio

+

Escolha “Moldura quadrada”, coloque sua foto e baixe.

+
+

jorgesolla1313.com.br

+ Jorge Solla 1313 — Mais Saúde, Mais Futuro +
+

+ Correção: o punho oficial é claramente + ilustração, não “logo de canto”. A marca completa positiva assina no fundo claro, sem + sombra ou alteração. +

+
+
+
+ +
+

02 · ROTEIRO VISUAL

+

+ Oito cenas, uma continuidade espacial +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CenaDuraçãoO que apareceMovimentoTrilho de comando
1 · hook2,6sCopy específica + coração + marca completa + eyebrow .15s; título em 2 beats; coração deriva 8px + não se aplica
2 · entrada3,2stopo real da home → seção #cards + pausa 450ms; scroll suave 1,6s; repouso 700ms + + COMECE · “Role até ‘Mostre que você está com Solla’.” +
3 · galeria4,0strack mobile real + Moldura quadrada + drawer + swipe 900ms; pausa; cursor chega; toque; drawer sobe + + 1/4 · “Deslize e toque em ‘Moldura quadrada’.” +
4 · foto3,8sEscolher foto → reveal dentro da moldura + toque; 300ms de espera; crossfade da foto 450ms + 2/4 · “Escolha uma foto sua.”
5 · criar2,4sbotão “Criar meu card” → resultado + zoom 1→1,14; clique; release 500ms + + 3/4 · “Confira e toque em ‘Criar meu card’.” +
6 · baixar2,0sresultado + botão “Baixar meu card”cursor; toque; microcheck 350ms4/4 · “Baixe o card no celular.”
7 · profile4,0smock Instagram + WhatsApp + arte viaja ao Instagram; depois WhatsApp; checks + PASSO FINAL no próprio frame
8 · cta3,0spunho + CTA + marca completa + punho entra pela esquerda; card sobe 18px; segura 1,8s + não se aplica
+
+
+
+ Sem teletransporte: a cena 2 começa no topo carregado da home, com 450ms + de repouso para orientação. O scroll é visível e contínuo até #cards. A cena + 3 começa no mesmo enquadramento final, faz um swipe horizontal humano no track e só então + abre o tile. O corte acontece depois do drawer terminar de subir, nunca entre gesto e + resposta. +
+
+ +
+

03 · CENA NOVA “PROFILE”

+

+ A arte baixada vira avatar — sem rede +

+
+
+

Geometria @ 360 × 640 CSS

+
+
.profile-heading
+
+ x22 · y88 · w316; eyebrow 10px/900; título Brexter 27px/.92. +
+
.profile-phones
+
+ x18 · y176 · w324; 2 colunas de 154px; gap 16px. +
+
.profile-phone
+
+ 154 × 226px; raio 22px; borda preta 12%; sombra azul 13%. +
+
.profile-avatar-shell
+
+ 82 × 82px; top 58px do painel; padding 4px; círculo. +
+
.profile-card
+
+ x145 · y330 · 70²; fixture atrás + photo-square-frame.png na frente. +
+
.profile-footnote
+
x22 · y370 · w316; Inter 11px/800 azul.
+
+
+
+

Timeline CSS em 4,0s

+
    +
  1. + 0–0,8s: heading entra em 420ms; os dois + painéis sobem 20px e estabilizam. +
  2. +
  3. + 0,55–2,70s: + .profile-card--ig aparece no centro, percorre −87/−100px e fecha em + círculo no avatar do Instagram. +
  4. +
  5. + 0,55–3,45s: + .profile-card--wa repete o gesto com +87/−100px até o WhatsApp. +
  6. +
  7. + 3,05–3,70s: checks verdes entram com + overshoot 1,15→1; os dois painéis recebem um único halo amarelo. +
  8. +
  9. + 3,70–4,0s: frame final parado. Nada depende + de áudio. +
  10. +
+

+ Renderer: recebe o mesmo + fontCss já embutido por loadFontCss; o HTML não importa + fontes nem chama rede. O executor adiciona body.play depois de o frame + recorder estar pronto. +

+
+
+
+ +
+

04 · COPY POR SHOT LIST

+

+ O template carrega estrutura; o reel carrega a mensagem +

+
+
+

HOOK

+

eyebrow

+

Tutorial · Foto de perfil

+

headlineLines

+

+ Sua foto
com a moldura
do Solla. +

+

sub

+

+ Veja como fazer pelo celular e usar no Instagram ou WhatsApp. +

+
+
+

CAPA

+

eyebrow

+

Tutorial do site

+

headlineLines

+

+ Use a moldura
na sua foto
de perfil. +

+

sub

+

Instagram e WhatsApp, direto pelo celular.

+
+
+

CTA

+

eyebrow

+

Agora é com você

+

headlineLines

+

+ Troque sua foto.
Mostre seu apoio. +

+

action

+

Acesse pelo link na bio

+

sub

+

+ Escolha “Moldura quadrada”, coloque sua foto e baixe. +

+
+
+

LABELS

+
    +
  • entry.label: COMECE
  • +
  • step1.label: ESCOLHA A MOLDURA
  • +
  • step2.label: COLOQUE SUA FOTO
  • +
  • step3.label: CRIE SEU CARD
  • +
  • step4.label: BAIXE O CARD
  • +
  • profile.eyebrow: PASSO FINAL
  • +
  • cover.tag: #cards
  • +
+
+
+

+ Variáveis mínimas do shot list: + graphics.hook.{eyebrow,headlineLines,accentLine,sub}, + graphics.cover.{eyebrow,headlineLines,accentLine,sub,tag}, + graphics.cta.{eyebrow,headlineLines,accentLine,action,sub,url}, + graphics.profile.{eyebrow,headlineLines,instruction,apps} e, em cada captura, + badge.{number,total,label} + caption.parts. Nenhum texto + temático (“Crie seu card de apoio”) permanece hardcoded no template. +

+
+ +
+

05 · CAPA ESPECÍFICA

+

+ A promessa sobrevive ao recorte central +

+
+
+ +
+ TUTORIAL DO SITE +

+ Use a moldura
na sua foto
de perfil. +

+

Instagram e WhatsApp, direto pelo celular.

+
+ #cards + Jorge Solla 1313 — Mais Saúde, Mais Futuro +
+
+
+

Regra do recorte

+

+ O núcleo central 1080×1350 equivale a + y95–545 @360. Eyebrow, headline inteira, + #cards e marca completa ficam dentro dele. O pattern pode ser cortado + porque é decoração redundante. +

+

+ Brexter: 44px / .86 / tracking −.035em. As + três linhas são explícitas; o renderer não decide quebras. Se a fonte oficial não + carregar, o render falha fechado — não aceita fallback em produção. +

+

+ Sem estrela isolada no canto. A assinatura única é + marca-negativa-completa.png, a 134px CSS (402px físicos), inteira e sem + filtro. +

+
+
+
+ +
+

+ 06 · ESPECIFICAÇÃO CLASSE-A-CLASSE +

+

+ Contrato visual para o port +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClassePosição / tamanhoVisualMotion / regra
.capture-brand-capx0 y0 w360 h38gradiente #E4102F→#184E92; borda inferior 2px #FFEB00 + redundante no topo inseguro; usa jorge-solla-negativo.png 86px, sem + plate branco +
.command-railx16 y88 w328 min-h50 + branco 96%; raio 12; borda 1px; barra esquerda vermelha 4px; sombra 0 8 24 /18% + + entra 10px→0 em 220ms; sai em 160ms antes de qualquer target cruzar y88–138 +
.step-badge34² dentro do rail#FFEB00; círculo; Inter 11px/900 preto + “1/4”, nunca mini-logo, número de campanha ou estrela +
.command-copymax-w258; máx. 2 linhasInter 12.5px/750, line-height 1.2; ação em #A21C1C/900 + copy curta e literal; não repete label; sem caixa separada no centro +
.hook-titlex22 y143 w310Brexter 40px/.88; tracking −.035em; branco/amarelo + linha 1–2 em .55s; linha de acento +.24s +
.hook-illustrationright −30 top58 w154coração.png intacto, sem recolorir + fade + translateX(8→0) .65s; não gira nem pulsa +
.cta-punhox−38 y42 w180 + punho.png intacto e deliberadamente sangrado + + translateX(−16→0) .6s; lê como ilustração, não assinatura +
.cta-cardx22 y328 w316branco; raio 18; padding 20/18; sombra 0 14 36 /14% + rise 18px/.5s; segura ≥1,8s; um único CTA verbal +
.cover-copy h3x24 y181 w316Brexter 44px/.86; tracking −.035emestático; três linhas explícitas
+
+
+
+
+

CORES DO KIT

+

+ #E4102F · vermelho
#184E92 · azul
#FFEB00 · amarelo
#009647 · verde
#FFF8F2 + · creme de superfície +

+
+
+

TIPOGRAFIA

+

Impacto só
onde importa.

+

+ Brexter: hook, capa, títulos de profile e CTA. Inter: comando, labels, app mocks e + URL. +

+
+
+

CÂMERA

+

+ Scroll e swipe sem zoom. Toques usam 108–114% por padrão; 118% só se o alvo continuar + inteiro. Cursor chega 180ms antes. Halo amarelo dura um pulso e some ao abrir o + drawer. +

+
+
+
+ +
+

07 · CRITÉRIO DE ACEITE VISUAL

+

+ O que conferir no MP4 e na capa +

+
    +
  1. +

    1. Entrada compreensível

    +

    + Há um frame orientador no topo da home, scroll contínuo até #cards e swipe real até + “Moldura quadrada”. Nenhum jump cut substitui navegação. +

    +
  2. +
  3. +

    2. Produto não é encoberto

    +

    + O rail fica em y88–138; nunca atravessa foto, moldura, controles ou CTAs. Não existe + legenda preta no centro. Em conflito real, o rail sai antes — não muda de lado em cima + do alvo. +

    +
  4. +
  5. +

    3. Safe zone do Instagram

    +

    + Instrução crítica permanece entre x22–338 e y83–416 CSS (65px laterais, 250px topo e + 672px base no arquivo @3x). Marca no cap/topo e URL no rodapé são redundantes. +

    +
  6. +
  7. +

    4. Legibilidade no mudo

    +

    + Cada rail tem uma ação, no máximo duas linhas, contraste AA e ≥37,5px físicos. Hook, + profile e CTA seguram o frame final pelo tempo indicado. +

    +
  8. +
  9. +

    5. Ponte até o perfil

    +

    + A arte quadrada vista no resultado é visualmente a mesma que entra nos avatares. + Instagram aparece primeiro, WhatsApp depois; os dois terminam com a moldura visível. +

    +
  10. +
  11. +

    6. Marca sem invenção

    +

    + Lockups oficiais intactos, variante negativa sobre vermelho e positiva sobre claro. + Nada de estrela isolada como logo de canto, texto recriando “Jorge Solla”, filtros, + recoloração ou sombra nos PNGs. +

    +
  12. +
  13. +

    7. Capa recortável

    +

    + Eyebrow, headline, #cards e marca estão inteiros em y95–545. Conferir 9:16, recorte + 4:5 e grade 1:1; pattern pode sangrar. +

    +
  14. +
  15. +

    8. Fontes e nitidez

    +

    + Brexter e Inter vêm embutidas; render falha fechado se faltarem. Conferir bordas + nítidas a 1080×1920, sem PNG escalado além da resolução útil e sem artefato de + compressão sobre amarelo/vermelho. +

    +
  16. +
+
+ +
+

+ ATIVOS USADOS — KIT: + public/campaign-kit/pattern-shapes.png, + public/campaign-kit/coração.png, public/campaign-kit/punho.png, + public/campaign-kit/jorge-solla-negativo.png, + public/campaign-kit/marca-negativa-completa.png, + public/campaign-kit/marca-positiva-completa.png. +

+

+ ATIVOS DE PRODUTO/FIXTURE: + public/cards/photo-square-frame.png e + scripts/reels/fixtures/profile-photo.png. São a moldura real e a pessoa + fictícia já usada no reel; não são marca nova. +

+

+ CHAVES A ADICIONAR EM campaignKitAssets.mjs: + patternShapes: 'pattern-shapes.png', heart: 'coração.png' e + fist: 'punho.png'. As chaves nameNegative, + completeNegative e completePositive já existem. Não adicionar + star a este reel. +

+

+ GATE: este arquivo é a revisão material pedida + pelo operador; exige aprovação humana antes do port. A referência da família continua + válida para codec, cursor/halo e matemática geral de safe zone, exceto onde esta extensão + substitui explicitamente hook, capa, CTA, chrome e legenda. +

+
+
+ + + diff --git a/scripts/build-reel.mjs b/scripts/build-reel.mjs index c0bf8ce7..b8fd1da1 100644 --- a/scripts/build-reel.mjs +++ b/scripts/build-reel.mjs @@ -20,7 +20,7 @@ import { mkdir, writeFile } from 'node:fs/promises' import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' -import { readKitAssets } from './lib/campaignKitAssets.mjs' +import { readImageDataUris, readKitAssets } from './lib/campaignKitAssets.mjs' import { dieWithLabel, loadCliEnv, parseEqualsFlags } from './lib/cli.mjs' import { gotoReelSite, @@ -61,11 +61,12 @@ import { } from './lib/reelScript.mjs' import { captureScenes, graphicScenes, loadShotList } from './lib/reelShotList.mjs' import { - captionOverlayHtml, - chromeOverlayHtml, + captureChromeHtml, + commandRailHtml, coverHtml, ctaHtml, hookHtml, + profileHtml, } from './lib/reelTemplates.mjs' import { captureScenePlan, graphicScenePlan, REEL, totalDurationMs } from './lib/reelTimeline.mjs' import { buildNarrationTrack, DEFAULT_TTS_VOICE, resolveEdgeTts } from './lib/reelTts.mjs' @@ -143,6 +144,18 @@ const main = async () => { await mkdir(workDir, { recursive: true }) await mkdir(outDir, { recursive: true }) const assets = await readKitAssets({ root: ROOT }) + if (graphicScenes(shotList).some((scene) => scene.template === 'profile')) { + Object.assign( + assets, + await readImageDataUris({ + root: ROOT, + files: { + cardFrame: 'public/cards/photo-square-frame.png', + fixturePhoto: shotList.fixture.photo, + }, + }), + ) + } const browser = await launchReelBrowser() const plans = new Map() const graphics = new Map() @@ -167,7 +180,7 @@ const main = async () => { } } const fontFaces = await extractFontFaces(page) - const actions = await runCaptureScenes({ page, shotList, log: [] }) + const actions = await runCaptureScenes({ page, shotList, log: [], root: ROOT }) const stats = await recorder.stop() if (stats.firstFrameWallMs === null) throw new Error('Nenhum frame foi gravado — o site não renderizou?') @@ -208,23 +221,31 @@ const main = async () => { for (const scene of captureScenes(shotList)) { await renderOverlay({ page: renderPage, - html: chromeOverlayHtml({ fontCss, badge: scene.badge, assets }), + html: captureChromeHtml({ assets }), output: join(workDir, `chrome-${scene.id}.png`), }) await renderOverlay({ page: renderPage, - html: captionOverlayHtml({ fontCss, caption: scene.caption }), - output: join(workDir, `caption-${scene.id}.png`), + html: commandRailHtml({ fontCss, badge: scene.badge, caption: scene.caption }), + output: join(workDir, `command-${scene.id}.png`), }) } await renderCover({ page: renderPage, - html: coverHtml({ fontCss, assets }), + html: coverHtml({ fontCss, assets, copy: shotList.graphics.cover }), output: join(outDir, 'capa.png'), }) + const graphicHtmls = { + hook: hookHtml, + cta: ctaHtml, + profile: profileHtml, + } for (const scene of graphicScenes(shotList)) { - const html = - scene.template === 'hook' ? hookHtml({ fontCss, assets }) : ctaHtml({ fontCss, assets }) + const html = graphicHtmls[scene.template]({ + fontCss, + assets, + copy: shotList.graphics[scene.template], + }) const graphic = await recordGraphicScene({ context: renderContext, template: scene.template, @@ -263,20 +284,16 @@ const main = async () => { const plan = plans.get(scene.id) const output = join(workDir, `clip-${scene.id}.mp4`) const input = scene.kind === 'capture' ? capturePath : graphics.get(scene.id) - const captions = + const overlays = scene.kind === 'capture' - ? plan.captions.map((window) => ({ - path: join(workDir, `caption-${scene.id}.png`), - ...window, - })) + ? [ + { path: join(workDir, `chrome-${scene.id}.png`), startMs: 0, endMs: plan.durationMs }, + ...plan.captions.map((window) => ({ + path: join(workDir, `command-${scene.id}.png`), + ...window, + })), + ] : [] - if (scene.kind === 'capture') { - captions.unshift({ - path: join(workDir, `chrome-${scene.id}.png`), - startMs: 0, - endMs: plan.durationMs, - }) - } await runFfmpeg( ffmpeg.bin, buildSceneClipArgs({ @@ -285,7 +302,7 @@ const main = async () => { startMs: plan.startMs, durationMs: plan.durationMs, zoom: plan.zoom, - captions, + captions: overlays, fadeInMs: scene.id === firstScene ? FADE_IN_MS : 0, fadeOutMs: scene.id === lastScene ? FADE_OUT_MS : 0, fps: REEL.fps, diff --git a/scripts/lib/campaignKitAssets.mjs b/scripts/lib/campaignKitAssets.mjs index b183f8b0..4cb5a848 100644 --- a/scripts/lib/campaignKitAssets.mjs +++ b/scripts/lib/campaignKitAssets.mjs @@ -1,6 +1,6 @@ /** - * Official campaign kit assets (C196/C203): reads the committed PNGs under - * `public/campaign-kit` as data URIs, so every local renderer embeds the brand + * Official campaign kit assets (C196/C203) and the reel's product art: reads the + * committed PNGs as data URIs, so every local renderer embeds the brand * without network access. The default root resolves from this module (never * the cwd): the chart entry runs from temp dirs in the tests and the e2e. */ @@ -15,10 +15,33 @@ const KIT_ASSETS = { completePositive: 'marca-positiva-completa.png', completeNegative: 'marca-negativa-completa.png', star: 'estrela.png', + patternShapes: 'pattern-shapes.png', + heart: 'coração.png', + fist: 'punho.png', } const defaultRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..') +/** + * PNG files (repo-relative or kit-relative) as data URIs. + * + * @param {{ root?: string, files: Record }} options repo root + * (defaults to this module's repo) and one relative path per asset key. + * @returns {Promise>} data URI per key. + */ +export const readImageDataUris = async ({ root = defaultRoot, files }) => { + const entries = await Promise.all( + Object.entries(files).map(async ([key, file]) => { + if (typeof file !== 'string' || file.trim() === '') { + throw new Error(`caminho de imagem inválido para o ativo "${key}".`) + } + const bytes = await readFile(resolve(root, file)) + return [key, `data:image/png;base64,${bytes.toString('base64')}`] + }), + ) + return Object.fromEntries(entries) +} + /** * Official campaign kit marks as data URIs (committed under `public/campaign-kit`). * @@ -30,13 +53,11 @@ export const readKitAssets = async ({ root = defaultRoot, keys = Object.keys(KIT_ASSETS), } = {}) => { - const entries = await Promise.all( - keys.map(async (key) => { - const file = KIT_ASSETS[key] - if (!file) throw new Error(`ativo do kit desconhecido: ${key}`) - const bytes = await readFile(resolve(root, 'public/campaign-kit', file)) - return [key, `data:image/png;base64,${bytes.toString('base64')}`] - }), - ) - return Object.fromEntries(entries) + const files = {} + for (const key of keys) { + const file = KIT_ASSETS[key] + if (!file) throw new Error(`ativo do kit desconhecido: ${key}`) + files[key] = join('public', 'campaign-kit', file) + } + return readImageDataUris({ root, files }) } diff --git a/scripts/lib/reelCapture.mjs b/scripts/lib/reelCapture.mjs index 60bcb10b..f7f09fb3 100644 --- a/scripts/lib/reelCapture.mjs +++ b/scripts/lib/reelCapture.mjs @@ -14,8 +14,8 @@ * cursor) and therefore captured by the screencast. */ -import { mkdir, rm, writeFile } from 'node:fs/promises' -import { join } from 'node:path' +import { access, mkdir, rm, writeFile } from 'node:fs/promises' +import { join, resolve } from 'node:path' import { chromium } from '@playwright/test' @@ -33,7 +33,7 @@ const CURSOR_SVG = '' -const CURSOR_INIT = ` +const cursorInit = (startX, startY) => ` ;(() => { const install = () => { if (!document.body || document.getElementById('${CURSOR_ID}')) return @@ -56,6 +56,7 @@ const CURSOR_INIT = ` const cursor = document.createElement('div') cursor.id = '${CURSOR_ID}' cursor.innerHTML = '${CURSOR_SVG}' + cursor.style.transform = 'translate(${startX}px,${startY}px)' document.body.appendChild(cursor) const ticker = document.createElement('div') ticker.id = '${TICKER_ID}' @@ -87,7 +88,9 @@ export const openReelContext = (browser) => }) export const installCursor = async (page) => { - await page.addInitScript({ content: CURSOR_INIT }) + await page.addInitScript({ + content: cursorInit(REEL_VIEWPORT.width / 2, REEL_VIEWPORT.height / 2), + }) } const cursorPositions = new WeakMap() @@ -157,6 +160,23 @@ const resolveLocator = async (page, selector) => { return locator } +/** Waits until the element's box stops moving (smooth scroll/snap settle). */ +const waitForSettle = async (locator, timeoutMs = 2000) => { + const deadline = Date.now() + timeoutMs + let previous = await locator.boundingBox().catch(() => null) + for (;;) { + await sleep(120) + const current = await locator.boundingBox().catch(() => null) + if (!current || !previous) return + const stable = + Math.abs(current.x - previous.x) < 1 && + Math.abs(current.y - previous.y) < 1 && + Math.abs(current.width - previous.width) < 1 + if (stable || Date.now() > deadline) return + previous = current + } +} + const waitForEnabled = async (locator, timeoutMs = 12000) => { const deadline = Date.now() + timeoutMs for (;;) { @@ -198,7 +218,7 @@ const clickTarget = async ({ page, selector, log, scene, zoom }) => { await hideTargetRing(page) } -const runStep = async ({ page, scene, step, log, fixture }) => { +const runStep = async ({ page, scene, step, log, fixture, root }) => { if (step.action === 'click') { await clickTarget({ page, selector: step.selector, log, scene: scene.id, zoom: step.zoom }) await sleep(step.pauseMs ?? 650) @@ -225,6 +245,49 @@ const runStep = async ({ page, scene, step, log, fixture }) => { await sleep(step.pauseMs ?? 450) return } + if (step.action === 'scrollIntoView') { + const locator = await resolveLocator(page, step.selector) + await locator.evaluate((element) => + element.scrollIntoView({ behavior: 'smooth', block: 'center' }), + ) + await waitForSettle(locator, 2200) + await sleep(step.pauseMs ?? 700) + return + } + if (step.action === 'swipe') { + const locator = await resolveLocator(page, step.selector) + await locator.scrollIntoViewIfNeeded() + const box = await locator.boundingBox() + if (!box) throw new Error(`Cena "${scene.id}": swipe "${step.selector}" sem bounding box.`) + const distance = step.distance ?? 220 + const y = Math.round(box.y + box.height / 2) + const startX = Math.round(box.x + box.width * 0.82) + const endX = Math.round(Math.max(box.x + 8, startX - distance)) + const client = await page.context().newCDPSession(page) + try { + await client.send('Input.dispatchTouchEvent', { + type: 'touchStart', + touchPoints: [{ x: startX, y }], + }) + const steps = 10 + for (let index = 1; index <= steps; index += 1) { + const x = Math.round(startX + ((endX - startX) * index) / steps) + await client.send('Input.dispatchTouchEvent', { + type: 'touchMove', + touchPoints: [{ x, y }], + }) + await sleep(35) + } + // Hold before lifting so the gesture does not fling past the next snap. + await sleep(160) + await client.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] }) + } finally { + await client.detach().catch(() => undefined) + } + await waitForSettle(locator, 1600) + await sleep(step.pauseMs ?? 700) + return + } if (step.action === 'download') { const locator = await resolveLocator(page, step.selector) await waitForEnabled(locator) @@ -258,6 +321,61 @@ const runStep = async ({ page, scene, step, log, fixture }) => { await sleep(step.pauseMs ?? 650) return } + if (step.action === 'upload') { + const value = step.value ?? fixture.photo + if (typeof value !== 'string' || value.trim() === '') { + throw new Error(`Cena "${scene.id}": upload sem valor e sem fixture.photo.`) + } + const filePath = resolve(root, value) + try { + await access(filePath) + } catch { + throw new Error(`Cena "${scene.id}": arquivo de upload não encontrado em "${value}".`) + } + const locator = await resolveLocator(page, step.selector) + await waitForEnabled(locator) + await locator.scrollIntoViewIfNeeded() + const center = await centerOf(locator) + if (!center) throw new Error(`Cena "${scene.id}": upload "${step.selector}" sem bounding box.`) + const approachAt = Date.now() + await moveCursor({ page, x: center.x, y: center.y }) + const arriveAt = Date.now() + await showTargetRing({ page, box: center.box }) + await sleep(180) + const at = Date.now() + pushClick({ + log, + scene: scene.id, + selector: step.selector, + center, + zoom: true, + approachAt, + arriveAt, + at, + }) + const chooserPromise = page.waitForEvent('filechooser', { timeout: 15000 }) + await page.mouse.click(center.x, center.y) + await showTargetRing({ page, box: center.box, pulse: true }) + let chooser + try { + chooser = await chooserPromise + } catch { + throw new Error( + `Cena "${scene.id}": o clique em "${step.selector}" não abriu o seletor de arquivo.`, + ) + } + try { + await chooser.setFiles(filePath) + } catch (error) { + throw new Error( + `Cena "${scene.id}": falha ao enviar "${value}" — ${error instanceof Error ? error.message : error}`, + ) + } + await sleep(250) + await hideTargetRing(page) + await sleep(step.pauseMs ?? 900) + return + } throw new Error(`Cena "${scene.id}": ação "${step.action}" não implementada.`) } @@ -276,7 +394,7 @@ const setTicking = (page, on) => * Drives the recorded capture scenes. `setup` steps run before the scene's * `sceneStart` marker (invisible prep), then the visible steps are paced. */ -export const runCaptureScenes = async ({ page, shotList, log = [] }) => { +export const runCaptureScenes = async ({ page, shotList, log = [], root = process.cwd() }) => { const capture = shotList.scenes.filter((scene) => scene.kind === 'capture') for (const scene of capture) { for (const step of scene.setup) { @@ -288,7 +406,7 @@ export const runCaptureScenes = async ({ page, shotList, log = [] }) => { log.push({ scene: scene.id, kind: 'sceneStart', at: Date.now() }) await sleep(scene.leadInMs) for (const step of scene.steps) { - await runStep({ page, scene, step, log, fixture: shotList.fixture }) + await runStep({ page, scene, step, log, fixture: shotList.fixture, root }) } await sleep(scene.trailOutMs) log.push({ scene: scene.id, kind: 'sceneEnd', at: Date.now() }) diff --git a/scripts/lib/reelShotList.mjs b/scripts/lib/reelShotList.mjs index 10129042..1ed075b6 100644 --- a/scripts/lib/reelShotList.mjs +++ b/scripts/lib/reelShotList.mjs @@ -6,14 +6,19 @@ * A shot list has a `hook` (graphic), N `capture` scenes (the real site driven * in a mobile viewport) and a `cta` (graphic). Capture scenes declare invisible * `setup` steps (scroll/prepare, outside the recorded clip) and visible `steps` - * (paced clicks/typing that the viewer sees), plus the burned caption and the - * step badge of the design artifact. + * (paced clicks/typing/file pick that the viewer sees), plus the burned caption + * and the step badge of the design artifact. * * C197 adds the spoken script: an optional `narration` per scene (capture * scenes fall back to the burned caption, graphic scenes are silent without * one) and the top-level `coverAlt` of the cover. Both enter the normalized * object, so the shot list hash identifies them too. * + * The graphic copy lives in the shot list (`graphics.hook/cta/profile` and the + * package `graphics.cover`): the templates carry structure, the reel carries + * its message. The capture overlay is a command rail, not a central caption + * card. + * * Pure module: no browser, no ffmpeg, no I/O beyond reading the JSON file. */ @@ -30,10 +35,15 @@ const NARRATION_MAX_LENGTH = 600 * slug: string, title: string, feature: string, route: string, * coverAlt: string, * fixture: Record, + * graphics: Record, * scenes: Array<{ * id: string, kind: string, * template?: string, durationMs?: number, - * badge?: { number: number, total: number, label: string }, + * badge?: { number?: number, total?: number, label: string }, * caption?: { parts: Array<{ text: string, accent?: boolean }> } | null, * narration?: string | null, * setup: Array<{ action: string, selector: string }>, @@ -44,9 +54,28 @@ const NARRATION_MAX_LENGTH = 600 */ const SCENE_KINDS = new Set(['capture', 'graphic']) -const GRAPHIC_TEMPLATES = new Set(['hook', 'cta']) +const GRAPHIC_TEMPLATES = new Set(['hook', 'cta', 'profile']) +/** Copy keys of each graphic template; `cover` is a package template, not a scene. */ +const GRAPHIC_COPY_KEYS = { + hook: ['eyebrow', 'headlineLines', 'accentIndex', 'sub'], + cta: ['eyebrow', 'headlineLines', 'accentIndex', 'actionLines', 'sub', 'url'], + profile: ['eyebrow', 'headlineLines', 'accentIndex', 'instruction', 'apps'], + cover: ['eyebrow', 'headlineLines', 'accentIndex', 'sub', 'tag'], +} +const COPY_ORDER = ['hook', 'cta', 'profile', 'cover'] +const MAX_HEADLINE_LINES = 3 const SETUP_ACTIONS = new Set(['scrollIntoView']) -const STEP_ACTIONS = new Set(['click', 'fill', 'waitFor', 'download']) +const STEP_ACTIONS = new Set([ + 'click', + 'fill', + 'waitFor', + 'download', + 'upload', + 'scrollIntoView', + 'swipe', +]) +/** Actions whose `value` is meaningful: the inline text (`fill`) or file (`upload`). */ +const VALUE_ACTIONS = new Set(['fill', 'upload']) const fail = (message) => { throw new Error(`Shot list: ${message}`) @@ -101,13 +130,62 @@ const normalizeNarration = (raw, label) => { const normalizeBadge = (raw, label) => { assertObject(raw, label) + const labelText = assertString(raw.label, `${label}.label`) + // The guiding scene carries a label pill; numbered steps carry `n/total`. + if (raw.number === undefined && raw.total === undefined) return { label: labelText } const number = assertPositiveInt(raw.number, `${label}.number`, { max: 99 }) const total = assertPositiveInt(raw.total, `${label}.total`, { max: 99 }) if (number > total) fail(`${label}.number não pode ser maior que total.`) - const labelText = assertString(raw.label, `${label}.label`) return { number, total, label: labelText } } +const normalizeCopyLines = (raw, label, max) => { + if (!Array.isArray(raw) || raw.length === 0 || raw.length > max) { + fail(`${label} deve ter de 1 a ${max} linhas.`) + } + return raw.map((line, index) => assertString(line, `${label}[${index}]`)) +} + +const normalizeAccentIndex = (raw, lines, label) => { + if (raw === undefined || raw === null) return null + if (!Number.isInteger(raw) || raw < 0 || raw >= lines.length) { + fail(`${label} deve indexar uma linha do título.`) + } + return raw +} + +const normalizeGraphicCopy = (raw, label, keys) => { + assertObject(raw, label) + for (const key of Object.keys(raw)) { + if (!keys.includes(key)) fail(`${label}.${key} não é uma chave válida (${keys}).`) + } + const copy = { eyebrow: assertString(raw.eyebrow, `${label}.eyebrow`) } + copy.headlineLines = normalizeCopyLines( + raw.headlineLines, + `${label}.headlineLines`, + MAX_HEADLINE_LINES, + ) + copy.accentIndex = normalizeAccentIndex( + raw.accentIndex, + copy.headlineLines, + `${label}.accentIndex`, + ) + if (keys.includes('sub')) copy.sub = assertString(raw.sub, `${label}.sub`) + if (keys.includes('tag')) copy.tag = assertString(raw.tag, `${label}.tag`) + if (keys.includes('instruction')) { + copy.instruction = assertString(raw.instruction, `${label}.instruction`) + } + if (keys.includes('apps')) { + if (!Array.isArray(raw.apps) || raw.apps.length !== 2) fail(`${label}.apps deve ter 2 apps.`) + copy.apps = raw.apps.map((app, index) => assertString(app, `${label}.apps[${index}]`)) + } + if (keys.includes('actionLines')) { + copy.actionLines = normalizeCopyLines(raw.actionLines, `${label}.actionLines`, 2) + } + if (keys.includes('url')) copy.url = assertString(raw.url, `${label}.url`) + return copy +} + const normalizeSetup = (raw, label) => { if (raw === undefined) return [] if (!Array.isArray(raw)) fail(`${label} deve ser uma lista.`) @@ -123,7 +201,7 @@ const normalizeSetup = (raw, label) => { const normalizeSteps = (raw, label) => { if (!Array.isArray(raw) || raw.length === 0) fail(`${label} deve ter ao menos um passo.`) - const allowedKeys = new Set(['action', 'selector', 'value', 'pauseMs', 'zoom']) + const allowedKeys = new Set(['action', 'selector', 'value', 'pauseMs', 'zoom', 'distance']) return raw.map((step, index) => { assertObject(step, `${label}[${index}]`) for (const key of Object.keys(step)) { @@ -137,13 +215,22 @@ const normalizeSteps = (raw, label) => { action, selector: assertSelector(step.selector, `${label}[${index}].selector`), } - if (action === 'fill' && step.value !== undefined) { + if (step.value !== undefined && !VALUE_ACTIONS.has(action)) { + fail(`${label}[${index}].value só é válido em ${[...VALUE_ACTIONS]} (ação "${action}").`) + } + if (step.value !== undefined) { normalized.value = assertString(step.value, `${label}[${index}].value`) } if (step.zoom !== undefined) { if (typeof step.zoom !== 'boolean') fail(`${label}[${index}].zoom deve ser boolean.`) normalized.zoom = step.zoom } + if (step.distance !== undefined) { + if (action !== 'swipe') fail(`${label}[${index}].distance só é válido em swipe.`) + normalized.distance = assertPositiveInt(step.distance, `${label}[${index}].distance`, { + max: 1000, + }) + } if (step.pauseMs !== undefined) { normalized.pauseMs = assertPositiveInt(step.pauseMs, `${label}[${index}].pauseMs`, { max: 10000, @@ -234,10 +321,39 @@ export const normalizeShotList = (raw) => { if (usesFixtureName && typeof fixture.cardName !== 'string') { fail('fixture.cardName é obrigatório quando um passo fill não declara value.') } + const usesFixturePhoto = scenes.some( + (scene) => + (scene.kind === 'capture' && + scene.steps.some((step) => step.action === 'upload' && step.value === undefined)) || + (scene.kind === 'graphic' && scene.template === 'profile'), + ) + if (usesFixturePhoto && typeof fixture.photo !== 'string') { + fail('fixture.photo é obrigatório no upload de foto e na cena profile.') + } if (scenes[0].kind !== 'graphic' || scenes[0].template !== 'hook') fail('a primeira cena deve ser o hook.') const last = scenes[scenes.length - 1] if (last.kind !== 'graphic' || last.template !== 'cta') fail('a última cena deve ser o CTA.') + const rawGraphics = raw.graphics ?? {} + assertObject(rawGraphics, 'graphics') + for (const key of Object.keys(rawGraphics)) { + if (!(key in GRAPHIC_COPY_KEYS)) fail(`graphics.${key} não é um template válido.`) + } + const usedTemplates = new Set( + scenes.filter((scene) => scene.kind === 'graphic').map((scene) => scene.template), + ) + usedTemplates.add('cover') + const graphics = {} + for (const template of COPY_ORDER) { + if (!usedTemplates.has(template)) continue + const rawCopy = rawGraphics[template] + if (rawCopy === undefined) fail(`graphics.${template} é obrigatório (copy do template).`) + graphics[template] = normalizeGraphicCopy( + rawCopy, + `graphics.${template}`, + GRAPHIC_COPY_KEYS[template], + ) + } const title = assertString(raw.title ?? slug, 'title') return { slug, @@ -249,6 +365,7 @@ export const normalizeShotList = (raw) => { ? title : assertString(raw.coverAlt, 'coverAlt'), fixture, + graphics, scenes, } } diff --git a/scripts/lib/reelTemplates.mjs b/scripts/lib/reelTemplates.mjs index 6e34a42b..f754f448 100644 --- a/scripts/lib/reelTemplates.mjs +++ b/scripts/lib/reelTemplates.mjs @@ -1,53 +1,129 @@ /** - * C196 — the visual templates of the reel, ported class-by-class from the - * approved artifact `docs/plans/reels-tutoriais-ui-design.html` (official - * campaign kit: palette, marks, star, Brexter display face, caption box, step - * badge, safe area, cover). Every template is a pure string builder rendered at - * 360×640 with deviceScaleFactor 3, so the physical asset is 1080×1920. + * C196/C197 — the visual templates of the reel, ported class-by-class from the + * design artifacts: the family design `docs/plans/reels-tutoriais-ui-design.html` + * and the revision `docs/plans/reels-tutoriais-foto-de-perfil-ui-design.html` + * (official campaign kit: palette, marks, illustrations, Brexter display face, + * command rail, capture brand cap, profile handoff, cover). Every template is a + * pure string builder rendered at 360×640 with deviceScaleFactor 3, so the + * physical asset is 1080×1920. + * + * The reel's message is data: hook/cover/CTA/profile copy comes from the shot + * list (`graphics.*`); the templates carry structure only. * * Pure module: the caller passes the font CSS and the kit asset data URIs. */ -import { REEL } from './reelTimeline.mjs' - -/** Official kit palette (extracted from the campaign assets). */ const BRAND = { red: '#e4102f', blue: '#184e92', yellow: '#ffeb00', + green: '#009647', cream: '#fff8f2', + ink: '#111111', + muted: '#6c615e', + line: 'rgb(0 0 0 / 12%)', } -/** Design §Safe zone: the side inset and the caption center in CSS pixels. */ -const SIDE_INSET_PX = REEL.safeArea.sidePx / REEL.deviceScale -const CAPTION_CENTER_PX = REEL.caption.centerYPx / REEL.deviceScale - -const BRAND_GRADIENT = `linear-gradient(160deg, ${BRAND.red} 0%, ${BRAND.red} 62%, ${BRAND.blue} 145%)` - const BASE_CSS = ` *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} -html,body{width:${REEL.viewport.width}px;height:${REEL.viewport.height}px;overflow:hidden;background:transparent} -body{font-family:ReelInter,Inter,ui-sans-serif,system-ui,sans-serif;color:#000;-webkit-font-smoothing:antialiased} -.frame{position:relative;width:${REEL.viewport.width}px;height:${REEL.viewport.height}px;overflow:hidden} +html,body{width:360px;height:640px;overflow:hidden;background:transparent} +body{font-family:ReelInter,Inter,ui-sans-serif,system-ui,sans-serif;color:${BRAND.ink};-webkit-font-smoothing:antialiased} +.frame{position:relative;width:360px;height:640px;overflow:hidden} .brexter{font-family:ReelBrexter,Impact,Haettenschweiler,'Arial Narrow Bold',sans-serif;font-weight:700;text-transform:uppercase} -.accent{color:${BRAND.yellow}} -.star-disc{position:absolute;border-radius:50%;overflow:hidden;background:#fff;pointer-events:none} -.star-disc img{position:absolute;width:186.92%;height:auto;left:-47.38%;top:-12.8%} -.brand-plate{position:absolute;left:214px;top:22px;display:flex;align-items:center;justify-content:center;width:124px;height:68px;border-radius:8px;background:rgb(255 255 255 / 92%);box-shadow:0 4px 14px rgb(0 0 0 / 18%)} -.brand-plate img{display:block;width:112px;height:auto} -.badge{position:absolute;left:${SIDE_INSET_PX}px;top:15%;display:flex;align-items:center;gap:7.2px;border-radius:999px;background:${BRAND.yellow};padding:7.2px 14.4px;font-size:11.6px;font-weight:900;color:#000;box-shadow:0 6px 18px rgb(0 0 0 / 28%)} -.badge b{display:grid;place-items:center;width:21.6px;height:21.6px;border-radius:999px;background:#000;color:#fff;font-size:10px} -.caption-box{position:absolute;left:50%;top:${CAPTION_CENTER_PX}px;transform:translate(-50%,-50%);width:max-content;max-width:${REEL.caption.maxWidthPx / REEL.deviceScale}px;border-radius:14.4px;background:rgb(0 0 0 / 88%);padding:12.6px 18px;text-align:center;color:#fff;font-size:14.8px;font-weight:700;line-height:1.18;box-shadow:0 10px 30px rgb(0 0 0 / 25%)} +.kit-asset{display:block;height:auto;object-fit:contain} +.pattern-wash{position:absolute;inset:0 0 auto;width:360px;height:540px;object-fit:cover;object-position:top center;opacity:.16;pointer-events:none} +.official-mark{position:absolute;display:block;height:auto} + +/* Capture chrome — a quiet top cap and one command rail, never a central card. */ +.capture-brand-cap{position:absolute;z-index:20;inset:0 0 auto;height:38px;background:linear-gradient(90deg,${BRAND.red} 0 78%,${BRAND.blue} 100%);border-bottom:2px solid ${BRAND.yellow};box-shadow:0 4px 12px rgb(0 0 0 / 14%)} +.capture-brand-cap img{position:absolute;right:16px;top:7px;width:86px;height:auto} +.command-rail{position:absolute;z-index:24;left:16px;top:88px;width:328px;min-height:50px;display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:10px;padding:8px 12px 8px 8px;border:1px solid rgb(0 0 0 / 10%);border-left:4px solid ${BRAND.red};border-radius:12px;background:rgb(255 255 255 / 96%);box-shadow:0 8px 24px rgb(0 0 0 / 18%);color:${BRAND.ink}} +.step-badge{display:grid;width:34px;height:34px;place-items:center;border-radius:50%;background:${BRAND.yellow};color:#000;font-size:11px;font-weight:900;line-height:1;box-shadow:inset 0 0 0 1px rgb(0 0 0 / 8%)} +.step-badge--label{width:auto;min-width:70px;border-radius:999px;padding:0 10px} +.command-copy{margin:0;max-width:258px;font-size:12.5px;font-weight:800;line-height:1.2;letter-spacing:-.01em} +.command-copy strong{color:#a21c1c;font-weight:900} + +/* Graphic scene: hook. */ +.hook-scene{background:linear-gradient(156deg,${BRAND.red} 0 68%,${BRAND.blue} 145%)} +.hook-illustration{position:absolute;right:-30px;top:58px;width:154px;opacity:.98} +.hook-copy{position:absolute;left:22px;right:20px;top:94px;color:#fff} +.hook-eyebrow{display:inline-flex;border-radius:999px;padding:6px 12px;background:${BRAND.yellow};color:#000;font-size:10.5px;font-weight:900;letter-spacing:.06em} +.hook-title{width:310px;margin:24px 0 0;font-size:40px;line-height:.88;letter-spacing:-.035em} +.hook-sub{width:205px;margin:20px 0 0;font-size:14px;line-height:1.3;font-weight:650} + +/* Graphic scene: profile handoff. Class names are the port contract. */ +.profile-scene{background:linear-gradient(180deg,#fff8f2 0%,#fff 72%,#f3f6fb 100%)} +.profile-scene::before{content:'';position:absolute;inset:0 0 auto;height:64px;background:${BRAND.red};border-bottom:4px solid ${BRAND.yellow}} +.profile-brand{position:absolute;z-index:3;right:18px;top:16px;width:95px} +.profile-heading{position:absolute;z-index:3;left:22px;right:22px;top:88px;text-align:center} +.profile-eyebrow{color:${BRAND.red};font-size:10px;font-weight:900;letter-spacing:.12em;text-transform:uppercase} +.profile-title{margin-top:5px;color:${BRAND.blue};font-size:27px;line-height:.92;letter-spacing:-.02em} +.profile-phones{position:absolute;left:18px;right:18px;top:176px;display:grid;grid-template-columns:1fr 1fr;gap:16px} +.profile-phone{position:relative;height:226px;overflow:hidden;border:1px solid rgb(0 0 0 / 12%);border-radius:22px;background:#fff;box-shadow:0 12px 30px rgb(24 78 146 / 13%)} +.profile-phone__top{height:38px;display:flex;align-items:center;justify-content:center;border-bottom:1px solid ${BRAND.line};font-size:11px;font-weight:800} +.profile-phone--ig .profile-phone__top{color:#9d174d} +.profile-phone--wa .profile-phone__top{color:#087a3f} +.profile-avatar-shell{position:absolute;left:50%;top:58px;width:82px;height:82px;transform:translateX(-50%);border-radius:50%;padding:4px;background:linear-gradient(135deg,${BRAND.red},${BRAND.yellow},${BRAND.green})} +.profile-phone--wa .profile-avatar-shell{background:#d9f6e5} +.profile-avatar{position:relative;width:100%;height:100%;overflow:hidden;border:3px solid #fff;border-radius:50%;background:#e7eaee} +.profile-avatar img{width:100%;height:100%;object-fit:cover;filter:saturate(0.72);opacity:.58} +.profile-handle{position:absolute;left:12px;right:12px;top:151px;text-align:center;font-size:10px;font-weight:800} +.profile-lines{position:absolute;left:34px;right:34px;top:172px;display:grid;gap:6px} +.profile-lines span{height:5px;border-radius:999px;background:#e7e7e7} +.profile-lines span:last-child{margin-inline:11px} +.profile-card{position:absolute;z-index:10;left:145px;top:330px;width:70px;height:70px;overflow:hidden;border:3px solid #fff;border-radius:12px;background:#dfe9f0;box-shadow:0 10px 24px rgb(0 0 0 / 24%);opacity:0} +.profile-card__photo{width:100%;height:100%;object-fit:cover} +.profile-card__frame{position:absolute;inset:0;width:100%;height:100%} +.profile-check{position:absolute;z-index:12;top:344px;display:grid;width:28px;height:28px;place-items:center;border:3px solid #fff;border-radius:50%;background:${BRAND.green};color:#fff;font-size:15px;font-weight:900;opacity:0;transform:scale(.65)} +.profile-check--ig{left:118px} +.profile-check--wa{right:34px} +.profile-footnote{position:absolute;left:22px;right:22px;top:370px;text-align:center;color:${BRAND.blue};font-size:11px;font-weight:800} + +@keyframes profile-heading-in{0%{opacity:0;transform:translateY(12px)}100%{opacity:1;transform:translateY(0)}} +@keyframes profile-phones-in{0%{opacity:0;transform:translateY(20px)}100%{opacity:1;transform:translateY(0)}} +@keyframes profile-to-instagram{0%,12%{opacity:0;transform:translate(0,12px) scale(.88);border-radius:12px}24%{opacity:1;transform:translate(0,0) scale(1);border-radius:12px}62%,100%{opacity:1;transform:translate(-87px,-100px) scale(1.06);border-radius:50%}} +@keyframes profile-to-whatsapp{0%,46%{opacity:0;transform:translate(0,12px) scale(.88);border-radius:12px}58%{opacity:1;transform:translate(0,0) scale(1);border-radius:12px}88%,100%{opacity:1;transform:translate(87px,-100px) scale(1.06);border-radius:50%}} +@keyframes profile-check-in{0%,78%{opacity:0;transform:scale(.65)}88%{opacity:1;transform:scale(1.15)}100%{opacity:1;transform:scale(1)}} +@keyframes profile-done-pulse{0%,85%,100%{box-shadow:0 12px 30px rgb(24 78 146 / 13%)}92%{box-shadow:0 0 0 4px rgb(255 235 0 / 65%),0 12px 30px rgb(24 78 146 / 13%)}} +.play .profile-heading{animation:profile-heading-in .42s cubic-bezier(.2,.7,.2,1) both} +.play .profile-phones{animation:profile-phones-in .5s cubic-bezier(.2,.7,.2,1) .3s both} +.play .profile-card--ig{animation:profile-to-instagram 2.15s cubic-bezier(.2,.7,.2,1) .55s both} +.play .profile-card--wa{animation:profile-to-whatsapp 2.9s cubic-bezier(.2,.7,.2,1) .55s both} +.play .profile-check{animation:profile-check-in 3.35s cubic-bezier(.2,.8,.2,1) .35s both} +.play .profile-phone{animation:profile-done-pulse 4s ease-out both} + +/* Graphic scene: CTA. */ +.cta-scene{background:${BRAND.cream}} +.cta-red{position:absolute;inset:0 0 auto;height:306px;overflow:hidden;background:${BRAND.red}} +.cta-punho{position:absolute;left:-38px;top:42px;width:180px;opacity:.98} +.cta-copy{position:absolute;left:132px;right:20px;top:90px;color:#fff;text-align:left} +.cta-copy p{margin:0;color:${BRAND.yellow};font-size:10px;font-weight:900;letter-spacing:.1em;text-transform:uppercase} +.cta-copy h3{margin:10px 0 0;font-size:36px;line-height:.88;letter-spacing:-.025em} +.cta-card{position:absolute;left:22px;right:22px;top:328px;padding:20px 18px;border:1px solid ${BRAND.line};border-radius:18px;background:#fff;text-align:center;box-shadow:0 14px 36px rgb(71 19 14 / 14%)} +.cta-card__eyebrow{color:${BRAND.red};font-size:10px;font-weight:900;letter-spacing:.1em;text-transform:uppercase} +.cta-card__action{margin-top:5px;color:${BRAND.blue};font-size:29px;line-height:.95} +.cta-card__sub{margin-top:8px;color:${BRAND.muted};font-size:11px;line-height:1.35} +.cta-url{position:absolute;left:22px;bottom:30px;color:${BRAND.blue};font-size:10px;font-weight:800} + +/* Cover. */ +.cover-scene{background:linear-gradient(156deg,${BRAND.red} 0 72%,${BRAND.blue} 155%)} +.cover-copy{position:absolute;left:24px;right:20px;top:132px;color:#fff} +.cover-copy h3{margin:24px 0 0;font-size:44px;line-height:.86;letter-spacing:-.035em} +.cover-copy p{width:232px;margin:20px 0 0;font-size:13px;line-height:1.3;font-weight:650} +.cover-tag{position:absolute;left:24px;top:500px;border-radius:7px;background:rgb(0 0 0 / 35%);padding:6px 11px;color:#fff;font-size:10px;font-weight:800} + +/* Family motion vocabulary (hook/CTA). */ @keyframes reel-beat{0%{opacity:0;transform:translateY(14px)}100%{opacity:1;transform:translateY(0)}} @keyframes reel-fade{0%{opacity:0}100%{opacity:1}} -@keyframes reel-star{0%{opacity:0;transform:translateY(8px)}100%{opacity:1;transform:translateY(0)}} -@keyframes reel-rise{0%{opacity:0;transform:translateY(22px)}100%{opacity:1;transform:translateY(0)}} -body.play .beat-1{animation:reel-beat .55s cubic-bezier(.2,.7,.2,1) both} -body.play .beat-2{animation:reel-beat .55s cubic-bezier(.2,.7,.2,1) .28s both} -body.play .fade-1{animation:reel-fade .45s ease-out .1s both} -body.play .fade-2{animation:reel-fade .45s ease-out .65s both} -body.play .star-1{animation:reel-star .7s ease-out .15s both} -body.play .rise-1{animation:reel-rise .5s cubic-bezier(.2,.7,.2,1) .35s both} +@keyframes reel-derive{0%{opacity:0;transform:translateX(8px)}100%{opacity:1;transform:translateX(0)}} +@keyframes reel-rise{0%{opacity:0;transform:translateY(18px)}100%{opacity:1;transform:translateY(0)}} +@keyframes reel-punch{0%{opacity:0;transform:translateX(-16px)}100%{opacity:1;transform:translateX(0)}} +.play .fade-1{animation:reel-fade .45s ease-out .15s both} +.play .beat-1{animation:reel-beat .55s cubic-bezier(.2,.7,.2,1) both} +.play .beat-2{animation:reel-beat .55s cubic-bezier(.2,.7,.2,1) .28s both} +.play .rise-1{animation:reel-rise .5s cubic-bezier(.2,.7,.2,1) .35s both} +.play .derive-1{animation:reel-derive .65s ease-out .1s both} +.play .punch-1{animation:reel-punch .6s ease-out both} ` const htmlDocument = ({ title, fontCss, body }) => ` @@ -63,91 +139,128 @@ ${fontCss ? `` : ''} ` -/** Scene 01 — hook (recorded): official red field, star, Brexter in two beats. */ -export const hookHtml = ({ fontCss, assets }) => +/** Explicit headline breaks; only the declared accent line changes color. */ +const headline = (copy, accentColor = BRAND.yellow) => + copy.headlineLines + .map((line, index) => + index === copy.accentIndex ? `${line}` : line, + ) + .join('
') + +/** Capture chrome — the brand cap (always on for the whole scene). */ +export const captureChromeHtml = ({ assets }) => + htmlDocument({ + title: 'Reel — chrome', + body: ` +
+
Jorge Solla — Deputado Federal
+
`, + }) + +/** Transparent overlay of one command rail (step badge + action in brand red). */ +export const commandRailHtml = ({ fontCss, badge, caption }) => { + const badgeHtml = + typeof badge.number === 'number' + ? `${badge.number}/${badge.total}` + : `${badge.label}` + const parts = caption.parts + .map((part) => (part.accent ? `${part.text}` : part.text)) + .join('') + return htmlDocument({ + title: 'Reel — comando', + fontCss, + body: `
${badgeHtml}

${parts}

`, + }) +} + +/** Scene 01 — hook: red field, official pattern + heart, reel's own headline. */ +export const hookHtml = ({ fontCss, assets, copy }) => htmlDocument({ title: 'Reel — hook', fontCss, body: ` -
-
-
- FAÇA PARTE -

- Seu apoio - vira card - em segundos. -

+
+ + +
+ ${copy.eyebrow} +

${headline(copy)}

+

${copy.sub}

-

Veja como criar o seu pelo celular.

- Jorge Solla 1313 — Mais Saúde, Mais Futuro + Jorge Solla 1313 — Mais Saúde, Mais Futuro
`, }) -/** Scene last — CTA (recorded): official red header, white card with "link na bio". */ -export const ctaHtml = ({ fontCss, assets }) => +/** Scene 07 — profile handoff: the downloaded art becomes the avatar, no network. */ +export const profileHtml = ({ fontCss, assets, copy }) => { + const phone = (variant, appLabel) => ` +
+
${appLabel}
+
+

${variant === 'ig' ? '@seuperfil' : 'Seu perfil'}

+
+
` + return htmlDocument({ + title: 'Reel — profile', + fontCss, + body: ` +
+ Jorge Solla — Deputado Federal +
+

${copy.eyebrow}

+

${headline(copy, BRAND.blue)}

+
+
${phone('ig', copy.apps[0])}${phone('wa', copy.apps[1])}
+ + + ✓ +

${copy.instruction}

+
`, + }) +} + +/** Scene last — CTA: official fist, one verbal CTA, positive mark on the light field. */ +export const ctaHtml = ({ fontCss, assets, copy }) => htmlDocument({ title: 'Reel — CTA', fontCss, body: ` -
-
-
-
-

Seu card, sua voz

-

Agora é
com você.

+
+
+
+

${copy.eyebrow}

+

${headline(copy)}

-
-
- -
-

Acesse o site pelo

-

link na bio

-

Escolha seu modelo, crie e compartilhe.

+
+

FAÇA A SUA

+

${copy.actionLines.join('
')}

+

${copy.sub}

-

jorgesolla1313.com.br

- Jorge Solla 1313 — Mais Saúde, Mais Futuro +

${copy.url}

+ Jorge Solla 1313 — Mais Saúde, Mais Futuro
`, }) -/** Transparent overlay of a capture scene: step badge + positive mark on a plate. */ -export const chromeOverlayHtml = ({ fontCss, badge, assets }) => - htmlDocument({ - title: `Reel — chrome ${badge.number}`, - fontCss, - body: ` -
-
${badge.number}${badge.label}
-
Jorge Solla — Deputado Federal
-
`, - }) - -/** Transparent overlay of one burned caption (accent parts in brand yellow). */ -export const captionOverlayHtml = ({ fontCss, caption }) => { - const parts = caption.parts - .map((part) => (part.accent ? `${part.text}` : part.text)) - .join('') - return htmlDocument({ - title: 'Reel — caption', - fontCss, - body: `

${parts}

`, - }) -} - -/** Scene 05 — cover: own composition, title inside the central 1080×1350 crop. */ -export const coverHtml = ({ fontCss, assets }) => +/** Cover: own composition, headline inside the central 1080×1350 crop. */ +export const coverHtml = ({ fontCss, assets, copy }) => htmlDocument({ title: 'Reel — capa', fontCss, body: ` -
-
-
- TUTORIAL DO SITE -

Crie seu
card de
apoio.

-

Seu nome ou sua foto, direto pelo celular.

+
+ +
+ ${copy.eyebrow} +

${headline(copy)}

+

${copy.sub}

-

#cards

- Jorge Solla 1313 — Mais Saúde, Mais Futuro + ${copy.tag} + Jorge Solla 1313 — Mais Saúde, Mais Futuro
`, }) diff --git a/scripts/reels/fixtures/profile-photo.png b/scripts/reels/fixtures/profile-photo.png new file mode 100644 index 0000000000000000000000000000000000000000..3c316f0f5fff749cb428e8ca461c8abad95a927e GIT binary patch literal 75680 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 = {}) => ({ + hook: { + eyebrow: 'FAÇA PARTE', + headlineLines: ['Seu apoio', 'vira card'], + accentIndex: 1, + sub: 'Veja como criar o seu pelo celular.', + }, + cta: { + eyebrow: 'Seu card, sua voz', + headlineLines: ['Agora é', 'com você.'], + actionLines: ['Acesse o site pelo', 'link na bio'], + sub: 'Escolha seu modelo, crie e compartilhe.', + url: 'jorgesolla1313.com.br', + }, + cover: { + eyebrow: 'TUTORIAL DO SITE', + headlineLines: ['Crie seu', 'card de', 'apoio.'], + accentIndex: 2, + sub: 'Seu nome ou sua foto, direto pelo celular.', + tag: '#cards', + }, + ...overrides, +}) + const rawShotList = (overrides: Record = {}) => ({ slug: 'cards', title: 'Crie seu card de apoio', feature: 'cards', route: '/', fixture: { cardName: 'Joana' }, + graphics: rawGraphics(), scenes: [ { id: 'hook', kind: 'graphic', template: 'hook', durationMs: 2400 }, { @@ -119,6 +144,161 @@ describe('reelShotList', () => { expect(() => normalizeShotList({ ...withoutFixture, scenes })).toThrow(/fixture.cardName/) }) + const withUpload = (step: Record, overrides: Record = {}) => { + const base = rawShotList(overrides) + const scenes = (base.scenes as Array>).slice() + scenes[1] = { ...scenes[1], steps: [step] } + return { ...base, scenes } + } + + it('normalizes an upload step with the fixture.photo fallback', () => { + const shotList = normalizeShotList( + withUpload( + { action: 'upload', selector: 'role=button[name="Escolher foto"]' }, + { fixture: { photo: 'scripts/reels/fixtures/profile-photo.png' } }, + ), + ) + const step = captureScenes(shotList)[0].steps[0] + expect(step.action).toBe('upload') + expect(step.value).toBeUndefined() + }) + + it('accepts an inline upload value and hashes the fixture photo', () => { + const inline = normalizeShotList( + withUpload({ + action: 'upload', + selector: '#foto', + value: 'scripts/reels/fixtures/a.png', + }), + ) + expect(captureScenes(inline)[0].steps[0].value).toBe('scripts/reels/fixtures/a.png') + + const photoA = normalizeShotList( + withUpload( + { action: 'upload', selector: '#foto' }, + { fixture: { photo: 'scripts/reels/fixtures/profile-photo.png' } }, + ), + ) + const photoB = normalizeShotList( + withUpload( + { action: 'upload', selector: '#foto' }, + { fixture: { photo: 'scripts/reels/fixtures/outra.png' } }, + ), + ) + expect(shotListHash(photoA)).not.toBe(shotListHash(photoB)) + }) + + it('requires fixture.photo when an upload step has no value', () => { + expect(() => + normalizeShotList(withUpload({ action: 'upload', selector: '#foto' }, { fixture: {} })), + ).toThrow(/fixture.photo/) + }) + + it('requires the copy of every used graphic template', () => { + const graphics = rawGraphics() + delete (graphics as Record).cover + expect(() => normalizeShotList(rawShotList({ graphics }))).toThrow(/graphics.cover/) + const unknown = rawGraphics({ banner: { eyebrow: 'x', headlineLines: ['y'] } }) + expect(() => normalizeShotList(rawShotList({ graphics: unknown }))).toThrow( + /graphics.banner não é um template válido/, + ) + }) + + it('normalizes the graphic copy and validates the accent line', () => { + const shotList = normalizeShotList(rawShotList()) + expect(shotList.graphics.hook).toMatchObject({ + eyebrow: 'FAÇA PARTE', + headlineLines: ['Seu apoio', 'vira card'], + accentIndex: 1, + }) + expect(shotList.graphics.cta.actionLines).toEqual(['Acesse o site pelo', 'link na bio']) + + const graphics = rawGraphics({ + hook: { eyebrow: 'x', headlineLines: ['uma linha'], accentIndex: 2 }, + }) + expect(() => normalizeShotList(rawShotList({ graphics }))).toThrow(/accentIndex/) + + const first = normalizeShotList(rawShotList()) + const changed = normalizeShotList( + rawShotList({ + graphics: rawGraphics({ + hook: { + eyebrow: 'FAÇA PARTE', + headlineLines: ['Seu apoio', 'vira vídeo'], + accentIndex: 1, + sub: 'Veja como criar o seu pelo celular.', + }, + }), + }), + ) + expect(shotListHash(changed)).not.toBe(shotListHash(first)) + }) + + it('accepts a label-only badge and a swipe distance', () => { + const base = rawShotList() + const scenes = (base.scenes as Array>).slice() + scenes[1] = { + ...scenes[1], + badge: { label: 'COMECE' }, + steps: [{ action: 'swipe', selector: '#track', distance: 300 }], + } + const shotList = normalizeShotList({ ...base, scenes }) + expect(captureScenes(shotList)[0].badge).toEqual({ label: 'COMECE' }) + expect(captureScenes(shotList)[0].steps[0].distance).toBe(300) + + const invalid = (base.scenes as Array>).slice() + invalid[1] = { ...invalid[1], steps: [{ action: 'click', selector: '#a', distance: 300 }] } + expect(() => normalizeShotList({ ...base, scenes: invalid })).toThrow(/distance/) + }) + + it('requires fixture.photo for the profile scene and normalizes its copy', () => { + const profileCopy = { + eyebrow: 'PASSO FINAL', + headlineLines: ['Use a arte', 'no seu perfil.'], + instruction: 'Escolha a imagem baixada como nova foto de perfil.', + apps: ['Instagram', 'WhatsApp'], + } + const graphics = rawGraphics({ profile: profileCopy }) + const profileScene = { + id: 'profile', + kind: 'graphic', + template: 'profile', + durationMs: 4000, + } + const withoutPhoto = rawShotList({ graphics, fixture: {} }) + const withoutScenes = (withoutPhoto.scenes as Array>).slice() + withoutScenes.splice(1, 0, profileScene) + expect(() => normalizeShotList({ ...withoutPhoto, scenes: withoutScenes })).toThrow( + /fixture.photo/, + ) + + const withPhoto = rawShotList({ + graphics, + fixture: { photo: 'scripts/reels/fixtures/profile-photo.png' }, + }) + const withScenes = (withPhoto.scenes as Array>).slice() + withScenes.splice(1, 0, profileScene) + const shotList = normalizeShotList({ ...withPhoto, scenes: withScenes }) + expect(graphicScenes(shotList).map((scene: { template: string }) => scene.template)).toEqual([ + 'hook', + 'profile', + 'cta', + ]) + expect(shotList.graphics.profile).toMatchObject({ + accentIndex: null, + apps: ['Instagram', 'WhatsApp'], + }) + }) + + it('rejects value on an action that takes no value', () => { + const scenes = (rawShotList().scenes as Array>).slice() + scenes[1] = { + ...scenes[1], + steps: [{ action: 'click', selector: '#a', value: 'não' }], + } + expect(() => normalizeShotList({ ...rawShotList(), scenes })).toThrow(/value só é válido/) + }) + it('normalizes the narration and preserves the authored caption spacing', () => { const shotList = normalizeShotList( rawShotList({