Inclusão dos temas sazonais
This commit is contained in:
parent
7d885b8a03
commit
444d76a159
@ -40,7 +40,37 @@
|
||||
"Bash(\"/c/Program Files/Google/Chrome/Application/chrome.exe\" --headless --disable-gpu --screenshot=\"/c/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/3716cfa9-a698-43f6-a74f-de0707c027aa/scratchpad/dc-header5.png\" --window-size=1680,300 \"file:///C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/3716cfa9-a698-43f6-a74f-de0707c027aa/scratchpad/test-dc-header5.html\")",
|
||||
"Bash(\"/c/Program Files/Google/Chrome/Application/chrome.exe\" --headless --disable-gpu --screenshot=\"/c/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/3716cfa9-a698-43f6-a74f-de0707c027aa/scratchpad/dc-header6.png\" --window-size=1680,300 \"file:///C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/3716cfa9-a698-43f6-a74f-de0707c027aa/scratchpad/test-dc-header6.html\")",
|
||||
"Bash(sed 's/justify-content: space-between;/justify-content: space-between;\\\\n flex-wrap: wrap;/' test-dc-header2.html)",
|
||||
"Bash(\"/c/Program Files/Google/Chrome/Application/chrome.exe\" --headless --disable-gpu --screenshot=\"/c/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/3716cfa9-a698-43f6-a74f-de0707c027aa/scratchpad/dc-header7.png\" --window-size=1200,300 \"file:///C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/3716cfa9-a698-43f6-a74f-de0707c027aa/scratchpad/test-dc-header7.html\")"
|
||||
"Bash(\"/c/Program Files/Google/Chrome/Application/chrome.exe\" --headless --disable-gpu --screenshot=\"/c/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/3716cfa9-a698-43f6-a74f-de0707c027aa/scratchpad/dc-header7.png\" --window-size=1200,300 \"file:///C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/3716cfa9-a698-43f6-a74f-de0707c027aa/scratchpad/test-dc-header7.html\")",
|
||||
"Bash(python3 -c ' *)",
|
||||
"Bash(\"/c/Users/Depaula/Documents/Portal/.venv/Scripts/python.exe\" --version)",
|
||||
"Bash(/c/Users/Depaula/Documents/Portal/.venv/Scripts/python.exe -c ' *)",
|
||||
"Bash(cp \"C:\\\\Users\\\\Depaula\\\\Documents\\\\Logos\\\\P.I.D. Logo Design Halloween\\\\pid-marca-halloween\\\\pid-halloween-favicon.svg\" \"static/img/pid-halloween-favicon.svg\")",
|
||||
"Bash(cp \"C:\\\\Users\\\\Depaula\\\\Documents\\\\Logos\\\\P.I.D. Logo Design Halloween\\\\pid-marca-halloween\\\\pid-halloween-icone-escuro.svg\" \"static/img/pid-halloween-icone-escuro.svg\")",
|
||||
"Bash(cp \"C:\\\\Users\\\\Depaula\\\\Documents\\\\Logos\\\\P.I.D. Logo Design Halloween\\\\pid-marca-halloween\\\\pid-halloween-logo-horizontal-escuro.svg\" \"static/img/pid-halloween-logo-horizontal-escuro.svg\")",
|
||||
"Bash(\"/c/Users/Depaula/Documents/Portal/.venv/Scripts/python.exe\" _scratch_insert_seasonal.py)",
|
||||
"Bash(rm _scratch_insert_seasonal.py)",
|
||||
"Bash(\"/c/Users/Depaula/Documents/Portal/.venv/Scripts/python.exe\" manage.py check)",
|
||||
"Bash(grep -n '`' static/js/seasonal-theme.js)",
|
||||
"Bash(netstat -ano)",
|
||||
"Bash(tasklist)",
|
||||
"Bash(curl -s -o /dev/null -w \"index.html: %{http_code}\\\\n\" http://127.0.0.1:8000/)",
|
||||
"Bash(curl -s -o /dev/null -w \"seasonal-theme.js: %{http_code}\\\\n\" http://127.0.0.1:8000/static/js/seasonal-theme.js)",
|
||||
"Bash(curl -s -o /dev/null -w \"login-intro-halloween.js: %{http_code}\\\\n\" http://127.0.0.1:8000/static/js/login-intro-halloween.js)",
|
||||
"Bash(curl -s -o /dev/null -w \"login-intro-halloween.css: %{http_code}\\\\n\" http://127.0.0.1:8000/static/css/login-intro-halloween.css)",
|
||||
"Bash(curl -s -o /dev/null -w \"pid-halloween-favicon.svg: %{http_code}\\\\n\" http://127.0.0.1:8000/static/img/pid-halloween-favicon.svg)",
|
||||
"Bash(curl -s http://127.0.0.1:8000/)",
|
||||
"Bash(curl -s http://127.0.0.1:8000/portal.html)",
|
||||
"Bash(curl -sI http://127.0.0.1:8000/)",
|
||||
"Bash(curl -sI http://127.0.0.1:8000/static/js/seasonal-theme.js)",
|
||||
"Bash(curl -sI http://127.0.0.1:8000/static/img/pid-icone-escuro.svg)",
|
||||
"Bash(curl -s http://127.0.0.1:8000/static/js/seasonal-theme.js)",
|
||||
"Bash(\"/c/Users/Depaula/Documents/Portal/.venv/Scripts/python.exe\" -)",
|
||||
"Bash(curl -s -o /dev/null -w \"halloween-cobweb.css: %{http_code}\\\\n\" http://127.0.0.1:8000/static/css/halloween-cobweb.css)",
|
||||
"Bash(curl -s http://127.0.0.1:8000/static/css/login.css)",
|
||||
"Bash(curl -s http://127.0.0.1:8000/static/css/layout.css)",
|
||||
"Bash(curl -s http://127.0.0.1:8000/static/css/halloween-cobweb.css)",
|
||||
"Bash(\"/c/Users/Depaula/Documents/Portal/.venv/Scripts/python.exe\" _scratch_insert_seasonal_toggle.py)",
|
||||
"Bash(rm _scratch_insert_seasonal_toggle.py)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@ Aplicações sem pacote Python dedicado (código ainda em `portal_api/models.py`
|
||||
| Favoritos (grade de `portal.html`) | `docs/favoritos/` — `favoritos.md` (técnico), `README.md`, `CHANGELOG.md` |
|
||||
| Perfis de Acesso / Usuários (telas administrativas, Liderança, inativação) | `docs/perfis-usuarios/` — `perfis-usuarios.md` (técnico), `README.md`, `CHANGELOG.md` |
|
||||
| Solicitações | `docs/solicitacoes/` — `solicitacoes.md` (técnico), `README.md`, `CHANGELOG.md` |
|
||||
| Temas Sazonais (marca P.I.D. sazonal — hoje só Halloween) | `docs/temas-sazonais/` — `temas-sazonais.md` (técnico), `README.md`, `CHANGELOG.md` |
|
||||
|
||||
## Como rodar / testar localmente
|
||||
|
||||
@ -232,7 +233,7 @@ Padrão de guarda por página: `profiles.js`, `users-admin.js` e `widgets.js` ve
|
||||
|
||||
| Onde | O quê |
|
||||
|---|---|
|
||||
| `localStorage` (`pid_theme`, `pid_color_theme`) | Só preferência de tema — ver `theme.js`. |
|
||||
| `localStorage` (`pid_theme`, `pid_color_theme`, `pid_seasonal_theme_opt_out`, `pid_seasonal_dead_spiders`) | Preferência de tema (`theme.js`) + preferências de Temas Sazonais (`seasonal-theme.js`, ver `docs/temas-sazonais/`). |
|
||||
| Postgres, via API | Tudo o mais: sessão (cookie do Django), usuários, perfis de acesso, favoritos, widgets, compromissos do Calendário Individual, notificações dispensadas. |
|
||||
|
||||
`notifications.js` usa uma lista mockada em memória (`PID_NEW_TOOLS_NOTIFICATIONS`) para os anúncios de "nova ferramenta" — os itens de compromisso vêm de `/api/compromissos/` de verdade. O conteúdo das notificações continua mockado/derivado a cada carregamento, mas **quais delas o usuário já dispensou** (X individual ou "Limpar tudo") persiste por usuário via `NotificacaoDispensada`/`/api/notificacoes-dispensadas/` — por isso um item dispensado não reaparece depois de um reload.
|
||||
@ -437,3 +438,9 @@ Ao apertar "Entrar" com sucesso, `index.html` toca uma animação de marca em te
|
||||
**"A barra lateral surgindo da esquerda para a direita, e em seguida o resto da tela"** (pedido explícito do usuário sobre como a tela Principal deveria surgir depois da animação, refinado duas vezes: a primeira versão só escondia `.main-content` e deixava a sidebar sempre visível desde o início, sem nenhuma entrada própria; a segunda versão deu à sidebar um fade + deslize sutil de 16px, considerado "ainda não satisfatório" — pequeno demais pra ler como "surgindo da esquerda pra direita"): `static/js/portal-reveal.js` (só `portal.html`, incluído `defer` logo depois de `theme.js` — mesmo padrão de "script que roda como IIFE de topo antes da primeira pintura pra evitar flash", ver "Ordem de `<script>`" acima) checa a `sessionStorage` marcada por `auth.js`; se presente, remove a marca (não sobrevive a um F5) e aplica **duas** classes em `<html>` antes do `DOMContentLoaded`: `pid-entering-sidebar` (esconde só `.sidebar`, `opacity:0` + `transform:translateX(-100%)` — a largura inteira dela, não um deslize de poucos pixels, pra realmente ler como um slide de fora da tela) e `pid-entering` (esconde `.main-content`, o `<div>` que envolve topbar **e** conteúdo da página). As duas são removidas **em sequência**, não juntas: `pid-entering-sidebar` sai primeiro (120ms depois do `DOMContentLoaded`), a sidebar desliza da esquerda pra direita ao longo de 420ms (`.sidebar` em `layout.css` tem uma `transition` própria — `opacity 420ms ease, transform 420ms cubic-bezier(0.16, 1, 0.3, 1)`, mais longa e com uma curva de "chegada" suave, não `var(--transition-base)` — genérica demais pra um movimento desse tamanho); só depois de esperar essa mesma duração (420ms) é que `pid-entering` sai, revelando o resto do app num fade, garantindo que as duas entradas não se sobreponham. Acessar `portal.html` direto (sem passar pelo login) nunca aciona nada disso, já que a `sessionStorage` só é setada no caminho de login bem-sucedido.
|
||||
|
||||
Testado pelo usuário em três rodadas (funcional) — ajustes até agora: a cor do overlay (era `#241c33`, virou `#121017`), o fade de entrada antes do ícone começar a se desenhar (não existia, overlay aparecia de repente; a duração foi ajustada depois de 350ms pra 500ms, "muito rápido... só pra ficar mais fluído"), a compressão da cena Portal (era 2,3s com pausa parada, foi pra 0,95s corrida numa rodada e depois ~0,9s), a aceleração de Build/Face (de 2,6s/2s cada pra 1s cada, Wordmark mantida em 2,8s de propósito) e a entrada da sidebar em `portal.html` (de um fade sutil de 16px pra um slide de fora da tela inteiro, `translateX(-100%)`, 420ms). Ainda falta conferir o posicionamento do "voo" final em diferentes tamanhos de tela/com a sidebar colapsada.
|
||||
|
||||
## Temas Sazonais
|
||||
|
||||
Troca sazonal da marca P.I.D. (logo, ícone da sidebar/login, favicon, intro de abertura pós-login) por uma variante temática durante uma janela de datas — hoje só o Halloween (mascote "vampiro", outubro), mecanismo pensado pra outro tema sazonal futuro reaproveitar. Inclui também, só durante o Halloween: uma animação de "susto" ao clicar na logo (substitui a piscada normal) e teias de aranha decorativas nos cantos do login/tela Principal (com uma aranha que pode ser "morta" ao clicar). Um toggle "Temas sazonais" no menu da conta deixa o próprio usuário desligar isso (preferência pessoal ou acessibilidade, ex.: aracnofobia). 100% runtime — fora da janela ativa (ou com o opt-out ligado), tudo volta sozinho ao P.I.D. de sempre, sem nenhuma ação manual.
|
||||
|
||||
Ver `docs/temas-sazonais/temas-sazonais.md`.
|
||||
|
||||
@ -41,6 +41,7 @@ Não há suíte de testes, lint ou build configurados neste projeto.
|
||||
| Indicador de Desempenho | Geradoc | [portal_api/indicadores/](portal_api/indicadores/README.md) |
|
||||
| Não Conformidades | Relatórios > Qualidade | [portal_api/nao_conformidades/](portal_api/nao_conformidades/README.md) |
|
||||
| Relatório Contábil | Relatórios > Contabilidade | [portal_api/dashboard_contabil/](portal_api/dashboard_contabil/README.md) |
|
||||
| Temas Sazonais (marca P.I.D. sazonal, hoje só Halloween) | — (recurso visual, não é item de menu) | [docs/temas-sazonais/](docs/temas-sazonais/README.md) |
|
||||
|
||||
Cada pasta acima tem um `README.md` (o que a aplicação faz, estado atual) e um `CHANGELOG.md` (histórico rodada a rodada, extraído de `plano.md`). As 5 aplicações com pacote Python próprio também têm um `CLAUDE.md` com o detalhamento técnico completo, carregado automaticamente pelo Claude Code; as demais têm esse detalhe no arquivo dentro da própria pasta em `docs/`.
|
||||
|
||||
|
||||
49
docs/temas-sazonais/CHANGELOG.md
Normal file
49
docs/temas-sazonais/CHANGELOG.md
Normal file
@ -0,0 +1,49 @@
|
||||
# Changelog — Temas Sazonais
|
||||
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`. Rodadas que mudaram mais de uma aplicação ao mesmo tempo continuam em `plano.md` — a numeração de rodada é a mesma usada lá, para referência cruzada.
|
||||
|
||||
### Rodada 94 — Tema sazonal Halloween: logo/mascote vampiro, intro de abertura e "susto" ao clicar
|
||||
|
||||
Pedido (2026-09-15): deixar uma logo e uma introdução de abertura com tema de Halloween, aplicadas só durante outubro, revertendo pro P.I.D. normal depois — sem descartar nada do que já existe. Mascote (a marca P.I.D. fantasiada de vampiro) e a composição da intro foram desenhados fora do repositório, em `C:\Users\Depaula\Documents\Logos\P.I.D. Logo Design Halloween\` (`pid-marca-halloween/` + `Logo intro animation Halloween/export-halloween/pid-intro-halloween.html`, mesmo formato de bundle "Design Canvas" da intro original) e portados fielmente pra vanilla JS/CSS, mesma técnica já usada na intro normal (ver `CLAUDE.md` raiz → "Animação de intro do login" → "Origem do arquivo"). Implementado como mecanismo **100% runtime**, sem editar nenhum arquivo original: `static/js/seasonal-theme.js` (novo, incluído em toda página) decide se o tema está ativo (`pidIsHalloweenActive()`, janela de outubro) e, se sim, troca em memória o favicon, o ícone/assinatura da sidebar e o ícone do login pelo vampiro, e troca a piscada de olhos ao clicar por uma animação de "susto" (capa se abre, rosto fica raivoso, tremida + brilho vermelho — `.is-scaring` em `base.css`); `static/css/login-intro-halloween.css` + `static/js/login-intro-halloween.js` são uma intro de abertura completa alternativa (cenas Rise/Scare/Settle/Wordmark + uma cena "Fly" acrescentada pra aterrissar na sidebar, mesmo espírito da intro normal), escolhida por `auth.js` no lugar da intro normal quando o tema está ativo. Detalhe técnico completo em `temas-sazonais.md`.
|
||||
|
||||
**Ainda em preview, não confirmado**: `PID_HALLOWEEN_PREVIEW_FORCE = true` (em `seasonal-theme.js`) força o tema ligado agora só pra essa revisão visual — o usuário ainda precisa (a) rodar o `runserver` e conferir a logo/intro/susto nos dois temas, (b) confirmar se a janela é outubro inteiro (já implementado) ou um recorte diferente, e (c) desligar essa constante depois de validado (daí em diante só a data decide, sem mais nenhuma mudança de código). Depois de outubro, tudo volta ao P.I.D. normal sozinho, sem nenhuma ação manual — nenhum arquivo original (`login-intro.js`/`.css`, ícones `pid-icone*`/`pid-logo-horizontal*`, `pid-icon-eye`/`is-blinking`) foi alterado ou removido.
|
||||
|
||||
**Ajuste na mesma rodada**: o usuário reportou o favicon aparecendo na cor errada (branco, destoando do resto da marca) — `pidApplySeasonalFavicon()` trocava pra `pid-halloween-favicon.svg` (variante simplificada, sem capa/gola/cores completas); corrigido pra usar `pid-halloween-icone-escuro.svg` (o mesmo ícone/cores do resto da UI), mesmo padrão que o P.I.D. normal já seguia.
|
||||
|
||||
### Rodada 95 — Teias de aranha nos cantos do login e da tela Principal
|
||||
|
||||
Pedido (2026-09-15, mesma rodada do tema Halloween acima): teias de aranha com aranha animada nos cantos do login e da tela Principal, marcados pelo usuário em prints da tela real. Antes de aplicar, foi publicado um protótipo isolado (Artifact "Cantos Assombrados") com 4 vs 2 cantos e aranha andando vs parada, pra decidir o desenho sem mexer no Portal de verdade — o usuário confirmou **2 cantos** (superior-esquerdo/inferior-direito, os mesmos dos prints) e pediu a teia da tela Principal **menor** que a do login, "evitando cobrir muito as informações que constam na página" (a Principal tem cards/widgets perto das bordas; o login só tem o card centralizado).
|
||||
|
||||
Aplicado só em `index.html` e `portal.html` (nenhum outro shell), mesmo mecanismo 100% runtime do resto do tema: `static/css/halloween-cobweb.css` (novo) define a teia (6 fios + 3 anéis, coordenadas por trigonometria) e a aranha (corpo+8 pernas, anda e volta ao longo de um fio via `@keyframes` puro CSS, pernas mexendo continuamente); o markup (`PID_COBWEB_SVG` em `seasonal-theme.js`) só é injetado num container já presente e `hidden` no template (`#pw-cobwebs-login`/`#pw-cobwebs-portal`) quando `pidIsHalloweenActive()`, chamado por `login-logo-blink.js`/`sidebar-brand.js`. Na Principal, a teia é ancorada em `.main-content`, não no viewport — a sidebar já é sólida e cobriria o canto esquerdo de qualquer jeito. Detalhe técnico completo em `temas-sazonais.md` → "Teias de aranha de canto".
|
||||
|
||||
**Ajuste na mesma rodada**: a teia do login não aparecia de jeito nenhum, e a da Principal ficou "muito pequena". Causa raiz do primeiro: `z-index:-1` só funciona dentro de um contexto de empilhamento isolado — sem isso, o próprio fundo em tela cheia de `.login-page` (conteúdo estático normal) pintava por cima da teia, escondendo-a por completo; corrigido com `isolation:isolate` em `.login-page` (`login.css`) e em `.main-content` (`layout.css`), que tinha o mesmo problema de raiz (só não percebido porque a Principal não tem um fundo cobrindo tudo). Tamanho aumentado nas duas telas (`clamp(120px,14vw,190px)` → `clamp(160px,17vw,240px)` no login; `clamp(80px,8vw,120px)` → `clamp(110px,11vw,170px)` na Principal), mantendo a Principal um pouco menor que o login.
|
||||
|
||||
**Mais um ajuste, ainda na mesma rodada**: (a) a topbar da Principal (`position:sticky`, fundo opaco) cobria a metade de cima da teia do canto superior-esquerdo — `.pw-corner--tl` na Principal ganhou `top:var(--topbar-height)` pra começar logo abaixo dela; (b) tamanho aumentado de novo nas duas telas (login → `clamp(220px,22vw,320px)`, Principal → `clamp(160px,15vw,230px)`); (c) contraste por tema: no escuro, a aranha (quase a mesma cor do fundo) ganhou um `drop-shadow` branco pra não sumir, sobrando só o olho vermelho visível como antes; no claro, a teia (creme, pensada pro fundo escuro) viraria invisível no fundo branco — agora fica cinza-chumbo escuro nesse tema, e o brilho branco da aranha é desligado (já contrasta sozinha, escura sobre claro).
|
||||
|
||||
### Rodada 96 — Opt-out de temas sazonais (menu da conta)
|
||||
|
||||
Pedido (2026-09-15, mesma rodada do tema Halloween): um jeito do próprio usuário desligar temas sazonais (hoje só o Halloween), "caso o usuário tenha alguma preferência com relação aos temas, ou no caso do halloween, se tiver aracnofobia". Botão-toggle "Temas sazonais" (`.seasonal-theme-toggle`, `components.css`) no dropdown da conta, logo abaixo de "Tema de cores", com dois estados "Ativo"/"Inativo" — duplicado nos 13 shells, mesmo padrão dos demais elementos replicados por template. Preferência salva em `localStorage` (`pid_seasonal_theme_opt_out`), mesmo padrão do tema claro/escuro (nunca migrada pro banco — é preferência de navegador). `pidIsHalloweenActive()` (`seasonal-theme.js`) checa essa preferência **antes** de qualquer outra coisa, inclusive antes do `PID_HALLOWEEN_PREVIEW_FORCE` (prioridade máxima: se o usuário desligou, fica desligado). Alternar o toggle recarrega a página (`account.js`) — o tema sazonal já foi aplicado via substituição de markup no carregamento atual, não dá pra desfazer in-place, e pra quem desligou por aracnofobia a aranha precisa sumir na hora. Detalhe técnico completo em `temas-sazonais.md` → "Opt-out do usuário".
|
||||
|
||||
### Rodada 97 — Clicar na aranha da teia mata ela
|
||||
|
||||
Pedido (2026-09-15, mesma rodada): "ao clicar nas aranhas, deve sair uma pequena caveirinha dela (como se tivesse morrido) e ela cair, deixando a teia vazia. Ao atualizar a página, deve voltar como estava." A aranha (única parte da teia com `pointer-events:auto`) ganhou um listener de clique `{once:true}` que dispara duas animações CSS em paralelo: a própria aranha cai girando e some (`pwSpiderDie`, pernas encolhendo numa animação própria, `pwLegsDie`) enquanto uma caveirinha (`<g class="pw-skull">`, novo, sempre presente mas invisível) sobe e desaparece (`pwSkullRise`), como se a alma saísse do corpo. Sem nenhum estado salvo nesta rodada — a teia ficava vazia só até a página ser recarregada, já que `pidApplyHalloweenCobwebs()` reconstruía tudo do zero a cada carregamento (persistência veio na rodada 99). Detalhe técnico completo em `temas-sazonais.md` → "Teias de aranha de canto" → "Clicar mata a aranha".
|
||||
|
||||
### Rodada 98 — Teia mais profissional + correção da aranha/caveira invertidas no canto inferior-direito
|
||||
|
||||
Pedido (2026-09-15, mesma rodada): (a) o desenho da teia (6 fios retos + 3 anéis poligonais) estava "muito bruto", o usuário anexou uma referência visual e pediu algo "nesse sentido", mais profissional, "para que não fique parecendo que só foi colado um PNG bruto"; (b) no canto inferior-direito, a aranha caía **pra cima** ao morrer e a caveira aparecia **de cabeça pra baixo**.
|
||||
|
||||
**Teia redesenhada**: agora 8 fios (ângulos irregulares, não uniformes — mais parecido com uma teia real) + 5 anéis em espiral **levemente arqueados** (`<path class="pw-ring">` com curvas `Q`, controle empurrado ~8% mais longe do centro que o ponto médio da corda — simula a curva natural de uma espiral de captura, em vez de segmentos retos ligando os pontos) + 1 fio "perdido" bem mais comprido que os demais, se estendendo pra fora do canto (detalhe da referência do usuário) e cortado naturalmente pela borda da página (`.pw-corners{overflow:hidden}`).
|
||||
|
||||
**Causa raiz do bug do canto inferior-direito**: esse canto reaproveitava o mesmo desenho do superior-esquerdo com `transform:scale(-1,-1)` no `<div>` inteiro — um jeito simples de não duplicar o markup, mas que vira **tudo** de cabeça pra baixo, inclusive a orientação da aranha/caveira e a direção "pra baixo"/"pra cima" das animações de queda/subida. Corrigido separando as duas coisas: só a teia (`.pw-strands`, que não tem um "lado certo pra cima") continua espelhada, com `transform-box:view-box;transform-origin:100px 100px` (o centro do viewBox autorado — não a caixa delimitadora dos fios, que o fio "perdido" deslocaria); a aranha e a caveira passaram a usar variáveis CSS (`--rx`/`--ry`/`--rr`/`--ox`/`--oy`/`--sx`/`--sy`) redefinidas com os valores espelhados só para `.pw-corner--br`, sem nenhum `scale`/`rotate` extra nelas — ficam sempre em pé e caem/sobem na direção real da tela nos dois cantos. Detalhe técnico completo em `temas-sazonais.md` → "Teias de aranha de canto".
|
||||
|
||||
### Rodada 99 — Aranha morta persiste entre carregamentos + atalho Tab+B pra resetar
|
||||
|
||||
Pedido (2026-09-15, mesma rodada): "guardar que a aranha foi morta na memória... até executar o comando tab+b que reseta a aranha". Cada aranha ganhou um id próprio (`${containerId}__tl`/`__br`, 4 possíveis — 2 cantos × 2 páginas) guardado num array em `localStorage` (`pid_seasonal_dead_spiders`, `pidMarkSpiderDead()`/`pidGetDeadSpiders()`/`pidResetDeadSpiders()` em `seasonal-theme.js`) — mesmo padrão de armazenamento das outras preferências sazonais, nunca no banco. `pidApplyHalloweenCobwebs()` agora checa essa lista a cada carregamento: aranha já morta nasce direto com `.is-dead` (invisível, sem tocar a animação de queda nem a caveira de novo), em vez de ligar o clique. Segurar Tab e apertar B (sem interceptar o Tab em si — a navegação por foco continua normal) limpa a lista e recarrega a página, trazendo todas de volta. Detalhe técnico completo em `temas-sazonais.md` → "Teias de aranha de canto".
|
||||
|
||||
### Rodada 100 — Documentação separada em `docs/temas-sazonais/`
|
||||
|
||||
Pedido (2026-09-15): "gerar um changelog específico para esta questão dos temas sazonais... estruturar suas informações como se fossem aplicação específica". Seguindo o mesmo padrão das demais aplicações sem pacote Python dedicado (`docs/<app>/<app>.md`+`README.md`+`CHANGELOG.md`), o conteúdo técnico detalhado que vivia na seção "Tema sazonal (Halloween)" do `CLAUDE.md` raiz foi movido pra este arquivo/`temas-sazonais.md`/`README.md`; o histórico rodada a rodada (94-99) foi extraído de `plano.md` pra este `CHANGELOG.md` (mesma numeração). `CLAUDE.md` e `plano.md` ficaram só com a parte estrutural (a existência do mecanismo + onde encontrar o resto), sem duplicar o detalhe aqui.
|
||||
|
||||
### Rodada 101 — Preview confirmado, tema programado pra outubro de verdade
|
||||
|
||||
Pedido (2026-09-15): "pode parar de forçar o tema e deixar somente programado para aplicar a partir de outubro". `PID_HALLOWEEN_PREVIEW_FORCE` (`seasonal-theme.js`) virou `false` — mantida na constante (não removida), documentando a decisão em vez de apagar o rastro. A partir daqui, `pidIsHalloweenActive()` decide só pela data (`PID_HALLOWEEN_MES = 9`, outubro inteiro, todo ano), sem nenhuma ação manual pra ligar/desligar. Encerra o período de preview aberto na rodada 94.
|
||||
22
docs/temas-sazonais/README.md
Normal file
22
docs/temas-sazonais/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# Temas Sazonais
|
||||
|
||||
> Ver `docs/temas-sazonais/temas-sazonais.md` para o detalhamento técnico. 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 pós-login) por uma variante temática durante uma janela de datas — hoje só o **Halloween** (mascote "vampiro", ativo em outubro), mas pensado desde já pra outro tema sazonal futuro reaproveitar o mesmo mecanismo. Fora da janela ativa, tudo volta sozinho ao P.I.D. de sempre, sem nenhuma ação manual.
|
||||
|
||||
**Status**: confirmado e programado — ativa sozinho todo mês de outubro, sem nenhuma ação manual (ver `temas-sazonais.md`, seção "Status atual").
|
||||
|
||||
Durante o Halloween, o tema também inclui:
|
||||
- Uma animação de "susto" ao clicar na logo (substitui a piscada de olhos normal).
|
||||
- Teias de aranha decorativas nos cantos do login e da tela Principal, com uma aranha animada que pode ser "morta" ao clicar (persiste entre carregamentos; atalho Tab+B reseta todas de volta).
|
||||
- Um toggle "Temas sazonais" no menu da conta, pra o próprio usuário desligar (preferência pessoal, ou acessibilidade — ex.: aracnofobia).
|
||||
|
||||
## Onde mexer
|
||||
|
||||
- `static/js/seasonal-theme.js` — toda a lógica: checagem de data/opt-out, troca de ícone/favicon, teias de aranha, óbito persistido, atalho Tab+B.
|
||||
- `static/css/halloween-cobweb.css` — desenho/animação da teia de aranha, aranha e caveira.
|
||||
- `static/css/login-intro-halloween.css` + `static/js/login-intro-halloween.js` — intro de abertura pós-login alternativa.
|
||||
- `static/js/sidebar-brand.js` / `static/js/login-logo-blink.js` — aplicam a troca de ícone e o susto ao clicar.
|
||||
- `static/js/account.js` + `components.css` (`.seasonal-theme-toggle`) — toggle de opt-out no menu da conta.
|
||||
- `static/js/auth.js` — escolhe a intro normal ou a sazonal no sucesso do login.
|
||||
- `static/img/pid-halloween-*.svg` — cópias de referência da marca sazonal (a UI injeta o markup inline, não referencia esses arquivos diretamente).
|
||||
63
docs/temas-sazonais/temas-sazonais.md
Normal file
63
docs/temas-sazonais/temas-sazonais.md
Normal file
@ -0,0 +1,63 @@
|
||||
# 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/`" 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">` 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.
|
||||
4
plano.md
4
plano.md
@ -389,6 +389,10 @@ Nova aplicação em Relatórios > Contabilidade (nasce restrita ao perfil "Inova
|
||||
|
||||
Pedido (2026-09-14): remover o item "Relatório Setorial" (`href="#"`, sem tela própria) do menu Relatórios, já que o tópico agora tem itens de verdade ("Qualidade" → Não Conformidades, "Contabilidade" → Relatório Contábil). Removido de `MODULE_APPS["relatorios"]` em `catalogo.py` e do `<li data-app="relatorio-setorial">` hardcoded nas 13 páginas-shell (o menu é replicado por template, não um componente único — ver "Ordem de `<script>`" no `CLAUDE.md` raiz pro mesmo padrão de duplicação entre shells). O item "Indicadores" (outro placeholder `href="#"` dentro de Relatórios) não foi tocado, não fazia parte do pedido. Perfis já existentes no banco podem manter uma chave órfã `"relatorio-setorial"` dentro de `permissoes["relatorios"]["apps"]` — inofensiva, mesmo padrão já visto quando "Gerar Contrato"/"Gerar Procuração" foram removidos de Geradoc (ver comentário em `catalogo.py`).
|
||||
|
||||
### 94. Temas sazonais (Halloween) — logo/mascote vampiro, intro de abertura, teias de aranha e opt-out
|
||||
|
||||
Pedido (2026-09-15): deixar uma logo e uma introdução de abertura com tema de Halloween, aplicadas só durante outubro, revertendo pro P.I.D. normal depois — sem descartar nada do que já existe. Implementado como mecanismo **100% runtime** (nenhum arquivo original alterado ou removido — fora da janela ativa, tudo volta sozinho): troca de ícone/favicon/assinatura pelo mascote vampiro, uma intro de abertura pós-login alternativa, uma animação de "susto" ao clicar na logo, teias de aranha decorativas nos cantos do login/tela Principal (com uma aranha "matável" ao clicar, óbito persistido) e um toggle no menu da conta pro usuário desligar temas sazonais por preferência própria (ou acessibilidade — aracnofobia). Ainda em preview (`PID_HALLOWEEN_PREVIEW_FORCE` força ligado pra revisão visual, antes de confirmar a janela de outubro definitivamente). Histórico técnico completo (rodadas 94-100) e detalhamento em `docs/temas-sazonais/CHANGELOG.md`/`temas-sazonais.md` — a partir daqui, rodadas específicas desta aplicação não duplicam entrada aqui (ver nota no topo deste arquivo).
|
||||
|
||||
## Limitações conhecidas / decisões assumidas
|
||||
|
||||
- Calendário De Paula (empresa) não tem mais versão interna — depende
|
||||
|
||||
@ -136,6 +136,119 @@ svg {
|
||||
}
|
||||
}
|
||||
|
||||
/* Variante "susto" do clique na logo, só durante o tema sazonal Halloween
|
||||
(`seasonal-theme.js`/`pidIsHalloweenActive()`) — substitui a piscada acima
|
||||
enquanto o ícone P.I.D. normal é trocado pelo vampiro
|
||||
(`pidApplyVampireIcon`/`pidApplyVampireFull`). `.pid-vamp-icon-rig` é o
|
||||
grupo raiz injetado dentro do <svg> (sidebar colapsada/expandida e botão
|
||||
do login); `.pid-vamp-face--friendly`/`--scare` são dois grupos sempre
|
||||
presentes, cruzando opacidade entre si. Mesmo cuidado de `.pid-icon-eye`
|
||||
com `transform-box:fill-box` pra girar/escalar em torno do próprio ícone,
|
||||
não da origem do SVG. */
|
||||
.pid-vamp-icon-rig,
|
||||
.pid-vamp-cape-group {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.pid-vamp-face--friendly {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.pid-vamp-face--scare {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.pid-vamp-icon-rig.is-scaring {
|
||||
animation: pidVampShake 900ms ease;
|
||||
}
|
||||
|
||||
.pid-vamp-icon-rig.is-scaring .pid-vamp-cape-group {
|
||||
animation: pidVampCapeBurst 900ms ease;
|
||||
}
|
||||
|
||||
.pid-vamp-icon-rig.is-scaring .pid-vamp-face--friendly {
|
||||
animation: pidVampFaceOut 900ms ease;
|
||||
}
|
||||
|
||||
.pid-vamp-icon-rig.is-scaring .pid-vamp-face--scare {
|
||||
animation: pidVampFaceIn 900ms ease;
|
||||
}
|
||||
|
||||
@keyframes pidVampFaceOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
14% {
|
||||
opacity: 0.15;
|
||||
}
|
||||
22%, 70% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pidVampFaceIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
14% {
|
||||
opacity: 0.85;
|
||||
}
|
||||
22%, 70% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pidVampCapeBurst {
|
||||
0%, 100% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
16% {
|
||||
transform: scale(1.12) rotate(-3deg);
|
||||
}
|
||||
30% {
|
||||
transform: scale(1.05) rotate(1.5deg);
|
||||
}
|
||||
55% {
|
||||
transform: scale(1.07) rotate(-1deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pidVampShake {
|
||||
0% {
|
||||
transform: translateX(0) scale(1);
|
||||
filter: drop-shadow(0 0 0 rgba(158, 50, 65, 0));
|
||||
}
|
||||
10% {
|
||||
transform: translateX(-2.4px) scale(1.05);
|
||||
}
|
||||
18% {
|
||||
transform: translateX(2.6px) scale(1.08);
|
||||
filter: drop-shadow(0 0 10px rgba(158, 50, 65, 0.6));
|
||||
}
|
||||
28% {
|
||||
transform: translateX(-1.8px) scale(1.04);
|
||||
}
|
||||
38% {
|
||||
transform: translateX(1.4px) scale(1.02);
|
||||
filter: drop-shadow(0 0 12px rgba(158, 50, 65, 0.65));
|
||||
}
|
||||
55% {
|
||||
transform: translateX(0) scale(1.01);
|
||||
filter: drop-shadow(0 0 8px rgba(158, 50, 65, 0.4));
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0) scale(1);
|
||||
filter: drop-shadow(0 0 0 rgba(158, 50, 65, 0));
|
||||
}
|
||||
}
|
||||
|
||||
/* scrollbar discreta */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
|
||||
@ -783,6 +783,51 @@
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Botão "Temas sazonais" (menu da conta) — liga/desliga temas sazonais
|
||||
(Halloween etc.) por preferência do usuário (motivos: gosto pessoal ou,
|
||||
no caso do Halloween, aracnofobia). Estado "Ativo" (padrão) usa a cor de
|
||||
destaque, igual a um toggle ligado; "Inativo" fica neutro/apagado, igual
|
||||
a um toggle desligado — mesmo padrão visual de outros toggles booleanos
|
||||
do Portal (ex.: `.theme-swatch.is-active`, mas aqui como texto+ponto em
|
||||
vez de amostra de cor, já que não há uma cor própria pra representar). */
|
||||
.seasonal-theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-pill);
|
||||
border: 1px solid var(--border-subtle);
|
||||
background: var(--bg-surface-raised);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.seasonal-theme-toggle:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.seasonal-theme-toggle__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
opacity: 0.5;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.seasonal-theme-toggle[aria-pressed="true"] {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.seasonal-theme-toggle[aria-pressed="true"] .seasonal-theme-toggle__dot {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Notificações */
|
||||
.notifications {
|
||||
position: relative;
|
||||
|
||||
321
static/css/halloween-cobweb.css
Normal file
321
static/css/halloween-cobweb.css
Normal file
@ -0,0 +1,321 @@
|
||||
/* Teias de aranha decorativas de canto, tema sazonal Halloween — só no
|
||||
canto superior-esquerdo e no inferior-direito (pedido explícito do
|
||||
usuário, mesmos 2 cantos marcados nos prints que ele mandou), só no login
|
||||
(`index.html`) e na tela Principal (`portal.html`) — nenhuma outra
|
||||
página. Puramente decorativa (`pointer-events:none`, `z-index:-1` pra
|
||||
ficar atrás de todo o conteúdo real, exceto a própria aranha — ver
|
||||
"Clicar mata a aranha" abaixo), escondida por padrão (`hidden` no HTML) e
|
||||
só exibida via `pidApplyHalloweenCobwebs()` (seasonal-theme.js) quando
|
||||
`pidIsHalloweenActive()` — ver `docs/temas-sazonais/temas-sazonais.md`.
|
||||
Desenho/cores validados no protótipo (artifact "Cantos Assombrados")
|
||||
antes de aplicar aqui; o desenho da teia em si (fios/anéis) foi refeito
|
||||
numa rodada seguinte a partir de uma referência visual do usuário, pra
|
||||
parecer uma teia real (espiral levemente arqueada, mais fios, um fio
|
||||
"perdido" se estendendo bem mais longe que o resto) em vez de um desenho
|
||||
geométrico cru.
|
||||
|
||||
No login, ancorada em `.login-page` (`position:absolute`, ela já cobre
|
||||
`min-height:100vh` e ganhou `isolation:isolate` em `login.css` — sem
|
||||
isolar, o próprio fundo de `.login-page` pintava por cima da teia,
|
||||
escondendo-a por completo). Na tela Principal, ancorada em
|
||||
`.main-content` (mesma ideia, `isolation:isolate` em `layout.css`) — não
|
||||
no viewport, porque a sidebar já é sólida e cobriria a teia do canto
|
||||
esquerdo de qualquer jeito; assim a teia nunca fica escondida atrás dela.
|
||||
A teia da Principal também é um pouco menor que a do 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 as duas foram aumentadas numa rodada
|
||||
seguinte, "ficaram muito pequenas". */
|
||||
|
||||
.pw-corners {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.pw-corner {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pw-corner--tl {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.pw-corner--br {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* O canto inferior-direito reaproveita o MESMO desenho/markup do
|
||||
superior-esquerdo (a teia é autorada assumindo o canto no (0,0) do seu
|
||||
próprio viewBox), só espelhando a TEIA em si (`.pw-strands`) em torno do
|
||||
centro do viewBox — não a aranha nem a caveira. Espelhar o `<div
|
||||
class="pw-corner--br">` inteiro (como era antes) virava a aranha de
|
||||
cabeça pra baixo e fazia ela "cair" pra cima ao morrer (bug relatado pelo
|
||||
usuário): `scale(-1,-1)` em qualquer ancestral inverte literalmente tudo
|
||||
dentro, inclusive a direção "pra baixo" das animações de queda/subida.
|
||||
Por isso a aranha e a caveira têm sua própria posição "espelhada" abaixo
|
||||
(variáveis `--rx`/`--ry`/`--rr`/`--ox`/`--oy` e `--sx`/`--sy`,
|
||||
recalculadas pra esse canto), sem nenhum `scale`/`rotate` adicional nelas
|
||||
— assim ficam sempre "em pé" e caem/sobem na direção real da tela nos
|
||||
dois cantos. `transform-box:view-box` garante que o centro (100,100) do
|
||||
espelhamento é sempre o centro do viewBox autorado, não a caixa
|
||||
delimitadora visual dos fios (que dispararia pro lado errado por causa
|
||||
do fio "perdido", bem mais comprido que o resto). */
|
||||
.pw-corner--br .pw-strands {
|
||||
transform-box: view-box;
|
||||
transform-origin: 100px 100px;
|
||||
transform: scale(-1, -1);
|
||||
}
|
||||
|
||||
/* Na Principal, a topbar (`position:sticky`, `z-index:20`, fundo opaco —
|
||||
layout.css) pintava por cima da metade de cima da teia do canto
|
||||
superior-esquerdo, escondendo a aranha; empurra a teia pra começar
|
||||
logo abaixo dela (`var(--topbar-height)`, mesmo token que a topbar já
|
||||
usa). Não se aplica ao canto inferior-direito (nada cobre o rodapé) nem
|
||||
ao login (sem topbar). */
|
||||
.pw-corners--portal .pw-corner--tl {
|
||||
top: var(--topbar-height);
|
||||
}
|
||||
|
||||
.pw-corners--login .pw-corner {
|
||||
width: clamp(220px, 22vw, 320px);
|
||||
}
|
||||
|
||||
.pw-corners--portal .pw-corner {
|
||||
width: clamp(160px, 15vw, 230px);
|
||||
}
|
||||
|
||||
.pw-corner svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.pw-strands {
|
||||
fill: none;
|
||||
stroke: rgba(242, 237, 227, 0.24);
|
||||
stroke-width: 0.7;
|
||||
stroke-linecap: round;
|
||||
animation: pwShimmer 6.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.pw-strand-stray {
|
||||
stroke: rgba(242, 237, 227, 0.16);
|
||||
}
|
||||
|
||||
.pw-ring {
|
||||
stroke: rgba(242, 237, 227, 0.16);
|
||||
stroke-width: 0.6;
|
||||
}
|
||||
|
||||
.pw-legs {
|
||||
stroke: #17111d;
|
||||
stroke-width: 1.6;
|
||||
stroke-linecap: round;
|
||||
fill: none;
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
animation: pwLegsWiggle 0.85s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.pw-abdomen,
|
||||
.pw-head {
|
||||
fill: #17111d;
|
||||
}
|
||||
|
||||
.pw-eye {
|
||||
fill: #9e3241;
|
||||
}
|
||||
|
||||
.pw-spider {
|
||||
/* Posição/ângulo de descanso (--r*) e do "passeio" pra fora (--o*) — o
|
||||
canto inferior-direito redefine essas 4 variáveis abaixo com os valores
|
||||
espelhados (200-x, 200-y, +180deg), em vez de espelhar a aranha via
|
||||
`transform` (ver comentário grande em `.pw-corner--br .pw-strands`). */
|
||||
--rx: 85px;
|
||||
--ry: 113px;
|
||||
--rr: 51deg;
|
||||
--ox: 138px;
|
||||
--oy: 178px;
|
||||
transform: translate(var(--rx), var(--ry)) rotate(var(--rr));
|
||||
animation: pwWalk 8s ease-in-out infinite;
|
||||
/* Corpo/pernas são quase a mesma cor do fundo escuro padrão (#17111d vs
|
||||
--bg-canvas #121017) — sem esse brilho claro a aranha praticamente some,
|
||||
sobrando só o olho vermelho visível. Removido no tema claro abaixo
|
||||
(lá o contraste já é natural: aranha escura sobre fundo claro). */
|
||||
filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.6)) drop-shadow(0 0 5px rgba(255, 255, 255, 0.3));
|
||||
/* Única parte "viva" com `pointer-events:auto` — o resto da teia
|
||||
(`.pw-corners`/`.pw-corner`) é `pointer-events:none`, decorativo. */
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pw-corner--br .pw-spider {
|
||||
--rx: 115px;
|
||||
--ry: 87px;
|
||||
--rr: 231deg;
|
||||
--ox: 62px;
|
||||
--oy: 22px;
|
||||
}
|
||||
|
||||
/* Clicar mata a aranha (pedido explícito do usuário) — some caindo/girando,
|
||||
pernas encolhem, e uma caveirinha sobe da teia (ver `.pw-skull` abaixo);
|
||||
a teia fica vazia pelo resto desta visita à página (só volta com um F5
|
||||
de verdade — `pidApplyHalloweenCobwebs()`, seasonal-theme.js, reconstrói
|
||||
tudo do zero a cada carregamento). `forwards` mantém o estado final
|
||||
(invisível) depois que a animação termina, sem precisar de mais JS. Os
|
||||
deslocamentos em `calc()` são sempre "queda pra baixo" (`--ry` + valor
|
||||
positivo) em relação ao próprio ponto de descanso do canto, então caem
|
||||
do jeito certo nos dois cantos sem precisar de uma segunda animação. */
|
||||
.pw-spider.is-dying {
|
||||
animation: pwSpiderDie 950ms ease-in forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Aranha já morta numa visita anterior (persistido em `localStorage`, ver
|
||||
`pidGetDeadSpiders()`/`pidApplyHalloweenCobwebs()` em seasonal-theme.js)
|
||||
— nasce direto no estado final, sem tocar `pwSpiderDie`/`pwWalk` de novo;
|
||||
a caveira nem chega a aparecer (já subiu e sumiu na visita em que a
|
||||
aranha morreu, não faz sentido reprisar isso a cada carregamento). Só
|
||||
volta com o atalho Tab+B. */
|
||||
.pw-spider.is-dead {
|
||||
animation: none;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pw-spider.is-dying .pw-legs {
|
||||
animation: pwLegsDie 450ms ease-out forwards;
|
||||
}
|
||||
|
||||
.pw-skull {
|
||||
--sx: 85px;
|
||||
--sy: 105px;
|
||||
opacity: 0;
|
||||
transform: translate(var(--sx), var(--sy)) scale(0.4);
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pw-corner--br .pw-skull {
|
||||
--sx: 115px;
|
||||
--sy: 95px;
|
||||
}
|
||||
|
||||
.pw-skull circle {
|
||||
fill: #f2ede3;
|
||||
}
|
||||
|
||||
.pw-skull-socket {
|
||||
fill: #17111d;
|
||||
}
|
||||
|
||||
.pw-skull path {
|
||||
stroke: #17111d;
|
||||
stroke-width: 0.9;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.pw-skull.is-rising {
|
||||
animation: pwSkullRise 1300ms ease-out forwards;
|
||||
}
|
||||
|
||||
/* Tema claro: a teia (cor creme, pensada pra contrastar com fundo escuro)
|
||||
ficaria invisível sobre o fundo branco do tema claro — vira escura. A
|
||||
aranha já é escura por natureza (#17111d), então já contrasta sozinha
|
||||
contra um fundo claro; o brilho branco de cima (pensado pro fundo escuro)
|
||||
só seria ruído aqui, por isso desligado. */
|
||||
:root[data-theme="light"] .pw-strands {
|
||||
stroke: rgba(20, 16, 28, 0.32);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .pw-strand-stray,
|
||||
:root[data-theme="light"] .pw-ring {
|
||||
stroke: rgba(20, 16, 28, 0.2);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .pw-spider {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
@keyframes pwShimmer {
|
||||
0%, 100% {
|
||||
opacity: 0.85;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pwLegsWiggle {
|
||||
0%, 100% {
|
||||
transform: rotate(0deg) scaleY(1);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(1.4deg) scaleY(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pwWalk {
|
||||
0%, 14% {
|
||||
transform: translate(var(--rx), var(--ry)) rotate(var(--rr));
|
||||
}
|
||||
32%, 55% {
|
||||
transform: translate(var(--ox), var(--oy)) rotate(var(--rr));
|
||||
}
|
||||
74%, 100% {
|
||||
transform: translate(var(--rx), var(--ry)) rotate(var(--rr));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pwSpiderDie {
|
||||
0% {
|
||||
transform: translate(var(--rx), var(--ry)) rotate(var(--rr));
|
||||
opacity: 1;
|
||||
}
|
||||
45% {
|
||||
transform: translate(calc(var(--rx) + 8px), calc(var(--ry) + 52px)) rotate(calc(var(--rr) + 99deg));
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translate(calc(var(--rx) + 13px), calc(var(--ry) + 117px)) rotate(calc(var(--rr) + 209deg));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pwLegsDie {
|
||||
to {
|
||||
transform: rotate(22deg) scaleY(0.72);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pwSkullRise {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(var(--sx), var(--sy)) scale(0.35);
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
transform: translate(var(--sx), calc(var(--sy) - 15px)) scale(1.15);
|
||||
}
|
||||
32% {
|
||||
transform: translate(var(--sx), calc(var(--sy) - 11px)) scale(1);
|
||||
}
|
||||
78% {
|
||||
opacity: 1;
|
||||
transform: translate(var(--sx), calc(var(--sy) - 37px)) scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(var(--sx), calc(var(--sy) - 47px)) scale(0.9);
|
||||
}
|
||||
}
|
||||
@ -360,6 +360,13 @@
|
||||
}
|
||||
|
||||
.main-content {
|
||||
/* `isolation:isolate` garante um contexto de empilhamento próprio — sem
|
||||
isso, `position:relative` sozinho (z-index auto) não isola nada, e a
|
||||
teia de aranha sazonal (z-index:-1, ver halloween-cobweb.css) escaparia
|
||||
pro contexto raiz da página em vez de ficar só atrás do conteúdo desta
|
||||
área. Mesmo raciocínio de `isolation` em `.login-page` (login.css). */
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
min-width: 0;
|
||||
transition: opacity var(--transition-base);
|
||||
}
|
||||
|
||||
259
static/css/login-intro-halloween.css
Normal file
259
static/css/login-intro-halloween.css
Normal file
@ -0,0 +1,259 @@
|
||||
/* Intro pós-login sazonal "Halloween" (vampiro) — variante de
|
||||
login-intro.css, ativa só quando `pidIsHalloweenActive()`
|
||||
(seasonal-theme.js) é verdadeiro; fora da janela de outubro,
|
||||
login-intro.css/login-intro.js (arquivo original, intocado) continuam
|
||||
sendo os únicos usados. Porta a composição de cenas Rise/Scare/Settle/
|
||||
Wordmark autorada em "Logo intro animation Halloween/export-halloween/
|
||||
pid-intro-halloween.html" (pasta de logos, fora do repositório) — ver
|
||||
`login-intro-halloween.js` e `docs/temas-sazonais/temas-sazonais.md`. Mesma
|
||||
paleta da marca P.I.D., acrescida do vinho da capa (#7A2733/#9E3241),
|
||||
hardcoded aqui como no arquivo de origem (não são tokens de tema). Só
|
||||
usado em index.html. */
|
||||
|
||||
.login-intro-h {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
background: radial-gradient(120% 80% at 50% 38%, rgba(123, 91, 168, 0.16), transparent 60%), #151022;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 500ms ease;
|
||||
}
|
||||
|
||||
.login-intro-h.is-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.login-intro-h__flash {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #9e3241;
|
||||
opacity: 0;
|
||||
mix-blend-mode: screen;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login-intro-h__stage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 36px;
|
||||
}
|
||||
|
||||
.login-intro-h__icon {
|
||||
width: 200px;
|
||||
height: 210px;
|
||||
flex: none;
|
||||
overflow: visible;
|
||||
opacity: 0;
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
|
||||
.login-intro-h__cape path {
|
||||
fill: #2e2740;
|
||||
}
|
||||
|
||||
.login-intro-h__cape-line {
|
||||
stroke: #3d3454;
|
||||
stroke-width: 1;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.login-intro-h__collar {
|
||||
fill: #7a2733;
|
||||
}
|
||||
|
||||
.login-intro-h__body {
|
||||
fill: #7b5ba8;
|
||||
}
|
||||
|
||||
.login-intro-h__tab {
|
||||
fill: #c6a24a;
|
||||
}
|
||||
|
||||
.login-intro-h__tab-shadow {
|
||||
fill: #7b5ba8;
|
||||
}
|
||||
|
||||
.login-intro-h__screen {
|
||||
fill: #241c33;
|
||||
}
|
||||
|
||||
.login-intro-h__face {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.login-intro-h__face--scare circle:not(.login-intro-h__pupil-red) {
|
||||
fill: #f2ede3;
|
||||
}
|
||||
|
||||
.login-intro-h__pupil-red {
|
||||
fill: #9e3241;
|
||||
}
|
||||
|
||||
.login-intro-h__brow-angry {
|
||||
stroke: #7a2733;
|
||||
stroke-width: 1.9;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.login-intro-h__mouth-fill {
|
||||
fill: #0c0814;
|
||||
}
|
||||
|
||||
.login-intro-h__mouth {
|
||||
stroke: #c6a24a;
|
||||
stroke-width: 3;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.login-intro-h__fang {
|
||||
fill: #f2ede3;
|
||||
}
|
||||
|
||||
.login-intro-h__face--friendly circle:not(.login-intro-h__pupil) {
|
||||
fill: #f2ede3;
|
||||
}
|
||||
|
||||
.login-intro-h__pupil {
|
||||
fill: #241c33;
|
||||
}
|
||||
|
||||
.login-intro-h__brow {
|
||||
stroke: #7a2733;
|
||||
stroke-width: 1.3;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.login-intro-h__mouth-friendly {
|
||||
stroke: #c6a24a;
|
||||
stroke-width: 3;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.login-intro-h__fang-friendly {
|
||||
fill: #f2ede3;
|
||||
}
|
||||
|
||||
/* A capa fica "levantada" cobrindo o rosto até a cena Scare abrir — por
|
||||
isso, ao contrário de `.login-intro-h__face` (que começa escondido),
|
||||
começa visível (T=0 já é capa cobrindo, ver `flapGoneFn` em
|
||||
login-intro-halloween.js). */
|
||||
.login-intro-h__flap-wrap {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.login-intro-h__flap-rotate path:first-child {
|
||||
fill: #2e2740;
|
||||
}
|
||||
|
||||
/* Olhos semicerrados espiando por cima da capa levantada — também visíveis
|
||||
desde o início (T=0), mesmo raciocínio do comentário acima. */
|
||||
.login-intro-h__squint {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.login-intro-h__squint path {
|
||||
stroke: #f2ede3;
|
||||
stroke-width: 2.6;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.login-intro-h__squint circle {
|
||||
fill: #9e3241;
|
||||
}
|
||||
|
||||
.login-intro-h__wordmark {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.login-intro-h__letters {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.login-intro-h__letter {
|
||||
display: inline-block;
|
||||
font-family: "Creepster", "Space Grotesk", sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 3.4rem;
|
||||
letter-spacing: 0.02em;
|
||||
color: #f2ede3;
|
||||
opacity: 0;
|
||||
text-shadow: 0 0 16px rgba(122, 39, 51, 0.75), 0 3px 0 rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.login-intro-h__dot {
|
||||
color: #c6a24a;
|
||||
}
|
||||
|
||||
.login-intro-h__rule {
|
||||
width: 240px;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #c6a24a, #7a2733);
|
||||
transform-origin: left center;
|
||||
transform: scaleX(0);
|
||||
}
|
||||
|
||||
.login-intro-h__tagline {
|
||||
font-family: "DM Mono", ui-monospace, monospace;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: #c6a24a;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Tema claro: mesmo raciocínio de login-intro.css — fundo escuro fixo
|
||||
destoava do resto do Portal nesse tema, então vira `--bg-canvas` claro; o
|
||||
texto do wordmark (pensado pra contrastar com fundo escuro) precisa virar
|
||||
escuro. O ícone não precisa de override (mesmo motivo de login-intro.css:
|
||||
o roxo/vinho tem contraste de sobra nos dois fundos). */
|
||||
:root[data-theme="light"] .login-intro-h {
|
||||
background: radial-gradient(120% 80% at 50% 38%, rgba(123, 91, 168, 0.1), transparent 60%), var(--bg-canvas);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .login-intro-h__letter {
|
||||
color: #3a1620;
|
||||
text-shadow: 0 0 10px rgba(122, 39, 51, 0.35), 0 2px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .login-intro-h__tagline {
|
||||
color: #7a2733;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.login-intro-h__stage {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.login-intro-h__icon {
|
||||
width: 130px;
|
||||
height: 137px;
|
||||
}
|
||||
|
||||
.login-intro-h__letter {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.login-intro-h__rule {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.login-intro-h__tagline {
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,12 @@
|
||||
.login-page {
|
||||
/* Isola um contexto de empilhamento próprio — sem isso, o fundo desta
|
||||
regra (abaixo) é conteúdo estático normal e pinta por CIMA de qualquer
|
||||
filho com z-index negativo (caso da teia de aranha sazonal, ver
|
||||
halloween-cobweb.css), já que `position:relative` sozinho (sem z-index)
|
||||
não cria um novo contexto. Com `isolation:isolate`, a teia (z-index:-1,
|
||||
filha direta daqui) fica corretamente atrás deste fundo e na frente
|
||||
do `.login-card`. */
|
||||
isolation: isolate;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
9
static/img/pid-halloween-favicon.svg
Normal file
9
static/img/pid-halloween-favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.2 KiB |
17
static/img/pid-halloween-icone-escuro.svg
Normal file
17
static/img/pid-halloween-icone-escuro.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.0 KiB |
21
static/img/pid-halloween-logo-horizontal-escuro.svg
Normal file
21
static/img/pid-halloween-logo-horizontal-escuro.svg
Normal file
File diff suppressed because one or more lines are too long
@ -47,6 +47,32 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
});
|
||||
}
|
||||
|
||||
/* Toggle "Temas sazonais" — liga/desliga temas sazonais (Halloween etc.),
|
||||
ver seasonal-theme.js. Recarrega a página ao alternar: o tema sazonal já
|
||||
foi aplicado (ou não) via troca de innerHTML/markup no carregamento
|
||||
desta mesma página, então não dá pra simplesmente desfazer isso in-place
|
||||
— o reload garante que tudo (ícone, teias, favicon) reflita a nova
|
||||
preferência de forma correta e imediata (importante sobretudo pra quem
|
||||
desligou por aracnofobia — a aranha precisa sumir na hora, não só na
|
||||
próxima navegação). */
|
||||
const seasonalToggleBtn = document.getElementById("seasonal-theme-toggle");
|
||||
const seasonalToggleLabel = document.getElementById("seasonal-theme-toggle-label");
|
||||
if (seasonalToggleBtn && seasonalToggleLabel && typeof pidSeasonalThemesOptedOut === "function") {
|
||||
const syncSeasonalToggle = () => {
|
||||
const optedOut = pidSeasonalThemesOptedOut();
|
||||
seasonalToggleBtn.setAttribute("aria-pressed", String(!optedOut));
|
||||
seasonalToggleLabel.textContent = optedOut ? "Inativo" : "Ativo";
|
||||
};
|
||||
|
||||
syncSeasonalToggle();
|
||||
|
||||
seasonalToggleBtn.addEventListener("click", () => {
|
||||
const optedOut = pidSeasonalThemesOptedOut();
|
||||
pidSetSeasonalThemesOptedOut(!optedOut);
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
chipBtn.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
dropdown.hidden = !dropdown.hidden;
|
||||
|
||||
@ -64,7 +64,11 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
redirectOn401: false,
|
||||
body: { username, password },
|
||||
});
|
||||
pidPlayLoginIntro(() => {
|
||||
const tocaIntro =
|
||||
typeof pidIsHalloweenActive === "function" && pidIsHalloweenActive() && typeof pidPlayLoginIntroHalloween === "function"
|
||||
? pidPlayLoginIntroHalloween
|
||||
: pidPlayLoginIntro;
|
||||
tocaIntro(() => {
|
||||
sessionStorage.setItem("pid_reveal_portal", "1");
|
||||
window.location.href = "portal.html";
|
||||
});
|
||||
|
||||
174
static/js/login-intro-halloween.js
Normal file
174
static/js/login-intro-halloween.js
Normal file
@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Intro pós-login sazonal "Halloween" — variante vampiro de
|
||||
* pidPlayLoginIntro (login-intro.js), portada da composição autorada em
|
||||
* "Logo intro animation Halloween/export-halloween/pid-intro-halloween.html"
|
||||
* (pasta de logos, fora do repositório; cenas Rise/Scare/Settle/Wordmark,
|
||||
* 1.3s/1.5s/1.4s/1.8s). Reaproveita PID_INTRO_EASING/pidIntroAnimate/
|
||||
* pidIntroClamp/pidIntroCornerTarget já definidos em login-intro.js (mesma
|
||||
* página, index.html, sempre presente) em vez de duplicá-los — só a
|
||||
* coreografia (cues/timing/elementos) é própria daqui. Só é chamada quando
|
||||
* `pidIsHalloweenActive()` (seasonal-theme.js); fora da janela de outubro,
|
||||
* pidPlayLoginIntro() (arquivo original, intocado) continua sendo a única
|
||||
* usada.
|
||||
*
|
||||
* Diferença em relação à composição original: ela termina com a wordmark e
|
||||
* o ícone sumindo juntos (sem "pouso" nenhum, pensada como vídeo solto); aqui
|
||||
* foi acrescentada uma cena extra "Fly" (mesma ideia/matemática do
|
||||
* `pidIntroCornerTarget` de login-intro.js) pra o ícone voar até o canto da
|
||||
* sidebar antes de tudo desaparecer, mesmo espírito de adaptação já usado no
|
||||
* intro normal ("só o destino final do voo foi recalibrado").
|
||||
*/
|
||||
|
||||
const PID_INTRO_H_CUES = { Rise: 0, Scare: 1.3, Settle: 2.8, Wordmark: 4.2, Fly: 6.0 };
|
||||
const PID_INTRO_H_TOTAL = 6.9;
|
||||
const PID_INTRO_H_ICON_K = 200 / 460; // coreografia original autorada pra um ícone de 460px; reescala pros nossos 200px
|
||||
|
||||
function pidPlayLoginIntroHalloween(onDone) {
|
||||
const overlay = document.getElementById("login-intro-halloween");
|
||||
const icon = document.getElementById("login-intro-h-icon");
|
||||
const stage = document.getElementById("login-intro-h-stage");
|
||||
const wordmark = document.getElementById("login-intro-h-wordmark");
|
||||
const flash = document.getElementById("login-intro-h-flash");
|
||||
if (!overlay || !icon || !stage || !wordmark || !flash) {
|
||||
onDone();
|
||||
return;
|
||||
}
|
||||
|
||||
const cape = icon.querySelector(".login-intro-h__cape");
|
||||
const collar = icon.querySelector(".login-intro-h__collar");
|
||||
const faceScare = icon.querySelector(".login-intro-h__face--scare");
|
||||
const faceFriendly = icon.querySelector(".login-intro-h__face--friendly");
|
||||
const flapWrap = icon.querySelector(".login-intro-h__flap-wrap");
|
||||
const flapRotate = document.getElementById("login-intro-h-flap-rotate");
|
||||
const squint = icon.querySelector(".login-intro-h__squint");
|
||||
const letters = Array.from(wordmark.querySelectorAll(".login-intro-h__letter"));
|
||||
const rule = wordmark.querySelector(".login-intro-h__rule");
|
||||
const tagline = wordmark.querySelector(".login-intro-h__tagline");
|
||||
|
||||
const { easeOutCubic, easeInOutQuad, easeOutBack } = PID_INTRO_EASING;
|
||||
const easeOutExpo = (t) => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t));
|
||||
const CUES = PID_INTRO_H_CUES;
|
||||
const K = PID_INTRO_H_ICON_K;
|
||||
|
||||
const riseFn = pidIntroAnimate({ from: 46, to: 0, start: CUES.Rise + 0.1, end: CUES.Rise + 1.1, ease: easeOutCubic });
|
||||
const appearFn = pidIntroAnimate({ start: CUES.Rise + 0.1, end: CUES.Rise + 0.75, ease: easeOutCubic });
|
||||
|
||||
const openFn = pidIntroAnimate({ start: CUES.Scare + 0.02, end: CUES.Scare + 0.42, ease: easeOutExpo });
|
||||
const settleSwayFn = pidIntroAnimate({ start: CUES.Settle + 0.3, end: CUES.Settle + 1.0, ease: easeInOutQuad });
|
||||
const flapGoneFn = pidIntroAnimate({ from: 1, to: 0, start: CUES.Scare + 0.42, end: CUES.Scare + 0.95, ease: easeInOutQuad });
|
||||
|
||||
const lungeFn = pidIntroAnimate({ start: CUES.Scare + 0.05, end: CUES.Scare + 0.32, ease: easeOutExpo });
|
||||
const relaxFn = pidIntroAnimate({ start: CUES.Scare + 0.75, end: CUES.Settle + 0.55, ease: easeInOutQuad });
|
||||
|
||||
const toFriendlyFn = pidIntroAnimate({ start: CUES.Settle + 0.2, end: CUES.Settle + 0.85, ease: easeInOutQuad });
|
||||
|
||||
const shiftFn = pidIntroAnimate({ start: CUES.Wordmark - 0.3, end: CUES.Wordmark + 0.55, ease: easeOutCubic });
|
||||
const ruleFn = pidIntroAnimate({ start: CUES.Wordmark + 0.55, end: CUES.Wordmark + 1.1, ease: easeInOutQuad });
|
||||
const tagFn = pidIntroAnimate({ start: CUES.Wordmark + 0.7, end: CUES.Wordmark + 1.25, ease: easeOutCubic });
|
||||
|
||||
const letterFns = letters.map((_, i) => {
|
||||
const s = CUES.Wordmark + 0.05 + i * 0.14;
|
||||
return {
|
||||
o: pidIntroAnimate({ start: s, end: s + 0.45, ease: easeOutCubic }),
|
||||
y: pidIntroAnimate({ from: 30, to: 0, start: s, end: s + 0.6, ease: easeOutBack }),
|
||||
};
|
||||
});
|
||||
|
||||
const flyFn = pidIntroAnimate({ start: CUES.Fly + 0.05, end: CUES.Fly + 0.55, ease: easeOutCubic });
|
||||
const wordmarkOutFn = pidIntroAnimate({ from: 1, to: 0, start: CUES.Fly, end: CUES.Fly + 0.3, ease: easeOutCubic });
|
||||
const stageFadeFn = pidIntroAnimate({ from: 1, to: 0, start: CUES.Fly + 0.5, end: CUES.Fly + 0.9, ease: easeOutCubic });
|
||||
|
||||
const SHIFT_X = -110;
|
||||
const ICON_SIZE = 200;
|
||||
const FINAL_ICON_SIZE = 40;
|
||||
const FINAL_SCALE = FINAL_ICON_SIZE / ICON_SIZE;
|
||||
const CORNER_MARGIN = 40;
|
||||
|
||||
const loginCard = document.querySelector(".login-card");
|
||||
if (loginCard) loginCard.classList.add("is-leaving");
|
||||
overlay.hidden = false;
|
||||
void overlay.offsetWidth;
|
||||
overlay.classList.add("is-visible");
|
||||
|
||||
let start;
|
||||
|
||||
function frame(now) {
|
||||
const T = Math.min((now - start) / 1000, PID_INTRO_H_TOTAL);
|
||||
|
||||
const rise = riseFn(T);
|
||||
const appear = appearFn(T);
|
||||
|
||||
const open = openFn(T);
|
||||
const settleSway = settleSwayFn(T);
|
||||
const flapGone = flapGoneFn(T);
|
||||
const flapAngle = -64 * open + settleSway * 6;
|
||||
|
||||
const lunge = lungeFn(T);
|
||||
const relax = relaxFn(T);
|
||||
const push = pidIntroClamp(lunge - relax, 0, 1);
|
||||
const shakeWin = pidIntroClamp(1 - Math.abs(T - (CUES.Scare + 0.34)) / 0.4, 0, 1);
|
||||
const shake = Math.sin(T * 78) * 4.2 * shakeWin;
|
||||
|
||||
const toFriendly = toFriendlyFn(T);
|
||||
const scare = pidIntroClamp(open * 1.6, 0, 1) * (1 - toFriendly);
|
||||
const friendly = toFriendly;
|
||||
const squintOpacity = pidIntroClamp(1 - open * 2.4, 0, 1);
|
||||
const glow = pidIntroClamp(push * 1.1, 0, 1);
|
||||
|
||||
icon.style.opacity = String(appear);
|
||||
|
||||
const shift = shiftFn(T);
|
||||
const fly = flyFn(T);
|
||||
const lockupX = SHIFT_X * shift;
|
||||
const lockupScale = 1 - 0.15 * shift;
|
||||
const target = pidIntroCornerTarget(ICON_SIZE, FINAL_ICON_SIZE, CORNER_MARGIN);
|
||||
const flyX = lockupX + (target.x - lockupX) * fly;
|
||||
const flyY = target.y * fly;
|
||||
const flyScale = lockupScale + (FINAL_SCALE - lockupScale) * fly;
|
||||
|
||||
const baseX = shake * K;
|
||||
const baseY = (rise - push * 26) * K;
|
||||
const baseScale = 1 + push * 0.14;
|
||||
icon.style.transform = `translate(${baseX + flyX}px, ${baseY + flyY}px) scale(${baseScale * flyScale})`;
|
||||
icon.style.filter = `drop-shadow(0 ${(10 * K).toFixed(1)}px ${(24 * K).toFixed(1)}px rgba(0,0,0,0.5)) drop-shadow(0 0 ${((18 + glow * 46) * K).toFixed(1)}px rgba(158,50,65,${(0.18 + glow * 0.5).toFixed(2)}))`;
|
||||
|
||||
cape.setAttribute("transform", `translate(38 44) scale(${1 + push * 0.2} ${1 + push * 0.06}) translate(-38 -44)`);
|
||||
collar.style.fill = glow > 0.05 ? "#9e3241" : "#7a2733";
|
||||
|
||||
faceScare.style.opacity = String(scare);
|
||||
faceFriendly.style.opacity = String(friendly);
|
||||
squint.style.opacity = String(squintOpacity);
|
||||
|
||||
flapWrap.style.opacity = String(flapGone);
|
||||
flapRotate.setAttribute("transform", `rotate(${flapAngle} 62 46)`);
|
||||
|
||||
wordmark.style.opacity = String(pidIntroClamp(shift * 1.4, 0, 1) * wordmarkOutFn(T));
|
||||
|
||||
letters.forEach((el, i) => {
|
||||
el.style.opacity = String(letterFns[i].o(T));
|
||||
el.style.transform = `translateY(${letterFns[i].y(T)}px)`;
|
||||
});
|
||||
|
||||
rule.style.transform = `scaleX(${ruleFn(T)})`;
|
||||
|
||||
const tag = tagFn(T);
|
||||
tagline.style.opacity = String(tag);
|
||||
tagline.style.transform = `translateY(${(1 - tag) * 12}px)`;
|
||||
|
||||
const flashWin = pidIntroClamp(1 - Math.abs(T - (CUES.Scare + 0.16)) / 0.26, 0, 1);
|
||||
flash.style.opacity = String(flashWin * 0.34);
|
||||
|
||||
stage.style.opacity = String(stageFadeFn(T));
|
||||
|
||||
if (T < PID_INTRO_H_TOTAL) {
|
||||
requestAnimationFrame(frame);
|
||||
} else {
|
||||
onDone();
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
start = performance.now();
|
||||
requestAnimationFrame(frame);
|
||||
}, PID_INTRO_FADE_MS);
|
||||
}
|
||||
@ -1,15 +1,32 @@
|
||||
/*
|
||||
* Ícone "P.I.D." do card de login (`#login-logo-btn`) — clicar faz os olhos
|
||||
* do disquete piscarem (`.pid-icon-eye`/`pidIconBlink` em base.css, mesmo
|
||||
* mecanismo da logo da sidebar em sidebar-brand.js). Sem navegação nenhuma
|
||||
* aqui — já estamos na tela de login, é só um toque de graça ao clicar no
|
||||
* mascote, não um atalho pra lugar nenhum.
|
||||
* Ícone "P.I.D." do card de login (`#login-logo-btn`). Fora do tema sazonal
|
||||
* Halloween: clicar faz os olhos do disquete piscarem
|
||||
* (`.pid-icon-eye`/`pidIconBlink` em base.css, mesmo mecanismo da logo da
|
||||
* sidebar em sidebar-brand.js). Durante o Halloween
|
||||
* (`pidIsHalloweenActive()`, seasonal-theme.js): o ícone vira o vampiro
|
||||
* (`pidApplyVampireIcon`) e o clique dispara a animação de "susto"
|
||||
* (`pidTriggerVampireScare`, `.is-scaring` em base.css) em vez da piscada.
|
||||
* Sem navegação nenhuma aqui — já estamos na tela de login, é só um toque de
|
||||
* graça ao clicar no mascote.
|
||||
*/
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const btn = document.getElementById("login-logo-btn");
|
||||
if (!btn) return;
|
||||
|
||||
const halloween = typeof pidIsHalloweenActive === "function" && pidIsHalloweenActive();
|
||||
|
||||
if (halloween) {
|
||||
pidApplyVampireIcon(btn.querySelector(".login-card__logo"));
|
||||
if (typeof pidApplySeasonalFavicon === "function") pidApplySeasonalFavicon();
|
||||
if (typeof pidApplyHalloweenCobwebs === "function") pidApplyHalloweenCobwebs("pw-cobwebs-login");
|
||||
}
|
||||
|
||||
btn.addEventListener("click", () => {
|
||||
if (halloween) {
|
||||
pidTriggerVampireScare(btn);
|
||||
return;
|
||||
}
|
||||
|
||||
btn.querySelectorAll(".pid-icon-eye").forEach((eye) => {
|
||||
eye.classList.remove("is-blinking");
|
||||
void eye.getBoundingClientRect();
|
||||
|
||||
304
static/js/seasonal-theme.js
Normal file
304
static/js/seasonal-theme.js
Normal file
@ -0,0 +1,304 @@
|
||||
/*
|
||||
* 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 = `
|
||||
<circle cx="30" cy="58" r="5" fill="#F2EDE3"></circle>
|
||||
<circle cx="46" cy="58" r="5" fill="#F2EDE3"></circle>
|
||||
<circle cx="32" cy="56.2" r="1.6" fill="#241C33"></circle>
|
||||
<circle cx="48" cy="56.2" r="1.6" fill="#241C33"></circle>
|
||||
<path d="M26 63.2 q4 2.6 8 -0.5" stroke="#7A2733" stroke-width="1.3" fill="none" stroke-linecap="round"></path>
|
||||
<path d="M42 63.2 q4 2.6 8 -0.5" stroke="#7A2733" stroke-width="1.3" fill="none" stroke-linecap="round"></path>
|
||||
<path d="M31 67 q7 6 14 0" stroke="#C6A24A" stroke-width="3" fill="none" stroke-linecap="round"></path>
|
||||
<path d="M33.6 68.6 L36.4 68.9 L35 72 Z" fill="#F2EDE3"></path>
|
||||
<path d="M40.2 68.9 L43 68.6 L41.6 72 Z" fill="#F2EDE3"></path>`;
|
||||
|
||||
const PID_VAMP_FACE_SCARE = `
|
||||
<circle cx="30" cy="57.4" r="5.6" fill="#F2EDE3"></circle>
|
||||
<circle cx="46" cy="57.4" r="5.6" fill="#F2EDE3"></circle>
|
||||
<circle cx="30" cy="57.4" r="2.1" fill="#9E3241"></circle>
|
||||
<circle cx="46" cy="57.4" r="2.1" fill="#9E3241"></circle>
|
||||
<path d="M24.6 51.4 L34.6 54.2" stroke="#7A2733" stroke-width="1.9" fill="none" stroke-linecap="round"></path>
|
||||
<path d="M51.4 51.4 L41.4 54.2" stroke="#7A2733" stroke-width="1.9" fill="none" stroke-linecap="round"></path>
|
||||
<path d="M29 66.4 q9 7.4 18 0 q-9 3 -18 0 Z" fill="#0C0814"></path>
|
||||
<path d="M29 66.4 q9 7.4 18 0" stroke="#C6A24A" stroke-width="3" fill="none" stroke-linecap="round"></path>
|
||||
<path d="M31.4 67.4 L34.6 67.9 L32.9 73 Z" fill="#F2EDE3"></path>
|
||||
<path d="M41.4 67.9 L44.6 67.4 L43.1 73 Z" fill="#F2EDE3"></path>`;
|
||||
|
||||
function pidVampireIconRigMarkup() {
|
||||
return `
|
||||
<g class="pid-vamp-cape-group">
|
||||
<path d="M4 9 L20 19 Q38 24 56 19 L72 9 C74 32 74 56 71 78 L5 78 C2 56 2 32 4 9 Z" fill="#2E2740"></path>
|
||||
<path class="pid-vamp-collar" d="M9 16 L21 22 Q38 26 55 22 L67 16 C68 26 68 31 66 36 L10 36 C8 31 8 26 9 16 Z" fill="#7A2733"></path>
|
||||
</g>
|
||||
<path d="M16 22 h34 l14 14 v40 a2 2 0 0 1 -2 2 H14 a2 2 0 0 1 -2 -2 V24 a2 2 0 0 1 2 -2 z" fill="#7B5BA8"></path>
|
||||
<rect x="28" y="22" width="20" height="16" fill="#C6A24A"></rect>
|
||||
<rect x="40" y="25" width="5" height="10" fill="#7B5BA8"></rect>
|
||||
<rect x="18" y="46" width="40" height="28" rx="2" fill="#241C33"></rect>
|
||||
<g class="pid-vamp-face pid-vamp-face--friendly">${PID_VAMP_FACE_FRIENDLY}</g>
|
||||
<g class="pid-vamp-face pid-vamp-face--scare">${PID_VAMP_FACE_SCARE}</g>`;
|
||||
}
|
||||
|
||||
/* Substitui o conteúdo de um <svg> 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 = `<g class="pid-vamp-icon-rig">${pidVampireIconRigMarkup()}</g>`;
|
||||
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 = `
|
||||
<g transform="translate(4,8)"><g class="pid-vamp-icon-rig">${pidVampireIconRigMarkup()}</g></g>
|
||||
<text x="96" y="50" font-family="'Space Grotesk',Helvetica,Arial,sans-serif" font-size="34" font-weight="700" letter-spacing="1.4" fill="#F2EDE3">P.I.D.</text>
|
||||
<text x="98" y="69" font-family="'DM Mono',ui-monospace,monospace" font-size="9.5" letter-spacing="2.6" fill="#C6A24A">PORTAL INTERNO DA DE PAULA</text>`;
|
||||
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 = `<svg viewBox="0 0 200 200" role="img" aria-hidden="true">
|
||||
<g class="pw-strands">
|
||||
<line x1="0" y1="0" x2="200.0" y2="10.5"></line>
|
||||
<line x1="0" y1="0" x2="200.0" y2="53.6"></line>
|
||||
<line x1="0" y1="0" x2="200.0" y2="106.3"></line>
|
||||
<line x1="0" y1="0" x2="200.0" y2="180.1"></line>
|
||||
<line x1="0" y1="0" x2="134.9" y2="200.0"></line>
|
||||
<line x1="0" y1="0" x2="76.8" y2="200.0"></line>
|
||||
<line x1="0" y1="0" x2="35.3" y2="200.0"></line>
|
||||
<line x1="0" y1="0" x2="7.0" y2="200.0"></line>
|
||||
<line class="pw-strand-stray" x1="0" y1="0" x2="296.3" y2="46.9"></line>
|
||||
<path class="pw-ring" d="M 30.0,1.6 Q 32.4,5.2 30.0,8.0 Q 32.4,13.0 30.0,16.0 Q 32.4,23.2 30.0,27.0 Q 27.1,30.8 20.2,30.0 Q 17.1,32.4 11.5,30.0 Q 9.1,32.4 5.3,30.0 Q 3.4,32.4 1.0,30.0"></path>
|
||||
<path class="pw-ring" d="M 58.0,3.0 Q 62.6,10.0 58.0,15.5 Q 62.6,25.0 58.0,30.8 Q 62.6,44.9 58.0,52.2 Q 52.4,59.5 39.1,58.0 Q 33.1,62.6 22.3,58.0 Q 17.5,62.6 10.2,58.0 Q 6.6,62.6 2.0,58.0"></path>
|
||||
<path class="pw-ring" d="M 90.0,4.7 Q 97.2,15.6 90.0,24.1 Q 97.2,38.9 90.0,47.9 Q 97.2,69.6 90.0,81.0 Q 81.4,92.4 60.7,90.0 Q 51.4,97.2 34.5,90.0 Q 27.2,97.2 15.9,90.0 Q 10.3,97.2 3.1,90.0"></path>
|
||||
<path class="pw-ring" d="M 126.0,6.6 Q 136.1,21.8 126.0,33.8 Q 136.1,54.4 126.0,67.0 Q 136.1,97.4 126.0,113.5 Q 113.9,129.3 85.0,126.0 Q 72.0,136.1 48.4,126.0 Q 38.1,136.1 22.2,126.0 Q 14.4,136.1 4.4,126.0"></path>
|
||||
<path class="pw-ring" d="M 166.0,8.7 Q 179.3,28.7 166.0,44.5 Q 179.3,71.7 166.0,88.3 Q 179.3,128.4 166.0,149.5 Q 150.1,170.4 112.0,166.0 Q 94.9,179.3 63.7,166.0 Q 50.2,179.3 29.3,166.0 Q 18.9,179.3 5.8,166.0"></path>
|
||||
</g>
|
||||
<g class="pw-spider">
|
||||
<g class="pw-legs">
|
||||
<path d="M-3,-2 L-14,-8 L-22,-6"></path>
|
||||
<path d="M-2,-1 L-13,2 L-21,5"></path>
|
||||
<path d="M-2,1 L-13,7 L-20,13"></path>
|
||||
<path d="M-3,2 L-12,11 L-18,18"></path>
|
||||
<path d="M3,-2 L14,-8 L22,-6"></path>
|
||||
<path d="M2,-1 L13,2 L21,5"></path>
|
||||
<path d="M2,1 L13,7 L20,13"></path>
|
||||
<path d="M3,2 L12,11 L18,18"></path>
|
||||
</g>
|
||||
<ellipse class="pw-abdomen" cx="0" cy="6" rx="7" ry="9"></ellipse>
|
||||
<circle class="pw-head" cx="0" cy="-6" r="4.5"></circle>
|
||||
<circle class="pw-eye" cx="-1.6" cy="-7" r="0.9"></circle>
|
||||
<circle class="pw-eye" cx="1.6" cy="-7" r="0.9"></circle>
|
||||
</g>
|
||||
<g class="pw-skull">
|
||||
<circle cx="0" cy="0" r="6.5"></circle>
|
||||
<ellipse class="pw-skull-socket" cx="-2.4" cy="-0.6" rx="1.5" ry="1.9"></ellipse>
|
||||
<ellipse class="pw-skull-socket" cx="2.4" cy="-0.6" rx="1.5" ry="1.9"></ellipse>
|
||||
<path d="M-1.2 3.4 L-0.4 2.2 L0.4 3.4 L1.2 2.2"></path>
|
||||
</g>
|
||||
</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 =
|
||||
`<div class="pw-corner pw-corner--tl">${PID_COBWEB_SVG}</div>` +
|
||||
`<div class="pw-corner pw-corner--br">${PID_COBWEB_SVG}</div>`;
|
||||
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;
|
||||
});
|
||||
});
|
||||
@ -1,20 +1,42 @@
|
||||
/*
|
||||
* Logo da sidebar (`#sidebar-brand-link`, presente nos 10 shells) — link de
|
||||
* verdade pra `portal.html` (tela Principal), com os olhos do ícone
|
||||
* piscando antes de navegar. A navegação é adiada (`setTimeout`) só o
|
||||
* suficiente pra a piscada (`.pid-icon-eye`/`pidIconBlink` em base.css,
|
||||
* 200ms) terminar de tocar antes da página trocar; ctrl/cmd/shift/clique do
|
||||
* meio continuam abrindo em nova aba normalmente (não intercepta esses
|
||||
* casos).
|
||||
* Logo da sidebar (`#sidebar-brand-link`, presente em todo shell) — link de
|
||||
* verdade pra `portal.html` (tela Principal). Fora do tema sazonal
|
||||
* Halloween: os olhos do ícone piscam antes de navegar
|
||||
* (`.pid-icon-eye`/`pidIconBlink` em base.css, 200ms), navegação adiada só o
|
||||
* suficiente pra a piscada terminar de tocar. Durante o Halloween
|
||||
* (`pidIsHalloweenActive()`, seasonal-theme.js): o ícone/assinatura viram o
|
||||
* vampiro (`pidApplyVampireIcon`/`pidApplyVampireFull`) e o clique dispara a
|
||||
* animação de "susto" (`pidTriggerVampireScare`, `.is-scaring` em base.css)
|
||||
* em vez da piscada, com a navegação adiada o suficiente pra ela ser vista.
|
||||
* ctrl/cmd/shift/clique do meio continuam abrindo em nova aba normalmente
|
||||
* (não intercepta esses casos).
|
||||
*/
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const brand = document.getElementById("sidebar-brand-link");
|
||||
if (!brand) return;
|
||||
|
||||
const halloween = typeof pidIsHalloweenActive === "function" && pidIsHalloweenActive();
|
||||
|
||||
if (halloween) {
|
||||
pidApplyVampireIcon(brand.querySelector(".sidebar__logo--icon"));
|
||||
pidApplyVampireFull(brand.querySelector(".sidebar__logo--full"));
|
||||
if (typeof pidApplySeasonalFavicon === "function") pidApplySeasonalFavicon();
|
||||
// Container só existe em portal.html (#pw-cobwebs-portal) — no-op nos demais shells.
|
||||
if (typeof pidApplyHalloweenCobwebs === "function") pidApplyHalloweenCobwebs("pw-cobwebs-portal");
|
||||
}
|
||||
|
||||
brand.addEventListener("click", (event) => {
|
||||
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
||||
event.preventDefault();
|
||||
|
||||
if (halloween) {
|
||||
pidTriggerVampireScare(brand);
|
||||
setTimeout(() => {
|
||||
window.location.href = brand.getAttribute("href");
|
||||
}, PID_HALLOWEEN_SCARE_MS - 100);
|
||||
return;
|
||||
}
|
||||
|
||||
brand.querySelectorAll(".pid-icon-eye").forEach((eye) => {
|
||||
eye.classList.remove("is-blinking");
|
||||
void eye.getBoundingClientRect();
|
||||
|
||||
@ -308,6 +308,13 @@
|
||||
<p class="account-dropdown__section-title">Tema de cores</p>
|
||||
<div class="theme-swatches" id="theme-swatches"></div>
|
||||
</div>
|
||||
<div class="account-dropdown__section">
|
||||
<p class="account-dropdown__section-title">Temas sazonais</p>
|
||||
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
|
||||
<span class="seasonal-theme-toggle__dot"></span>
|
||||
<span id="seasonal-theme-toggle-label">Ativo</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -559,6 +566,7 @@
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/confirm-modal.js' %}"></script>
|
||||
<script src="{% static 'js/seasonal-theme.js' %}"></script>
|
||||
<script src="{% static 'js/sidebar-brand.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
|
||||
@ -314,6 +314,13 @@
|
||||
<p class="account-dropdown__section-title">Tema de cores</p>
|
||||
<div class="theme-swatches" id="theme-swatches"></div>
|
||||
</div>
|
||||
<div class="account-dropdown__section">
|
||||
<p class="account-dropdown__section-title">Temas sazonais</p>
|
||||
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
|
||||
<span class="seasonal-theme-toggle__dot"></span>
|
||||
<span id="seasonal-theme-toggle-label">Ativo</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -695,6 +702,7 @@
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/confirm-modal.js' %}"></script>
|
||||
<script src="{% static 'js/seasonal-theme.js' %}"></script>
|
||||
<script src="{% static 'js/sidebar-brand.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
|
||||
@ -308,6 +308,13 @@
|
||||
<p class="account-dropdown__section-title">Tema de cores</p>
|
||||
<div class="theme-swatches" id="theme-swatches"></div>
|
||||
</div>
|
||||
<div class="account-dropdown__section">
|
||||
<p class="account-dropdown__section-title">Temas sazonais</p>
|
||||
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
|
||||
<span class="seasonal-theme-toggle__dot"></span>
|
||||
<span id="seasonal-theme-toggle-label">Ativo</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -681,6 +688,7 @@
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/confirm-modal.js' %}"></script>
|
||||
<script src="{% static 'js/seasonal-theme.js' %}"></script>
|
||||
<script src="{% static 'js/sidebar-brand.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
|
||||
@ -309,6 +309,13 @@
|
||||
<p class="account-dropdown__section-title">Tema de cores</p>
|
||||
<div class="theme-swatches" id="theme-swatches"></div>
|
||||
</div>
|
||||
<div class="account-dropdown__section">
|
||||
<p class="account-dropdown__section-title">Temas sazonais</p>
|
||||
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
|
||||
<span class="seasonal-theme-toggle__dot"></span>
|
||||
<span id="seasonal-theme-toggle-label">Ativo</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -858,6 +865,7 @@
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/confirm-modal.js' %}"></script>
|
||||
<script src="{% static 'js/seasonal-theme.js' %}"></script>
|
||||
<script src="{% static 'js/sidebar-brand.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
|
||||
@ -309,6 +309,13 @@
|
||||
<p class="account-dropdown__section-title">Tema de cores</p>
|
||||
<div class="theme-swatches" id="theme-swatches"></div>
|
||||
</div>
|
||||
<div class="account-dropdown__section">
|
||||
<p class="account-dropdown__section-title">Temas sazonais</p>
|
||||
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
|
||||
<span class="seasonal-theme-toggle__dot"></span>
|
||||
<span id="seasonal-theme-toggle-label">Ativo</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -876,6 +883,7 @@
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/confirm-modal.js' %}"></script>
|
||||
<script src="{% static 'js/seasonal-theme.js' %}"></script>
|
||||
<script src="{% static 'js/sidebar-brand.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
|
||||
@ -309,6 +309,13 @@
|
||||
<p class="account-dropdown__section-title">Tema de cores</p>
|
||||
<div class="theme-swatches" id="theme-swatches"></div>
|
||||
</div>
|
||||
<div class="account-dropdown__section">
|
||||
<p class="account-dropdown__section-title">Temas sazonais</p>
|
||||
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
|
||||
<span class="seasonal-theme-toggle__dot"></span>
|
||||
<span id="seasonal-theme-toggle-label">Ativo</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -876,6 +883,7 @@
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/confirm-modal.js' %}"></script>
|
||||
<script src="{% static 'js/seasonal-theme.js' %}"></script>
|
||||
<script src="{% static 'js/sidebar-brand.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
|
||||
@ -8,15 +8,18 @@
|
||||
<link rel="icon" type="image/svg+xml" href="{% static 'img/pid-icone-escuro.svg' %}" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bree+Serif&family=Pinyon+Script&family=Space+Grotesk:wght@700&family=DM+Mono&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bree+Serif&family=Pinyon+Script&family=Space+Grotesk:wght@700&family=DM+Mono&family=Creepster&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="{% static 'css/tokens.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'css/base.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'css/login.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'css/login-intro.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'css/login-intro-halloween.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'css/halloween-cobweb.css' %}" />
|
||||
<script src="{% static 'js/theme.js' %}" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-page">
|
||||
<div class="pw-corners pw-corners--login" id="pw-cobwebs-login" hidden aria-hidden="true"></div>
|
||||
<div class="login-card">
|
||||
<button type="button" class="login-card__logo-btn" id="login-logo-btn" aria-label="P.I.D.">
|
||||
<svg class="login-card__logo" viewBox="0 0 64 64" aria-hidden="true" focusable="false">
|
||||
@ -117,10 +120,88 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Variante sazonal (Halloween/vampiro) da intro acima — ver
|
||||
login-intro-halloween.css/.js e `docs/temas-sazonais/temas-sazonais.md`.
|
||||
Só é exibida quando `pidIsHalloweenActive()` (seasonal-theme.js); fora
|
||||
da janela de outubro, o bloco #login-intro acima continua sendo o
|
||||
único usado. -->
|
||||
<div class="login-intro-h" id="login-intro-halloween" hidden aria-hidden="true">
|
||||
<div class="login-intro-h__flash" id="login-intro-h-flash"></div>
|
||||
<div class="login-intro-h__stage" id="login-intro-h-stage">
|
||||
<svg class="login-intro-h__icon" id="login-intro-h-icon" viewBox="0 0 76 80">
|
||||
<defs>
|
||||
<clipPath id="pidHIntroBodyClip">
|
||||
<path d="M16 22 h34 l14 14 v40 a2 2 0 0 1 -2 2 H14 a2 2 0 0 1 -2 -2 V24 a2 2 0 0 1 2 -2 z"></path>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g class="login-intro-h__cape">
|
||||
<path d="M4 9 L20 19 Q38 24 56 19 L72 9 C74 32 74 56 71 78 L5 78 C2 56 2 32 4 9 Z"></path>
|
||||
<path class="login-intro-h__cape-line" d="M4 9 L20 19 Q38 24 56 19 L72 9"></path>
|
||||
</g>
|
||||
<path class="login-intro-h__collar" d="M9 16 L21 22 Q38 26 55 22 L67 16 C68 26 68 31 66 36 L10 36 C8 31 8 26 9 16 Z"></path>
|
||||
<path class="login-intro-h__body" d="M16 22 h34 l14 14 v40 a2 2 0 0 1 -2 2 H14 a2 2 0 0 1 -2 -2 V24 a2 2 0 0 1 2 -2 z"></path>
|
||||
<rect class="login-intro-h__tab" x="28" y="22" width="20" height="16"></rect>
|
||||
<rect class="login-intro-h__tab-shadow" x="40" y="25" width="5" height="10"></rect>
|
||||
<rect class="login-intro-h__screen" x="18" y="46" width="40" height="28" rx="2"></rect>
|
||||
|
||||
<g class="login-intro-h__face login-intro-h__face--scare">
|
||||
<circle cx="30" cy="57.4" r="5"></circle>
|
||||
<circle cx="46" cy="57.4" r="5"></circle>
|
||||
<circle class="login-intro-h__pupil-red" cx="30" cy="57.4" r="2.1"></circle>
|
||||
<circle class="login-intro-h__pupil-red" cx="46" cy="57.4" r="2.1"></circle>
|
||||
<path class="login-intro-h__brow-angry" d="M24.6 51.4 L34.6 54.2"></path>
|
||||
<path class="login-intro-h__brow-angry" d="M51.4 51.4 L41.4 54.2"></path>
|
||||
<path class="login-intro-h__mouth-fill" d="M29 66.4 q9 7.4 18 0 q-9 3 -18 0 Z"></path>
|
||||
<path class="login-intro-h__mouth" d="M29 66.4 q9 7.4 18 0"></path>
|
||||
<path class="login-intro-h__fang" d="M31.4 67.4 L34.6 67.9 L32.9 73 Z"></path>
|
||||
<path class="login-intro-h__fang" d="M41.4 67.9 L44.6 67.4 L43.1 73 Z"></path>
|
||||
</g>
|
||||
|
||||
<g class="login-intro-h__face login-intro-h__face--friendly">
|
||||
<circle cx="30" cy="58" r="5"></circle>
|
||||
<circle cx="46" cy="58" r="5"></circle>
|
||||
<circle class="login-intro-h__pupil" cx="32" cy="56.2" r="1.6"></circle>
|
||||
<circle class="login-intro-h__pupil" cx="48" cy="56.2" r="1.6"></circle>
|
||||
<path class="login-intro-h__brow" d="M26 63.2 q4 2.6 8 -0.5"></path>
|
||||
<path class="login-intro-h__brow" d="M42 63.2 q4 2.6 8 -0.5"></path>
|
||||
<path class="login-intro-h__mouth-friendly" d="M31 67 q7 6 14 0"></path>
|
||||
<path class="login-intro-h__fang-friendly" d="M33.6 68.6 L36.4 68.9 L35 72 Z"></path>
|
||||
<path class="login-intro-h__fang-friendly" d="M40.2 68.9 L43 68.6 L41.6 72 Z"></path>
|
||||
</g>
|
||||
|
||||
<g class="login-intro-h__flap-wrap" clip-path="url(#pidHIntroBodyClip)">
|
||||
<g class="login-intro-h__flap-rotate" id="login-intro-h-flap-rotate">
|
||||
<path d="M66 58 C50 52 26 52 13 58 C10 64 10 72 11 78 C30 80 52 80 67 76 C68 68 68 63 66 58 Z"></path>
|
||||
<path class="login-intro-h__cape-line" d="M66 58 C50 52 26 52 13 58"></path>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g class="login-intro-h__squint">
|
||||
<path d="M25.5 53.4 q4.5 -3.4 9 0"></path>
|
||||
<path d="M41.5 53.4 q4.5 -3.4 9 0"></path>
|
||||
<circle cx="30" cy="52.4" r="1.1"></circle>
|
||||
<circle cx="46" cy="52.4" r="1.1"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div class="login-intro-h__wordmark" id="login-intro-h-wordmark">
|
||||
<div class="login-intro-h__letters">
|
||||
<span class="login-intro-h__letter">P<span class="login-intro-h__dot">.</span></span>
|
||||
<span class="login-intro-h__letter">I<span class="login-intro-h__dot">.</span></span>
|
||||
<span class="login-intro-h__letter">D<span class="login-intro-h__dot">.</span></span>
|
||||
</div>
|
||||
<div class="login-intro-h__rule"></div>
|
||||
<div class="login-intro-h__tagline">Portal Interno da De Paula</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/confirm-modal.js' %}"></script>
|
||||
<script src="{% static 'js/seasonal-theme.js' %}"></script>
|
||||
<script src="{% static 'js/login-logo-blink.js' %}"></script>
|
||||
<script src="{% static 'js/login-intro.js' %}"></script>
|
||||
<script src="{% static 'js/login-intro-halloween.js' %}"></script>
|
||||
<script src="{% static 'js/auth.js' %}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -309,6 +309,13 @@
|
||||
<p class="account-dropdown__section-title">Tema de cores</p>
|
||||
<div class="theme-swatches" id="theme-swatches"></div>
|
||||
</div>
|
||||
<div class="account-dropdown__section">
|
||||
<p class="account-dropdown__section-title">Temas sazonais</p>
|
||||
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
|
||||
<span class="seasonal-theme-toggle__dot"></span>
|
||||
<span id="seasonal-theme-toggle-label">Ativo</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -935,6 +942,7 @@
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/confirm-modal.js' %}"></script>
|
||||
<script src="{% static 'js/seasonal-theme.js' %}"></script>
|
||||
<script src="{% static 'js/sidebar-brand.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
|
||||
@ -314,6 +314,13 @@
|
||||
<p class="account-dropdown__section-title">Tema de cores</p>
|
||||
<div class="theme-swatches" id="theme-swatches"></div>
|
||||
</div>
|
||||
<div class="account-dropdown__section">
|
||||
<p class="account-dropdown__section-title">Temas sazonais</p>
|
||||
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
|
||||
<span class="seasonal-theme-toggle__dot"></span>
|
||||
<span id="seasonal-theme-toggle-label">Ativo</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -545,6 +552,7 @@
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/confirm-modal.js' %}"></script>
|
||||
<script src="{% static 'js/seasonal-theme.js' %}"></script>
|
||||
<script src="{% static 'js/sidebar-brand.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
|
||||
@ -309,6 +309,13 @@
|
||||
<p class="account-dropdown__section-title">Tema de cores</p>
|
||||
<div class="theme-swatches" id="theme-swatches"></div>
|
||||
</div>
|
||||
<div class="account-dropdown__section">
|
||||
<p class="account-dropdown__section-title">Temas sazonais</p>
|
||||
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
|
||||
<span class="seasonal-theme-toggle__dot"></span>
|
||||
<span id="seasonal-theme-toggle-label">Ativo</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -749,6 +756,7 @@
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/confirm-modal.js' %}"></script>
|
||||
<script src="{% static 'js/seasonal-theme.js' %}"></script>
|
||||
<script src="{% static 'js/sidebar-brand.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
|
||||
@ -308,6 +308,13 @@
|
||||
<p class="account-dropdown__section-title">Tema de cores</p>
|
||||
<div class="theme-swatches" id="theme-swatches"></div>
|
||||
</div>
|
||||
<div class="account-dropdown__section">
|
||||
<p class="account-dropdown__section-title">Temas sazonais</p>
|
||||
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
|
||||
<span class="seasonal-theme-toggle__dot"></span>
|
||||
<span id="seasonal-theme-toggle-label">Ativo</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -610,6 +617,7 @@
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/confirm-modal.js' %}"></script>
|
||||
<script src="{% static 'js/seasonal-theme.js' %}"></script>
|
||||
<script src="{% static 'js/sidebar-brand.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
<link rel="stylesheet" href="{% static 'css/components.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'css/widgets.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'css/ramais-lookup.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'css/halloween-cobweb.css' %}" />
|
||||
<script src="{% static 'js/theme.js' %}" defer></script>
|
||||
<script src="{% static 'js/portal-reveal.js' %}" defer></script>
|
||||
</head>
|
||||
@ -264,6 +265,7 @@
|
||||
</aside>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="pw-corners pw-corners--portal" id="pw-cobwebs-portal" hidden aria-hidden="true"></div>
|
||||
<header class="topbar">
|
||||
<button type="button" class="icon-btn" id="sidebar-toggle" aria-label="Alternar menu">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16" stroke-linecap="round"/></svg>
|
||||
@ -313,6 +315,13 @@
|
||||
<p class="account-dropdown__section-title">Tema de cores</p>
|
||||
<div class="theme-swatches" id="theme-swatches"></div>
|
||||
</div>
|
||||
<div class="account-dropdown__section">
|
||||
<p class="account-dropdown__section-title">Temas sazonais</p>
|
||||
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
|
||||
<span class="seasonal-theme-toggle__dot"></span>
|
||||
<span id="seasonal-theme-toggle-label">Ativo</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -538,6 +547,7 @@
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/confirm-modal.js' %}"></script>
|
||||
<script src="{% static 'js/seasonal-theme.js' %}"></script>
|
||||
<script src="{% static 'js/sidebar-brand.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
|
||||
@ -309,6 +309,13 @@
|
||||
<p class="account-dropdown__section-title">Tema de cores</p>
|
||||
<div class="theme-swatches" id="theme-swatches"></div>
|
||||
</div>
|
||||
<div class="account-dropdown__section">
|
||||
<p class="account-dropdown__section-title">Temas sazonais</p>
|
||||
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
|
||||
<span class="seasonal-theme-toggle__dot"></span>
|
||||
<span id="seasonal-theme-toggle-label">Ativo</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -760,6 +767,7 @@
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/confirm-modal.js' %}"></script>
|
||||
<script src="{% static 'js/seasonal-theme.js' %}"></script>
|
||||
<script src="{% static 'js/sidebar-brand.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
|
||||
@ -308,6 +308,13 @@
|
||||
<p class="account-dropdown__section-title">Tema de cores</p>
|
||||
<div class="theme-swatches" id="theme-swatches"></div>
|
||||
</div>
|
||||
<div class="account-dropdown__section">
|
||||
<p class="account-dropdown__section-title">Temas sazonais</p>
|
||||
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
|
||||
<span class="seasonal-theme-toggle__dot"></span>
|
||||
<span id="seasonal-theme-toggle-label">Ativo</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -661,6 +668,7 @@
|
||||
|
||||
<script src="{% static 'js/api.js' %}"></script>
|
||||
<script src="{% static 'js/confirm-modal.js' %}"></script>
|
||||
<script src="{% static 'js/seasonal-theme.js' %}"></script>
|
||||
<script src="{% static 'js/sidebar-brand.js' %}"></script>
|
||||
<script src="{% static 'js/dual-select.js' %}"></script>
|
||||
<script src="{% static 'js/profiles.js' %}"></script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user