64 lines
18 KiB
Markdown
64 lines
18 KiB
Markdown
# 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.
|