/* * Temas sazonais do Portal — trocam a marca P.I.D. (favicon, ícone do login, * ícone/assinatura da sidebar) por uma variante temática durante uma janela * de datas. Dois hoje: "Halloween" (vampiro, 16 a 31/10) e "Aniversário" * (56 anos da De Paula Contadores, só 15/10 — balões + intro de festa) — as * duas janelas são mutuamente exclusivas por desenho (15 vs 16-31), nunca * ativas ao mesmo tempo. Fora de qualquer janela, tudo volta ao P.I.D. normal * (`pid-icon-eye`/`is-blinking`). Ver `docs/temas-sazonais/temas-sazonais.md`. * Consumido por `sidebar-brand.js`, `login-logo-blink.js` e `auth.js` (troca * de intro); incluído em toda página (mesmo padrão de `confirm-modal.js`) * por isso mora aqui, não num arquivo específico de página. * * PID_HALLOWEEN_PREVIEW_FORCE/PID_ANIVERSARIO_PREVIEW_FORCE forçam o tema * ligado o ano todo pra revisão visual; os dois já foram validados e * desligados pelo usuário — a partir daqui só a checagem de data abaixo * decide (todo ano). As constantes ficam aqui de propósito, em vez de * removidas, pra revisar o visual de novo sem ter que mexer no relógio. * * O Halloween só passou a começar em 16/10 (antes: outubro inteiro) porque * 15/10 é o aniversário da empresa e ganhou tema próprio — os dois nunca * disputavam o mesmo dia antes disso por acaso, agora é por definição. */ const PID_HALLOWEEN_PREVIEW_FORCE = false; const PID_ANIVERSARIO_PREVIEW_FORCE = false; const PID_HALLOWEEN_MES = 9; // outubro, 0-indexed (Date#getMonth()) const PID_HALLOWEEN_DIA_INICIO = 16; // 16 a 31/10 — dia 15 é do tema de aniversário const PID_ANIVERSARIO_MES = 9; // outubro, 0-indexed const PID_ANIVERSARIO_DIA = 15; // aniversário da De Paula Contadores const PID_HALLOWEEN_SCARE_MS = 900; const PID_ANIVERSARIO_PARTY_MS = 650; /* Preferência do usuário (menu da conta → "Temas sazonais") pra desligar qualquer tema sazonal (hoje só o Halloween, mas vale pra futuros também) e manter sempre o P.I.D. padrão — motivos: gosto pessoal ou, no caso do Halloween, aracnofobia. Mesmo padrão de armazenamento do tema claro/escuro (`theme.js`, só `localStorage`, nunca migrado pro banco — é preferência de navegador, não dado de negócio). Tem prioridade sobre tudo, inclusive `PID_HALLOWEEN_PREVIEW_FORCE`: se o usuário desligou, fica desligado. */ const PID_SEASONAL_OPT_OUT_KEY = "pid_seasonal_theme_opt_out"; function pidSeasonalThemesOptedOut() { try { return localStorage.getItem(PID_SEASONAL_OPT_OUT_KEY) === "1"; } catch (e) { return false; } } function pidSetSeasonalThemesOptedOut(optedOut) { try { if (optedOut) { localStorage.setItem(PID_SEASONAL_OPT_OUT_KEY, "1"); } else { localStorage.removeItem(PID_SEASONAL_OPT_OUT_KEY); } } catch (e) { // localStorage indisponível (modo privado, storage bloqueado etc.) — a // preferência simplesmente não persiste, mas a página continua funcionando. } } function pidIsHalloweenActive(data) { if (pidSeasonalThemesOptedOut()) return false; if (PID_HALLOWEEN_PREVIEW_FORCE) return true; const hoje = data || new Date(); return hoje.getMonth() === PID_HALLOWEEN_MES && hoje.getDate() >= PID_HALLOWEEN_DIA_INICIO; } function pidIsAniversarioActive(data) { if (pidSeasonalThemesOptedOut()) return false; if (PID_ANIVERSARIO_PREVIEW_FORCE) return true; const hoje = data || new Date(); return hoje.getMonth() === PID_ANIVERSARIO_MES && hoje.getDate() === PID_ANIVERSARIO_DIA; } /* `iconFile` é o nome do arquivo em `static/img/` a usar como favicon (`pid-halloween-icone-escuro.svg` ou `pid-aniversario-icone-escuro.svg`) — sempre a variante "ícone/cores completas" de cada tema, nunca um favicon dedicado simplificado: o favicon "sem balões"/"sem capa" pensado só pra tamanhos bem pequenos destoava do resto da marca sazonal na aba do navegador (mesmo motivo pelo qual o P.I.D. normal também usa `pid-icone-escuro.svg` como favicon em vez do `pid-favicon.svg` dedicado — ver "Logos em `static/img/`" no CLAUDE.md). */ function pidApplySeasonalFavicon(iconFile) { const link = document.querySelector('link[rel="icon"]'); if (link && link.href.indexOf("pid-icone-escuro.svg") !== -1) { link.href = link.href.replace("pid-icone-escuro.svg", iconFile); } } const PID_VAMP_FACE_FRIENDLY = ` `; const PID_VAMP_FACE_SCARE = ` `; function pidVampireIconRigMarkup() { return ` ${PID_VAMP_FACE_FRIENDLY} ${PID_VAMP_FACE_SCARE}`; } /* Substitui o conteúdo de um de ícone (viewBox 0 0 64 64, o P.I.D. de sempre) pelo vampiro (viewBox 0 0 76 80) — usado tanto no ícone da sidebar colapsada quanto no botão da logo do login. `dataset.vampApplied` evita reaplicar se a função rodar de novo (não deveria, mas por segurança). */ function pidApplyVampireIcon(svg) { if (!svg || svg.dataset.vampApplied) return; svg.setAttribute("viewBox", "0 0 76 80"); svg.innerHTML = `${pidVampireIconRigMarkup()}`; svg.dataset.vampApplied = "1"; } /* Idem, pra assinatura horizontal (ícone + "P.I.D." + tagline) da sidebar expandida — viewBox 0 0 320 96 (a versão vampiro é um pouco mais larga que os 300x80 do P.I.D. normal, por causa da capa). */ function pidApplyVampireFull(svg) { if (!svg || svg.dataset.vampApplied) return; svg.setAttribute("viewBox", "0 0 320 96"); svg.innerHTML = ` ${pidVampireIconRigMarkup()} P.I.D. PORTAL INTERNO DA DE PAULA`; svg.dataset.vampApplied = "1"; } /* Dispara a animação de "susto" (`.is-scaring`/`pidVampShake` etc. em base.css) em todo `.pid-vamp-icon-rig` dentro de `root` — substitui a piscada de olhos (`.pid-icon-eye`/`is-blinking`) enquanto o tema Halloween está ativo. Remove e reaplica a classe (em vez de só adicionar) pra permitir cliques repetidos re-tocarem a animação do zero. */ function pidTriggerVampireScare(root) { root.querySelectorAll(".pid-vamp-icon-rig").forEach((rig) => { rig.classList.remove("is-scaring"); void rig.getBoundingClientRect(); rig.classList.add("is-scaring"); }); } /* Tema "Aniversário" (56 anos da De Paula Contadores, só 15/10) — porta da marca comemorativa autorada em "P.I.D. Logo Design Aniversário/pid-marca-56" (pasta de logos, fora do repositório; ver leiame.md lá: "o personagem continua o mesmo, o acento é o número"). Ao contrário do Halloween, a cara de repouso não muda (mesmos olhos/sorriso do P.I.D. normal, por isso o markup abaixo preserva os dois `.pid-icon-eye` — a piscada ao clicar continua funcionando fora do clique de festa) — só ganha três balões (azul/âmbar/rosa) segurados por um braço saindo da lateral direita do corpo, mais um bico-de-pato/confete escondidos que só aparecem no clique de festa (ver `pidTriggerAniversarioParty()` abaixo). Cópias dos SVGs de referência vivem em `static/img/pid-aniversario-*.svg` (mesmo papel de `pid-halloween-*.svg`: não usadas diretamente pela UI, que injeta o markup inline — só o favicon aponta pro arquivo de verdade). "56 ANOS" é específico deste ano — ano que vem, atualizar o número aqui, no rótulo da pílula abaixo e nos 3 arquivos `pid-aniversario-*.svg` a partir de uma pasta `pid-marca-57` nova (mesmo espírito de "marca temporária" do leiame.md original). Estrutura em dois `` aninhados: `.pid-aniv-hop` (sem nenhum `transform` de atributo XML — recebe o "pulo" via CSS/`is-partying`, ver base.css) por fora, `.pid-aniv-icon-rig` (o `transform="translate(...) scale(...)"` estático que posiciona o desenho dentro da caixa 64×64/300×80) por dentro — separar os dois evita o mesmo problema já documentado em `.pid-icon-eye` (base.css): um `transform` de CSS aplicado ao MESMO elemento que já tem um `transform` de atributo substitui o atributo inteiro, perdendo a posição. */ function pidAniversarioIconRigMarkup() { return ` `; } /* Substitui o conteúdo de um de ícone (viewBox 0 0 64 64, mesmo do P.I.D. normal — ao contrário do vampiro, não precisa alargar a caixa) pelo ícone com balões. `dataset.aniversarioApplied` evita reaplicar (mesmo padrão de `dataset.vampApplied`). */ function pidApplyAniversarioIcon(svg) { if (!svg || svg.dataset.aniversarioApplied) return; svg.innerHTML = `${pidAniversarioIconRigMarkup()}`; svg.dataset.aniversarioApplied = "1"; } /* Idem, pra assinatura horizontal da sidebar expandida — viewBox 0 0 300 80, igual ao P.I.D. normal (os balões cabem na mesma caixa, sem alargar como o vampiro precisou por causa da capa). Ganha uma pílula dourada "56 ANOS" ao lado do wordmark, no lugar da tagline normal. */ function pidApplyAniversarioFull(svg) { if (!svg || svg.dataset.aniversarioApplied) return; svg.innerHTML = ` ${pidAniversarioIconRigMarkup()} P.I.D. 56 ANOS PORTAL INTERNO DA DE PAULA`; svg.dataset.aniversarioApplied = "1"; } /* Dispara o "clique de festa" (`.is-partying`, keyframes em base.css) em todo `.pid-aniv-hop` dentro de `root` — o personagem pula, o braço joga confete pro alto e a língua-de-sogra desenrola da boca, num ciclo só de `PID_ANIVERSARIO_PARTY_MS`. Ao contrário do susto do vampiro, isso não substitui a piscada de olhos — os dois convivem (a piscada continua disparando `.pid-icon-eye`/`is-blinking` de dentro do próprio clique de festa, ver `sidebar-brand.js`/`login-logo-blink.js`). Mesmo truque de remover+reaplicar a classe (em vez de só adicionar) de `pidTriggerVampireScare` pra permitir cliques repetidos re-tocarem do zero. */ function pidTriggerAniversarioParty(root) { root.querySelectorAll(".pid-aniv-hop").forEach((hop) => { hop.classList.remove("is-partying"); void hop.getBoundingClientRect(); hop.classList.add("is-partying"); }); } /* Teia de aranha decorativa de canto (`static/css/halloween-cobweb.css`) — markup só existe em memória (nunca hardcoded no template) pra não sobrar nada no HTML fora da janela do Halloween. `.pw-spider`/`.pw-legs` (mesmas classes usadas pela animação em CSS) andam/mexem sozinhas via `@keyframes` contínuo, sem precisar de mais JS depois de injetadas. */ const PID_COBWEB_SVG = ``; /* Quais aranhas já "morreram" (clicadas numa visita anterior) — persistido pra sobreviver a um F5/nova navegação, pedido explícito do usuário ("guardar que a aranha foi morta... até executar o comando tab+b que reseta a aranha"). Cada aranha tem um id próprio (`${containerId}__tl`/ `__br` — os dois cantos de cada uma das 2 páginas, 4 ids possíveis no total) guardado como array JSON num único `localStorage`, mesmo padrão de armazenamento das outras preferências sazonais (`pid_theme`, `pid_seasonal_theme_opt_out`) — nunca migrado pro banco. */ const PID_DEAD_SPIDERS_KEY = "pid_seasonal_dead_spiders"; function pidGetDeadSpiders() { try { const raw = JSON.parse(localStorage.getItem(PID_DEAD_SPIDERS_KEY) || "[]"); return Array.isArray(raw) ? raw : []; } catch (e) { return []; } } function pidMarkSpiderDead(spiderId) { try { const dead = pidGetDeadSpiders(); if (!dead.includes(spiderId)) { dead.push(spiderId); localStorage.setItem(PID_DEAD_SPIDERS_KEY, JSON.stringify(dead)); } } catch (e) { // localStorage indisponível — a aranha "morre" só nesta visita, sem persistir } } function pidResetDeadSpiders() { try { localStorage.removeItem(PID_DEAD_SPIDERS_KEY); } catch (e) { // ignorado } } /* Preenche o container (já presente e `hidden` no template, `#pw-cobwebs-login` em index.html ou `#pw-cobwebs-portal` em portal.html) com os 2 cantos (superior-esquerdo/inferior-direito, pedido explícito do usuário) e revela. Se a aranha daquele canto já morreu numa visita anterior (`pidGetDeadSpiders()`), nasce direto com `.is-dead` (sem tocar a animação de novo — só um "estado final" instantâneo). Senão, liga o clique (`.pw-spider`, `pointer-events:auto` só nela — o resto da teia continua `pointer-events:none`): "morre" (cai girando e some, pernas encolhem — `.is-dying`/`pwSpiderDie` em halloween-cobweb.css) e uma caveirinha sobe da teia (`.pw-skull`, `.is-rising`/`pwSkullRise`), deixando a teia vazia — e grava o óbito (`pidMarkSpiderDead()`), que só é desfeito pelo atalho de teclado Tab+B (ver `pidBindSeasonalResetShortcut()` abaixo). É um no-op seguro em qualquer outra página (o id não existe ali). */ function pidApplyHalloweenCobwebs(containerId) { const el = document.getElementById(containerId); if (!el || el.dataset.cobwebApplied) return; el.innerHTML = `
${PID_COBWEB_SVG}
` + `
${PID_COBWEB_SVG}
`; el.hidden = false; el.dataset.cobwebApplied = "1"; const deadSpiders = pidGetDeadSpiders(); el.querySelectorAll(".pw-corner").forEach((cornerEl) => { const corner = cornerEl.classList.contains("pw-corner--tl") ? "tl" : "br"; const spiderId = `${containerId}__${corner}`; const spider = cornerEl.querySelector(".pw-spider"); if (!spider) return; if (deadSpiders.includes(spiderId)) { spider.classList.add("is-dead"); return; } spider.addEventListener( "click", () => { const skull = spider.parentElement.querySelector(".pw-skull"); spider.classList.add("is-dying"); if (skull) skull.classList.add("is-rising"); pidMarkSpiderDead(spiderId); }, { once: true } ); }); } /* Bolo de aniversário decorativo, tema sazonal Aniversário — só na tela Principal (`portal.html`), fixo no canto inferior-direito da JANELA (`position:fixed`, não da página — sempre visível, mesmo com scroll). Clicar apaga as velinhas (`.is-blowing`/`pidCakeFlameOut` + `pidCakeSmokeRise` em `aniversario-decor.css`, 1x só), e elas ficam apagadas até o próximo carregamento da página. **O estado apagado NÃO é persistido**, ao contrário da aranha morta do Halloween — e isso é deliberado, não esquecimento. A persistência chegou a existir aqui (copiada de `pidMarkSpiderDead`) e foi removida: as velas nasciam apagadas pro resto da vida depois do primeiro clique, sem nenhuma pista disso na tela, e só voltavam com o atalho Tab+B. O usuário relatou como bug ("não aparece o fogo das velinhas"), porque é um easter egg que some sozinho e não volta. A aranha persiste porque o usuário PEDIU explicitamente; aqui ele pediu só "ao clicar nele as velas se apagam". */ /* As chamas nascem exatamente no topo de cada vela (`y` do ``), não alguns pontos acima — na primeira versão flutuavam soltas bem longe do pavio. O gotejamento de cada camada é da cor da camada DE CIMA (creme escorrendo sobre o dourado, dourado escorrendo sobre o roxo), senão fica invisível contra a própria camada. `.pid-cake-halo` é o halo de luz no ar em volta das velas (o brilho colado em cada chama vem de um `drop-shadow` no CSS) — fica DEPOIS das camadas do bolo e ANTES das velas, pra a luz lavar o topo do bolo sem desbotar as chamas, que continuam nítidas por cima. O `` mora num `` com id fixo: o bolo é injetado uma vez só, em portal.html, então não há risco de id duplicado (mesmo padrão do `clipPath` da intro do Halloween em index.html). */ const PID_CAKE_SVG = ``; /* Preenche o container (já presente e `hidden` no template, `#pid-aniv-cake` em portal.html) e liga o clique — botão porque é interativo (`aria-label`, foco por teclado funcionam de graça). O clique adiciona `.is-blowing` (toca `pidCakeFlameOut`/`pidCakeSmokeRise` uma vez) e, depois de `PID_CAKE_BLOW_MS`, troca pra `.is-blown` (estado final em repouso, sem `animation` nenhuma rodando) — que dura só até o próximo carregamento, ver o comentário acima. É um no-op seguro em qualquer outra página (o id não existe ali). */ const PID_CAKE_BLOW_MS = 1400; function pidApplyAniversarioCake(containerId) { const el = document.getElementById(containerId); if (!el || el.dataset.cakeApplied) return; el.innerHTML = ``; el.hidden = false; el.dataset.cakeApplied = "1"; const btn = el.querySelector(".pid-aniv-cake__btn"); if (!btn) return; btn.addEventListener( "click", () => { el.classList.add("is-blowing"); setTimeout(() => { el.classList.remove("is-blowing"); el.classList.add("is-blown"); }, PID_CAKE_BLOW_MS); }, { once: true } ); } /* Atalho de teclado Tab+B (segurar Tab e apertar B) — reseta as aranhas "mortas" (`pidResetDeadSpiders()`) salvas e recarrega a página, mesmo raciocínio do toggle de opt-out (`account.js`): o estado já foi aplicado no carregamento atual, não dá pra desfazer in-place. Não chama `preventDefault()` no Tab (continuaria navegando o foco normalmente, acessibilidade intacta) — só rastreia se ele está pressionado no momento em que o B chega. Incluído em toda página (mesmo padrão de `confirm-modal.js`), mas só tem efeito onde há aranha salva como morta. O bolo do tema de Aniversário não entra aqui: as velinhas reacendem sozinhas a cada carregamento, então não há o que resetar. */ document.addEventListener("DOMContentLoaded", () => { let tabHeld = false; document.addEventListener("keydown", (event) => { if (event.key === "Tab") { tabHeld = true; return; } if (tabHeld && (event.key === "b" || event.key === "B") && pidGetDeadSpiders().length) { pidResetDeadSpiders(); window.location.reload(); } }); document.addEventListener("keyup", (event) => { if (event.key === "Tab") tabHeld = false; }); });