primocode 8.26.0 → 8.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.26.0",
3
+ "version": "8.27.0",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/primocode.js",
11
- "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js && node test/vocabulario.test.js && node test/video-export.test.js && node test/monotonia.test.js && node test/clipe.test.js && node test/midia-envio.test.js && node test/pausas.test.js && node test/app.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js && node test/vocabulario.test.js && node test/video-export.test.js && node test/monotonia.test.js && node test/clipe.test.js && node test/midia-envio.test.js && node test/pausas.test.js && node test/app.test.js && node test/elementos.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"
@@ -57,7 +57,8 @@ Em dúvida entre duas, escolha a que o usuário vai **mostrar para outra pessoa*
57
57
  - **Ritmo.** Em decks longos, um `layout: "secao"` a cada 3–5 cenas.
58
58
  - **Escolha o tema pelo assunto:** `meia-noite` produto e tecnologia · `claro`
59
59
  corporativo e projetor · `papel` editorial e longo · `neon` técnico e dev ·
60
- `brasa` energia e urgência · `floresta` sustentabilidade e saúde.
60
+ `brasa` energia e urgência · `floresta` sustentabilidade e saúde ·
61
+ `ambar` curso, aula e conteúdo longo · `grafite` técnico sóbrio e relatório.
61
62
  - **Notas do apresentador** (`notas` na cena) em quem vai falar por cima.
62
63
  - **Nunca invente dado com cara de real.** Sem fonte, escreva a ordem de grandeza
63
64
  ou deixe o campo como pergunta explícita ao usuário.
@@ -134,9 +134,10 @@ Sem declarar nada, os elementos já entram em cascata na ordem em que aparecem.
134
134
 
135
135
  ### Temas
136
136
 
137
- `meia-noite` `claro` `papel` `neon` `brasa` `floresta` `retro` `oceano` `doce` `mono`
137
+ `meia-noite` `claro` `papel` `neon` `brasa` `floresta` `retro` `oceano` `doce`
138
+ `mono` `ambar` `grafite`
138
139
 
139
- São esses dez, e só. Nome fora da lista é recusado: escuro é `meia-noite`, não
140
+ São esses doze, e só. Nome fora da lista é recusado: escuro é `meia-noite`, não
140
141
  `noite`. Vale no documento ou por cena.
141
142
 
142
143
  ### Identidade — o que separa um vídeo genérico de um da marca
@@ -89,6 +89,8 @@ cena. Se o assunto não empurrar para nenhuma, escolha a que você NÃO usou da
89
89
  | explicação, tutorial, passo a passo | `papel` ou `claro` | `lofi` | **Aula** |
90
90
  | conquista, agradecimento, celebração | `doce` ou `neon` | `alegre` | **Brinde** |
91
91
  | memória, história, retrospectiva | `retro` ou `mono` | `lofi` | **Linha** |
92
+ | curso, aula, treinamento, método | `ambar` | `lofi` | **Aula** |
93
+ | relatório, número, análise técnica | `grafite` | `ambiente` | **Manchete** |
92
94
 
93
95
  ### As seis estruturas
94
96
 
@@ -178,6 +180,85 @@ livre de direitos, ou uma das trilhas geradas acima.
178
180
 
179
181
  ---
180
182
 
183
+ ## Elementos que dão cara de produção
184
+
185
+ Estes seis existem porque um vídeo genérico é feito só de título + texto. São
186
+ baratos de escrever e mudam completamente o resultado.
187
+
188
+ ### `selo` — a pílula de estado
189
+
190
+ ```json
191
+ { "tipo": "selo", "texto": "Módulo 2 de 5" }
192
+ { "tipo": "selo", "texto": "Resultado 4", "icone": "★", "estilo": "vivo" }
193
+ ```
194
+
195
+ `estilo`: sem nada (contorno), `"vivo"` (preenchido) ou `"solto"` (só o texto
196
+ em versalete). Vai sozinho para cima do título.
197
+
198
+ ### `progresso` — onde estamos no caminho
199
+
200
+ ```json
201
+ { "tipo": "progresso", "atual": 4, "total": 15 }
202
+ ```
203
+
204
+ Mostra "4/15" e os pontinhos. Use em série de cenas: diz ao espectador que
205
+ existe um percurso, e isso segura ele até o fim. `"numero": false` deixa só
206
+ os pontos.
207
+
208
+ ### `avatar` — a pessoa
209
+
210
+ ```json
211
+ { "tipo": "avatar", "src": "/midia/foto.jpg", "nome": "Cleiton Paris", "tamanho": 280 }
212
+ ```
213
+
214
+ Círculo com anel aceso. Sem `src`, mostra as iniciais do `nome` — melhor que
215
+ um buraco cinza. Vai para cima do nome sozinho.
216
+
217
+ ### `dispositivo` — celular, notebook ou navegador com conteúdo dentro
218
+
219
+ ```json
220
+ { "tipo": "dispositivo", "forma": "celular", "dentro": [ ...elementos... ] }
221
+ ```
222
+
223
+ `forma`: `celular` | `notebook` | `tablet` | `navegador` (este aceita
224
+ `"endereco": "primocode.dev"`). `dentro` é uma lista de elementos comuns — o
225
+ que couber. Nos layouts `imagem-direita` / `imagem-esquerda` o aparelho vai
226
+ para o lado sozinho, com o texto do outro. É o enquadramento mais forte que
227
+ existe aqui: use quando estiver mostrando uma tela.
228
+
229
+ ### `conversa` — balões de pergunta e resposta
230
+
231
+ ```json
232
+ { "tipo": "conversa", "itens": [
233
+ { "de": "eu", "texto": "Como acelero meu workflow?" },
234
+ { "de": "ela", "texto": "Peça o storyboard em seis quadros." } ] }
235
+ ```
236
+
237
+ Entram um de cada vez, como diálogo. `de: "eu"` à esquerda; qualquer outra
238
+ coisa à direita, com a cor do tema. Combina com `dispositivo` — a conversa
239
+ dentro do celular é a cena mais usada em vídeo de IA.
240
+
241
+ ### `passos` e `opcoes`
242
+
243
+ ```json
244
+ { "tipo": "passos", "atual": 2, "itens": ["Passo 1|roteiro", "Passo 2|storyboard", "Passo 3|corte"] }
245
+ { "tipo": "opcoes", "pergunta": "O que garante o look?", "certa": 1,
246
+ "itens": ["LUT uniforme", "Saturação aleatória", "Espaços de cor diferentes"] }
247
+ ```
248
+
249
+ `passos` acende a etapa `atual` e apaga as outras — mude o `atual` a cada
250
+ cena e a série ganha movimento sem esforço. `opcoes` marca a alternativa
251
+ `certa` com ✓ (não escreva o ✓ no texto: ele vem sozinho). Alternativa faz o
252
+ espectador pensar antes da resposta, e pensar é o que segura alguém no vídeo.
253
+
254
+ ### Vidro
255
+
256
+ `"estilo": "vidro"` em `cartoes` dá painel translúcido com bisel de luz em
257
+ cima e sombra embaixo — o "liquid glass". Funciona em tema escuro; em tema
258
+ claro use os cartões normais.
259
+
260
+ ---
261
+
181
262
  ## Editar o vídeo do usuário (`clipe`)
182
263
 
183
264
  Quando a pessoa manda uma gravação dela — o rosto falando, uma tela gravada,
package/studio/server.py CHANGED
@@ -784,7 +784,7 @@ def gravar_analise(ident, dados):
784
784
  # "Ele não alterna entre os temas, usa um tema só pra todos os vídeos, e o
785
785
  # vídeo sempre tem a mesma estrutura. Fica tudo genérico."
786
786
  #
787
- # O estúdio tem dez temas, onze fundos, oito transições e seis trilhas. O
787
+ # O estúdio tem doze temas, onze fundos, oito transições e seis trilhas. O
788
788
  # agente usa uma fatia estreita e repete — porque a spec tem UM exemplo
789
789
  # completo e modelo ancora em exemplo. Conselho escrito na spec ajuda pouco
790
790
  # quando ela nem sempre é relida.
@@ -922,7 +922,7 @@ def _texto_do_doc(doc):
922
922
  # Os temas que o render.css de fato define. Fora desta lista não existe cor
923
923
  # nenhuma — foi o que aconteceu com "noite", que passou como se fosse tema.
924
924
  TEMAS_VALIDOS = {"claro", "meia-noite", "papel", "neon", "brasa", "floresta",
925
- "retro", "oceano", "doce", "mono"}
925
+ "retro", "oceano", "doce", "mono", "ambar", "grafite"}
926
926
 
927
927
 
928
928
  def _tema_invalido(tema, onde=""):
@@ -989,7 +989,13 @@ def validar_doc(modelo, doc):
989
989
  # preto": documento válido na forma, invisível na tela.
990
990
  TIPOS_VALIDOS = {"texto", "lista", "cartoes", "numeros", "tabela", "codigo",
991
991
  "citacao", "forma", "imagem", "icone", "grafico", "no", "conector",
992
- "objeto3d", "clipe"}
992
+ "objeto3d", "clipe",
993
+ "selo", "progresso", "avatar", "dispositivo", "conversa",
994
+ "passos", "opcoes"}
995
+ # Os sete últimos são o vocabulário do vídeo de referência. O comentário
996
+ # mora aqui fora porque test/vocabulario.test.js lê esta lista com
997
+ # expressão regular, comparando com a do render.js — palavra de comentário
998
+ # dentro das chaves entrava na lista como se fosse um tipo válido.
993
999
 
994
1000
  # Tema inventado tinha o mesmo destino silencioso do tipo inventado: o CSS
995
1001
  # não casa com [data-tema="noite"], nenhuma cor é definida e a peça sai
@@ -2,10 +2,10 @@
2
2
  ferramentas.js — o que cada ferramenta oferece na interface.
3
3
  ========================================================================== */
4
4
 
5
- // Os dez que o render.css define. A lista tinha seis: quatro temas existiam
5
+ // Os doze que o render.css define. A lista tinha seis: quatro temas existiam
6
6
  // no CSS, a IA podia usá-los, e o editor não os oferecia a ninguém.
7
7
  var TEMAS = ["meia-noite", "claro", "papel", "neon", "brasa", "floresta",
8
- "retro", "oceano", "doce", "mono"];
8
+ "retro", "oceano", "doce", "mono", "ambar", "grafite"];
9
9
 
10
10
  var PALETA = ["#ff5a36", "#ffb03a", "#f4e04d", "#2fbf7f", "#1f8fe0", "#8b5cf6",
11
11
  "#f472b6", "#ffffff", "#9aa4b2", "#111318"];
@@ -162,7 +162,16 @@ function svgGrafico(e, l, a) {
162
162
 
163
163
  var TIPOS_CONHECIDOS = { texto: 1, lista: 1, cartoes: 1, numeros: 1, tabela: 1, codigo: 1,
164
164
  citacao: 1, forma: 1, imagem: 1, icone: 1, grafico: 1, no: 1, conector: 1,
165
- objeto3d: 1, clipe: 1 };
165
+ objeto3d: 1, clipe: 1,
166
+ selo: 1, progresso: 1, avatar: 1, dispositivo: 1, conversa: 1, passos: 1,
167
+ opcoes: 1 };
168
+ /* Os sete últimos são o vocabulário do vídeo de referência: pílula de estado,
169
+ trilha de progresso, foto de pessoa, celular/notebook com conteúdo dentro,
170
+ balões de conversa, etapas com uma acesa, e pergunta com alternativas.
171
+
172
+ O comentário mora AQUI FORA porque test/vocabulario.test.js lê esta lista
173
+ com expressão regular, comparando com a do servidor — e palavra de
174
+ comentário dentro das chaves entrava na lista como se fosse um tipo. */
166
175
 
167
176
  /* ── chaves de animação ──────────────────────────────────────────────────
168
177
  Transforma a lista de instantes de um elemento numa regra @keyframes de
@@ -528,6 +537,192 @@ function renderElemento(e, ctx) {
528
537
  break;
529
538
  }
530
539
 
540
+ /* ── selo ────────────────────────────────────────────────────────────
541
+ A pílula pequena, em versalete, que diz onde a cena está: "AVANÇADO",
542
+ "MÓDULO 2 DE 5", "RESULTADO 4". Custa quase nada e é o que separa uma
543
+ cena que parece produzida de uma cena que parece um slide.
544
+
545
+ Antes disso a saída era um `texto` com papel "chapeu", que é só letra
546
+ menor — sem a pílula, sem a marcação de estado. */
547
+ case "selo": {
548
+ no.textContent = "";
549
+ if (e.icone) {
550
+ var ic = document.createElement("span");
551
+ ic.className = "selo-icone";
552
+ ic.textContent = e.icone;
553
+ no.appendChild(ic);
554
+ }
555
+ var rot = document.createElement("span");
556
+ rot.textContent = e.texto || e.rotulo || "";
557
+ no.appendChild(rot);
558
+ if (e.estilo) no.setAttribute("data-estilo", e.estilo); // "vivo" | "solto"
559
+ break;
560
+ }
561
+
562
+ /* ── progresso ───────────────────────────────────────────────────────
563
+ "4 de 15", com os pontinhos. Diz ao espectador que existe um caminho e
564
+ onde ele está nele — é o que faz um vídeo de dez cenas não parecer dez
565
+ cenas soltas. */
566
+ case "progresso": {
567
+ var total = Math.max(1, Math.min(40, Number(e.total) || 1));
568
+ var atual = Math.max(1, Math.min(total, Number(e.atual) || 1));
569
+ if (e.numero !== false) {
570
+ var num = document.createElement("b");
571
+ num.textContent = atual + "/" + total;
572
+ no.appendChild(num);
573
+ }
574
+ var pontos = document.createElement("span");
575
+ pontos.className = "pontos";
576
+ for (var pi = 1; pi <= total; pi++) {
577
+ var pt = document.createElement("i");
578
+ if (pi === atual) pt.className = "aqui";
579
+ else if (pi < atual) pt.className = "feito";
580
+ pontos.appendChild(pt);
581
+ }
582
+ no.appendChild(pontos);
583
+ break;
584
+ }
585
+
586
+ /* ── avatar ──────────────────────────────────────────────────────────
587
+ A foto redonda com o anel aceso. É como se apresenta uma PESSOA — e
588
+ vídeo com rosto retém mais que vídeo sem. Antes só existia `imagem`,
589
+ que é retângulo. */
590
+ case "avatar": {
591
+ var d = e.tamanho || 260;
592
+ no.style.width = _px(d);
593
+ no.style.height = _px(d);
594
+ if (e.src) {
595
+ var foto = document.createElement("img");
596
+ foto.src = e.src;
597
+ foto.alt = e.nome || "";
598
+ no.appendChild(foto);
599
+ } else {
600
+ // Sem foto, as iniciais — melhor que um buraco cinza.
601
+ var ini = document.createElement("b");
602
+ // Em px, calculado do diâmetro. Em `%` o navegador mede contra a
603
+ // fonte do PAI, não contra a caixa: as iniciais saíam com 8px dentro
604
+ // de um círculo de 280 — um pontinho no meio do nada.
605
+ ini.style.fontSize = _px(Math.round(d * 0.34));
606
+ ini.textContent = String(e.nome || "?").trim().split(/\s+/)
607
+ .slice(0, 2).map(function (w) { return w[0]; }).join("").toUpperCase();
608
+ no.appendChild(ini);
609
+ }
610
+ break;
611
+ }
612
+
613
+ /* ── dispositivo ─────────────────────────────────────────────────────
614
+ O celular, o notebook ou a janela de navegador com conteúdo DENTRO.
615
+ É o enquadramento que faz uma tela parecer uma tela em vez de um
616
+ retângulo de texto — e o vídeo de referência usa isso o tempo todo,
617
+ com a conversa acontecendo dentro do celular.
618
+
619
+ O conteúdo é `dentro`: uma lista de elementos comuns, renderizados
620
+ recursivamente. Assim qualquer coisa cabe ali — conversa, lista,
621
+ imagem — sem inventar um vocabulário só para o miolo. */
622
+ case "dispositivo": {
623
+ var forma = /^(celular|notebook|navegador|tablet)$/.test(e.forma || "")
624
+ ? e.forma : "celular";
625
+ no.setAttribute("data-forma", forma);
626
+ if (e.altura) no.style.height = _px(e.altura);
627
+ if (e.largura) no.style.width = _px(e.largura);
628
+
629
+ var moldura = document.createElement("div");
630
+ moldura.className = "moldura";
631
+
632
+ if (forma === "navegador") {
633
+ var barra = document.createElement("div");
634
+ barra.className = "barra-nav";
635
+ barra.innerHTML = "<i></i><i></i><i></i>"
636
+ + (e.endereco ? '<span class="url">' + _fugir(e.endereco) + "</span>" : "");
637
+ moldura.appendChild(barra);
638
+ }
639
+
640
+ var tela = document.createElement("div");
641
+ tela.className = "tela";
642
+ if (e.imagem) {
643
+ tela.style.backgroundImage = "url('" + e.imagem + "')";
644
+ tela.style.backgroundSize = "cover";
645
+ tela.style.backgroundPosition = "center";
646
+ }
647
+ (e.dentro || []).forEach(function (filho) {
648
+ if (!filho || !TIPOS_CONHECIDOS[filho.tipo]) return;
649
+ ctx.i++;
650
+ tela.appendChild(renderElemento(filho, ctx));
651
+ });
652
+ moldura.appendChild(tela);
653
+ no.appendChild(moldura);
654
+
655
+ if (forma === "notebook") {
656
+ var base = document.createElement("div");
657
+ base.className = "base";
658
+ no.appendChild(base);
659
+ }
660
+ break;
661
+ }
662
+
663
+ /* ── conversa ────────────────────────────────────────────────────────
664
+ Balões de pergunta e resposta. É a forma mais direta de mostrar como
665
+ se usa uma ferramenta de IA — e é o que o vídeo de referência faz
666
+ dentro do celular. `de: "eu"` alinha à direita; qualquer outra coisa
667
+ vai para a esquerda, com destaque. */
668
+ case "conversa": {
669
+ (e.itens || []).forEach(function (fala, iFala) {
670
+ var b = document.createElement("div");
671
+ var quem = typeof fala === "string" ? (iFala % 2 ? "ela" : "eu") : (fala.de || "eu");
672
+ b.className = "balao " + (quem === "eu" ? "meu" : "dela");
673
+ b.style.setProperty("--atraso", (0.12 + iFala * 0.28).toFixed(2));
674
+ b.textContent = typeof fala === "string" ? fala : (fala.texto || "");
675
+ no.appendChild(b);
676
+ });
677
+ break;
678
+ }
679
+
680
+ /* ── passos ──────────────────────────────────────────────────────────
681
+ Os cartões de etapa com UM aceso. Diferente de `cartoes`: aqui existe
682
+ ordem e existe "onde estamos agora", e é isso que se quer mostrar. */
683
+ case "passos": {
684
+ var itens = e.itens || [];
685
+ var vivo = Number(e.atual) || 0; // 1-based; 0 = nenhum
686
+ no.style.gridTemplateColumns = "repeat(" + Math.max(1, itens.length) + ", 1fr)";
687
+ itens.forEach(function (it, k) {
688
+ var p = document.createElement("div");
689
+ p.className = "passo" + (k + 1 === vivo ? " agora" : (k + 1 < vivo ? " feito" : ""));
690
+ var partes = typeof it === "string" ? it.split("|") : [it.titulo, it.texto];
691
+ var cab = document.createElement("b");
692
+ cab.textContent = (partes[0] || "").trim();
693
+ p.appendChild(cab);
694
+ if (partes[1]) {
695
+ var sub = document.createElement("span");
696
+ sub.textContent = String(partes[1]).trim();
697
+ p.appendChild(sub);
698
+ }
699
+ no.appendChild(p);
700
+ });
701
+ break;
702
+ }
703
+
704
+ /* ── opcoes ──────────────────────────────────────────────────────────
705
+ A pergunta com alternativas e uma certa. Faz o espectador PENSAR antes
706
+ de a resposta aparecer — e uma pausa de pensamento é o que segura
707
+ alguém num vídeo. `certa` é o índice (1-based) da alternativa boa. */
708
+ case "opcoes": {
709
+ if (e.pergunta) {
710
+ var q = document.createElement("b");
711
+ q.className = "pergunta";
712
+ q.textContent = e.pergunta;
713
+ no.appendChild(q);
714
+ }
715
+ var certa = Number(e.certa) || 0;
716
+ (e.itens || []).forEach(function (it, k) {
717
+ var o = document.createElement("div");
718
+ o.className = "opcao" + (k + 1 === certa ? " certa" : "");
719
+ o.textContent = typeof it === "string" ? it : (it.texto || "");
720
+ o.style.setProperty("--atraso", (0.1 + k * 0.18).toFixed(2));
721
+ no.appendChild(o);
722
+ });
723
+ break;
724
+ }
725
+
531
726
  case "numeros": {
532
727
  var m = (e.itens || []).length;
533
728
  no.style.gridTemplateColumns = "repeat(" + Math.min(m || 1, e.colunas || m) + ", 1fr)";
@@ -975,9 +1170,21 @@ function renderCena(cena, doc, op) {
975
1170
  elementos.forEach(function (e) {
976
1171
  if (e.tipo === "conector") return;
977
1172
  if (e.x != null || e.y != null) { flutuantes.push(e); return; }
978
- if ((layout === "imagem-direita" || layout === "imagem-esquerda") && e.tipo === "imagem" && !imagem) { imagem = e; return; }
979
- if (e.papel === "chapeu" || e.papel === "titulo") cabecalho.push(e);
980
- else corpo.push(e);
1173
+ /* O aparelho também vai para o lado, não só a imagem. É o enquadramento
1174
+ do vídeo de referência: texto de um lado, celular com a conversa do
1175
+ outro. Sem isto o celular caía na coluna de texto e o outro lado ficava
1176
+ vazio — metade do quadro em branco. */
1177
+ if ((layout === "imagem-direita" || layout === "imagem-esquerda")
1178
+ && (e.tipo === "imagem" || e.tipo === "dispositivo") && !imagem) { imagem = e; return; }
1179
+ /* Selo, progresso e foto são material de CABEÇALHO: vêm antes do título,
1180
+ como sobrancelha da cena. Caindo no corpo, apareciam DEPOIS do título —
1181
+ a pílula "Módulo 2 de 5" embaixo do nome do módulo, e o rosto da pessoa
1182
+ embaixo do nome dela. Dentro do grupo a ordem é a do documento, então
1183
+ quem escreveu decide o resto. */
1184
+ if (e.papel === "chapeu" || e.papel === "titulo"
1185
+ || e.tipo === "selo" || e.tipo === "progresso" || e.tipo === "avatar") {
1186
+ cabecalho.push(e);
1187
+ } else corpo.push(e);
981
1188
  });
982
1189
 
983
1190
  function solta(lista, alvo) {
@@ -992,7 +1199,14 @@ function renderCena(cena, doc, op) {
992
1199
  solta(cabecalho, lado); solta(corpo, lado);
993
1200
  var box = document.createElement("div");
994
1201
  box.className = "lado-imagem";
995
- if (imagem) {
1202
+ if (imagem && imagem.tipo === "dispositivo") {
1203
+ // O aparelho tem moldura e sombra próprias: encostar nas bordas do
1204
+ // quadro comeria as duas. Ele entra com respiro.
1205
+ box.classList.add("com-aparelho");
1206
+ box.style.padding = margem + "px " + Math.round(W * 0.04) + "px";
1207
+ ctx.i++;
1208
+ box.appendChild(renderElemento(imagem, ctx));
1209
+ } else if (imagem) {
996
1210
  var im = renderElemento({ tipo: "imagem", src: imagem.src, ajuste: imagem.ajuste, raio: 0 }, ctx);
997
1211
  im.style.position = "absolute";
998
1212
  im.style.inset = "0";
@@ -65,6 +65,23 @@
65
65
  --c-fundo: #ffffff; --c-fundo2: #eeeeee; --c-texto: #000000; --c-fraco: #666666;
66
66
  --c-tom: #000000; --c-tom2: #000000; --c-linha: #000000; --c-cartao: #f2f2f2;
67
67
  }
68
+ /* "âmbar" — o marrom quente com dourado, que é a cara de curso e de aula.
69
+ Entrou porque faltava: os dez temas tinham roxo, ciano, laranja de fogo e
70
+ verde, e nenhum tom terroso onde o texto branco descansa. É o tema que
71
+ sustenta cena longa sem cansar a vista. */
72
+ [data-tema="ambar"] {
73
+ --f-display: "Primo Geo", "Space Grotesk", system-ui, sans-serif;
74
+ --c-fundo: #1a100b; --c-fundo2: #2a1710; --c-texto: #f7efe8; --c-fraco: #b8a08f;
75
+ --c-tom: #e8a33d; --c-tom2: #d0703a; --c-linha: rgba(232, 163, 61, .22); --c-cartao: rgba(232, 163, 61, .07);
76
+ }
77
+ /* "grafite" — o cinza-azulado sóbrio, para conteúdo técnico e relatório.
78
+ Sem cor de festa: quando o assunto é número, a cor não pode competir. */
79
+ [data-tema="grafite"] {
80
+ --f-display: "Primo Geo", "Space Grotesk", system-ui, sans-serif;
81
+ --c-fundo: #0d1117; --c-fundo2: #161d26; --c-texto: #e6edf3; --c-fraco: #8b97a6;
82
+ --c-tom: #4a9eff; --c-tom2: #7ee0c0; --c-linha: rgba(255, 255, 255, .1); --c-cartao: rgba(255, 255, 255, .04);
83
+ }
84
+
68
85
  [data-tema="mono"] .el.no,
69
86
  [data-tema="mono"] .el.cartoes .cartao { border-width: 3px; border-radius: 0; }
70
87
  [data-tema="mono"] .el.txt { letter-spacing: -.04em; }
@@ -382,6 +399,259 @@
382
399
  .el.cartoes .cartao b { font-family: var(--f-display); font-size: 27px; font-weight: 620; letter-spacing: -.02em; }
383
400
  .el.cartoes .cartao span { font-size: 19px; line-height: 1.45; color: var(--c-fraco); }
384
401
 
402
+ /* ═══════════════════════════════════════════════════════════════════════
403
+ O vocabulário do vídeo de referência.
404
+
405
+ Tudo aqui existe porque o dono mandou um vídeo e disse "pega tudo que tem
406
+ esse vídeo, e coloca pro Primo poder fazer também" — cartão com relevo,
407
+ vidro, foto, celular com conversa dentro.
408
+
409
+ O fio comum é o VIDRO: painel translúcido, borda clara em cima (onde a luz
410
+ bateria) e sombra embaixo. Não é enfeite — é o que dá profundidade sem
411
+ precisar de imagem, e o que separa "cartão" de "retângulo com borda".
412
+ ═══════════════════════════════════════════════════════════════════════ */
413
+
414
+ .vidro, .el.cartoes[data-estilo="vidro"] .cartao, .el.passos .passo {
415
+ background:
416
+ linear-gradient(180deg, rgba(255, 255, 255, .075), rgba(255, 255, 255, .015)),
417
+ var(--c-cartao);
418
+ border: 1px solid var(--c-linha);
419
+ /* A luz de cima e a sombra de baixo, dentro da própria caixa. É o truque
420
+ inteiro do "liquid glass": a borda superior clara finge um bisel. */
421
+ box-shadow:
422
+ inset 0 1px 0 rgba(255, 255, 255, .13),
423
+ 0 18px 44px rgba(0, 0, 0, .34);
424
+ }
425
+
426
+ /* ── selo ──────────────────────────────────────────────────────────────── */
427
+ .el.selo {
428
+ display: inline-flex; align-items: center; gap: .5em;
429
+ align-self: flex-start;
430
+ font-family: var(--f-corpo);
431
+ font-size: 21px; font-weight: 620;
432
+ letter-spacing: .13em; text-transform: uppercase;
433
+ color: var(--c-tom);
434
+ background: color-mix(in srgb, var(--c-tom) 13%, transparent);
435
+ border: 1px solid color-mix(in srgb, var(--c-tom) 42%, transparent);
436
+ border-radius: 999px;
437
+ padding: .42em 1.05em;
438
+ }
439
+ .el.selo[data-estilo="vivo"] {
440
+ color: var(--c-fundo);
441
+ background: var(--c-tom);
442
+ border-color: var(--c-tom);
443
+ }
444
+ .el.selo[data-estilo="solto"] {
445
+ background: none; border-color: transparent; padding-left: 0; padding-right: 0;
446
+ }
447
+ .el.selo .selo-icone { font-size: 1.15em; letter-spacing: 0; }
448
+ .cena .fluxo[data-layout="capa"] > .el.selo,
449
+ .cena .fluxo[data-layout="centro"] > .el.selo { align-self: center; }
450
+
451
+ /* ── progresso ─────────────────────────────────────────────────────────── */
452
+ .el.progresso {
453
+ display: flex; align-items: center; gap: 18px;
454
+ color: var(--c-tom); font-family: var(--f-corpo);
455
+ }
456
+ .el.progresso > b { font-size: 24px; font-weight: 620; letter-spacing: .04em; }
457
+ .el.progresso .pontos { display: flex; align-items: center; gap: 9px; }
458
+ .el.progresso .pontos i {
459
+ width: 9px; height: 9px; border-radius: 50%;
460
+ background: var(--c-linha);
461
+ transition: none;
462
+ }
463
+ .el.progresso .pontos i.feito { background: color-mix(in srgb, var(--c-tom) 45%, transparent); }
464
+ .el.progresso .pontos i.aqui {
465
+ background: var(--c-tom); width: 12px; height: 12px;
466
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--c-tom) 18%, transparent);
467
+ }
468
+
469
+ /* ── avatar ────────────────────────────────────────────────────────────── */
470
+ .el.avatar {
471
+ border-radius: 50%; overflow: hidden; position: relative;
472
+ display: grid; place-items: center; flex: 0 0 auto;
473
+ background: var(--c-cartao);
474
+ /* O anel aceso: dois anéis, um forte colado e um aberto e fraco. Um só
475
+ fica duro; dois viram luz. */
476
+ box-shadow:
477
+ 0 0 0 3px var(--c-tom),
478
+ 0 0 0 12px color-mix(in srgb, var(--c-tom) 16%, transparent),
479
+ 0 22px 50px rgba(0, 0, 0, .42);
480
+ }
481
+ .el.avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
482
+ .el.avatar b {
483
+ font-family: var(--f-display); font-weight: 640;
484
+ color: var(--c-tom); letter-spacing: .02em;
485
+ }
486
+ .cena .fluxo[data-layout="capa"] > .el.avatar,
487
+ .cena .fluxo[data-layout="centro"] > .el.avatar { align-self: center; }
488
+
489
+ /* ── dispositivo ───────────────────────────────────────────────────────── */
490
+ /* O aparelho ENCOLHE para caber na cena. Preso só pela largura, o celular de
491
+ 380px virava 800px de altura e vazava pelo pé do quadro — metade da
492
+ conversa ficava fora do vídeo, e na tela do editor parecia certo porque ali
493
+ a página rola. A altura é que manda; a largura vem da proporção. */
494
+ .el.dispositivo {
495
+ display: grid; align-content: center; justify-items: center;
496
+ flex: 0 1 auto; min-height: 0; max-height: 100%;
497
+ }
498
+ .el.dispositivo .moldura {
499
+ background: linear-gradient(180deg, #22262c, #14171b);
500
+ box-shadow:
501
+ inset 0 1px 0 rgba(255, 255, 255, .16),
502
+ 0 30px 70px rgba(0, 0, 0, .5);
503
+ overflow: hidden;
504
+ display: flex; flex-direction: column;
505
+ }
506
+ .el.dispositivo .tela {
507
+ flex: 1; overflow: hidden;
508
+ background: var(--c-fundo2);
509
+ display: flex; flex-direction: column; justify-content: center;
510
+ gap: 16px;
511
+ }
512
+
513
+ .el.dispositivo[data-forma="celular"] { width: 100%; }
514
+ .el.dispositivo[data-forma="celular"] .moldura {
515
+ height: min(100%, 780px); width: auto; max-width: 100%;
516
+ aspect-ratio: 9 / 19;
517
+ border-radius: 42px; padding: 12px;
518
+ }
519
+ .el.dispositivo[data-forma="celular"] .tela { border-radius: 31px; padding: 26px 20px; }
520
+ /* A ilha da câmera. Sem ela um retângulo arredondado é só um retângulo
521
+ arredondado — é este detalhe que faz o olho ler "celular". */
522
+ .el.dispositivo[data-forma="celular"] .moldura::before {
523
+ content: ""; position: absolute; left: 50%; top: 20px;
524
+ width: 88px; height: 9px; border-radius: 999px;
525
+ background: rgba(0, 0, 0, .55); transform: translateX(-50%); z-index: 2;
526
+ }
527
+ .el.dispositivo[data-forma="celular"] .moldura { position: relative; }
528
+
529
+ .el.dispositivo[data-forma="tablet"] .moldura {
530
+ height: min(100%, 740px); width: auto; max-width: 100%;
531
+ aspect-ratio: 3 / 4;
532
+ border-radius: 26px; padding: 16px;
533
+ }
534
+ .el.dispositivo[data-forma="tablet"] .tela { border-radius: 12px; padding: 26px; }
535
+
536
+ .el.dispositivo[data-forma="notebook"] { width: 100%; }
537
+ .el.dispositivo[data-forma="notebook"] .moldura {
538
+ width: 100%; max-height: 100%; aspect-ratio: 16 / 10;
539
+ border-radius: 16px 16px 4px 4px; padding: 12px 12px 14px;
540
+ }
541
+ .el.dispositivo[data-forma="notebook"] .tela { border-radius: 7px; padding: 26px 28px; }
542
+ .el.dispositivo[data-forma="notebook"] .base {
543
+ width: 116%; height: 13px; margin-top: -1px;
544
+ border-radius: 0 0 12px 12px;
545
+ background: linear-gradient(180deg, #2c3138, #171a1e);
546
+ box-shadow: 0 12px 26px rgba(0, 0, 0, .45);
547
+ }
548
+
549
+ .el.dispositivo[data-forma="navegador"] { width: 100%; }
550
+ .el.dispositivo[data-forma="navegador"] .moldura {
551
+ width: 100%; max-height: 100%; aspect-ratio: 16 / 10; border-radius: 14px; padding: 0;
552
+ }
553
+ .el.dispositivo .barra-nav {
554
+ display: flex; align-items: center; gap: 8px;
555
+ padding: 13px 16px; flex: 0 0 auto;
556
+ border-bottom: 1px solid rgba(255, 255, 255, .07);
557
+ }
558
+ .el.dispositivo .barra-nav i {
559
+ width: 11px; height: 11px; border-radius: 50%; background: rgba(255, 255, 255, .17);
560
+ }
561
+ .el.dispositivo .barra-nav .url {
562
+ margin-left: 12px; flex: 1;
563
+ font-family: var(--f-mono); font-size: 15px; color: rgba(255, 255, 255, .45);
564
+ background: rgba(0, 0, 0, .3); border-radius: 999px; padding: 5px 14px;
565
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
566
+ }
567
+ .el.dispositivo[data-forma="navegador"] .tela { padding: 24px 26px; }
568
+
569
+ /* ── conversa ──────────────────────────────────────────────────────────── */
570
+ .el.conversa {
571
+ display: flex; flex-direction: column; gap: 14px; justify-content: flex-end;
572
+ }
573
+ .el.conversa .balao {
574
+ max-width: 88%; padding: 13px 17px; line-height: 1.42;
575
+ font-size: 20px; border-radius: 16px;
576
+ }
577
+ .el.conversa .balao.meu {
578
+ align-self: flex-start;
579
+ background: var(--c-cartao); border: 1px solid var(--c-linha);
580
+ color: var(--c-texto); border-bottom-left-radius: 5px;
581
+ }
582
+ /* A resposta é o que importa: ela leva a cor do tema. */
583
+ .el.conversa .balao.dela {
584
+ align-self: flex-end;
585
+ background: color-mix(in srgb, var(--c-tom) 20%, transparent);
586
+ border: 1px solid color-mix(in srgb, var(--c-tom) 40%, transparent);
587
+ color: var(--c-texto); border-bottom-right-radius: 5px;
588
+ }
589
+ /* Os balões entram um de cada vez, como numa conversa de verdade. Todos ao
590
+ mesmo tempo lê como captura de tela, não como diálogo. */
591
+ .cena.animar .el.conversa .balao {
592
+ animation: balaoEntra .46s cubic-bezier(.22, 1, .36, 1) both;
593
+ animation-delay: calc(var(--atraso, 0) * 1s);
594
+ }
595
+ @keyframes balaoEntra {
596
+ from { opacity: 0; transform: translateY(14px) scale(.96); }
597
+ to { opacity: 1; transform: none; }
598
+ }
599
+
600
+ /* ── passos ────────────────────────────────────────────────────────────── */
601
+ /* Sem largura declarada, o grid encolhe até o conteúdo e as três etapas
602
+ viram três selinhos no meio da tela — em `capa` e `centro`, onde o eixo
603
+ está centralizado, isso é o padrão. Etapa é estrutura: ocupa a linha. */
604
+ .el.passos { display: grid; gap: 18px; align-items: stretch; width: 100%; }
605
+ .el.passos .passo {
606
+ border-radius: 18px; padding: 22px 20px;
607
+ display: grid; gap: 6px; align-content: start;
608
+ color: var(--c-fraco);
609
+ }
610
+ .el.passos .passo b {
611
+ font-family: var(--f-display); font-size: 23px; font-weight: 620;
612
+ letter-spacing: -.015em; color: inherit;
613
+ }
614
+ .el.passos .passo span { font-size: 17px; line-height: 1.4; }
615
+ .el.passos .passo.feito { color: var(--c-texto); }
616
+ /* O de agora cresce e acende. Escala pequena de propósito: 1.06 lê como
617
+ "este", 1.2 lê como erro de layout. */
618
+ .el.passos .passo.agora {
619
+ color: var(--c-texto);
620
+ transform: scale(1.06);
621
+ border-color: color-mix(in srgb, var(--c-tom) 55%, transparent);
622
+ background:
623
+ linear-gradient(180deg, color-mix(in srgb, var(--c-tom) 17%, transparent),
624
+ color-mix(in srgb, var(--c-tom) 6%, transparent));
625
+ box-shadow:
626
+ inset 0 1px 0 rgba(255, 255, 255, .16),
627
+ 0 20px 46px color-mix(in srgb, var(--c-tom) 16%, transparent);
628
+ }
629
+ .el.passos .passo.agora b { color: var(--c-tom); }
630
+
631
+ /* ── opcoes ────────────────────────────────────────────────────────────── */
632
+ .el.opcoes { display: grid; gap: 12px; align-content: center; }
633
+ .el.opcoes .pergunta {
634
+ font-family: var(--f-display); font-size: 26px; font-weight: 640;
635
+ letter-spacing: -.018em; margin-bottom: 6px; color: var(--c-texto);
636
+ }
637
+ .el.opcoes .opcao {
638
+ border: 1px solid var(--c-linha); border-radius: 12px;
639
+ padding: 14px 18px; font-size: 19px; color: var(--c-fraco);
640
+ background: var(--c-cartao);
641
+ }
642
+ .el.opcoes .opcao.certa {
643
+ color: var(--c-tom);
644
+ border-color: color-mix(in srgb, var(--c-tom) 60%, transparent);
645
+ background: color-mix(in srgb, var(--c-tom) 12%, transparent);
646
+ }
647
+ /* O ✓ vem do CSS, não do texto: assim o autor escreve só a alternativa e a
648
+ marca nunca aparece por engano na errada. */
649
+ .el.opcoes .opcao.certa::before { content: "✓ "; font-weight: 700; }
650
+ .cena.animar .el.opcoes .opcao {
651
+ animation: balaoEntra .4s cubic-bezier(.22, 1, .36, 1) both;
652
+ animation-delay: calc(var(--atraso, 0) * 1s);
653
+ }
654
+
385
655
  .el.numeros { display: grid; gap: calc(30px * var(--esc, 1)); align-content: center; }
386
656
  .el.numeros .num { display: grid; gap: 6px; }
387
657
  .el.numeros .num b {
@@ -656,3 +926,10 @@
656
926
 
657
927
  /* painel: sem moldura, é uma superfície flutuando em profundidade */
658
928
  .el.obj3d.f-painel .face3d { border: 0; box-shadow: 0 40px 90px -30px rgba(0,0,0,.7); }
929
+
930
+ /* O lado do aparelho não é o lado da imagem: a imagem sangra até a borda, o
931
+ celular precisa de ar em volta para a moldura e a sombra aparecerem. */
932
+ .cena .fluxo .lado-imagem.com-aparelho {
933
+ display: grid; place-items: center; min-height: 0; overflow: visible;
934
+ }
935
+ .cena .fluxo .lado-imagem.com-aparelho > .el { max-height: 100%; }