/* * Tema sazonal "Halloween" do Portal — troca a marca P.I.D. (favicon, ícone * do login, ícone/assinatura da sidebar) pela variante "vampiro" durante uma * janela de datas (outubro) e troca a piscada de olhos ao clicar na logo por * uma animação de "susto" (capa se abre, olhos ficam vermelhos e raivosos, * presas de fora, tremida + brilho vermelho) só nesse período — fora da * 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 forçava o tema ligado o ano todo pra revisão * visual; validado e desligado pelo usuário — a partir daqui só a checagem * de mês abaixo decide (outubro, todo ano). */ const PID_HALLOWEEN_PREVIEW_FORCE = false; const PID_HALLOWEEN_MES = 9; // outubro, 0-indexed (Date#getMonth()) const PID_HALLOWEEN_SCARE_MS = 900; /* 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; } /* Usa `pid-halloween-icone-escuro.svg` (o mesmo ícone/cores do vampiro usado na sidebar e no login), não `pid-halloween-favicon.svg` — esse último é uma variante simplificada (sem capa/gola, cores diferentes, pensada só pra tamanhos bem pequenos) e destoava do resto da marca sazonal na aba do navegador. Mesmo padrão do P.I.D. normal, cujo favicon também usa `pid-icone-escuro.svg` em vez do `pid-favicon.svg` dedicado — ver "Logos em `static/img/`" no CLAUDE.md. */ function pidApplySeasonalFavicon() { 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", "pid-halloween-icone-escuro.svg"); } } 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"); }); } /* 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 } ); }); } /* Atalho de teclado Tab+B (segurar Tab e apertar B) — reseta todas as aranhas "mortas" salvas (`pidResetDeadSpiders()`) 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. */ 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; }); });