| Componente | Técnica principal | Seção deste guia |
|---|---|---|
| Grain overlay | SVG feTurbulence via CSS background-image | ✦ Grain |
| Layout raiz | CSS Flexbox com sidebar sticky | 📐 Layout |
| Sidebar responsiva | position:sticky → flex-row em mobile | ◧ Sidebar |
| Hero com glow | radial-gradient absoluto + CSS vars dinâmicas | ⬛ Hero |
| Cards de setor | CSS Grid + animação escalonada com delay | ▦ Cards |
| Modal | position:fixed + createElement + animação CSS | ◻ Modal |
| Troca de tema por empresa | JS setCSSVar() muda --acc em runtime | ✦ Dynamic CSS |
| Fade de conteúdo | Classe .fading + setTimeout(render, 140ms) | ⟳ JS Render |
| Score bars | div com width% calculado em JS | ⊡ JS Modal |
| Dados (30 setores) | Objeto JS const PLAYBOOK injetado no HTML | 📦 JS Data |
<!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>Growth Playbook 2026</title> <!-- Fonte via Google Fonts (requer internet) --> <link rel="preconnect" href="https://fonts.googleapis.com"> <link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,300;12..96,500;12..96,700&family=DM+Mono:wght@400;500&family=Plus+Jakarta+Sans:wght@300;400;500&display=swap" rel="stylesheet"> </head>
12..96 no Bricolage ativa o eixo de tamanho óptico — a fonte ajusta seus detalhes automaticamente conforme o tamanho de renderização.#co-tabs, #metrics, #content etc. é injetado pelo JavaScript.<body> <div class="layout"> <!-- SIDEBAR: menu de empresas --> <aside class="sidebar"> <div class="sb-logo"><span>Growth</span> Playbook</div> <div id="co-tabs"></div> <!-- preenchido por renderCompanyTabs() --> <div class="sb-foot">6 empresas · 30 setores</div> </aside> <!-- MAIN: todo o conteúdo principal --> <div class="main" id="main-panel"> <!-- HERO: nome, métricas, estratégia da empresa --> <div class="hero" id="hero"> <div class="hero-glow" id="hero-glow"></div> <div class="hero-period" id="hero-period"></div> <div class="hero-name" id="hero-name"></div> <div class="hero-tag" id="hero-tag"></div> <div class="metrics" id="metrics"></div> <div class="strat-box"> <div class="strat-lbl">Estratégia central</div> <div class="strat-txt" id="strat"></div> </div> </div> <!-- TAB BAR: alterna entre "setores" e "plano" --> <div class="tab-bar"> <button class="tab" id="tab-steps" onclick="switchTab('steps')"> Plano da estratégia <span class="tab-badge">8</span> </button> <button class="tab on" id="tab-sec" onclick="switchTab('sectors')"> 5 setores aplicados <span class="tab-badge">5</span> </button> </div> <!-- CONTENT: área dinâmica preenchida por _render() --> <div class="content" id="content"></div> </div> <!-- .main --> </div> <!-- .layout --> </body>
id="" são alvos do JavaScript. O conteúdo real (nomes, textos, cards, steps) não existe no HTML — é gerado dinamicamente por renderHero(), _render(), renderCompanyTabs().<aside> para a sidebar porque semanticamente ela é conteúdo complementar ao conteúdo principal — melhor para acessibilidade e SEO.onclick inline nas tabs e botões por simplicidade — o projeto tem escopo pequeno. Para projetos maiores, addEventListener separado é melhor prática.:root. Isso permite que o JavaScript mude o tema inteiro mudando apenas --acc./* RESET: aplica box-sizing em tudo */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } /* DESIGN TOKENS: toda a paleta em um lugar só */ :root { /* Backgrounds */ --bg: #050c18; /* fundo da página (navy mais escuro) */ --surf: #080f1e; /* superfície de sidebar e tab bar */ --card: #0d1626; /* cards e steps */ --card2: #111e33; /* hover dos cards, badges */ /* Borders: usam rgba para transparência */ --b0: rgba(255,255,255,0.05); /* borda sutil */ --b1: rgba(255,255,255,0.09); /* borda visível */ --b2: rgba(255,255,255,0.15); /* borda de hover */ /* Texto */ --t1: #eef2ff; /* texto principal (quase branco) */ --t2: #8892a8; /* texto secundário (cinza médio) */ --t3: #3d4a60; /* texto terciário (cinza escuro) */ /* Accent: esta variável é TROCADA pelo JS a cada empresa */ --acc: #f59e0b; /* cor de acento ativa */ --acc-d: rgba(245,158,11,0.12); /* acento com transparência */ --acc-g: rgba(245,158,11,0.05); /* acento muito sutil (fundo) */ /* Pros e Cons */ --pro: #10b981; /* verde para vantagens */ --pro-d: rgba(16,185,129,0.10); /* fundo verde */ --con: #ef4444; /* vermelho para desvantagens */ --con-d: rgba(239,68,68,0.10); /* fundo vermelho */ /* Bordas arredondadas */ --r: 10px; /* radius padrão (cards grandes) */ --rs: 6px; /* radius pequeno (badges, inputs) */ /* Sidebar width: usada em calc() e media queries */ --sb: 230px; /* Fontes */ --font-d: 'Bricolage Grotesque', system-ui, sans-serif; --font-b: 'Plus Jakarta Sans', system-ui, sans-serif; --font-m: 'DM Mono', monospace; }
--acc, --acc-d e --acc-g toda vez que o usuário troca de empresa. Isso propaga automaticamente para todos os elementos que usam essa variável — hero glow, tab ativa, badges, botões ativos, score bars.#ffffff10 (hexadecimal com alpha), usamos rgba(255,255,255,0.05). Ambos funcionam, mas rgba é mais legível para quem está aprendendo — deixa explícito que é branco com 5% de opacidade./* O grain é um pseudo-elemento :before no body */ /* Ele fica fixed (cobre a tela inteira) com z-index alto */ /* pointer-events:none para não bloquear cliques */ body::before { content: ''; position: fixed; inset: 0; /* top:0; right:0; bottom:0; left:0 */ pointer-events: none; z-index: 9998; /* SVG inline como data URL — sem arquivo externo */ background-image: url( "data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E %3Cfilter id='n'%3E %3CfeTurbulence type='fractalNoise' baseFrequency='.75' numOctaves='4' stitchTiles='stitch'/%3E %3C/filter%3E %3Crect width='300' height='300' filter='url(%23n)' opacity='.06'/%3E %3C/svg%3E" ); }
feTurbulence é um filtro SVG nativo que gera ruído fractal. baseFrequency='.75' controla a granularidade — valores maiores = grão mais fino. numOctaves='4' controla a complexidade do padrão.%3C = <, %3E = >, %23 = #. O SVG precisa ser URL-encoded para funcionar dentro de uma string CSS. Você pode usar ferramentas online de URL encode para SVG..06 é muito sutil — visível mas não distrai. Valores entre .04 e .12 são os mais usados. Acima de .15 começa a parecer sujo.flex:1). Em mobile, vira coluna com sidebar horizontal no topo./* Container raiz: flex row em desktop */ .layout { display: flex; min-height: 100vh; } /* SIDEBAR: coluna esquerda fixa */ .sidebar { width: var(--sb); /* 230px */ min-width: var(--sb); /* não encolhe quando o main precisar de espaço */ background: var(--surf); border-right: 1px solid var(--b0); display: flex; flex-direction: column; height: 100vh; position: sticky; /* gruda no topo do viewport ao rolar */ top: 0; overflow-y: auto; /* scroll interno se houver muitos itens */ padding: 28px 14px 20px; gap: 6px; } /* MAIN: ocupa todo o espaço restante */ .main { flex: 1; /* cresce para preencher o espaço */ min-width: 0; /* impede overflow em flex items */ height: 100vh; overflow-y: auto; /* scroll independente do sidebar */ display: flex; flex-direction: column; }
position:sticky; top:0; height:100vh, a sidebar fica visível enquanto o usuário rola o conteúdo principal. O overflow-y:auto no main permite que ele role independentemente.flex:1 pode causar overflow horizontal quando o conteúdo interno for muito largo. min-width:0 corrige esse bug clássico do Flexbox.overflow-y:auto próprio e o main tem o seu. Isso permite que a sidebar role separadamente do conteúdo — útil quando há muitas empresas no menu.::before..co-btn { background: transparent; border: none; border-radius: var(--rs); padding: 10px 10px; text-align: left; cursor: pointer; color: var(--t2); font-family: var(--font-b); font-size: 13px; transition: all .18s; display: flex; align-items: center; gap: 10px; position: relative; /* necessário para o ::before funcionar */ width: 100%; } .co-btn:hover { background: var(--card); color: var(--t1); } .co-btn.active { background: var(--acc-d); /* fundo com cor da empresa */ color: var(--acc); /* texto com cor da empresa */ font-weight: 500; } /* LINHA VERTICAL ESQUERDA: o indicador de item ativo */ .co-btn.active::before { content: ''; position: absolute; left: 0; top: 25%; /* não vai do topo ao fundo — fica centrada */ bottom: 25%; width: 2.5px; background: var(--acc); border-radius: 2px; } /* DOT: indicador visual pequeno no início de cada item */ .co-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--t3); /* cinza escuro no default */ flex-shrink: 0; transition: background .18s; } .co-btn.active .co-dot { background: var(--acc); /* muda de cor junto com o estado ativo */ }
::before. Isso mantém o HTML limpo e a lógica de estilo toda no CSS. Só funciona porque o botão tem position:relative.top:0; bottom:0 (linha do topo ao fundo), usamos 25%–25% para que a linha fique centrada e mais elegante, sem tocar as bordas do botão.background .18s, color .18s), mas all é mais rápido de escrever para protótipos.radial-gradient e border-radius:50%. O JS muda sua cor a cada empresa. A tipografia usa clamp() para escalar automaticamente..hero { padding: 40px 44px 32px; border-bottom: 1px solid var(--b0); position: relative; /* necessário para o glow funcionar com absolute */ overflow: hidden; /* corta o glow que sai da borda */ } /* GLOW: círculo radial com a cor da empresa */ .hero-glow { position: absolute; top: -80px; /* parcialmente fora da hero */ right: -80px; width: 380px; height: 380px; border-radius: 50%; /* background é definido pelo JS: */ /* background: radial-gradient(circle, COR 0%, transparent 70%) */ opacity: .05; pointer-events: none; transition: background .4s; /* transição suave ao trocar empresa */ } /* EYEBROW: label de período acima do nome */ .hero-period { font-family: var(--font-m); font-size: 10px; color: var(--acc); letter-spacing: .12em; text-transform: uppercase; margin-bottom: 10px; display: flex; align-items: center; gap: 7px; } .hero-period::before { content: '▸'; /* símbolo decorativo via CSS puro */ opacity: .7; } /* NOME: tamanho responsivo com clamp() */ .hero-name { font-family: var(--font-d); /* clamp(min, preferred, max) */ /* mínimo: 36px (mobile) */ /* preferido: 5vw (5% da largura da tela) */ /* máximo: 58px (desktop grande) */ font-size: clamp(36px, 5vw, 58px); font-weight: 700; line-height: 1.05; letter-spacing: -.02em; /* tracking negativo em títulos grandes */ margin-bottom: 6px; color: var(--t1); } /* MÉTRICAS: container flex com wrap */ .metrics { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px; } .met { background: var(--card); border: 1px solid var(--b0); border-radius: var(--rs); padding: 11px 16px; min-width: 100px; } .met-l { font-size: 10px; color: var(--t3); font-family: var(--font-m); margin-bottom: 4px; } .met-v { font-size: 17px; font-weight: 500; font-family: var(--font-m); }
clamp(36px, 5vw, 58px) significa: nunca menor que 36px, nunca maior que 58px, mas tenta ser 5% da largura da tela. Elimina media queries só para ajustar tamanho de texto.overflow:hidden garante que esse excesso não cause scroll horizontal na página.auto-fill com minmax para ser responsivo sem media queries. O hover lift usa transform:translateY + box-shadow. A animação de entrada é escalonada via animation-delay./* GRID: auto-fill com minmax = responsivo sem media query */ .sec-grid { display: grid; /* auto-fill: preenche com quantas colunas couberem */ /* minmax(240px, 1fr): mínimo 240px, máximo proporcional */ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; } /* CARD BASE */ .sec-card { background: var(--card); border: 1px solid var(--b0); border-radius: var(--r); padding: 20px; cursor: pointer; display: flex; flex-direction: column; gap: 8px; /* Animação: ativa no load com delay escalonado */ animation: fadeIn .35s ease both; /* Transições suaves para hover */ transition: border-color .2s, transform .2s, background .2s, box-shadow .2s; } /* HOVER LIFT EFFECT */ .sec-card:hover { border-color: var(--b2); /* borda mais visível */ background: var(--card2); /* fundo mais claro */ transform: translateY(-3px); /* "levantamento" */ box-shadow: 0 8px 32px rgba(0,0,0,.4); /* sombra embaixo */ } /* ANIMAÇÃO DE ENTRADA ESCALONADA */ /* Cada card entra com delay diferente, criando efeito cascata */ .sec-card:nth-child(1) { animation-delay: .04s; } .sec-card:nth-child(2) { animation-delay: .09s; } .sec-card:nth-child(3) { animation-delay: .14s; } .sec-card:nth-child(4) { animation-delay: .19s; } .sec-card:nth-child(5) { animation-delay: .24s; } /* KEYFRAME: fadeIn usado em cards E nos steps do modal */ @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } /* CTA dentro do card: seta que move no hover */ .sc-cta::after { content: '→'; transition: transform .2s; } .sec-card:hover .sc-cta::after { transform: translateX(4px); /* seta se move para a direita no hover */ } /* FADE DO CONTEÚDO na troca de empresa */ .content.fading { animation: fadeOut .14s ease forwards; } @keyframes fadeOut { to { opacity: 0; transform: translateY(-5px); } }
auto-fill mantém colunas vazias se houver espaço — o grid preserva o tamanho das colunas. auto-fit colapsa colunas vazias e expande as preenchidas. Para cards, auto-fill é mais previsível.both aplica o estado inicial do keyframe antes da animação começar (prevenindo flash) e mantém o estado final depois (prevenindo resetar para invisível).nth-child. O incremento de .05s entre cards é intencional — pequeno o suficiente para ser sutil, grande o suficiente para ser percebido.position:fixed com inset:0 para cobrir toda a tela. A animação de entrada usa cubic-bezier com overshoot (spring effect). Em mobile, o modal sobe de baixo como um bottom sheet./* OVERLAY: cobre toda a tela */ .modal-bg { position: fixed; inset: 0; /* top/right/bottom/left: 0 */ background: rgba(5,12,24,.88); /* semi-transparente */ z-index: 200; display: flex; align-items: flex-start; justify-content: center; padding: 20px; overflow-y: auto; animation: bgIn .2s ease; } @keyframes bgIn { from { opacity: 0; } to { opacity: 1; } } /* Classe adicionada pelo JS ao fechar — anima para fora */ .modal-bg.out { animation: bgOut .18s ease forwards; } @keyframes bgOut { to { opacity: 0; } } /* MODAL: o card branco/escuro dentro do overlay */ .modal { background: var(--surf); border: 1px solid var(--b1); border-radius: var(--r); width: 100%; max-width: 860px; margin: auto; /* centraliza verticalmente no flex */ position: relative; overflow: hidden; /* corta o glow interno */ /* SPRING ANIMATION: cubic-bezier com overshoot */ animation: modalIn .25s cubic-bezier(.34, 1.56, .64, 1); } @keyframes modalIn { from { opacity: 0; transform: translateY(20px) scale(.97); } to { opacity: 1; transform: none; } } /* SCORE BARS: barras de progresso dentro do modal */ .score-bar { height: 4px; background: var(--b1); /* trilho cinza */ border-radius: 2px; overflow: hidden; margin-bottom: 4px; } .score-fill { height: 100%; border-radius: 2px; transition: width .6s ease; /* animação da barra ao abrir */ /* width e background são definidos inline pelo JS */ /* Ex: style="width:80%;background:#10b981" */ } /* MOBILE: modal vira bottom sheet */ @media(max-width:639px) { .modal-bg { align-items: flex-end; /* empurra para baixo */ padding: 0; } .modal { /* arredondamento só no topo — sem bordas embaixo */ border-radius: var(--r) var(--r) 0 0; margin-top: auto; margin-bottom: 0; } }
1.56 ultrapassa 1.0 no eixo Y — isso cria o "overshoot" (o modal vai um pouco além e volta). É o efeito de mola que dá personalidade à animação. Sem isso, seria linear e sem graça..out (que anima para opacidade 0), aguardar 180ms com setTimeout, e só então remover o elemento do DOM.width:80%) e a cor (background:#10b981) são definidas pelo JavaScript via style inline no HTML gerado. CSS não consegue calcular isso com dados dinâmicos sem JS./* TV (>1920px): mais espaço, mais colunas */ @media(min-width:1921px) { :root { --sb: 280px; } /* sidebar mais larga */ body { font-size: 16px; } .hero { padding: 50px 56px 40px; } .hero-name { font-size: 70px; } .sec-grid { grid-template-columns: repeat(4, 1fr); } .modal { max-width: 1100px; } .modal-body { grid-template-columns: 1fr 1fr; } } /* Notebook/Desktop: modal body em 2 colunas */ @media(min-width:1024px) and (max-width:1920px) { .modal-body { grid-template-columns: 1fr 1fr; } } /* Tablet (<1024px): sidebar vira barra horizontal */ @media(max-width:1023px) { /* Layout: coluna em vez de linha */ .layout { flex-direction: column; } /* Sidebar: vira barra horizontal */ .sidebar { width: 100%; min-width: 0; height: auto; flex-direction: row; /* botões em linha */ flex-wrap: nowrap; padding: 10px 12px; border-right: none; border-bottom: 1px solid var(--b0); overflow-x: auto; /* scroll horizontal nos botões */ overflow-y: hidden; position: sticky; top: 0; z-index: 100; } /* Esconde elementos que não cabem na barra horizontal */ .sb-logo, .sb-foot { display: none; } .co-yr { display: none; } /* remove anos dos botões */ /* Indicador ativo: muda de linha vertical para underline */ .co-btn.active::before { top: auto; bottom: 0; left: 20%; right: 20%; width: auto; height: 2.5px; /* vira linha embaixo do botão */ } .main { height: auto; } /* main não tem mais height fixo */ .hero { padding: 24px 20px 20px; } .tab-bar, .content { padding-left: 20px; padding-right: 20px; } } /* Mobile (<640px): compactar tudo */ @media(max-width:639px) { .hero-tag { font-size: 13px; } .met-v { font-size: 14px; } .metrics { gap: 8px; } .met { min-width: 90px; padding: 9px 12px; } .sec-grid { grid-template-columns: 1fr; } /* uma coluna */ .pc-grid { grid-template-columns: 1fr; } /* pros/cons: uma coluna */ }
min-width em todos os breakpoints).overflow-x:auto). O indicador ativo muda de linha vertical para underline — a mesma lógica CSS, só as dimensões mudam.--sb: 280px no :root — todos os lugares que usam essa variável automaticamente atualizam.<script>. A segunda tag JS (app) lê esse objeto via a variável global PLAYBOOK.// PRIMEIRO <script>: só dados const PLAYBOOK = { companies: [ // Cada empresa é um objeto com esses campos: { name: "Instagram", strategy: "Foco Radical — Um recurso perfeito", color: "#e1306c", // cor de acento usada no glow e theme emoji: "📸", // Array de setores (5 por empresa = 30 total) sectors: [ { icon: "🩺", setor: "Saúde", title: "App de dermatologia só por foto", desc: "Descrição curta para o card", difficulty: 4, // 1–5, usado nas score bars potential: 5, // 1–5, usado nas score bars time: "12–18 meses", // 8 passos de execução steps: [ { t: "Título do passo", d: "Descrição detalhada..." }, // ... mais 7 passos ], // 4 vantagens pros: [ { t: "Título", d: "Descrição..." }, // ... mais 3 ], // 4 desvantagens cons: [ { t: "Título", d: "Descrição..." }, // ... mais 3 ] }, // ... mais 4 setores ] }, // ... mais 5 empresas (WhatsApp, Uber, Airbnb, ChatGPT, Lovable) ] };
PLAYBOOK está em uma tag <script> separada. Isso facilita substituir os dados (trocar o JSON) sem tocar na lógica do app. É a separação de dados e lógica no JavaScript.t (title) e d (description) em vez de nomes completos. Com 240 steps × 2 chaves, isso economiza ~2KB no arquivo final.radial-gradient), na variável CSS --acc e na variável derivada --acc-d (com rgba).renderCompanyTabs(), renderHero(), renderContent() e _render(). Cada mudança de empresa chama essas 3 em sequência.// Referências aos elementos DOM — só buscados uma vez const CO = PLAYBOOK.companies; // array de empresas let ci = 0; // índice da empresa ativa let tab = 'sectors'; // tab ativa: 'sectors' ou 'steps' const co_tabs = document.getElementById('co-tabs'); const hero_glow = document.getElementById('hero-glow'); const hero_name = document.getElementById('hero-name'); const metrics_el = document.getElementById('metrics'); const content_el = document.getElementById('content'); // ---- HELPERS ---- // Muda uma CSS Custom Property no :root via JS function setCSSVar(k, v) { document.documentElement.style.setProperty(k, v); } // Converte #e1306c → "225,48,108" para usar em rgba() function hexToRGB(hex) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `${r},${g},${b}`; } // ---- RENDERIZAÇÃO ---- function renderCompanyTabs() { // Gera os botões da sidebar com template literal co_tabs.innerHTML = CO.map((c, i) => ` <button class="co-btn ${i === ci ? 'active' : ''}" onclick="selectCo(${i})"> <span class="co-dot"></span> ${c.name} <span class="co-yr">${YEARS[c.name] || ''}</span> </button>`).join(''); } function renderHero() { const c = CO[ci]; // 1. Muda o glow visual hero_glow.style.background = `radial-gradient(circle, ${c.color} 0%, transparent 70%)`; // 2. Muda o tema global via CSS vars setCSSVar('--acc', c.color); const rgb = hexToRGB(c.color); setCSSVar('--acc-d', `rgba(${rgb},.12)`); setCSSVar('--acc-g', `rgba(${rgb},.05)`); // 3. Preenche os textos hero_name.textContent = c.name; metrics_el.innerHTML = METRICS[c.name].map(m => ` <div class="met"> <div class="met-l">${m.l}</div> <div class="met-v">${m.v}</div> </div>`).join(''); } // Fade out → 140ms → render novo conteúdo function renderContent(animate = true) { if (animate) { content_el.classList.add('fading'); setTimeout(() => { content_el.classList.remove('fading'); _render(); }, 140); } else { _render(); // sem animação no load inicial } } // Render real do conteúdo (chamado após o fade) function _render() { const c = CO[ci]; if (tab === 'sectors') { // Grid de cards de setor content_el.innerHTML = `<div class="sec-grid"> ${c.sectors.map((s, i) => ` <div class="sec-card" onclick="openModal(${i})"> <div class="sc-icon">${s.icon}</div> <div class="sc-label">${s.setor}</div> <div class="sc-title">${s.title}</div> <div class="sc-desc">${s.desc}</div> <div class="sc-cta">Ver plano completo</div> </div> `).join('')} </div>`; } else { // Lista de 8 steps genéricos da estratégia content_el.innerHTML = `<div class="steps-list">...</div>`; } } // Troca de tab (sectors ↔ steps) function switchTab(t) { tab = t; document.getElementById('tab-sec').classList.toggle('on', t === 'sectors'); document.getElementById('tab-steps').classList.toggle('on', t === 'steps'); renderContent(); } // Seleciona empresa: atualiza estado, re-renderiza tudo function selectCo(i) { ci = i; renderCompanyTabs(); renderHero(); renderContent(); document.querySelector('.main').scrollTo({ top: 0, behavior: 'smooth' }); } // INIT: carrega sem animação (a página já aparece pronta) renderCompanyTabs(); renderHero(); renderContent(false);
`string ${expressão}` permite criar HTML com dados embutidos. .map().join('') transforma um array de objetos em uma string de HTML — esse é o padrão central do projeto.setTimeout antes de injetar o novo conteúdo. Se o JS renderizar antes do fade terminar, o usuário vê o conteúdo novo aparecer enquanto ainda está saindo.classList.toggle('on', condicao) é mais elegante que if/else com add/remove. Se a condição for true, adiciona a classe. Se false, remove. Um método substitui dois.createElement e innerHTML. Ao fechar, a classe .out é adicionada primeiro (animação de saída), e o elemento só é removido após 180ms.function openModal(si) { // si = índice do setor const c = CO[ci]; // empresa atual const s = c.sectors[si]; // setor clicado // Calcula as porcentagens das score bars const diffPct = (s.difficulty / 5 * 100) + '%'; const potPct = (s.potential / 5 * 100) + '%'; // Cor da barra baseada no valor const diffColor = s.difficulty >= 4 ? '#ef4444' // difícil = vermelho : s.difficulty >= 3 ? '#f59e0b' // médio = laranja : '#10b981'; // fácil = verde // Gera HTML dos steps com animação escalonada const stepsHtml = s.steps.map((st, i) => ` <div class="step" style="animation:fadeIn .3s ease ${i*0.05}s both"> <div class="step-n">${String(i+1).padStart(2,'0')}</div> <div> <div class="step-t">${st.t}</div> <div class="step-d">${st.d}</div> </div> </div>`).join(''); // Gera HTML dos pros (vantagens) const prosHtml = s.pros.map(p => ` <div class="pc-card pro"> <div class="pc-icon">✓</div> <div class="pc-t">${p.t}</div> <div class="pc-d">${p.d}</div> </div>`).join(''); // Gera HTML dos cons (desvantagens) const consHtml = s.cons.map(p => ` <div class="pc-card con"> <div class="pc-icon">✕</div> <div class="pc-t">${p.t}</div> <div class="pc-d">${p.d}</div> </div>`).join(''); // Monta o HTML completo do modal const html = ` <div class="modal-bg" id="mbg" onclick="closeModalBg(event)"> <div class="modal"> <div class="modal-head"> <div class="modal-glow" style="background:radial-gradient(circle,${c.color} 0%,transparent 70%)"></div> <div class="modal-icon">${s.icon}</div> <div> <div class="modal-setor">${c.name} → ${s.setor}</div> <div class="modal-title">${s.title}</div> </div> <button class="modal-close" onclick="closeModal()">✕</button> </div> <div class="scores"> <div class="score-item"> <div class="score-lbl">Dificuldade</div> <div class="score-bar"> <div class="score-fill" style="width:${diffPct};background:${diffColor}"></div> </div> </div> </div> <div class="modal-body"> <div>${stepsHtml}</div> <div>${prosHtml}${consHtml}</div> </div> </div> </div>`; // Injeta no DOM via createElement const div = document.createElement('div'); div.id = 'modal-root'; div.innerHTML = html; document.body.appendChild(div); // Impede scroll da página enquanto modal está aberto document.body.style.overflow = 'hidden'; } // Fecha se clicou no fundo (não no modal em si) function closeModalBg(e) { if (e.target.id === 'mbg') closeModal(); } function closeModal() { const bg = document.getElementById('mbg'); if (bg) { bg.classList.add('out'); // trigger da animação CSS de saída setTimeout(() => { document.getElementById('modal-root')?.remove(); // remove do DOM document.body.style.overflow = ''; // restaura scroll }, 180); // aguarda a animação .out terminar (180ms) } } // ESC fecha o modal document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
1 em 01, 2 em 02 etc. O argumento 2 é o comprimento mínimo e '0' é o caractere de preenchimento. Cria os números dos steps com zero à esquerda.id="mbg". Quando o usuário clica no modal em si (não no overlay), o event.target é um elemento interno. Essa verificação garante que fechar só acontece ao clicar fora do modal.element?.remove() só chama remove() se o elemento existir. Sem o ?., se o usuário clicar ESC duas vezes rapidamente, o segundo clique tentaria remover um elemento já removido e causaria erro.// Função auxiliar: muda uma CSS var no :root function setCSSVar(name, value) { document.documentElement.style.setProperty(name, value); // document.documentElement = o elemento <html> // setProperty('--acc', '#e1306c') é equivalente a: // :root { --acc: #e1306c } no CSS } // Função auxiliar: converte hex para string RGB function hexToRGB(hex) { const r = parseInt(hex.slice(1, 3), 16); // pega os 2 chars de R const g = parseInt(hex.slice(3, 5), 16); // pega os 2 chars de G const b = parseInt(hex.slice(5, 7), 16); // pega os 2 chars de B return `${r},${g},${b}`; // '#e1306c' → '225,48,108' } // Quando o usuário muda de empresa: function renderHero() { const c = CO[ci]; const rgb = hexToRGB(c.color); // Instagram → '#e1306c' // WhatsApp → '#25d366' // Uber → '#1d4ed8' // Airbnb → '#ff5a5f' // ChatGPT → '#10a37f' // Lovable → '#7c3aed' setCSSVar('--acc', c.color); setCSSVar('--acc-d', `rgba(${rgb},.12)`); setCSSVar('--acc-g', `rgba(${rgb},.05)`); } // Resultado: ao mudar para WhatsApp, o CSS vira: // :root { // --acc: #25d366; // --acc-d: rgba(37,211,102,.12); // --acc-g: rgba(37,211,102,.05); // } // // Todos esses elementos atualizam automaticamente: // .co-btn.active → fundo verde // .co-btn.active::before → linha verde // .tab.on → underline verde // .hero-period → texto verde // .strat-box → borda verde // .acc-d em badges → fundo verde suave // score-fill (via inline style) → usa a cor diretamente
<html> — o mesmo alvo do seletor :root no CSS. Ao fazer setProperty nele, é como editar o :root em tempo real.--acc para texto e bordas), com 12% de opacidade (--acc-d para fundos de cards) e com 5% (--acc-g para o fundo do bloco de estratégia). Separar permite usar a mesma cor com transparência diferente sem recalcular toda vez.parseInt('e1', 16) converte o string hexadecimal 'e1' para o número decimal 225. O segundo argumento 16 é a base (hexadecimal). Isso é como você converte cor hex para RGB em JavaScript puro.| Função | O que faz | Chamada quando |
|---|---|---|
| setCSSVar(k,v) | Muda CSS var no :root via JS | renderHero() |
| hexToRGB(hex) | Converte #rrggbb → 'r,g,b' | renderHero() |
| renderCompanyTabs() | Gera os botões da sidebar | selectCo(), init |
| renderHero() | Atualiza tema, nome, métricas | selectCo(), init |
| renderContent(animate) | Fade out → chama _render() | selectCo(), switchTab() |
| _render() | Injeta HTML do tab ativo no DOM | renderContent() |
| switchTab(t) | Alterna sectors ↔ steps | click nos botões de tab |
| selectCo(i) | Muda empresa, re-renderiza tudo | click nos botões da sidebar |
| openModal(si) | Cria e injeta o modal no DOM | click nos cards de setor |
| closeModalBg(e) | Fecha se clicou no fundo | onclick no .modal-bg |
| closeModal() | Anima saída e remove do DOM | botão ✕, ESC, closeModalBg |