portal_publico/docs/temas-sazonais/temas-sazonais.md

64 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Temas Sazonais
> Criado em 2026-09-15, seguindo o mesmo padrão das demais aplicações sem pacote Python dedicado (ver `CLAUDE.md` na 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 do `CLAUDE.md` raiz. 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. Ver `CLAUDE.md` na 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/`" em `docs/identidade-visual/identidade-visual.md`) 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">` de `pid-icone-escuro.svg` pra `pid-halloween-icone-escuro.svg` via substring no `href` (funciona com qualquer prefixo de `STATIC_URL`). **Não** usa `pid-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 usa `pid-icone-escuro.svg` em vez do `pid-favicon.svg` dedicado.
- `pidApplyVampireIcon(svg)`/`pidApplyVampireFull(svg)` — substituem o `viewBox`+`innerHTML` de 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`, ver `base.css`) em todo `.pid-vamp-icon-rig` dentro de `root`.
## 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`, total `6.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 de `pid-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 no `CLAUDE.md` raiz 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 (mesmo `pidIntroCornerTarget()`, 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óprio `viewBox` (76×80, igual ao das duas variantes vampiro em `static/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 e `hidden` (`#pw-cobwebs-login`/`#pw-cobwebs-portal`) por `pidApplyHalloweenCobwebs(containerId)`, chamada por `login-logo-blink.js`/`sidebar-brand.js` no mesmo `if (halloween)` que já troca o ícone — fora de outubro, o container continua vazio e `hidden`.
- **Ancoragem diferente em cada página**: no login, a teia é filha direta de `.login-page` (ela já cobre `min-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 vs `clamp(160px,15vw,230px)` na Principal (valores atuais).
- `z-index:-1` (não `pointer-events` sozinho) 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:-1` só 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-content` nã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 com `isolation:isolate` em `.login-page` (`login.css`) e em `.main-content` (`layout.css`, junto do `position:relative` já 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--tl` ganhou `top: 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 em `top: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-spider` ganhou `filter: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-ring` viram 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 continua `pointer-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:forwards` mantém o estado final) e `.is-rising` num `<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ópria `pwLegsDie` (encolhem/giram) via `.pw-spider.is-dying .pw-legs`, sobrescrevendo o `pwLegsWiggle` contí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 em `localStorage` (`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-dead` direto (`animation:none;opacity:0`, sem tocar `pwSpiderDie`/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 o `localStorage` e recarrega a página): segurar Tab (sem `preventDefault`, 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 `--br` originalmente era só o mesmo desenho do `--tl` com `transform:scale(-1,-1)` no `<div class="pw-corner">` inteiro (jeito mais simples de reaproveitar o mesmo markup pro canto oposto). Só que `scale(-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 com `transform-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 fosse `fill-box`); a aranha e a caveira passaram a usar variáveis CSS (`--rx`/`--ry`/`--rr`/`--ox`/`--oy` na aranha, `--sx`/`--sy` na caveira) pra descanso/passeio/posição, redefinidas com os valores espelhados (`200-x`, `200-y`, `+180deg`) só pra `.pw-corner--br`, **sem nenhum `scale`/`rotate` extra nelas** — ficam sempre em pé, e as `@keyframes` de queda/subida (agora usando `calc(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_out` no `localStorage` (`"1"` = desligado), mesmo padrão de `pid_theme`/`pid_color_theme` (`theme.js`) — preferência de navegador, nunca migrada pro banco.
- **Prioridade máxima**: `pidIsHalloweenActive()` checa `pidSeasonalThemesOptedOut()` **antes** de qualquer outra coisa, inclusive `PID_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 `innerHTML` no 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 chama `window.location.reload()`. Importante sobretudo pra quem desligou por aracnofobia: a aranha precisa sumir na hora, não só na próxima navegação.