primocode 9.8.0-beta.4 → 9.8.0-beta.6

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.
Files changed (52) hide show
  1. package/README.md +15 -4
  2. package/app/ansi.js +55 -0
  3. package/app/ide.css +345 -0
  4. package/app/ide.html +183 -0
  5. package/app/ide.js +639 -0
  6. package/bin/primocode.js +315 -83
  7. package/kits/acolhedor/index.html +130 -0
  8. package/kits/acolhedor/script.js +25 -0
  9. package/kits/acolhedor/style.css +146 -0
  10. package/kits/editorial/index.html +115 -0
  11. package/kits/editorial/script.js +19 -0
  12. package/kits/editorial/style.css +112 -0
  13. package/kits/premium/index.html +83 -0
  14. package/kits/premium/script.js +11 -0
  15. package/kits/premium/style.css +87 -0
  16. package/kits/vitrine/index.html +108 -0
  17. package/kits/vitrine/script.js +20 -0
  18. package/kits/vitrine/style.css +117 -0
  19. package/lib/app.js +41 -6
  20. package/lib/browser.js +14 -1
  21. package/lib/checkpoint-git.js +218 -0
  22. package/lib/checkpoint.js +159 -44
  23. package/lib/comandos-extra.js +481 -0
  24. package/lib/config.js +5 -2
  25. package/lib/ide.js +337 -0
  26. package/lib/kits.js +165 -54
  27. package/lib/skills.js +6 -1
  28. package/lib/ui.js +171 -17
  29. package/package.json +2 -2
  30. package/skills/apresentacao-pptx/SKILL.md +52 -0
  31. package/skills/cartaz-de-arte/LICENSE.txt +202 -0
  32. package/skills/cartaz-de-arte/SKILL.md +47 -0
  33. package/skills/criar-skill/LICENSE.txt +202 -0
  34. package/skills/criar-skill/SKILL.md +49 -0
  35. package/skills/depurar-de-verdade/LICENSE.txt +21 -0
  36. package/skills/depurar-de-verdade/SKILL.md +14 -5
  37. package/skills/documento-word/SKILL.md +57 -0
  38. package/skills/interface-revisada/SKILL.md +47 -0
  39. package/skills/me-ensina/LICENSE.txt +21 -0
  40. package/skills/me-ensina/SKILL.md +27 -0
  41. package/skills/me-entrevista/LICENSE.txt +21 -0
  42. package/skills/me-entrevista/SKILL.md +27 -0
  43. package/skills/pdf-bonito/SKILL.md +48 -0
  44. package/skills/planilha-excel/SKILL.md +47 -0
  45. package/skills/react-sem-dor/SKILL.md +48 -0
  46. package/skills/revisao-antes-de-entregar/LICENSE.txt +21 -0
  47. package/skills/revisao-antes-de-entregar/SKILL.md +6 -1
  48. package/skills/temas-prontos/LICENSE.txt +202 -0
  49. package/skills/temas-prontos/SKILL.md +50 -0
  50. package/skills/testar-o-site/LICENSE.txt +202 -0
  51. package/skills/testar-o-site/SKILL.md +46 -0
  52. package/skills/video-no-remotion/SKILL.md +48 -0
@@ -0,0 +1,11 @@
1
+ // Premium: um só movimento, na abertura. O arco sobe devagar quando a página
2
+ // carrega, e nada mais se mexe: animação em toda seção tira o ar de calma.
3
+ (function(){
4
+ var d=document;
5
+ d.documentElement.classList.add('js');
6
+ var arco=d.getElementById('arco');
7
+ if(!arco) return;
8
+ // Dois quadros de espera: garante que o navegador pintou o estado inicial
9
+ // antes de ligar a transição (senão ele pula direto para o fim).
10
+ requestAnimationFrame(function(){ requestAnimationFrame(function(){ arco.classList.add('pronto'); }); });
11
+ })();
@@ -0,0 +1,87 @@
1
+ :root{
2
+ /* ── A MARCA ───────────────────────────────────────────────────────
3
+ Para mudar a cara da página inteira, troque SÓ estas linhas:
4
+ --primaria a cor do arco e dos títulos (escura e sóbria)
5
+ --destaque o fio de luz no arco e os detalhes (um metal: latão,
6
+ cobre, prata)
7
+ --apoio as linhas finas e o fundo do depoimento
8
+ --fundo o fundo da página (claro e neutro)
9
+ --fonte-titulo / --fonte-texto as duas fontes (e o link do
10
+ Google Fonts no index.html)
11
+ O resto do arquivo usa estas variáveis: não precisa mexer nele. */
12
+ --primaria:#1f3a34;
13
+ --destaque:#b8955a;
14
+ --apoio:#dcd9d2;
15
+ --fundo:#f4f4f1;
16
+ --fonte-titulo:'Bodoni Moda',Georgia,serif;
17
+ --fonte-texto:'Manrope',system-ui,sans-serif;
18
+
19
+ --tinta:#1c1c1b;
20
+ --suave:#66655f;
21
+ }
22
+
23
+ /* Premium: muito espaço vazio e um único elemento marcante (o arco).
24
+ Tudo o mais é fino, calmo e centrado. O luxo aqui é o que NÃO tem. */
25
+ *{margin:0;padding:0;box-sizing:border-box;}
26
+ html{scroll-behavior:smooth;}
27
+ body{font-family:var(--fonte-texto);font-size:1.02rem;line-height:1.75;color:var(--tinta);background:var(--fundo);-webkit-font-smoothing:antialiased;}
28
+ a{color:inherit;}
29
+ :focus-visible{outline:2px solid var(--destaque);outline-offset:4px;}
30
+ h1,h2{font-family:var(--fonte-titulo);font-weight:400;line-height:1.12;color:var(--primaria);letter-spacing:-.01em;}
31
+ .pular{position:absolute;left:-9999px;top:8px;background:var(--tinta);color:var(--fundo);padding:8px 14px;z-index:10;}
32
+ .pular:focus{left:8px;}
33
+
34
+ .topo{display:flex;align-items:center;justify-content:space-between;padding:28px clamp(20px,5vw,64px);}
35
+ .marca{font-family:var(--fonte-titulo);font-size:1.6rem;font-weight:500;text-decoration:none;color:var(--primaria);}
36
+ .menu{display:flex;gap:32px;font-size:.95rem;}
37
+ .menu a{text-decoration:none;color:var(--suave);transition:color .3s;}
38
+ .menu a:hover{color:var(--tinta);}
39
+
40
+ .botao{display:inline-block;padding:14px 32px;border:1px solid var(--primaria);color:var(--primaria);font:500 .98rem var(--fonte-texto);text-decoration:none;letter-spacing:.01em;transition:background .35s,color .35s;}
41
+ .botao:hover{background:var(--primaria);color:var(--fundo);}
42
+ .botao-cheio{background:var(--primaria);color:var(--fundo);}
43
+ .botao-cheio:hover{background:transparent;color:var(--primaria);}
44
+
45
+ /* O arco: uma janela alta, com um fio de metal por dentro e um sol baixo */
46
+ .abertura{display:flex;flex-direction:column;align-items:center;text-align:center;padding:clamp(24px,5vw,56px) 24px clamp(90px,12vw,160px);}
47
+ .arco{position:relative;width:min(380px,70vw);aspect-ratio:3/4.2;border-radius:999px 999px 0 0;background:var(--primaria);overflow:hidden;margin-bottom:clamp(40px,6vw,72px);transform-origin:50% 100%;}
48
+ .arco::before{content:'';position:absolute;inset:18px 18px 0;border:1px solid var(--destaque);border-bottom:0;border-radius:999px 999px 0 0;opacity:.8;}
49
+ .arco-luz{position:absolute;left:50%;bottom:-18%;width:64%;aspect-ratio:1;transform:translateX(-50%);border-radius:50%;background:radial-gradient(circle at 50% 40%,var(--destaque),color-mix(in srgb,var(--destaque) 30%,var(--primaria)) 70%);opacity:.95;}
50
+ .abertura h1{font-size:clamp(2.2rem,4.8vw,4.1rem);max-width:18ch;margin-bottom:22px;}
51
+ .abertura-texto{color:var(--suave);font-size:1.1rem;margin-bottom:40px;}
52
+
53
+ /* Entrada: só o arco sobe, uma vez, ao abrir a página */
54
+ .js .arco{opacity:0;transform:scaleY(.86);}
55
+ .js .arco.pronto{opacity:1;transform:none;transition:opacity 1.4s ease,transform 1.6s cubic-bezier(.2,.7,.1,1);}
56
+
57
+ .manifesto{padding:clamp(80px,12vw,160px) 24px;border-top:1px solid var(--apoio);}
58
+ .manifesto p{max-width:30ch;margin:0 auto;text-align:center;font-family:var(--fonte-titulo);font-weight:400;font-size:clamp(1.5rem,2.7vw,2.2rem);line-height:1.4;color:var(--tinta);}
59
+
60
+ /* O trabalho: três colunas de texto com um fio em cima, sem caixa */
61
+ .trabalho{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(32px,5vw,80px);max-width:1160px;margin:0 auto;padding:clamp(60px,8vw,120px) clamp(24px,5vw,64px);}
62
+ .frente{border-top:1px solid var(--destaque);padding-top:28px;}
63
+ .frente h2{font-size:clamp(1.45rem,2vw,1.8rem);font-weight:400;margin-bottom:12px;}
64
+ .frente p{color:var(--suave);max-width:34ch;}
65
+
66
+ .depoimento{background:var(--apoio);padding:clamp(80px,11vw,150px) 24px;text-align:center;}
67
+ .depoimento p{font-family:var(--fonte-titulo);font-style:italic;font-weight:400;font-size:clamp(1.4rem,2.6vw,2.1rem);line-height:1.3;max-width:28ch;margin:0 auto;color:var(--primaria);}
68
+ .depoimento footer{margin-top:24px;color:var(--suave);font-size:.95rem;}
69
+
70
+ .contato{text-align:center;padding:clamp(90px,12vw,160px) 24px;}
71
+ .contato h2{font-size:clamp(2rem,4vw,3.2rem);margin-bottom:48px;}
72
+ .contato-grade{display:grid;grid-template-columns:repeat(3,auto);justify-content:center;gap:clamp(28px,6vw,96px);margin-bottom:56px;color:var(--suave);}
73
+ .contato-grade a{text-decoration:none;border-bottom:1px solid var(--apoio);transition:border-color .3s;}
74
+ .contato-grade a:hover{border-color:var(--destaque);}
75
+
76
+ .rodape{border-top:1px solid var(--apoio);padding:28px;text-align:center;font-size:.9rem;color:var(--suave);}
77
+
78
+ @media (max-width:760px){
79
+ .trabalho{grid-template-columns:1fr;}
80
+ .contato-grade{grid-template-columns:1fr;gap:20px;}
81
+ .menu{gap:20px;}
82
+ }
83
+ @media (prefers-reduced-motion:reduce){
84
+ html{scroll-behavior:auto;}
85
+ *{transition:none!important;}
86
+ .js .arco{opacity:1;transform:none;}
87
+ }
@@ -0,0 +1,108 @@
1
+ <!DOCTYPE html>
2
+ <html lang='pt-BR'>
3
+ <head>
4
+ <meta charset='UTF-8'>
5
+ <meta name='viewport' content='width=device-width, initial-scale=1.0'>
6
+ <!-- TROQUE: título e descrição com o nome e a oferta -->
7
+ <title>Sua Marca, a oferta que chama</title>
8
+ <meta name='description' content='Uma frase que diz o que é, quando e onde.'>
9
+ <link rel='preconnect' href='https://fonts.googleapis.com'>
10
+ <link rel='preconnect' href='https://fonts.gstatic.com' crossorigin>
11
+ <!-- TROQUE (se quiser): a fonte. Se trocar aqui, troque --fonte-titulo e --fonte-texto no topo do style.css -->
12
+ <link href='https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,800&display=swap' rel='stylesheet'>
13
+ <link rel='stylesheet' href='style.css'>
14
+ <script src='script.js' defer></script>
15
+ </head>
16
+ <body>
17
+ <a class='pular' href='#conteudo'>Pular para o conteúdo</a>
18
+
19
+ <!-- TROQUE: o nome e o link do pedido (WhatsApp, loja, ingresso) -->
20
+ <header class='topo'>
21
+ <a href='#' class='marca'>Sua Marca</a>
22
+ <a href='https://wa.me/5500000000000' class='botao botao-preto'>Quero o meu</a>
23
+ </header>
24
+
25
+ <main id='conteudo'>
26
+ <!-- TROQUE: o título curto e barulhento (poucas palavras), o selo e a frase de apoio -->
27
+ <section class='palco'>
28
+ <h1>Uma frase curta que dá fome.</h1>
29
+ <p class='selo'><span>Só nesta semana</span></p>
30
+ <div class='palco-base'>
31
+ <p class='palco-texto'>O que é, para quem é, e o motivo para pedir hoje. Duas frases, sem enrolar.</p>
32
+ <a href='https://wa.me/5500000000000' class='botao botao-preto'>Fazer meu pedido</a>
33
+ </div>
34
+ </section>
35
+
36
+ <!-- TROQUE: a data (no texto E no data-alvo, formato 2026-12-31T20:00), o lugar e o preço. Sem data? Apague o <p id='contagem'>. -->
37
+ <section class='quando' aria-label='Quando e onde'>
38
+ <div class='quando-data'>
39
+ <p class='quando-rotulo'>Quando</p>
40
+ <p class='quando-valor'>Sábado, 12 de dezembro</p>
41
+ <p id='contagem' class='contagem' data-alvo='2026-12-12T19:00'></p>
42
+ </div>
43
+ <div class='quando-lugar'>
44
+ <p class='quando-rotulo'>Onde</p>
45
+ <p class='quando-valor'>Rua Exemplo, 100, Sua Cidade</p>
46
+ </div>
47
+ <div class='quando-preco'>
48
+ <p class='quando-rotulo'>A partir de</p>
49
+ <p class='quando-valor'>R$ 00</p>
50
+ </div>
51
+ </section>
52
+
53
+ <!-- TROQUE: os produtos (ou atrações). O primeiro é a estrela e fica grande. Pode ter 4 a 6. -->
54
+ <section id='cardapio' class='mosaico' aria-label='O que tem'>
55
+ <article class='peca peca-estrela'>
56
+ <div class='arte arte-circulos' aria-hidden='true'></div>
57
+ <h2>O campeão da casa</h2>
58
+ <p>Por que todo mundo pede este. Uma frase que dá vontade.</p>
59
+ <p class='etiqueta'>R$ 00</p>
60
+ </article>
61
+ <article class='peca peca-azul'>
62
+ <div class='arte arte-meia' aria-hidden='true'></div>
63
+ <h3>Segundo item</h3>
64
+ <p>Uma frase curta e gostosa.</p>
65
+ <p class='etiqueta'>R$ 00</p>
66
+ </article>
67
+ <article class='peca peca-branca'>
68
+ <div class='arte arte-listras' aria-hidden='true'></div>
69
+ <h3>Terceiro item</h3>
70
+ <p>Uma frase curta e gostosa.</p>
71
+ <p class='etiqueta'>R$ 00</p>
72
+ </article>
73
+ <article class='peca peca-amarela'>
74
+ <div class='arte arte-gota' aria-hidden='true'></div>
75
+ <h3>Quarto item</h3>
76
+ <p>Uma frase curta e gostosa.</p>
77
+ <p class='etiqueta'>R$ 00</p>
78
+ </article>
79
+ <article class='peca peca-preta'>
80
+ <div class='arte arte-estrela' aria-hidden='true'></div>
81
+ <h3>Quinto item</h3>
82
+ <p>Uma frase curta e gostosa.</p>
83
+ <p class='etiqueta'>R$ 00</p>
84
+ </article>
85
+ </section>
86
+
87
+ <!-- TROQUE: como pedir, em três verbos (é uma sequência de verdade) e uma frase -->
88
+ <section class='passos'>
89
+ <p class='passos-verbos'>Escolha. <span>Chame.</span> Aproveite.</p>
90
+ <p class='passos-texto'>Uma frase que explica o caminho: onde pedir, em quanto tempo chega, como pagar.</p>
91
+ </section>
92
+
93
+ <!-- TROQUE: o último empurrão e os contatos -->
94
+ <section id='pedir' class='final'>
95
+ <h2>Bora?</h2>
96
+ <p>O convite final, com a urgência verdadeira (acaba quando?).</p>
97
+ <a href='https://wa.me/5500000000000' class='botao botao-preto'>Chamar no WhatsApp</a>
98
+ <p class='final-redes'><a href='https://instagram.com/'>@suamarca</a> no Instagram</p>
99
+ </section>
100
+ </main>
101
+
102
+ <!-- TROQUE: ano, nome e cidade -->
103
+ <footer class='rodape'>
104
+ <p>© 2026 Sua Marca, Sua Cidade</p>
105
+ <a href='#'>Voltar ao topo</a>
106
+ </footer>
107
+ </body>
108
+ </html>
@@ -0,0 +1,20 @@
1
+ // Vitrine: a pressa de verdade é a data. O script conta quantos dias faltam
2
+ // para o data-alvo e escreve em palavras ("Faltam 12 dias"), em vez de um
3
+ // relógio piscando. Se a data passou ou está vazia, o selo some sozinho.
4
+ (function(){
5
+ var el=document.getElementById('contagem');
6
+ if(!el) return;
7
+ var alvo=new Date(el.getAttribute('data-alvo')||'');
8
+ if(isNaN(alvo)) { el.textContent=''; return; }
9
+ function escrever(){
10
+ var ms=alvo-new Date();
11
+ if(ms<=0){ el.textContent=''; return; }
12
+ var dias=Math.floor(ms/864e5), horas=Math.floor(ms/36e5);
13
+ if(dias>1) el.textContent='Faltam '+dias+' dias';
14
+ else if(dias===1) el.textContent='É amanhã';
15
+ else if(horas>=1) el.textContent='Faltam '+horas+(horas===1?' hora':' horas');
16
+ else el.textContent='É agora';
17
+ }
18
+ escrever();
19
+ setInterval(escrever,6e4);
20
+ })();
@@ -0,0 +1,117 @@
1
+ :root{
2
+ /* ── A MARCA ───────────────────────────────────────────────────────
3
+ Para mudar a cara da página inteira, troque SÓ estas linhas:
4
+ --primaria o bloco de abertura e a peça estrela
5
+ --destaque o selo, as etiquetas de preço e o bloco final
6
+ --apoio a faixa de data e as peças de cor
7
+ --fonte-titulo / --fonte-texto a fonte (e o link do Google
8
+ Fonts no index.html)
9
+ Cores vivas e fortes funcionam melhor aqui. O resto do arquivo usa
10
+ estas variáveis: não precisa mexer nele. */
11
+ --primaria:#ffd23f;
12
+ --destaque:#ff4f8b;
13
+ --apoio:#2d5bff;
14
+ --fonte-titulo:'Bricolage Grotesque',system-ui,sans-serif;
15
+ --fonte-texto:'Bricolage Grotesque',system-ui,sans-serif;
16
+
17
+ --tinta:#141414;
18
+ --branco:#ffffff;
19
+ --lado:clamp(16px,3vw,40px);
20
+ }
21
+
22
+ /* Vitrine: blocos de cor chapada, tipo enorme e contorno grosso, como
23
+ cartaz de rua. Nada de sombra suave nem vidro: tudo é plano e forte. */
24
+ *{margin:0;padding:0;box-sizing:border-box;}
25
+ html{scroll-behavior:smooth;}
26
+ body{font-family:var(--fonte-texto);font-size:1.1rem;line-height:1.5;color:var(--tinta);background:var(--branco);-webkit-font-smoothing:antialiased;overflow-x:hidden;}
27
+ a{color:inherit;}
28
+ :focus-visible{outline:4px solid var(--apoio);outline-offset:3px;}
29
+ h1,h2,h3{font-family:var(--fonte-titulo);font-weight:800;line-height:.95;letter-spacing:-.035em;}
30
+ .pular{position:absolute;left:-9999px;top:8px;background:var(--tinta);color:var(--branco);padding:8px 14px;z-index:10;}
31
+ .pular:focus{left:8px;}
32
+
33
+ .topo{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px var(--lado);background:var(--primaria);border-bottom:3px solid var(--tinta);}
34
+ .marca{font-family:var(--fonte-titulo);font-weight:800;font-size:1.6rem;letter-spacing:-.04em;text-decoration:none;}
35
+
36
+ .botao{display:inline-block;padding:14px 26px;border:3px solid var(--tinta);border-radius:14px;background:var(--branco);color:var(--tinta);font:800 1.05rem var(--fonte-titulo);text-decoration:none;box-shadow:5px 5px 0 var(--tinta);transition:transform .15s,box-shadow .15s;}
37
+ .botao:hover{transform:translate(-2px,-2px);box-shadow:7px 7px 0 var(--tinta);}
38
+ .botao:active{transform:translate(3px,3px);box-shadow:2px 2px 0 var(--tinta);}
39
+ .botao-preto{background:var(--tinta);color:var(--branco);box-shadow:5px 5px 0 var(--destaque);}
40
+ .botao-preto:hover{box-shadow:7px 7px 0 var(--destaque);}
41
+ .botao-preto:active{box-shadow:2px 2px 0 var(--destaque);}
42
+
43
+ /* Palco: o título ocupa a largura toda, e o selo gira no canto */
44
+ .palco{position:relative;background:var(--primaria);padding:clamp(40px,6vw,80px) var(--lado) clamp(48px,6vw,80px);border-bottom:3px solid var(--tinta);overflow:hidden;}
45
+ .palco h1{font-size:clamp(3.4rem,12.5vw,13rem);max-width:10ch;position:relative;z-index:1;}
46
+ .selo{position:absolute;right:clamp(16px,6vw,110px);top:clamp(24px,5vw,70px);width:clamp(120px,15vw,210px);aspect-ratio:1;display:grid;place-items:center;text-align:center;border-radius:50%;background:var(--destaque);font-family:var(--fonte-titulo);font-weight:800;font-size:clamp(1rem,1.6vw,1.45rem);line-height:1.05;transform:rotate(-12deg);z-index:2;
47
+ /* a borda serrilhada do selo: um polígono de 24 pontas */
48
+ clip-path:polygon(50% 0%,56.5% 7.4%,65.4% 3.4%,68.8% 12.6%,78.6% 12.4%,78.3% 22.1%,87.6% 25%,84.1% 34.1%,92.1% 39.8%,86.4% 47.6%,92.1% 55.8%,84.1% 61%,87.6% 70%,78.3% 73%,78.6% 82.7%,68.8% 82.4%,65.4% 91.6%,56.5% 87.6%,50% 95%,43.5% 87.6%,34.6% 91.6%,31.2% 82.4%,21.4% 82.7%,21.7% 73%,12.4% 70%,15.9% 61%,7.9% 55.8%,13.6% 47.6%,7.9% 39.8%,15.9% 34.1%,12.4% 25%,21.7% 22.1%,21.4% 12.4%,31.2% 12.6%,34.6% 3.4%,43.5% 7.4%);}
49
+ .selo span{max-width:8ch;}
50
+ .palco-base{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:24px;margin-top:clamp(28px,4vw,48px);}
51
+ .palco-texto{font-size:clamp(1.2rem,1.8vw,1.5rem);font-weight:600;max-width:34ch;}
52
+
53
+ /* Quando e onde: uma faixa de três colunas com filete preto entre elas */
54
+ .quando{display:grid;grid-template-columns:1.3fr 1.3fr 1fr;background:var(--apoio);color:var(--branco);border-bottom:3px solid var(--tinta);}
55
+ .quando>div{padding:28px var(--lado);}
56
+ .quando>div+div{border-left:3px solid var(--tinta);}
57
+ .quando-rotulo{font-weight:600;opacity:.85;}
58
+ .quando-valor{font-family:var(--fonte-titulo);font-weight:800;font-size:clamp(1.5rem,2.6vw,2.3rem);line-height:1.05;letter-spacing:-.02em;margin-top:4px;}
59
+ .contagem{margin-top:10px;display:inline-block;background:var(--primaria);color:var(--tinta);font-weight:800;padding:4px 12px;border-radius:999px;border:2px solid var(--tinta);}
60
+ .contagem:empty{display:none;}
61
+
62
+ /* Mosaico: peças de tamanhos e cores diferentes, e não cards iguais */
63
+ .mosaico{display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:minmax(250px,auto);gap:3px;background:var(--tinta);border-bottom:3px solid var(--tinta);}
64
+ .peca{position:relative;display:flex;flex-direction:column;justify-content:flex-end;gap:8px;padding:28px;overflow:hidden;background:var(--branco);}
65
+ .peca h2,.peca h3,.peca p{position:relative;z-index:1;}
66
+ .peca h3{font-size:clamp(1.6rem,2.4vw,2.2rem);}
67
+ .peca p{max-width:28ch;font-weight:600;}
68
+ .peca-estrela{grid-column:span 2;grid-row:span 2;background:var(--destaque);}
69
+ .peca-estrela h2{font-size:clamp(2.6rem,5.5vw,5.2rem);max-width:9ch;}
70
+ .peca-estrela p{font-size:1.25rem;}
71
+ .peca-azul{background:var(--apoio);color:var(--branco);}
72
+ .peca-amarela{background:var(--primaria);}
73
+ .peca-preta{background:var(--tinta);color:var(--branco);}
74
+ .etiqueta{position:absolute!important;top:22px;right:22px;background:var(--branco);color:var(--tinta);font-family:var(--fonte-titulo);font-weight:800;font-size:1.3rem;padding:6px 14px;border:3px solid var(--tinta);border-radius:10px;transform:rotate(4deg);}
75
+ .peca-estrela .etiqueta{font-size:2rem;padding:8px 18px;transform:rotate(-5deg);}
76
+
77
+ /* As artes: formas geométricas no lugar da foto (o pedido pode não ter foto) */
78
+ .arte{position:absolute;z-index:0;}
79
+ .arte-circulos{right:-8%;top:10%;width:62%;aspect-ratio:1;border-radius:50%;background:radial-gradient(circle,var(--primaria) 0 22%,transparent 22.5% 36%,var(--primaria) 36.5% 50%,transparent 50.5%);}
80
+ .arte-meia{right:-30px;top:-30px;width:170px;aspect-ratio:1;border-radius:50%;background:var(--primaria);}
81
+ .arte-listras{left:0;right:0;top:0;height:46%;background:repeating-linear-gradient(-45deg,var(--destaque) 0 16px,transparent 16px 32px);opacity:.9;}
82
+ .arte-gota{right:24px;top:70px;width:110px;aspect-ratio:1;border-radius:50% 0 50% 50%;background:var(--destaque);transform:rotate(-45deg);}
83
+ .arte-estrela{right:22px;top:64px;width:120px;aspect-ratio:1;background:var(--primaria);clip-path:polygon(50% 0,61% 35%,98% 35%,68% 57%,79% 91%,50% 70%,21% 91%,32% 57%,2% 35%,39% 35%);}
84
+
85
+ /* Passos: três verbos em uma linha enorme */
86
+ .passos{padding:clamp(56px,8vw,110px) var(--lado);border-bottom:3px solid var(--tinta);}
87
+ .passos-verbos{font-family:var(--fonte-titulo);font-weight:800;font-size:clamp(2.6rem,9vw,8.5rem);line-height:.95;letter-spacing:-.04em;}
88
+ .passos-verbos span{color:var(--apoio);}
89
+ .passos-texto{margin-top:24px;font-size:1.3rem;font-weight:600;max-width:40ch;}
90
+
91
+ .final{background:var(--destaque);padding:clamp(56px,8vw,110px) var(--lado);text-align:center;border-bottom:3px solid var(--tinta);}
92
+ .final h2{font-size:clamp(4.5rem,16vw,14rem);}
93
+ .final p{font-size:1.3rem;font-weight:600;margin:16px auto 28px;max-width:34ch;}
94
+ .final .botao-preto{box-shadow:5px 5px 0 var(--primaria);}
95
+ .final-redes{font-size:1.05rem!important;margin-top:28px!important;}
96
+
97
+ .rodape{display:flex;justify-content:space-between;gap:16px;padding:22px var(--lado);background:var(--tinta);color:var(--branco);font-weight:600;}
98
+
99
+ @media (max-width:960px){
100
+ .mosaico{grid-template-columns:1fr 1fr;}
101
+ .peca-estrela{grid-column:span 2;grid-row:span 1;min-height:380px;}
102
+ }
103
+ @media (max-width:640px){
104
+ .quando{grid-template-columns:1fr;}
105
+ .quando>div+div{border-left:0;border-top:3px solid var(--tinta);}
106
+ .mosaico{grid-template-columns:1fr;}
107
+ .peca-estrela,.peca-preta{grid-column:auto;}
108
+ .peca{min-height:230px;}
109
+ .peca-estrela{min-height:400px;padding-top:110px;}
110
+ .selo{position:relative;right:auto;top:auto;width:116px;margin-top:22px;}
111
+ .arte-circulos{width:46%;top:22%;right:-12%;}
112
+ .palco{padding-bottom:40px;}
113
+ .palco-base{flex-direction:column;align-items:flex-start;}
114
+ .rodape{flex-direction:column;}
115
+ .topo .botao{padding:10px 16px;font-size:.95rem;}
116
+ }
117
+ @media (prefers-reduced-motion:reduce){html{scroll-behavior:auto;}*{transition:none!important;}}
package/lib/app.js CHANGED
@@ -98,6 +98,9 @@ function tipoDe(arquivo) {
98
98
  * @param {()=>void} op.interromper para o que estiver rodando
99
99
  * @param {()=>object} op.estado { pasta, modo, esforco, projeto }
100
100
  * @param {string} op.versao
101
+ * @param {()=>string} [op.pastaProjeto] liga a IDE (/ide) nesta pasta
102
+ * @param {'rc'|'ide'} [op.abrirEm] qual tela a janela abre
103
+ * @param {boolean} [op.semJanela] só o servidor, sem abrir navegador
101
104
  */
102
105
  async function abrir(op) {
103
106
  const chave = crypto.randomBytes(16).toString('hex');
@@ -117,6 +120,13 @@ async function abrir(op) {
117
120
  }
118
121
  }
119
122
 
123
+ /* A IDE (lib/ide.js) mora neste mesmo servidor e fala pelo mesmo fluxo:
124
+ "um app ao invés de CLI, aí fica uma IDE e uma CLI". Só sobe quando
125
+ quem abriu diz qual é a pasta do projeto. */
126
+ const ide = op.pastaProjeto
127
+ ? require('./ide.js').montar({ pasta: op.pastaProjeto, emitir: (e, d) => emitir(e, d) })
128
+ : null;
129
+
120
130
  const pararEspelho = espelhar((pedaco) => {
121
131
  if (ehRuido(pedaco)) return;
122
132
  memoria.push(pedaco);
@@ -175,7 +185,11 @@ async function abrir(op) {
175
185
  /app.js sem parâmetro nenhum. A página vinha, ficava sem estilo e
176
186
  sem script, e o "app bonito" era um formulário branco. */
177
187
  if (rota.startsWith('/api/')) {
178
- const dada = url.searchParams.get('k') || req.headers['x-primo-chave'];
188
+ // A prévia da IDE leva a chave no CAMINHO (ver lib/ide.js): os
189
+ // arquivos que a página do projeto pede por endereço relativo não
190
+ // carregam query nenhuma.
191
+ const naPrevia = rota.match(/^\/api\/ide\/previa\/([0-9a-f]+)/);
192
+ const dada = (naPrevia && naPrevia[1]) || url.searchParams.get('k') || req.headers['x-primo-chave'];
179
193
  if (dada !== chave) {
180
194
  res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
181
195
  return res.end('chave inválida — abra a janela com o comando /app');
@@ -185,7 +199,13 @@ async function abrir(op) {
185
199
  127.0.0.1 sem preflight. Ela não saberia a chave, mas o cinto e o
186
200
  suspensório custam três linhas. */
187
201
  const origem = req.headers.origin;
188
- if (origem && !origem.startsWith(`http://127.0.0.1:${servidor.__porta}`)
202
+ /* A exceção é a prévia da IDE: ela roda num iframe sandbox de origem
203
+ opaca, e o navegador manda `Origin: null` até num <script
204
+ type="module">. Só GET e só nas rotas da prévia, que apenas LEEM o
205
+ projeto — nenhuma delas dá ordem ao agente. */
206
+ const previaOpaca = origem === 'null' && req.method === 'GET'
207
+ && rota.startsWith('/api/ide/previa/');
208
+ if (origem && !previaOpaca && !origem.startsWith(`http://127.0.0.1:${servidor.__porta}`)
189
209
  && !origem.startsWith(`http://localhost:${servidor.__porta}`)) {
190
210
  res.writeHead(403).end();
191
211
  return;
@@ -254,6 +274,8 @@ async function abrir(op) {
254
274
  return res.end(JSON.stringify(r));
255
275
  }
256
276
 
277
+ if (ide && await ide.atender(req, res, url)) return;
278
+
257
279
  if (rota === '/api/estado') {
258
280
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
259
281
  return res.end(JSON.stringify(Object.assign({ versao: op.versao }, op.estado())));
@@ -262,7 +284,7 @@ async function abrir(op) {
262
284
  // Estáticos. `basename` corta qualquer "../" antes de virar caminho:
263
285
  // este servidor fala com o disco do usuário e não pode servir o que
264
286
  // estiver fora da pasta do aplicativo.
265
- const nome = rota === '/' ? 'index.html' : path.basename(rota);
287
+ const nome = rota === '/' ? 'index.html' : rota === '/ide' ? 'ide.html' : path.basename(rota);
266
288
  const arquivo = path.join(WEB, nome);
267
289
  fs.readFile(arquivo, (erro, dados) => {
268
290
  if (erro) { res.writeHead(404).end('não encontrado'); return; }
@@ -274,11 +296,22 @@ async function abrir(op) {
274
296
  const porta = await escutar(servidor, PORTA_PADRAO);
275
297
  servidor.__porta = porta;
276
298
  const url = `http://127.0.0.1:${porta}/?k=${chave}`;
299
+ const urlIde = `http://127.0.0.1:${porta}/ide?k=${chave}`;
277
300
 
278
- const janela = await abrirJanela(url);
301
+ // `semJanela` é para o teste (e para quem só quer o endereço): sem ele,
302
+ // rodar a suíte abriria uma janela de verdade na tela de quem roda.
303
+ const janela = op.semJanela ? { modo: 'nenhum' }
304
+ : await abrirJanela(op.abrirEm === 'ide' && ide ? urlIde : url);
279
305
  return {
280
- url, porta, chave, servidor, janela,
281
- fechar() { try { pararEspelho(); } catch {} try { servidor.close(); } catch {} },
306
+ url, urlIde: ide ? urlIde : null, porta, chave, servidor, janela,
307
+ fechar() {
308
+ try { pararEspelho(); } catch {}
309
+ try { if (ide) ide.fechar(); } catch {}
310
+ try { servidor.close(); } catch {}
311
+ // SSE abertos seguram o close() para sempre; a janela é que
312
+ // fechou, então as conexões dela também.
313
+ for (const r of ouvintes) { try { r.end(); } catch {} }
314
+ },
282
315
  };
283
316
  }
284
317
 
@@ -345,6 +378,8 @@ function abrirJanela(url) {
345
378
  const filho = execFile(exe, [
346
379
  `--app=${url}`,
347
380
  `--user-data-dir=${perfil}`,
381
+ // Sem Keychain: ver SEM_KEYCHAIN em lib/browser.js.
382
+ ...require('./browser.js').SEM_KEYCHAIN,
348
383
  '--window-size=1180,820',
349
384
  '--no-first-run',
350
385
  '--no-default-browser-check',
package/lib/browser.js CHANGED
@@ -177,6 +177,16 @@ function httpPutJson(url, timeoutMs = 3000) {
177
177
 
178
178
  function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
179
179
 
180
+ /* O Chrome NUNCA mexe no Keychain do Mac (nem no cofre de senhas do Linux).
181
+ * "Para de fazer coisas de tentar armazenar chave no Chrome, eu apaguei o
182
+ * Chrome." (24/09/2026)
183
+ * Cada perfil novo do PrimoCode (o do agente, o revisor escondido, a janela
184
+ * do /rc e do /ide) fazia o Chrome pedir o "Chrome Safe Storage" ao sistema —
185
+ * uma janela de senha do Mac, do nada, várias vezes. O PrimoCode não guarda
186
+ * senha em navegador nenhum: estas duas flags fazem o Chrome usar um cofre de
187
+ * mentira e texto simples no próprio perfil, sem tocar no do sistema. */
188
+ const SEM_KEYCHAIN = ['--use-mock-keychain', '--password-store=basic'];
189
+
180
190
  // ---- WebSocket client mínimo (RFC 6455) para CDP ----------------
181
191
  // Suficiente pra CDP (mensagens curtas, texto UTF-8, um cliente por página).
182
192
  class MiniWebSocket {
@@ -393,6 +403,7 @@ async function ensureChromeRunning() {
393
403
  const args = [
394
404
  `--remote-debugging-port=${DEBUG_PORT}`,
395
405
  `--user-data-dir=${PROFILE_DIR}`,
406
+ ...SEM_KEYCHAIN,
396
407
  '--no-first-run',
397
408
  '--no-default-browser-check',
398
409
  '--disable-features=Translate',
@@ -839,6 +850,7 @@ async function subirRevisor() {
839
850
  // Zero: o Chrome escolhe uma porta livre e escreve qual no perfil.
840
851
  `--remote-debugging-port=${process.env.PRIMOCODE_CDP_PORT_REVISOR || 0}`,
841
852
  `--user-data-dir=${perfilRevisor}`,
853
+ ...SEM_KEYCHAIN,
842
854
  // Sempre invisível: é o pedido, e é o que separa este do navegador da
843
855
  // pessoa. Não há variável para ligar a janela aqui de propósito.
844
856
  '--headless=new', '--disable-gpu', '--disable-dev-shm-usage',
@@ -1116,7 +1128,8 @@ function fecharRevisor() {
1116
1128
  }
1117
1129
 
1118
1130
  module.exports = {
1119
- scriptDaSeta, verSeta,
1131
+ SEM_KEYCHAIN,
1132
+ scriptDaSeta, verSeta, findChrome,
1120
1133
  open, click, clickText, clickSelector, type, typeInSelector,
1121
1134
  key, screenshot, evaluate, waitFor, wait, scroll, close,
1122
1135
  inspecionarEscondido, rodarEscondido, abrirEscondido, fecharRevisor,