18 KiB
Temas Sazonais
Criado em 2026-09-15, seguindo o mesmo padrão das demais aplicações sem pacote Python dedicado (ver
CLAUDE.mdna raiz, "Documentação dividida por aplicação") — como o Halloween foi só o primeiro de possivelmente vários temas sazonais, faz mais sentido documentar isso como uma "aplicação" própria desde já do que deixar acumulando dentro doCLAUDE.mdraiz. Este arquivo não é auto-carregado pelo Claude Code (não há pacote Python — o mecanismo é só frontend,static/js/static/css/templates) — leia manualmente ao mexer nisto. VerCLAUDE.mdna raiz para arquitetura geral/transversal do Portal.
Troca sazonal da marca P.I.D. (logo, ícone da sidebar/login, favicon, intro de abertura) por uma variante temática durante uma janela de datas — hoje só o Halloween (mascote "vampiro", outubro), mas o mecanismo (pidIsHalloweenActive()/seasonal-theme.js) foi pensado pra ser genérico o bastante pra outro tema sazonal futuro reaproveitar o mesmo esqueleto sem reescrever nada.
Status atual
Confirmado e programado — revisão visual concluída pelo usuário. PID_HALLOWEEN_PREVIEW_FORCE = false em static/js/seasonal-theme.js (deixado de propósito na constante, documentando a decisão, em vez de removido): daqui pra frente só a janela de outubro (PID_HALLOWEEN_MES = 9, Date#getMonth() 0-indexed, mês inteiro, todo ano) decide se o tema está ativo, sem nenhuma ação manual — nem pra ligar em outubro, nem pra desligar depois. Nada mais precisa mudar de código pra isso valer.
Origem dos assets (Halloween)
Mascote "vampiro" (a mesma marca P.I.D. fantasiada de Drácula — capa com gola erguida e forro vinho, lápis de olho, dois dentinhos) desenhada em C:\Users\Depaula\Documents\Logos\P.I.D. Logo Design Halloween\pid-marca-halloween\ (SVGs de referência + leiame.md, fora do repositório, mesmo papel de pid-marca/ pra marca "P.I.D." normal — ver "Logos em static/img/" no CLAUDE.md raiz) e a intro de abertura em ...\P.I.D. Logo Design Halloween\Logo intro animation Halloween\export-halloween\pid-intro-halloween.html (mesmo formato de bundle "Design Canvas" do pid-intro-escuro.html original — ver CLAUDE.md raiz, "Animação de intro do login" → "Origem do arquivo"; a composição (PidHalloween, cenas Rise/Scare/Settle/Wordmark, 1,3s/1,5s/1,4s/1,8s) foi extraída do bundle com o mesmo processo ad-hoc (gzip+base64 por uuid) e portada fielmente pra vanilla JS/CSS). Cópias dos SVGs de referência (não usadas diretamente pela UI, que injeta o markup inline — ver abaixo) vivem em static/img/pid-halloween-favicon.svg/pid-halloween-icone-escuro.svg/pid-halloween-logo-horizontal-escuro.svg.
static/js/seasonal-theme.js
Incluído em toda página (mesmo padrão de confirm-modal.js, logo depois dele) — concentra tudo que é compartilhado entre login e sidebar:
pidIsHalloweenActive()— a checagem de data (mais o preview force e o opt-out do usuário, ver "Opt-out do usuário" abaixo — checado antes de tudo, prioridade máxima).pidApplySeasonalFavicon()— troca<link rel="icon">depid-icone-escuro.svgprapid-halloween-icone-escuro.svgvia substring nohref(funciona com qualquer prefixo deSTATIC_URL). Não usapid-halloween-favicon.svg— essa é uma variante simplificada (sem capa/gola, cores diferentes) que destoava do resto da marca sazonal na aba do navegador (bug relatado pelo usuário logo na revisão visual); mesmo padrão do P.I.D. normal, cujo favicon também usapid-icone-escuro.svgem vez dopid-favicon.svgdedicado.pidApplyVampireIcon(svg)/pidApplyVampireFull(svg)— substituem oviewBox+innerHTMLde um<svg>existente (ícone 64×64 → vampiro 76×80; assinatura horizontal 300×80 → 320×96) pelo markup do vampiro, com dois grupos de rosto sempre presentes (.pid-vamp-face--friendly/--scare, cruzando opacidade) dentro de um<g class="pid-vamp-icon-rig">— mesma ideia de "dois estados sempre no DOM, alternando opacidade" já usada pelo crossfade da sidebar (sidebar__logo--full/--icon).pidTriggerVampireScare(root)— dispara a animação de susto (.is-scaring, verbase.css) em todo.pid-vamp-icon-rigdentro deroot.
Troca do ícone/assinatura (sidebar + login)
Como o markup do ícone P.I.D. normal já é idêntico (copiado inline) nas 13 páginas-shell + login, a troca é feita em runtime, não editando cada template — sidebar-brand.js/login-logo-blink.js chamam pidApplyVampireIcon/pidApplyVampireFull/pidApplySeasonalFavicon no próprio DOMContentLoaded, só se pidIsHalloweenActive(). Isso também é o que torna o "retorno" depois de outubro automático: o innerHTML original nunca é tocado no arquivo, só sobrescrito em memória a cada carregamento de página enquanto a janela estiver ativa.
Susto ao clicar (substitui a piscada de olhos)
Fora do Halloween, clicar na logo pisca os olhos (.pid-icon-eye/is-blinking, comportamento original, intocado). Durante o Halloween, o mesmo clique dispara pidTriggerVampireScare() — .pid-vamp-icon-rig.is-scaring (base.css, logo depois do bloco de piscada) toca 4 animações de 900ms em paralelo: pidVampFaceOut/pidVampFaceIn (crossfade rosto amigável → raivoso e de volta), pidVampCapeBurst (a capa "estufa" com scale+rotate) e pidVampShake (o ícone inteiro treme e ganha um brilho vermelho via filter:drop-shadow) — tudo CSS puro (sem requestAnimationFrame), disparado só pela troca de classe, mesmo padrão da piscada original. Na sidebar, a navegação pra portal.html é adiada PID_HALLOWEEN_SCARE_MS - 100 (800ms, contra 260ms da piscada normal) pra dar tempo de ver o susto antes de trocar de página; no login não há navegação, é só o toque decorativo.
Intro de abertura pós-login (static/css/login-intro-halloween.css + static/js/login-intro-halloween.js)
Variante completa de login-intro.css/.js, com seu próprio overlay (#login-intro-halloween, mesma estrutura de #login-intro em index.html, hidden por padrão) — reaproveita as funções de easing/animate/clamp/corner-target já globais de login-intro.js (PID_INTRO_EASING, pidIntroAnimate, pidIntroClamp, pidIntroCornerTarget) em vez de duplicá-las, só a coreografia é própria. auth.js escolhe qual intro chamar no sucesso do login (pidPlayLoginIntroHalloween vs pidPlayLoginIntro) com base em pidIsHalloweenActive().
- Cenas (
PID_INTRO_H_CUES/PID_INTRO_H_TOTAL—Rise:0, Scare:1.3, Settle:2.8, Wordmark:4.2, Fly:6.0, total6.9):Rise(a figura surge do escuro com a capa erguida cobrindo o rosto, só os olhos semicerrados espiando —squint),Scare(a capa se abre numa investida —lunge/push/tremida —, revelando o rosto raivoso com sobrancelhas e presas, mais um flash vermelho de tela inteira via#login-intro-h-flash),Settle(a capa volta a fechar, o rosto cruza pro amigável de sempre),Wordmark(a figura desliza, "P.I.D." aparece na fonte "Creepster" com régua dourado→vinho e a tagline). As primeiras 4 cenas (Rise/Scare/Settle/Wordmark, somando 6,0s) são a composição original praticamente 1:1 — mesmas fórmulas/janelas de tempo depid-intro-halloween.html, sem reproporcionar nada (ao contrário da intro normal, que teve que comprimir as cenas — aqui não foi pedido acelerar). Fly(~0,9s, acrescentada, não existe no arquivo original): a composição-fonte foi pensada como vídeo solto (tudo esmaece junto no final); pra se integrar ao fluxo do Portal (a mesma razão documentada noCLAUDE.mdraiz sobre o "voo" da intro normal), foi adicionada uma cena final equivalente à cena "Portal" da intro normal — a wordmark esmaece, o ícone encolhe e voa até o canto real da janela (mesmopidIntroCornerTarget(), reaproveitado) e o stage inteiro esmaece por cima do fim do voo.- Reescala de tamanho: a composição original foi autorada pra um ícone de 460px (uso como hero de vídeo); aqui o ícone roda a 200px (
PID_INTRO_H_ICON_K = 200/460), então todo deslocamento em "px" (a subida inicial, a tremida, o desvio vertical da investida, o blur do brilho) é multiplicado por esse fator — as transformações internas do SVG (a capa abrindo, o ângulo da aba) continuam em unidades do próprioviewBox(76×80, igual ao das duas variantes vampiro emstatic/img/) e não precisam de reescala. - Tema claro: mesmo raciocínio da intro normal — fundo acompanha
--bg-canvas, texto do wordmark vira escuro (#3a1620) pra continuar legível; o ícone não precisa de override (roxo/vinho com contraste de sobra nos dois fundos).
Teias de aranha de canto (static/css/halloween-cobweb.css)
Decoração de canto pedida pelo usuário depois de revisar um protótipo isolado publicado como Artifact ("Cantos Assombrados", fora do repositório — comparava 2 vs 4 cantos e aranha andando vs parada antes de aplicar de verdade). Só em 2 cantos (superior-esquerdo/inferior-direito, os mesmos marcados por ele num print da tela real) e só em duas páginas: index.html (login) e portal.html (tela Principal) — nenhum outro shell. Cada teia é um quarto de círculo (8 fios retos + 5 anéis em espiral levemente arqueados — <path class="pw-ring"> com curvas Q em vez de polilinhas retas, pra não parecer um desenho geométrico cru — mais 1 fio "perdido" bem mais comprido que os outros, se estendendo pra fora do canto; todas as coordenadas calculadas por trigonometria, não à mão) com uma aranha (<g class="pw-spider">, corpo+cabeça+8 pernas) que anda pra fora ao longo de um dos fios e volta (@keyframes pwWalk, 8s, com pausas nas pontas) enquanto as pernas mexem continuamente (pwLegsWiggle) — tudo CSS puro, sem requestAnimationFrame. Desenho da teia refeito numa rodada seguinte a partir de uma referência visual que o usuário anexou (queria "algo menos bruto", mais profissional, "sem parecer que só foi colado um PNG"): a versão original tinha só 6 fios retos + 3 anéis poligonais (linhas retas entre pontos, sem curva nenhuma) — a v2 acima é mais densa perto do canto e mais esparsa longe dele, com as curvas arqueadas simulando a espiral de captura de uma teia real.
- Markup só existe em memória: igual ao ícone vampiro, o HTML da teia não fica hardcoded no template —
PID_COBWEB_SVG(string,seasonal-theme.js) é injetado dentro de um container já presente ehidden(#pw-cobwebs-login/#pw-cobwebs-portal) porpidApplyHalloweenCobwebs(containerId), chamada porlogin-logo-blink.js/sidebar-brand.jsno mesmoif (halloween)que já troca o ícone — fora de outubro, o container continua vazio ehidden. - Ancoragem diferente em cada página: no login, a teia é filha direta de
.login-page(ela já cobremin-height:100vh); na Principal, filha de.main-content(não o viewport, porque a sidebar já é sólida e cobriria o canto esquerdo de qualquer jeito) — ancorar ali garante que a teia nunca fica escondida atrás dela, com ou sem a sidebar colapsada. As duas usam a mesma regra base.pw-corners{position:absolute;inset:0}. - Tamanho: menor na Principal que no login (pedido explícito do usuário, "evitando cobrir muito as informações que constam na página" — a tela Principal tem cards/widgets perto das bordas, o login só tem o card centralizado), mas aumentado em duas rodadas seguintes ("ficaram muito pequenas", nas duas telas):
clamp(220px,22vw,320px)no login vsclamp(160px,15vw,230px)na Principal (valores atuais). z-index:-1(nãopointer-eventssozinho) garante que a teia fica atrás do conteúdo real, não só sem receber clique.- Bug real — a teia do login não aparecia de jeito nenhum:
z-index:-1só funciona como esperado dentro de um contexto de empilhamento isolado; sem isso, um elemento negativo escapa pro contexto raiz da página e qualquer irmão estático com fundo opaco pinta por cima dele (é exatamente o caso de.login-page, que tem fundo cobrindo a tela inteira) — a teia ficava sempre atrás desse fundo, nunca visível. Na Principal o mesmo problema existia, só que passava despercebido porque.main-contentnão tem fundo opaco cobrindo tudo (a teia só aparecia nos vãos sem conteúdo por cima, "sumindo" só parcialmente/nos cantos exatos). Corrigido comisolation:isolateem.login-page(login.css) e em.main-content(layout.css, junto doposition:relativejá existente) — isola um contexto de empilhamento próprio pra cada uma, e a teia (filha direta,z-index:-1) passa a ficar corretamente atrás do fundo e na frente do conteúdo real nas duas. - Canto superior-esquerdo da Principal escondido atrás da topbar: a
.topbar(position:sticky,z-index:20, fundo opaco —layout.css) cobria a metade de cima da teia desse canto..pw-corners--portal .pw-corner--tlganhoutop: var(--topbar-height)(mesmo token que a topbar já usa) — só esse canto, o inferior-direito e os dois do login não têm nada cobrindo por cima e continuam emtop:0/bottom:0. - Contraste por tema: a aranha (
#17111d) é quase a mesma cor do fundo escuro padrão (--bg-canvas:#121017) e sumia, sobrando só o olho vermelho visível —.pw-spiderganhoufilter:drop-shadow(...)branco (dois drop-shadows, um mais fechado e um mais espalhado) só pra dar contraste no tema escuro. No tema claro é o oposto: a teia (creme, pensada pro fundo escuro) sumiria no fundo branco —:root[data-theme="light"] .pw-strands/.pw-strand-stray/.pw-ringviram um cinza-chumbo escuro semitransparente, e o filtro branco da aranha é desligado (filter:none, já que aranha escura sobre fundo claro já contrasta sozinha e o brilho branco só seria ruído ali). - Clicar mata a aranha (pedido explícito do usuário): a aranha (única parte da teia com
pointer-events:auto, o resto continuapointer-events:none, decorativo) tem um listener{once:true}(pidApplyHalloweenCobwebs()) que, ao clique, adiciona.is-dying(pwSpiderDie, 950ms — cai girando, opacity até 0,animation-fill-mode:forwardsmantém o estado final) e.is-risingnum<g class="pw-skull">irmão (pwSkullRise, 1300ms — sobe, aparece e some, como a "alma saindo do corpo" enquanto o corpo cai); as pernas ganham sua própriapwLegsDie(encolhem/giram) via.pw-spider.is-dying .pw-legs, sobrescrevendo opwLegsWigglecontínuo por especificidade. - O óbito é persistido (pedido explícito do usuário, numa rodada seguinte — "guardar que a aranha foi morta na memória... até executar o comando tab+b que reseta a aranha"): cada aranha tem um id próprio (
${containerId}__tl/__br— 4 ids possíveis, 2 cantos × 2 páginas) guardado num array JSON emlocalStorage(pid_seasonal_dead_spiders,pidMarkSpiderDead()/pidGetDeadSpiders()), mesmo padrão de armazenamento das outras preferências sazonais (nunca migrado pro banco). No carregamento seguinte,pidApplyHalloweenCobwebs()checa essa lista e, pra aranha já morta, aplica.is-deaddireto (animation:none;opacity:0, sem tocarpwSpiderDie/a caveira de novo — não faz sentido reprisar a queda a cada F5) em vez de ligar o clique. - Atalho Tab+B reseta todas as aranhas (
pidResetDeadSpiders(), limpa olocalStoragee recarrega a página): segurar Tab (sempreventDefault, a navegação por foco continua normal) e apertar B enquanto ele está pressionado. Listener global (seasonal-theme.js, incluído em toda página), só tem efeito de verdade nas duas páginas com teia e só quando há alguma aranha morta salva. - Bug real — aranha caindo pra cima e caveira de cabeça pra baixo no canto inferior-direito: o canto
--broriginalmente era só o mesmo desenho do--tlcomtransform:scale(-1,-1)no<div class="pw-corner">inteiro (jeito mais simples de reaproveitar o mesmo markup pro canto oposto). Só quescale(-1,-1)num ancestral vira tudo de cabeça pra baixo, inclusive a "aranha"/"caveira" (que têm uma orientação certa — pernas, olhos, queixo) e a direção das animações de queda/subida (pwSpiderDie/pwSkullRise, que "sobem" em vez de "descer" depois de espelhadas). Corrigido separando as duas coisas: só a teia em si (.pw-strands, sem opinião de "lado certo pra cima") continua sendo espelhada, agora comtransform-box:view-box; transform-origin:100px 100px(o centro do viewBox autorado, não a caixa delimitadora dos fios — importante porque o fio "perdido" mais comprido deslocaria esse centro se fossefill-box); a aranha e a caveira passaram a usar variáveis CSS (--rx/--ry/--rr/--ox/--oyna aranha,--sx/--syna caveira) pra descanso/passeio/posição, redefinidas com os valores espelhados (200-x,200-y,+180deg) só pra.pw-corner--br, sem nenhumscale/rotateextra nelas — ficam sempre em pé, e as@keyframesde queda/subida (agora usandocalc(var(--r*) + valor)) sempre significam "pra baixo"/"pra cima" de verdade na tela, nos dois cantos.
Opt-out do usuário ("Temas sazonais", menu da conta)
Pedido explícito do usuário — "essa medida serve para caso o usuário tenha alguma preferência com relação aos temas, ou no caso do halloween, se tiver aracnofobia". Botão-toggle (.seasonal-theme-toggle, components.css) no dropdown da conta (#account-dropdown, duplicado nos 13 shells — mesmo padrão de duplicação de sidebar__brand já documentado no CLAUDE.md raiz, "Ordem de <script>"), logo abaixo de "Tema de cores", com dois estados "Ativo"/"Inativo" (aria-pressed, cor de destaque quando ativo). Wiring em account.js, junto do bloco de theme-swatches — não em seasonal-theme.js, que só guarda a preferência.
- Armazenamento:
pid_seasonal_theme_opt_outnolocalStorage("1"= desligado), mesmo padrão depid_theme/pid_color_theme(theme.js) — preferência de navegador, nunca migrada pro banco. - Prioridade máxima:
pidIsHalloweenActive()checapidSeasonalThemesOptedOut()antes de qualquer outra coisa, inclusivePID_HALLOWEEN_PREVIEW_FORCE— se o usuário desligou, fica desligado mesmo com o preview forçado ligado. - Recarrega a página ao alternar: o tema sazonal (ícone, teias, favicon) já foi aplicado via substituição de
innerHTMLno carregamento da página atual — não dá pra desfazer isso in-place de forma confiável, então o clique já salva a preferência e chamawindow.location.reload(). Importante sobretudo pra quem desligou por aracnofobia: a aranha precisa sumir na hora, não só na próxima navegação.