9 Commits
56 changed files with 482 additions and 772 deletions
+1 -2
View File
@@ -126,8 +126,7 @@ Para detalhes completos sobre arquitetura, protocolos e manuais técnicos, acess
* 🩺 **[Diagnóstico e Troubleshooting](docs/diagnostics.md):** Auto-teste e guia de resolução de problemas. * 🩺 **[Diagnóstico e Troubleshooting](docs/diagnostics.md):** Auto-teste e guia de resolução de problemas.
* 💿 **[Instalação e Empacotamento Nativo](docs/installation.md):** Distribuição via `jpackage` sem JRE pré-instalado. * 💿 **[Instalação e Empacotamento Nativo](docs/installation.md):** Distribuição via `jpackage` sem JRE pré-instalado.
* ⚖️ **[Limitações Técnicas](docs/limitations.md):** Declaração de dependências conforme a Regra 23. * ⚖️ **[Limitações Técnicas](docs/limitations.md):** Declaração de dependências conforme a Regra 23.
* 🛠️ **[Modo de Desenvolvimento](docs/development-mode.md):** Parâmetro `--dev` e certificados de teste. * 🎨 **[Layout e Assinatura em Lote](docs/ui-layout.md):** Design da UI padronizada e operações em lote.
* 🎨 **[Layout Dual-Theme e Assinatura em Lote](docs/serpro-layout-and-batch.md):** Design da UI e operações em lote.
--- ---
-2
View File
@@ -25,8 +25,6 @@ assets/
│ ├── tutorial.svg # Ícone Tutorial e Ajuda (monitor com vídeo/play) │ ├── tutorial.svg # Ícone Tutorial e Ajuda (monitor com vídeo/play)
│ ├── cert.svg # Ícone Certificados Digitais (pasta com cadeado seguro) │ ├── cert.svg # Ícone Certificados Digitais (pasta com cadeado seguro)
│ ├── about.svg # Ícone Sobre o Sistema (círculo azul com 'i') │ ├── about.svg # Ícone Sobre o Sistema (círculo azul com 'i')
│ ├── sun.svg # Ícone Tema Claro (sol dourado com raios vetoriais)
│ ├── moon.svg # Ícone Tema Escuro (lua crescente azul/índigo)
│ ├── shield.svg # Ícone Brasão de Segurança (CyberShield com chave interna) │ ├── shield.svg # Ícone Brasão de Segurança (CyberShield com chave interna)
│ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais │ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais
│ ├── check.svg # Ícone Checkmark de sucesso e validação │ ├── check.svg # Ícone Checkmark de sucesso e validação
-3
View File
@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M 12 3 A 9 9 0 1 0 21 12 A 7.5 7.5 0 0 1 12 3 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 251 B

-11
View File
@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<circle cx="12" cy="12" r="4.5" fill="none" stroke="currentColor" stroke-width="2"/>
<line x1="12" y1="2" x2="12" y2="4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="12" y1="19.5" x2="12" y2="22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="2" y1="12" x2="4.5" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="19.5" y1="12" x2="22" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="4.9" y1="4.9" x2="6.7" y2="6.7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="17.3" y1="17.3" x2="19.1" y2="19.1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="4.9" y1="19.1" x2="6.7" y2="17.3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="17.3" y1="6.7" x2="19.1" y2="4.9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

+2 -2
View File
@@ -27,7 +27,7 @@ Se você atua no desenvolvimento, manutenção ou auditoria do código-fonte do
* **[Certificados A1, A3 e PKCS#11](a1-a3-certificates.md):** Funcionamento dos provedores `Pkcs12CertificateProvider`, `WindowsCertificateStoreProvider` (SunMSCAPI/CNG) e `Pkcs11CertificateProvider` (SunPKCS11), monitoramento dinâmico PC/SC (`SmartCardWatcher`) e extração de OIDs ICP-Brasil. * **[Certificados A1, A3 e PKCS#11](a1-a3-certificates.md):** Funcionamento dos provedores `Pkcs12CertificateProvider`, `WindowsCertificateStoreProvider` (SunMSCAPI/CNG) e `Pkcs11CertificateProvider` (SunPKCS11), monitoramento dinâmico PC/SC (`SmartCardWatcher`) e extração de OIDs ICP-Brasil.
* **[Assinaturas PDF (PAdES) e Selos Visuais SVG](pades.md):** Motor de assinatura PDFBox + Bouncy Castle, salvamento incremental, renderização de alta resolução (300 DPI Java2D) e gerenciamento de selos digitais. * **[Assinaturas PDF (PAdES) e Selos Visuais SVG](pades.md):** Motor de assinatura PDFBox + Bouncy Castle, salvamento incremental, renderização de alta resolução (300 DPI Java2D) e gerenciamento de selos digitais.
* **[Assinaturas XML (XAdES-BES e XMLDSig)](xml-xmldsig.md):** Conformidade com o padrão ETSI TS 101 903 / DOC-ICP-15.03 e suporte a notas fiscais eletrônicas (NF-e, CT-e, MDF-e, eSocial). * **[Assinaturas XML (XAdES-BES e XMLDSig)](xml-xmldsig.md):** Conformidade com o padrão ETSI TS 101 903 / DOC-ICP-15.03 e suporte a notas fiscais eletrônicas (NF-e, CT-e, MDF-e, eSocial).
* **[Layout Dual-Theme e Assinatura em Lote](serpro-layout-and-batch.md):** Especificação de interface gráfica moderna, alternância dinâmica de tema (Claro/Escuro) e fila de processamento em lote. * **[Layout de Interface e Assinatura em Lote](ui-layout.md):** Especificação de interface gráfica padronizada, acessibilidade WCAG AA e processamento em lote.
* **[Modo de Desenvolvimento](development-mode.md):** Guia de inicialização em modo `--dev`, políticas de não utilização de mocks em produção e geração de certificados A1 de teste locais. * **[Modo de Desenvolvimento](development-mode.md):** Guia de inicialização em modo `--dev`, políticas de não utilização de mocks em produção e geração de certificados A1 de teste locais.
--- ---
@@ -66,7 +66,7 @@ Se você precisa implantar, homologar ou validar os aspectos de conformidade e s
| [`browser-compatibility.md`](browser-compatibility.md) | Compatibilidade de navegadores, HTTPS para loopback e Private Network Access (PNA) | Integradores Web & Segurança | | [`browser-compatibility.md`](browser-compatibility.md) | Compatibilidade de navegadores, HTTPS para loopback e Private Network Access (PNA) | Integradores Web & Segurança |
| [`ccsp-v1.schema.json`](ccsp-v1.schema.json) | JSON Schema formal e versionado do protocolo CCSP/1 | Integradores & QA | | [`ccsp-v1.schema.json`](ccsp-v1.schema.json) | JSON Schema formal e versionado do protocolo CCSP/1 | Integradores & QA |
| [`sdk-release-process.md`](sdk-release-process.md) | Processo de publicação e release do SDK no registry npm privado | DevOps & Engenharia | | [`sdk-release-process.md`](sdk-release-process.md) | Processo de publicação e release do SDK no registry npm privado | DevOps & Engenharia |
| [`serpro-layout-and-batch.md`](serpro-layout-and-batch.md) | Design de interface e processamento em lote | UI/UX & Desenvolvedores | | [`ui-layout.md`](ui-layout.md) | Design de interface e processamento em lote | UI/UX & Desenvolvedores |
--- ---
Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

@@ -1,32 +1,32 @@
# Layout CyberCore Signer — Design Dual-Theme & Assinatura em Lote # Layout e Interface Gráfica — CyberCore Signer
O **CyberCore Signer** adota uma interface gráfica que une a ergonomia de hub consolidada no mercado nacional com uma identidade visual moderna, autoral e com suporte dinâmico a **Tema Claro (Light)** e **Tema Escuro (Dark)**. O **CyberCore Signer** adota uma interface gráfica profissional e moderna, com foco em usabilidade, segurança visual e conformidade rigorosa com os padrões de acessibilidade WCAG 2.1 (AA).
--- ---
## 1. Identidade Visual e Filosofia de Design ## 1. Identidade Visual e Filosofia de Design
Em conformidade com os requisitos do projeto, a interface **não é uma cópia datada**, mas sim uma evolução: A interface foi estruturada com foco na clareza operacional e consistência ergonômica:
1. **Identidade CyberCore:** Logotipo central em cristal tridimensional e escudo criptográfico vetorial (`SvgIcons`), subtítulo "Agente Criptográfico Local • ICP-Brasil" e indicador de segurança em tempo real. 1. **Identidade CyberCore:** Logotipo central em cristal e escudo criptográfico vetorial (`SvgIcons`), subtítulo "Agente Criptográfico Local • ICP-Brasil" e indicador de segurança em tempo real.
2. **Badge de Status Ativo:** Exibe o status da porta local (`● Agente Ativo • 127.0.0.1:21325`) com verde esmeralda no modo claro e verde neon no modo escuro. 2. **Badge de Status Ativo:** Exibe o status do agente e da porta local (`● Agente Ativo • 127.0.0.1:21325`) com indicação em verde esmeralda.
3. **Alternador Dinâmico de Temas:** Botão pílula localizado no cabeçalho superior que permite ao usuário alternar instantaneamente entre **Modo Claro** e **Modo Escuro**: 3. **Tema Único Padronizado:** Sistema visual unificado baseado em tokens CSS declarados em `.root`, garantindo contraste nítido, tipografia escalável e ausência de inconsistências de renderização.
- Ícone do Sol ☀️ para comutar para o tema Claro.
- Ícone da Lua 🌙 para comutar para o tema Escuro.
- Classe gerenciadora centralizada: [`ThemeManager.java`](file:///var/www/cybercore/cybercore-signer/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/theme/ThemeManager.java), que replica a escolha de tema para todos os diálogos secundários.
--- ---
## 2. Paleta de Cores dos Temas ## 2. Paleta de Cores e Tokens Semânticos
| Componente | Tema Claro (Light) | Tema Escuro (Dark) | | Componente / Token | Valor Canônico | Descrição |
|---|---|---| |---|---|---|
| **Fundo da Janela** | `#f1f5f9` (Slate suave) | `#0b0f17` (Dark profundo) | | **Fundo da Janela** (`-fx-color-background`) | `#f1f5f9` | Tom slate neutro e suave para leitura prolongada |
| **Header Superior** | `#ffffff`, borda `#e2e8f0` | `#121824`, borda `#1e293b` | | **Superfície de Cartões** (`-fx-color-surface`) | `#ffffff` | Branco puro para cartões, caixas e painéis |
| **Cartões de Ação** | `#ffffff`, borda `#e2e8f0`, hover `#0284c7` | `#131c2e`, borda `#1e293b`, hover `#38bdf8` | | **Bordas Principais** (`-fx-color-border`) | `#cbd5e1` | Linhas de divisão estrutural com contraste seguro |
| **Texto dos Botões** | `#1e293b` (Cinza escuro de alto contraste) | `#f8fafc` (Branco gelo nítido) | | **Bordas Suaves** (`-fx-color-border-subtle`) | `#e2e8f0` | Divisórias e contornos secundários |
| **Painel Lateral** | `#ffffff`, borda `#e2e8f0` | `#121824`, borda `#1e293b` | | **Texto Primário** (`-fx-color-text`) | `#0f172a` | Texto de alto contraste (WCAG ratio ~ 15.8:1) |
| **Badge de Status** | Fundo `#ecfdf5`, texto `#047857`, borda `#10b981` | Fundo `rgba(16, 185, 129, 0.15)`, texto `#34d399` | | **Texto Atenuado** (`-fx-color-text-muted`) | `#475569` | Rótulos secundários e metadados (WCAG ratio ~ 6.8:1) |
| **Alternador de Tema** | Fundo `#ffffff`, borda `#cbd5e1`, hover `#0284c7` | Fundo `#1e293b`, borda `#334155`, hover `#38bdf8` | | **Cor Primária** (`-fx-color-primary`) | `#0284c7` | Azul característico da marca CyberCore para ações padrão |
| **Sucesso** (`-fx-color-success`) | `#16a34a` | Indicadores de assinaturas válidas e status operacional |
| **Aviso** (`-fx-color-warning`) | `#d97706` | Alertas de atenção e certificados próximos ao vencimento |
| **Erro** (`-fx-color-error`) | `#dc2626` | Notificações de erro ou falha criptográfica |
--- ---
@@ -36,7 +36,7 @@ A interface principal organiza as operações em 6 cartões modernos com cantos
| Posição | Operação | Descrição | | Posição | Operação | Descrição |
|---|---|---| |---|---|---|
| (0, 0) | **Assinar PDF** | Assinatura digital de documentos PDF com cálculo de hash SHA-256 e gravação de assinatura. | | (0, 0) | **Assinar PDF** | Assinatura digital de documentos PDF (PAdES) com visualização e posicionamento interativo de selos. |
| (1, 0) | **Assinar XML** | Assinatura de documentos XML (estruturas fiscais, NF-e, XMLDSig). | | (1, 0) | **Assinar XML** | Assinatura de documentos XML (estruturas fiscais, NF-e, XMLDSig). |
| (2, 0) | **Assinatura Atachada** | Assinatura embutida/anexada ao documento original (CAdES enveloped). | | (2, 0) | **Assinatura Atachada** | Assinatura embutida/anexada ao documento original (CAdES enveloped). |
| (0, 1) | **Gerenciar Selos** | Abertura do Designer e Gerenciador de Selos Digitais visuais para PAdES. | | (0, 1) | **Gerenciar Selos** | Abertura do Designer e Gerenciador de Selos Digitais visuais para PAdES. |
@@ -52,7 +52,7 @@ Na coluna lateral direita, o usuário dispõe de atalhos rápidos para operaçõ
1. **`</> Logs`:** Visualizador de auditoria em tempo real, monitorando requisições locais CCSP/1 e eventos criptográficos. 1. **`</> Logs`:** Visualizador de auditoria em tempo real, monitorando requisições locais CCSP/1 e eventos criptográficos.
2. **`⚙ Configurações`:** Gerenciador de origens web autorizadas, revogação de acessos e opções de PIN. 2. **`⚙ Configurações`:** Gerenciador de origens web autorizadas, revogação de acessos e opções de PIN.
3. **`💻 Tutorial`:** Guia passo a passo interativo de uso do assinador e integração web. 3. **`💻 Tutorial`:** Guia passo a passo interativo de uso do assinador e integração web.
4. **`🗂 Certificados`:** Lista de certificados ICP-Brasil instalados (Windows-MY, A1 e A3) e ferramenta para importação de certificados A1 (`.pfx` / `.p12`). 4. **`🗂 Certificados`:** Lista de certificados ICP-Brasil disponíveis (Windows Certificate Store, A1 e A3) e ferramenta para importação de certificados A1 (`.pfx` / `.p12`).
5. **`ℹ Sobre`:** Informações de versão, engine JCA/JCE e garantias de privacidade local. 5. **`ℹ Sobre`:** Informações de versão, engine JCA/JCE e garantias de privacidade local.
--- ---
@@ -65,7 +65,6 @@ A funcionalidade de assinatura em lote permite:
- Seleção do certificado digital ICP-Brasil desejado (com exibição de titular, CPF/CNPJ, tipo A1/A3 e validade). - Seleção do certificado digital ICP-Brasil desejado (com exibição de titular, CPF/CNPJ, tipo A1/A3 e validade).
- Escolha da pasta de destino para os arquivos assinados (salvando arquivos `.p7s` correspondentes). - Escolha da pasta de destino para os arquivos assinados (salvando arquivos `.p7s` correspondentes).
- Execução assíncrona em thread isolada para manter a interface responsiva durante todo o lote. - Execução assíncrona em thread isolada para manter a interface responsiva durante todo o lote.
- Compatibilidade automática com o tema ativo (Claro ou Escuro).
--- ---
+74 -58
View File
@@ -1,79 +1,95 @@
# UI Troubleshooting & Resolução de Estilos (JavaFX CSS) # UI Troubleshooting & Resolução de Estilos (JavaFX CSS)
Este documento registra a investigação da causa raiz de problemas visuais na interface gráfica do CyberCore Signer, em especial a falha de renderização de textos escuros sobre fundo escuro no tema escuro. Este documento registra a investigação da causa raiz de problemas visuais na interface gráfica do CyberCore Signer, em especial a falha de renderização de textos com cor preta (`Color.BLACK`) via `setStyle()`, a eliminação definitiva de estilos inline de cor e a transição para o tema claro unificado.
--- ---
## 1. O Problema: Texto Preto sobre Fundo Escuro (Contraste ~1.1:1) ## 1. O Problema: Texto Renderizado em Preto (`#000000`) via `setStyle`
### Sintoma ### Sintoma
No tema escuro (`ThemeManager.AppTheme.DARK` / `.theme-dark`), determinados textos nos diálogos `ExitConfirmationDialog` (texto explicativo da ação), `PdfSignDialog` (nome e metadados do documento PDF selecionado, dica de arraste do selo visual) e `PdfCertificateSignDialog` (opção de memorização segura de PIN) eram renderizados com a cor preta (`#000000`) sobre um fundo escuro (`#090d16` ou `#111827`). Em diálogos modais como `ExitConfirmationDialog`, `PdfSignDialog` e `PdfCertificateSignDialog`, chamadas como:
```java
O contraste medido entre o texto e o fundo situava-se em aproximadamente **1.1:1 a 1.2:1**, violando completamente as diretrizes de acessibilidade WCAG AA (mínimo de 4.5:1 para texto normal) e tornando o texto praticamente invisível. subtitle.setStyle("-fx-text-fill: -fx-text-secondary;");
lblDocName.setStyle("-fx-text-fill: -fx-text-primary;");
dragTip.setStyle("-fx-text-fill: -fx-text-secondary;");
```
resultavam em texto renderizado com a cor preta opaca (`#000000`), mesmo quando o token `-fx-text-secondary` já se encontrava definido no arquivo `theme.css`.
--- ---
## 2. Investigação Técnica e Causa Raiz ## 2. Investigação Técnica e Causa Raiz
### 2.1 Lookups Inexistentes no CSS ### 2.1 Por que `setStyle("-fx-text-fill: -fx-text-secondary")` renderiza PRETO?
Ao inspecionar o código-fonte Java dessas telas, verificou-se o uso de estilos inline via `setStyle(...)`: No motor de estilização do JavaFX (`com.sun.javafx.css.StyleManager` / `CssStyleHelper`):
```java
subtitle.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;"); 1. **Momento da Avaliação (Timing) de Nós Fora da Árvore da Scene:**
lblDocName.setStyle("-fx-font-weight: bold; -fx-font-size: 12.5px; -fx-text-fill: -fx-text-primary;"); Quando um nó (como um `Label` ou `Text`) é instanciado no código Java dentro do construtor de um diálogo e recebe `.setStyle("-fx-text-fill: -fx-text-secondary;")`, ele ainda **não foi anexado ao grafo de cena** (`Scene`).
lblDocMeta.setStyle("-fx-font-size: 11px; -fx-text-fill: -fx-text-secondary;"); 2. **Escopo de Resolução de Lookups Ausente:**
previewBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; ..."); Como `-fx-text-secondary` é uma cor pesquisada (*looked-up color*) definida no seletor `.root` da folha de estilo da aplicação, a engine de CSS do JavaFX tenta resolver essa variável subindo a cadeia de nós pais (`parent chain`) até alcançar `.root`. Como o nó está solto na memória (sem parent e sem Scene vinculada no instante da chamada), o lookup não encontra nenhuma definição válida.
chkRememberPin.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-text-primary;"); 3. **Mecanismo de Fallback Silencioso do Modena (`Color.BLACK`):**
O analisador do JavaFX não lança exceção ao falhar na resolução de uma cor de lookup em estilo inline. Em vez disso, seu conversor (`PaintConverter` / `ColorConverter`) assume o valor padrão de salvaguarda da especificação Modena: **`Color.BLACK` (`rgba(0, 0, 0, 1.0)`)**.
4. **Imutabilidade e Prioridade do Estilo Inline:**
O estilo inline atribuído via `setStyle(...)` possui a especificidade mais alta na hierarquia CSS do JavaFX. O valor pré-resolvido como `Color.BLACK` fica gravado diretamente no cache de estilo do nó. Quando a Scene finalmente executa a passagem formal de CSS (`applyCss()` / `layout()`), a engine prioriza o estilo inline e não reavalia o lookup que falhou previamente.
### 2.2 Por que o problema não ocorre com Classes CSS?
Quando utilizamos classes semânticas declaradas em folha de estilo (ex.: `lbl.getStyleClass().add("text-muted");`):
```css
.text-muted {
-fx-text-fill: -fx-text-secondary;
}
``` ```
A resolução da regra é postergada até que o nó pertença à hierarquia da Scene e a passagem de CSS do JavaFX seja executada. Nesse momento, o nó tem acesso direto às definições de `.root`, resolvendo perfeitamente o token para a cor correta da paleta (`#475569`).
Os tokens `-fx-text-primary`, `-fx-text-secondary`, `-fx-bg-primary`, `-fx-border-light` e `-fx-accent` **não estavam declarados** no arquivo de folha de estilos `theme.css`.
### 2.2 Por que o JavaFX rebaixa para preto (`#000000`)?
No motor de estilização do JavaFX (`com.sun.javafx.css` / Modena):
1. Quando uma declaração inline em `setStyle(...)` utiliza uma cor de lookup (`-fx-text-fill: <variavel>`), o analisador CSS do JavaFX tenta resolver essa variável percorrendo os ancestrais do nó até o `.root` da cena.
2. Caso a variável **não seja encontrada**, o `StyleConverter` do JavaFX não lança exceção em tempo de execução. Em vez disso, seu mecanismo de tolerância a falhas adota o valor fallback padrão da especificação base de controles: **`Color.BLACK` (`rgba(0, 0, 0, 1.0)`)**.
3. Em um tema claro, o fundo é branco ou cinza claro (`#ffffff` / `#f1f5f9`), de modo que o texto preto parece ter funcionado por pura coincidência de luminância.
4. No tema escuro, no entanto, o fundo da aplicação é escuro (`#090d16` a `#111827`), fazendo com que o fallback para `#000000` destrua o contraste da interface.
### 2.3 Problemas Adicionais com `setStyle(...)`
- **Prioridade Forçada:** `setStyle(...)` tem especificidade de estilo inline (prioridade máxima), sobrescrevendo quaisquer regras de temas `.theme-light` e `.theme-dark` que tentem ajustar o elemento.
- **Acoplamento:** Espalha lógica visual e regras de cor pelo código Java, dificultando testes de regressão automatizados e auditorias de contraste.
--- ---
## 3. Diretrizes e Solução Adotada ## 3. Diretriz Arquitetural: Proibição de Cores em `setStyle()`
### 3.1 Definição Completa de Tokens e Aliases no `theme.css` Fica estabelecido como regra obrigatória de desenvolvimento no `cybercore-ui`:
Todos os tokens semânticos e seus aliases de compatibilidade são declarados formalmente nos blocos canônicos de tokens:
```css
.theme-light {
-fx-text-primary: -fx-color-text;
-fx-text-secondary: -fx-color-text-muted;
-fx-bg-primary: -fx-color-surface;
-fx-border-light: -fx-color-border-subtle;
-fx-accent: -fx-color-primary;
}
.theme-dark { 1. **`setStyle()` restrito exclusivamente à Geometria:**
-fx-text-primary: -fx-color-text; - Permitido: dimensões dinâmicas, larguras mínimas, alturas, alinhamentos e paddings específicos de cálculo em tempo de execução (`-fx-min-width`, `-fx-pref-width`, `-fx-padding`, `-fx-alignment`).
-fx-text-secondary: -fx-color-text-muted; - **Terminantemente proibido em `setStyle()`:**
-fx-bg-primary: -fx-color-surface; - `-fx-text-fill`
-fx-border-light: -fx-color-border; - `-fx-background-color`
-fx-accent: -fx-color-primary; - `-fx-border-color`
} - `-fx-fill` / `-fx-stroke`
``` - Códigos hexadecimais (`#...`) ou funções cromáticas (`rgb(...)`, `rgba(...)`)
- Nomes de lookups de cor (`-fx-accent`, `-fx-color-*`, etc.)
### 3.2 Eliminação de Cores Inline 2. **Uso Obrigatório de Classes CSS Semânticas:**
É terminantemente proibido utilizar `setStyle(...)` com propriedades que afetem cores: | Classe CSS | Finalidade | Propriedade Aplicada |
- Proibidos em `setStyle`: `-fx-text-fill`, `-fx-background-color`, `-fx-border-color`, `-fx-fill`, `-fx-stroke` ou códigos `#hex`. |---|---|---|
- Permitidos em `setStyle` exclusivamente quando estritamente necessário: geometria e layout (`-fx-min-width`, `-fx-pref-width`, `-fx-padding`). | `.text-primary` | Texto principal | `-fx-text-fill: -fx-text-primary;` |
| `.text-secondary`, `.text-muted` | Texto auxiliar / metadados | `-fx-text-fill: -fx-text-secondary;` |
| `.text-accent` | Destaque primário | `-fx-text-fill: -fx-accent;` |
| `.text-bold` | Tipografia em negrito | `-fx-font-weight: bold;` |
| `.text-small` | Texto compacto | `-fx-font-size: 11px;` |
| `.text-mono` | Fonte monoespaçada para hashes | `-fx-font-family: monospace;` |
| `.surface-box` | Container de superfície branca com borda | `-fx-background-color: -fx-color-surface;` |
| `.toolbar-box` | Barra de ferramentas compacta | `-fx-background-color: -fx-color-surface;` |
| `.scroll-transparent` | ScrollPane sem fundo cinza | `-fx-background-color: transparent;` |
| `.status-ok` / `.status-warn` / `.status-error` | Indicadores de status | Cores semânticas da paleta |
Toda estilização cromática deve ser feita atribuindo classes semânticas: ---
- `.text-primary`: Texto principal com alta luminância no escuro e baixa no claro.
- `.text-secondary`, `.text-muted`: Texto secundário / auxiliar com contraste garantido >= 4.5:1. ## 4. Salvaguardas Automatizadas
- `.text-accent`: Destaque com a cor primária da marca.
- `.surface-box`, `.toolbar-box`: Caixas de superfície neutra e barras de ferramentas. O teste de arquitetura [`DesignTokenGuardrailTest.java`](file:///var/www/cybercore/cybercore-signer/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/theme/DesignTokenGuardrailTest.java) atua como barreira de compilação contínua:
- Varre todos os arquivos `.java` no módulo `cybercore-ui` e reprova automaticamente o build se qualquer chamada `setStyle()` contiver propriedades ou valores de cor.
- Valida matematicamente a relação de contraste WCAG AA (mínimo de 4.5:1 para texto normal) em todas as combinações de tokens de texto sobre superfície/fundo.
- Assegura ausência de seletores ou classes residuais de temas escuros (`.theme-dark`, `AppTheme`, `isDark()`).
---
## 5. Comportamento da Barra de Título Nativa no Windows (DWM em Modo Escuro)
### Diagnóstico Operacional
Quando o sistema operacional Windows 10/11 está configurado no "Modo Escuro do Sistema" (*Personalização > Cores > Escolher o modo: Escuro*), o gerenciador de janelas do sistema operacional (**Desktop Window Manager — DWM**) aplica automaticamente o atributo `DWMWA_USE_IMMERSIVE_DARK_MODE` na moldura não-cliente da janela nativa (`HWND`).
Como o CyberCore Signer utiliza decoração nativa padrão (`StageStyle.DECORATED`) para preservar o comportamento esperado pelo usuário empresarial — incluindo cantos arredondados, atalhos de teclado nativos, suporte a múltiplos monitores com DPIs distintos e os *Snap Layouts* do Windows 11 — a barra de título nativa é desenhada pelo Windows em tom escuro, enquanto o conteúdo da janela exibe o tema claro unificado da aplicação.
### Diretriz de Arquitetura (Não Forçar)
Em estrita conformidade com as diretrizes do projeto (Regras 2 e 23):
1. **Não forçar a barra de título:** Evita-se o uso de hacks de baixo nível via JNA/JNI para sobrescrever o DWM do Windows ou janelas não decoradas (`UNDECORATED`), o que causaria quebra de acessibilidade e perda de recursos nativos do sistema.
2. **Harmonia Visual:** A moldura do Windows atua como moldura de sistema e o conteúdo claro do CyberCore Signer permanece legível, íntegro e em total conformidade com a especificação de interface.
### 3.3 Testes Automatizados de Guarda-Corpo
O projeto mantém testes unitários automatizados em `DesignTokenGuardrailTest` e `CssColorAndContrastGuardrailTest` que:
1. Fazem varredura estática de todo o código Java da UI para reprovar qualquer `setStyle` que declare cor ou hexadecimal.
2. Validam matematicamente a fórmula de luminância relativa WCAG 2.1 para cada par texto/fundo nos dois temas, garantindo conformidade rigorosa.
@@ -40,7 +40,6 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import br.com.cybercore.signer.ui.CyberCoreDesktopApplication; import br.com.cybercore.signer.ui.CyberCoreDesktopApplication;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import java.util.Optional; import java.util.Optional;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -422,8 +421,7 @@ public final class AssetManager {
Map<String, Paint> defsMap = new HashMap<>(); Map<String, Paint> defsMap = new HashMap<>();
parseDefs(svg, defsMap); parseDefs(svg, defsMap);
Color effectiveCurrentColor = (tint != null) ? tint : Color effectiveCurrentColor = (tint != null) ? tint : Color.web("#1E293B");
(ThemeManager.isDark() ? Color.web("#F1F5F9") : Color.web("#1E293B"));
List<Shape> dynamicStrokeShapes = new ArrayList<>(); List<Shape> dynamicStrokeShapes = new ArrayList<>();
List<Shape> dynamicFillShapes = new ArrayList<>(); List<Shape> dynamicFillShapes = new ArrayList<>();
RenderContext ctx = new RenderContext(defsMap, effectiveCurrentColor, dynamicStrokeShapes, dynamicFillShapes); RenderContext ctx = new RenderContext(defsMap, effectiveCurrentColor, dynamicStrokeShapes, dynamicFillShapes);
@@ -455,19 +453,6 @@ public final class AssetManager {
container.setMaxSize(targetWidth, targetHeight); container.setMaxSize(targetWidth, targetHeight);
container.setMinSize(targetWidth, targetHeight); container.setMinSize(targetWidth, targetHeight);
// Se for dinâmico (sem tint fixo) e contiver elementos currentColor, atualiza com a mudança de tema
if (tint == null && (!dynamicStrokeShapes.isEmpty() || !dynamicFillShapes.isEmpty())) {
ThemeManager.addListener(theme -> {
Color c = (theme == ThemeManager.AppTheme.DARK) ? Color.web("#F1F5F9") : Color.web("#1E293B");
for (Shape s : dynamicStrokeShapes) {
s.setStroke(c);
}
for (Shape s : dynamicFillShapes) {
s.setFill(c);
}
});
}
return container; return container;
} catch (Exception e) { } catch (Exception e) {
@@ -801,7 +786,7 @@ public final class AssetManager {
if (ctx != null && ctx.currentColor() != null) { if (ctx != null && ctx.currentColor() != null) {
return ctx.currentColor(); return ctx.currentColor();
} }
return ThemeManager.isDark() ? Color.web("#F1F5F9") : Color.web("#1E293B"); return Color.web("#1E293B");
} }
try { try {
return Color.web(trimmed); return Color.web(trimmed);
@@ -119,12 +119,6 @@ public final class Icons {
public static Node refresh() { return refresh(SIZE_16); } public static Node refresh() { return refresh(SIZE_16); }
public static Node refresh(int size) { return of("refresh", size); } public static Node refresh(int size) { return of("refresh", size); }
public static Node sun() { return sun(SIZE_16); }
public static Node sun(int size) { return of("sun", size); }
public static Node moon() { return moon(SIZE_16); }
public static Node moon(int size) { return of("moon", size); }
public static Node arrowRight() { return arrowRight(SIZE_16); } public static Node arrowRight() { return arrowRight(SIZE_16); }
public static Node arrowRight(int size) { return of("arrow-right", size); } public static Node arrowRight(int size) { return of("arrow-right", size); }
@@ -577,63 +577,6 @@ public final class SvgIcons {
return box; return box;
} }
/**
* Ícone de Sol (Light Mode).
*/
public static Node sunIcon(double size) {
return AssetManager.getIconOrDefault("sun", size, () -> buildSunIcon(size));
}
public static Node buildSunIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
Circle sunCore = new Circle(size * 0.5, size * 0.5, size * 0.24);
sunCore.setFill(new LinearGradient(
0, 0, 1, 1, true, CycleMethod.NO_CYCLE,
new Stop(0, Color.web("#f59e0b")),
new Stop(1, Color.web("#d97706"))
));
SVGPath rays = new SVGPath();
rays.setContent("M 12 2 L 12 5 M 12 19 L 12 22 M 2 12 L 5 12 M 19 12 L 22 12 " +
"M 4.93 4.93 L 7.05 7.05 M 16.95 16.95 L 19.07 19.07 " +
"M 4.93 19.07 L 7.05 16.95 M 16.95 7.05 L 19.07 4.93");
rays.setStroke(Color.web("#f59e0b"));
rays.setStrokeWidth(2.0);
rays.setScaleX(size / 24.0);
rays.setScaleY(size / 24.0);
box.getChildren().addAll(rays, sunCore);
return box;
}
/**
* Ícone de Lua (Dark Mode).
*/
public static Node moonIcon(double size) {
return AssetManager.getIconOrDefault("moon", size, () -> buildMoonIcon(size));
}
public static Node buildMoonIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
SVGPath moon = new SVGPath();
moon.setContent("M 21 12.79 A 9 9 0 1 1 11.21 3 A 7 7 0 0 0 21 12.79 Z");
moon.setFill(new LinearGradient(
0, 0, 1, 1, true, CycleMethod.NO_CYCLE,
new Stop(0, Color.web("#38bdf8")),
new Stop(1, Color.web("#818cf8"))
));
moon.setScaleX(size / 24.0);
moon.setScaleY(size / 24.0);
box.getChildren().add(moon);
return box;
}
/** /**
* Logotipo vetorial CyberShield. * Logotipo vetorial CyberShield.
@@ -4,70 +4,19 @@ import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import javafx.scene.Parent; import javafx.scene.Parent;
import javafx.scene.Scene; import javafx.scene.Scene;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.function.Consumer;
/** /**
* Gerenciador centralizado de temas visuais do CyberCore Signer. * Utilitário para aplicação da folha de estilo global do CyberCore Signer à Scene.
* Permite alternar dinamicamente entre Tema Claro (Light) e Tema Escuro (Dark) * Gerencia a folha de estilo padrão única do aplicativo.
* aplicando as classes CSS e folhas de estilo correspondentes em qualquer Scene.
*/ */
public final class ThemeManager { public final class ThemeManager {
public enum AppTheme {
LIGHT("theme-light", "Modo Claro"),
DARK("theme-dark", "Modo Escuro");
private final String cssClass;
private final String displayName;
AppTheme(String cssClass, String displayName) {
this.cssClass = cssClass;
this.displayName = displayName;
}
public String getCssClass() {
return cssClass;
}
public String getDisplayName() {
return displayName;
}
}
private static AppTheme currentTheme = AppTheme.LIGHT;
private static final List<Consumer<AppTheme>> listeners = new ArrayList<>();
private ThemeManager() { private ThemeManager() {
} }
public static AppTheme getCurrentTheme() {
return currentTheme;
}
public static boolean isDark() {
return currentTheme == AppTheme.DARK;
}
public static void setCurrentTheme(AppTheme theme) {
if (theme == null || theme == currentTheme) {
return;
}
currentTheme = theme;
notifyListeners();
}
public static AppTheme toggleTheme() {
currentTheme = (currentTheme == AppTheme.LIGHT) ? AppTheme.DARK : AppTheme.LIGHT;
notifyListeners();
return currentTheme;
}
/** /**
* Aplica o tema atualmente ativo a uma Scene, adicionando a folha de estilo global * Aplica a folha de estilo padrão única a uma Scene.
* e ajustando a classe CSS raiz (theme-light / theme-dark).
*/ */
public static void applyTheme(Scene scene) { public static void applyTheme(Scene scene) {
if (scene == null) { if (scene == null) {
@@ -81,40 +30,12 @@ public final class ThemeManager {
if (!scene.getStylesheets().contains(cssUrl)) { if (!scene.getStylesheets().contains(cssUrl)) {
scene.getStylesheets().add(cssUrl); scene.getStylesheets().add(cssUrl);
} }
Parent root = scene.getRoot();
if (root != null) {
applyThemeToNode(root);
}
} }
/** /**
* Aplica a classe de estilo do tema ativo no nó raiz fornecido. * Mantido para compatibilidade com chamadas de estilização de nós raiz.
*/ */
public static void applyThemeToNode(Parent node) { public static void applyThemeToNode(Parent node) {
if (node == null) { // No tema padrão único definido em .root, a vinculação da folha na Scene é suficiente.
return;
}
node.getStyleClass().removeAll(AppTheme.LIGHT.getCssClass(), AppTheme.DARK.getCssClass());
node.getStyleClass().add(currentTheme.getCssClass());
}
public static void addListener(Consumer<AppTheme> listener) {
if (listener != null && !listeners.contains(listener)) {
listeners.add(listener);
}
}
public static void removeListener(Consumer<AppTheme> listener) {
listeners.remove(listener);
}
private static void notifyListeners() {
for (Consumer<AppTheme> listener : new ArrayList<>(listeners)) {
try {
listener.accept(currentTheme);
} catch (Exception ignored) {
}
}
} }
} }
@@ -35,7 +35,6 @@ public final class MainAppView {
private final Stage stage; private final Stage stage;
private final VBox root = new VBox(16); private final VBox root = new VBox(16);
private final Button themeToggleBtn = new Button();
private Runnable onSignPdf; private Runnable onSignPdf;
private Runnable onSignXml; private Runnable onSignXml;
@@ -148,11 +147,8 @@ public final class MainAppView {
} }
} }
// Configuração e inicialização do Tema // Configuração e inicialização da folha de estilos global
ThemeManager.applyTheme(scene); ThemeManager.applyTheme(scene);
updateThemeToggleButton();
ThemeManager.addListener(theme -> updateThemeToggleButton());
stage.setTitle(CyberCoreUiConstants.APP_NAME); stage.setTitle(CyberCoreUiConstants.APP_NAME);
stage.getIcons().addAll(br.com.cybercore.signer.ui.assets.AssetManager.getApplicationIcons()); stage.getIcons().addAll(br.com.cybercore.signer.ui.assets.AssetManager.getApplicationIcons());
@@ -207,38 +203,10 @@ public final class MainAppView {
Label statusBadge = new Label("● Agente Ativo • 127.0.0.1:21325"); Label statusBadge = new Label("● Agente Ativo • 127.0.0.1:21325");
statusBadge.getStyleClass().add("cyber-status-badge"); statusBadge.getStyleClass().add("cyber-status-badge");
// Botão Alternador de Tema (Claro / Escuro) headerBar.getChildren().addAll(brandLogo, titleBox, leftSpacer, statusBadge);
themeToggleBtn.getStyleClass().add("theme-toggle-btn");
themeToggleBtn.setOnAction(e -> {
ThemeManager.toggleTheme();
if (stage.getScene() != null) {
ThemeManager.applyTheme(stage.getScene());
}
});
headerBar.getChildren().addAll(brandLogo, titleBox, leftSpacer, statusBadge, themeToggleBtn);
return headerBar; return headerBar;
} }
private void updateThemeToggleButton() {
boolean dark = ThemeManager.isDark();
HBox content = new HBox(8);
content.setAlignment(Pos.CENTER);
Node icon = dark ? Icons.sun(16) : Icons.moon(16);
StackPane iconBox = new StackPane(icon);
iconBox.setPrefSize(18, 18);
iconBox.setMinSize(18, 18);
iconBox.setMaxSize(18, 18);
iconBox.setAlignment(Pos.CENTER);
Label label = new Label(dark ? "Modo Claro" : "Modo Escuro");
label.getStyleClass().add("text-bold");
content.getChildren().addAll(iconBox, label);
themeToggleBtn.setGraphic(content);
}
private Button createActionButton(Node icon, String text, Runnable onAction) { private Button createActionButton(Node icon, String text, Runnable onAction) {
Button btn = new Button(); Button btn = new Button();
btn.getStyleClass().add("cyber-action-card"); btn.getStyleClass().add("cyber-action-card");
@@ -273,14 +273,8 @@ public final class PdfSignDialog {
btnCancel.setMinWidth(110); btnCancel.setMinWidth(110);
btnCancel.setOnAction(e -> stage.close()); btnCancel.setOnAction(e -> stage.close());
HBox btnNextContent = new HBox(8); btnNextToCert = new Button("Avançar", SvgIcons.arrowRightIcon(12));
btnNextContent.setAlignment(Pos.CENTER); btnNextToCert.setContentDisplay(javafx.scene.control.ContentDisplay.RIGHT);
Label lblBtnNextText = new Label("Avançar");
lblBtnNextText.getStyleClass().add("text-bold");
btnNextContent.getChildren().addAll(lblBtnNextText, SvgIcons.arrowRightIcon(12));
btnNextToCert = new Button();
btnNextToCert.setGraphic(btnNextContent);
btnNextToCert.getStyleClass().addAll("btn-primary", "btn-dialog-wide"); btnNextToCert.getStyleClass().addAll("btn-primary", "btn-dialog-wide");
btnNextToCert.setMinWidth(240); btnNextToCert.setMinWidth(240);
btnNextToCert.setPrefWidth(250); btnNextToCert.setPrefWidth(250);
@@ -683,11 +677,11 @@ public final class PdfSignDialog {
if (btnToggleResize != null) { if (btnToggleResize != null) {
if (isResizeModeActive) { if (isResizeModeActive) {
btnToggleResize.setText("✓ Concluir"); btnToggleResize.setText("✓ Concluir");
btnToggleResize.getStyleClass().setAll("btn-primary"); btnToggleResize.getStyleClass().setAll("button", "btn-primary", "btn-compact");
lblPositionFeedback.setText("👉 Puxe a alça azul no canto do selo para ajustar o tamanho"); lblPositionFeedback.setText("👉 Puxe a alça azul no canto do selo para ajustar o tamanho");
} else { } else {
btnToggleResize.setText("📏 Redimensionar"); btnToggleResize.setText("📏 Redimensionar");
btnToggleResize.getStyleClass().setAll("btn-secondary"); btnToggleResize.getStyleClass().setAll("button", "btn-secondary", "btn-compact");
updateFeedbackLabel(); updateFeedbackLabel();
} }
} }
@@ -25,8 +25,6 @@ assets/
│ ├── tutorial.svg # Ícone Tutorial e Ajuda (monitor com vídeo/play) │ ├── tutorial.svg # Ícone Tutorial e Ajuda (monitor com vídeo/play)
│ ├── cert.svg # Ícone Certificados Digitais (pasta com cadeado seguro) │ ├── cert.svg # Ícone Certificados Digitais (pasta com cadeado seguro)
│ ├── about.svg # Ícone Sobre o Sistema (círculo azul com 'i') │ ├── about.svg # Ícone Sobre o Sistema (círculo azul com 'i')
│ ├── sun.svg # Ícone Tema Claro (sol dourado com raios vetoriais)
│ ├── moon.svg # Ícone Tema Escuro (lua crescente azul/índigo)
│ ├── shield.svg # Ícone Brasão de Segurança (CyberShield com chave interna) │ ├── shield.svg # Ícone Brasão de Segurança (CyberShield com chave interna)
│ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais │ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais
│ ├── check.svg # Ícone Checkmark de sucesso e validação │ ├── check.svg # Ícone Checkmark de sucesso e validação
@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M 12 3 A 9 9 0 1 0 21 12 A 7.5 7.5 0 0 1 12 3 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 251 B

@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<circle cx="12" cy="12" r="4.5" fill="none" stroke="currentColor" stroke-width="2"/>
<line x1="12" y1="2" x2="12" y2="4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="12" y1="19.5" x2="12" y2="22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="2" y1="12" x2="4.5" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="19.5" y1="12" x2="22" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="4.9" y1="4.9" x2="6.7" y2="6.7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="17.3" y1="17.3" x2="19.1" y2="19.1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="4.9" y1="19.1" x2="6.7" y2="17.3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="17.3" y1="6.7" x2="19.1" y2="4.9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

@@ -3,14 +3,11 @@
.cyber-action-card, .cyber-action-card,
.cyber-side-card, .cyber-side-card,
.cyber-header-bar, .cyber-header-bar,
.theme-toggle-btn,
.cyber-status-badge { .cyber-status-badge {
-fx-effect: null !important; -fx-effect: null !important;
} }
.theme-light .cyber-action-card:hover, .cyber-action-card:hover,
.theme-dark .cyber-action-card:hover, .cyber-side-card:hover {
.cyber-side-card:hover,
.theme-toggle-btn:hover {
-fx-effect: null !important; -fx-effect: null !important;
} }
@@ -10,15 +10,7 @@
.root { .root {
-fx-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; -fx-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-text-primary: -fx-color-text;
-fx-text-secondary: -fx-color-text-muted;
-fx-bg-primary: -fx-color-surface;
-fx-border-light: -fx-color-border;
-fx-accent: -fx-color-primary;
}
/* --- Tema Claro (Light Theme) --- */
.theme-light {
/* Superfícies & Fundos */ /* Superfícies & Fundos */
-fx-color-background: #f1f5f9; -fx-color-background: #f1f5f9;
-fx-color-surface: #ffffff; -fx-color-surface: #ffffff;
@@ -73,7 +65,7 @@
-fx-control-inner-background: -fx-color-surface; -fx-control-inner-background: -fx-color-surface;
-fx-text-fill: -fx-color-text; -fx-text-fill: -fx-color-text;
/* Tokens semânticos complementares e aliases de compatibilidade */ /* Tokens semânticos essenciais usados pelo Java */
-fx-text-primary: -fx-color-text; -fx-text-primary: -fx-color-text;
-fx-text-secondary: -fx-color-text-muted; -fx-text-secondary: -fx-color-text-muted;
-fx-bg-primary: -fx-color-surface; -fx-bg-primary: -fx-color-surface;
@@ -81,70 +73,6 @@
-fx-accent: -fx-color-primary; -fx-accent: -fx-color-primary;
} }
/* --- Tema Escuro (Dark Theme) --- */
.theme-dark {
/* Superfícies & Fundos */
-fx-color-background: #090d16;
-fx-color-surface: #111827;
-fx-color-surface-hover: #1e293b;
-fx-color-surface-active: #334155;
-fx-color-surface-subtle: #0f172a;
/* Bordas */
-fx-color-border: #334155;
-fx-color-border-subtle: #1e293b;
-fx-color-border-hover: #475569;
/* Tipografia (WCAG AA: contraste >= 4.5:1) */
-fx-color-text: #f8fafc;
-fx-color-text-muted: #94a3b8;
-fx-color-text-inverse: #0f172a;
/* Primária (Azul da Marca CyberCore Dark) */
-fx-color-primary: #38bdf8;
-fx-color-primary-hover: #0284c7;
-fx-color-primary-active: #0369a1;
-fx-color-primary-subtle: rgba(56, 189, 248, 0.15);
/* Sucesso */
-fx-color-success: #34d399;
-fx-color-success-hover: #10b981;
-fx-color-success-subtle: rgba(52, 211, 153, 0.15);
-fx-color-success-border: rgba(52, 211, 153, 0.4);
/* Aviso */
-fx-color-warning: #fbbf24;
-fx-color-warning-hover: #f59e0b;
-fx-color-warning-subtle: rgba(251, 191, 36, 0.15);
-fx-color-warning-border: rgba(251, 191, 36, 0.4);
/* Erro */
-fx-color-error: #f87171;
-fx-color-error-hover: #ef4444;
-fx-color-error-subtle: rgba(248, 113, 113, 0.15);
-fx-color-error-border: rgba(248, 113, 113, 0.4);
/* Foco & Realce */
-fx-color-focus: #38bdf8;
/* Sombras & Efeitos */
-fx-color-shadow: rgba(0, 0, 0, 0.4);
-fx-color-shadow-hover: rgba(0, 0, 0, 0.6);
/* Lookups nativos do JavaFX */
-fx-base: -fx-color-background;
-fx-background: -fx-color-background;
-fx-control-inner-background: -fx-color-surface;
-fx-text-fill: -fx-color-text;
/* Tokens semânticos complementares e aliases de compatibilidade */
-fx-text-primary: -fx-color-text;
-fx-text-secondary: -fx-color-text-muted;
-fx-bg-primary: -fx-color-surface;
-fx-border-light: -fx-color-border;
-fx-accent: -fx-color-primary;
}
/* ========================================================================== /* ==========================================================================
2. REGRAS GERAIS DE ESTRUTURA E CONTAINERS 2. REGRAS GERAIS DE ESTRUTURA E CONTAINERS
========================================================================== */ ========================================================================== */
@@ -213,27 +141,6 @@
-fx-font-weight: bold; -fx-font-weight: bold;
} }
/* Botão de Alternância de Tema */
.theme-toggle-btn {
-fx-background-color: -fx-color-surface;
-fx-border-color: -fx-color-border;
-fx-border-radius: 18px;
-fx-border-width: 1px;
-fx-background-radius: 18px;
-fx-text-fill: -fx-color-text;
-fx-padding: 4px 12px;
-fx-font-size: 11px;
-fx-font-weight: bold;
-fx-cursor: hand;
-fx-effect: dropshadow(three-pass-box, -fx-color-shadow, 4, 0, 0, 1);
}
.theme-toggle-btn:hover {
-fx-background-color: -fx-color-surface-hover;
-fx-border-color: -fx-color-primary;
-fx-text-fill: -fx-color-primary;
}
/* ========================================================================== /* ==========================================================================
3. HUB DE AÇÕES E PAINEL LATERAL 3. HUB DE AÇÕES E PAINEL LATERAL
========================================================================== */ ========================================================================== */
@@ -45,6 +45,10 @@ import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Objects; import java.util.Objects;
/**
* Utilitário de captura automatizada das telas da interface do CyberCore Signer.
* Gera as capturas de tela atualizadas no tema claro padronizado.
*/
public class UiPreviewGenerator extends Application { public class UiPreviewGenerator extends Application {
private static final File OUTPUT_DIR = new File("docs/screenshots"); private static final File OUTPUT_DIR = new File("docs/screenshots");
@@ -74,277 +78,242 @@ public class UiPreviewGenerator extends Application {
// 5. Validação de Assinatura // 5. Validação de Assinatura
captureValidationScreenshots(primaryStage); captureValidationScreenshots(primaryStage);
// 6. Solicitação de Assinatura (Prompt CCSP) // 6. Solicitação de Assinatura Local (Prompt de Autorização)
capturePromptScreenshots(primaryStage); capturePromptScreenshots(primaryStage);
// 7. Selos Visuais // 7. Gerenciador de Selos
captureSealsScreenshots(primaryStage); captureSealsScreenshots(primaryStage);
// 8. Sobre // 8. Sobre
captureAboutScreenshots(primaryStage); captureAboutScreenshots(primaryStage);
// 9. Diálogo de Encerramento / Bandeja // 9. Confirmação de Saída
captureExitDialogScreenshots(primaryStage); captureExitDialogScreenshots(primaryStage);
// 10. Diálogo de Posicionamento e Assinatura PDF (PAdES) // 10. Assinador Visual de PDF
capturePdfSignDialogScreenshots(primaryStage); capturePdfSignDialogScreenshots(primaryStage);
// 11. Diálogo de Seleção de Certificado e PIN do PDF // 11. Seleção de Certificado e PIN do PDF
capturePdfCertificateSignDialogScreenshots(primaryStage); capturePdfCertificateSignDialogScreenshots(primaryStage);
System.out.println(">>> Capturas de tela concluídas com sucesso em: " + OUTPUT_DIR.getAbsolutePath());
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
Platform.exit(); Platform.exit();
System.exit(0);
} }
}); });
} }
private void captureHubScreenshots(Stage stage) throws Exception { private void captureHubScreenshots(Stage stage) throws Exception {
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) { MainAppView appView = new MainAppView(stage);
ThemeManager.setCurrentTheme(theme); stage.show();
MainAppView appView = new MainAppView(stage); stage.getScene().getRoot().applyCss();
stage.show(); stage.getScene().getRoot().layout();
stage.getScene().getRoot().applyCss();
stage.getScene().getRoot().layout();
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro"; saveNodeSnapshot(stage.getScene(), new File(OUTPUT_DIR, "01_hub.png"));
saveNodeSnapshot(stage.getScene(), new File(OUTPUT_DIR, "01_hub_" + suffix + ".png")); saveNodeSnapshot(stage.getScene(), new File(OUTPUT_DIR, "01_hub_claro.png"));
if (theme == ThemeManager.AppTheme.LIGHT) { saveNodeSnapshot(stage.getScene(), new File(OUTPUT_DIR, "01_layout_claro.png"));
saveNodeSnapshot(stage.getScene(), new File(OUTPUT_DIR, "01_layout_claro.png"));
} else {
saveNodeSnapshot(stage.getScene(), new File(OUTPUT_DIR, "02_layout_escuro.png"));
}
}
} }
private void captureDiagnosticsScreenshots(Stage stage) throws Exception { private void captureDiagnosticsScreenshots(Stage stage) throws Exception {
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) { DiagnosticsView diagView = new DiagnosticsView(new DiagnosticService());
ThemeManager.setCurrentTheme(theme);
DiagnosticsView diagView = new DiagnosticsView(new DiagnosticService());
VBox container = wrapInDialogContainer("DIAGNÓSTICO DO SISTEMA", VBox container = wrapInDialogContainer("DIAGNÓSTICO DO SISTEMA",
"Verificação de prontidão do runtime, engines criptográficas e dispositivos.", "Verificação de prontidão do runtime, engines criptográficas e dispositivos.",
SvgIcons.cyberShieldLogo(24), diagView, 800, 520); SvgIcons.cyberShieldLogo(24), diagView, 800, 520);
Scene scene = createScene(container, 800, 520); Scene scene = createScene(container, 800, 520);
stage.setScene(scene); stage.setScene(scene);
stage.show(); stage.show();
scene.getRoot().applyCss(); scene.getRoot().applyCss();
scene.getRoot().layout(); scene.getRoot().layout();
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro"; saveNodeSnapshot(scene, new File(OUTPUT_DIR, "02_diagnostico.png"));
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "02_diagnostico_" + suffix + ".png")); saveNodeSnapshot(scene, new File(OUTPUT_DIR, "02_diagnostico_claro.png"));
if (theme == ThemeManager.AppTheme.LIGHT) {
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "02_diagnostico.png"));
}
}
} }
private void captureSettingsScreenshots(Stage stage) throws Exception { private void captureSettingsScreenshots(Stage stage) throws Exception {
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) { SettingsView settingsView = new SettingsView();
ThemeManager.setCurrentTheme(theme);
SettingsView settingsView = new SettingsView();
ScrollPane sp = new ScrollPane(settingsView); ScrollPane sp = new ScrollPane(settingsView);
sp.setFitToWidth(true); sp.setFitToWidth(true);
sp.getStyleClass().add("cyber-scroll-pane"); sp.getStyleClass().add("cyber-scroll-pane");
VBox.setVgrow(sp, Priority.ALWAYS); VBox.setVgrow(sp, Priority.ALWAYS);
VBox container = wrapInDialogContainer("CONFIGURAÇÕES DO AGENTE", VBox container = wrapInDialogContainer("CONFIGURAÇÕES DO AGENTE",
"Parâmetros de segurança local, identidade visual e aplicações autorizadas.", "Parâmetros de segurança local, identidade visual e aplicações autorizadas.",
SvgIcons.settingsIcon(), sp, 800, 520); SvgIcons.settingsIcon(), sp, 800, 520);
Scene scene = createScene(container, 800, 520); Scene scene = createScene(container, 800, 520);
stage.setScene(scene); stage.setScene(scene);
stage.show(); stage.show();
scene.getRoot().applyCss(); scene.getRoot().applyCss();
scene.getRoot().layout(); scene.getRoot().layout();
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro"; saveNodeSnapshot(scene, new File(OUTPUT_DIR, "03_configuracoes.png"));
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "03_configuracoes_" + suffix + ".png")); saveNodeSnapshot(scene, new File(OUTPUT_DIR, "03_configuracoes_claro.png"));
if (theme == ThemeManager.AppTheme.LIGHT) {
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "03_configuracoes.png"));
}
}
} }
private void captureCertificatesScreenshots(Stage stage) throws Exception { private void captureCertificatesScreenshots(Stage stage) throws Exception {
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) { VBox cardsBox = new VBox(12);
ThemeManager.setCurrentTheme(theme); cardsBox.setPadding(new Insets(12));
VBox cardsBox = new VBox(12); ManagedCertificate cert1 = new ManagedCertificate(
cardsBox.setPadding(new Insets(12)); "cert-a1",
"JOAO DA SILVA:12345678900",
CertificateType.A1,
CertificateStatus.AVAILABLE,
"CN=JOAO DA SILVA:12345678900, OU=Autenticado por AR, O=ICP-Brasil, C=BR",
"CN=AC SERPRO RFB v5, O=ICP-Brasil, C=BR",
"1A2B3C4D5E",
"11223344556677889900",
"AABBCCDDEEFF00112233445566778899",
Instant.now().minus(60, ChronoUnit.DAYS),
Instant.now().plus(300, ChronoUnit.DAYS),
"RSA",
2048,
"JOAO DA SILVA:12345678900",
"12345678900",
null,
"SunJSSE",
null
);
ManagedCertificate cert1 = new ManagedCertificate( ManagedCertificate cert2 = new ManagedCertificate(
"cert-a1", "cert-a3",
"JOAO DA SILVA:12345678900", "EMPRESA EXEMPLO LTDA:12345678000199",
CertificateType.A1, CertificateType.A3,
CertificateStatus.AVAILABLE, CertificateStatus.AVAILABLE,
"CN=JOAO DA SILVA:12345678900, OU=Autenticado por AR, O=ICP-Brasil, C=BR", "CN=EMPRESA EXEMPLO LTDA:12345678000199, O=ICP-Brasil, C=BR",
"CN=AC SERPRO RFB v5, O=ICP-Brasil, C=BR", "CN=AC VALID Brasil v5, O=ICP-Brasil, C=BR",
"1A2B3C4D5E", "5E4D3C2B1A",
"11223344556677889900", "99887766554433221100",
"AABBCCDDEEFF00112233445566778899", "FFEEDDCCBBAA99887766554433221100",
Instant.now().minus(60, ChronoUnit.DAYS), Instant.now().minus(120, ChronoUnit.DAYS),
Instant.now().plus(300, ChronoUnit.DAYS), Instant.now().plus(600, ChronoUnit.DAYS),
"RSA", "RSA",
2048, 2048,
"JOAO DA SILVA:12345678900", "EMPRESA EXEMPLO LTDA:12345678000199",
"12345678900", null,
null, "12345678000199",
"SunJSSE", "SunPKCS11",
null "Token USB SafeNet eToken 5110"
); );
ManagedCertificate cert2 = new ManagedCertificate( cardsBox.getChildren().addAll(new CertificateCardNode(cert1), new CertificateCardNode(cert2));
"cert-a3",
"EMPRESA EXEMPLO LTDA:12345678000199",
CertificateType.A3,
CertificateStatus.AVAILABLE,
"CN=EMPRESA EXEMPLO LTDA:12345678000199, O=ICP-Brasil, C=BR",
"CN=AC VALID Brasil v5, O=ICP-Brasil, C=BR",
"5E4D3C2B1A",
"99887766554433221100",
"FFEEDDCCBBAA99887766554433221100",
Instant.now().minus(120, ChronoUnit.DAYS),
Instant.now().plus(600, ChronoUnit.DAYS),
"RSA",
2048,
"EMPRESA EXEMPLO LTDA:12345678000199",
null,
"12345678000199",
"SunPKCS11",
"Token USB SafeNet eToken 5110"
);
cardsBox.getChildren().addAll(new CertificateCardNode(cert1), new CertificateCardNode(cert2)); ScrollPane sp = new ScrollPane(cardsBox);
sp.setFitToWidth(true);
sp.getStyleClass().add("cyber-scroll-pane");
VBox.setVgrow(sp, Priority.ALWAYS);
ScrollPane sp = new ScrollPane(cardsBox); VBox container = wrapInDialogContainer("CERTIFICADOS DIGITAIS & ARQUIVOS SEGUROS",
sp.setFitToWidth(true); "Certificados ICP-Brasil disponíveis para assinatura local no dispositivo.",
sp.getStyleClass().add("cyber-scroll-pane"); SvgIcons.certificatesIcon(), sp, 760, 520);
VBox.setVgrow(sp, Priority.ALWAYS);
VBox container = wrapInDialogContainer("CERTIFICADOS DIGITAIS & ARQUIVOS SEGUROS", Scene scene = createScene(container, 760, 520);
"Certificados ICP-Brasil disponíveis para assinatura local no dispositivo.", stage.setScene(scene);
SvgIcons.certificatesIcon(), sp, 760, 520); stage.show();
scene.getRoot().applyCss();
scene.getRoot().layout();
Scene scene = createScene(container, 760, 520); saveNodeSnapshot(scene, new File(OUTPUT_DIR, "04_certificados.png"));
stage.setScene(scene); saveNodeSnapshot(scene, new File(OUTPUT_DIR, "04_certificados_claro.png"));
stage.show();
scene.getRoot().applyCss();
scene.getRoot().layout();
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro";
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "04_certificados_" + suffix + ".png"));
}
} }
private void captureValidationScreenshots(Stage stage) throws Exception { private void captureValidationScreenshots(Stage stage) throws Exception {
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) { SignatureValidationDialog dlg = new SignatureValidationDialog(null);
ThemeManager.setCurrentTheme(theme); Scene scene = createValidationSampleScene(stage);
SignatureValidationDialog dlg = new SignatureValidationDialog(null); stage.setScene(scene);
// Captura layout do diálogo stage.show();
Scene scene = createValidationSampleScene(stage); scene.getRoot().applyCss();
stage.setScene(scene); scene.getRoot().layout();
stage.show();
scene.getRoot().applyCss();
scene.getRoot().layout();
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro"; saveNodeSnapshot(scene, new File(OUTPUT_DIR, "05_validacao.png"));
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "05_validacao_" + suffix + ".png")); saveNodeSnapshot(scene, new File(OUTPUT_DIR, "05_validacao_claro.png"));
}
} }
private void capturePromptScreenshots(Stage stage) throws Exception { private void capturePromptScreenshots(Stage stage) throws Exception {
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) { ManagedCertificate cert = new ManagedCertificate(
ThemeManager.setCurrentTheme(theme); "cert-a1",
"MARIA OLIVEIRA:98765432100",
CertificateType.A1,
CertificateStatus.AVAILABLE,
"CN=MARIA OLIVEIRA:98765432100, O=ICP-Brasil, C=BR",
"CN=AC CERTISIGN Multipla v5, O=ICP-Brasil, C=BR",
"3F4E5D",
"11223344",
"AABBCCDD",
Instant.now().minus(30, ChronoUnit.DAYS),
Instant.now().plus(330, ChronoUnit.DAYS),
"RSA",
2048,
"MARIA OLIVEIRA:98765432100",
"98765432100",
null,
"SunJSSE",
null
);
ManagedCertificate cert = new ManagedCertificate( SigningContext ctx = new SigningContext(
"cert-a1", "req-9876-uuid",
"MARIA OLIVEIRA:98765432100", "app-contratos-web",
CertificateType.A1, new byte[32],
CertificateStatus.AVAILABLE, "SHA256withRSA",
"CN=MARIA OLIVEIRA:98765432100, O=ICP-Brasil, C=BR", null,
"CN=AC CERTISIGN Multipla v5, O=ICP-Brasil, C=BR", Instant.now().plusSeconds(120)
"3F4E5D", );
"11223344",
"AABBCCDD",
Instant.now().minus(30, ChronoUnit.DAYS),
Instant.now().plus(330, ChronoUnit.DAYS),
"RSA",
2048,
"MARIA OLIVEIRA:98765432100",
"98765432100",
null,
"SunJSSE",
null
);
SigningContext ctx = new SigningContext( SignaturePromptDialog dlg = new SignaturePromptDialog(
"req-9876-uuid", stage,
"app-contratos-web", "Portal de Assinaturas Web",
new byte[32], "https://assinador.empresa.com.br",
"SHA256withRSA", cert,
null, ctx,
Instant.now().plusSeconds(120) "Contrato_Prestacao_Servicos_2026.pdf",
); "PDF",
false
);
SignaturePromptDialog dlg = new SignaturePromptDialog( Scene scene = createPromptSampleScene(cert, ctx);
stage, stage.setScene(scene);
"Portal de Assinaturas Web", stage.show();
"https://assinador.empresa.com.br", scene.getRoot().applyCss();
cert, scene.getRoot().layout();
ctx,
"Contrato_Prestacao_Servicos_2026.pdf",
"PDF",
false
);
Scene scene = createPromptSampleScene(cert, ctx); saveNodeSnapshot(scene, new File(OUTPUT_DIR, "06_solicitacao.png"));
stage.setScene(scene); saveNodeSnapshot(scene, new File(OUTPUT_DIR, "06_solicitacao_claro.png"));
stage.show();
scene.getRoot().applyCss();
scene.getRoot().layout();
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro";
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "06_solicitacao_" + suffix + ".png"));
}
} }
private void captureSealsScreenshots(Stage stage) throws Exception { private void captureSealsScreenshots(Stage stage) throws Exception {
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) { SealManagementDialog dlg = new SealManagementDialog(stage);
ThemeManager.setCurrentTheme(theme); Scene scene = createSealsSampleScene();
SealManagementDialog dlg = new SealManagementDialog(stage); stage.setScene(scene);
Scene scene = createSealsSampleScene(); stage.show();
stage.setScene(scene); scene.getRoot().applyCss();
stage.show(); scene.getRoot().layout();
scene.getRoot().applyCss();
scene.getRoot().layout();
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro"; saveNodeSnapshot(scene, new File(OUTPUT_DIR, "07_selos.png"));
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "07_selos_" + suffix + ".png")); saveNodeSnapshot(scene, new File(OUTPUT_DIR, "07_selos_claro.png"));
}
} }
private void captureAboutScreenshots(Stage stage) throws Exception { private void captureAboutScreenshots(Stage stage) throws Exception {
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) { AboutView aboutView = new AboutView();
ThemeManager.setCurrentTheme(theme);
AboutView aboutView = new AboutView();
VBox container = wrapInDialogContainer("SOBRE O ASSINADOR", VBox container = wrapInDialogContainer("SOBRE O ASSINADOR",
"Agente de assinatura digital ICP-Brasil com segurança criptográfica em nível de hardware.", "Agente de assinatura digital ICP-Brasil com segurança criptográfica em nível de hardware.",
SvgIcons.aboutIcon(), aboutView, 620, 480); SvgIcons.aboutIcon(), aboutView, 620, 480);
Scene scene = createScene(container, 620, 480); Scene scene = createScene(container, 620, 480);
stage.setScene(scene); stage.setScene(scene);
stage.show(); stage.show();
scene.getRoot().applyCss(); scene.getRoot().applyCss();
scene.getRoot().layout(); scene.getRoot().layout();
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro"; saveNodeSnapshot(scene, new File(OUTPUT_DIR, "08_sobre.png"));
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "08_sobre_" + suffix + ".png")); saveNodeSnapshot(scene, new File(OUTPUT_DIR, "08_sobre_claro.png"));
}
} }
private Scene createScene(VBox root, double w, double h) { private Scene createScene(VBox root, double w, double h) {
@@ -358,105 +327,140 @@ public class UiPreviewGenerator extends Application {
private VBox wrapInDialogContainer(String title, String subtitle, Node icon, Node content, double w, double h) { private VBox wrapInDialogContainer(String title, String subtitle, Node icon, Node content, double w, double h) {
VBox container = new VBox(16); VBox container = new VBox(16);
container.getStyleClass().addAll("dialog-container", "cyber-root"); container.getStyleClass().add("dialog-container");
container.setPadding(new Insets(20)); container.setPadding(new Insets(24));
HBox header = new HBox(12); HBox header = new HBox(12);
header.setAlignment(Pos.CENTER_LEFT); header.setAlignment(Pos.CENTER_LEFT);
VBox titleBox = new VBox(2); VBox titleBox = new VBox(2);
Label titleLbl = new Label(title); Label lblTitle = new Label(title);
titleLbl.getStyleClass().add("dialog-header-title"); lblTitle.getStyleClass().addAll("dialog-header-title", "text-bold");
Label subLbl = new Label(subtitle);
subLbl.getStyleClass().add("dialog-header-subtitle"); Label lblSub = new Label(subtitle);
titleBox.getChildren().addAll(titleLbl, subLbl); lblSub.getStyleClass().add("dialog-subtitle");
titleBox.getChildren().addAll(lblTitle, lblSub);
header.getChildren().addAll(icon, titleBox); header.getChildren().addAll(icon, titleBox);
VBox contentBox = new VBox(content); container.getChildren().addAll(header, content);
VBox.setVgrow(contentBox, Priority.ALWAYS);
container.getChildren().addAll(header, contentBox);
return container; return container;
} }
private Scene createValidationSampleScene(Stage stage) { private Scene createValidationSampleScene(Stage stage) {
VBox root = new VBox(16); VBox root = new VBox(16);
root.setPadding(new Insets(20)); root.getStyleClass().add("dialog-container");
root.getStyleClass().addAll("dialog-container", "cyber-root"); root.setPadding(new Insets(24));
HBox header = new HBox(12); HBox header = new HBox(12);
header.setAlignment(Pos.CENTER_LEFT); header.setAlignment(Pos.CENTER_LEFT);
header.getChildren().addAll(
SvgIcons.validateIcon(),
new Label("VALIDAÇÃO DE ASSINATURA DIGITAL")
);
VBox box = new VBox(12); VBox titleBox = new VBox(2);
box.getStyleClass().add("dialog-box"); Label title = new Label("VALIDAÇÃO DE DOCUMENTO ASSINADO");
box.getChildren().addAll( title.getStyleClass().addAll("dialog-header-title", "text-bold");
new Label("Arquivo: Contrato_Prestacao_Servicos_Assinado.pdf"), Label sub = new Label("Verificação criptográfica de conformidade de assinaturas PAdES, CAdES e XMLDSig.");
new Label("Status: Assinatura Válida e Íntegra"), sub.getStyleClass().add("dialog-subtitle");
new Label("Assinado por: JOAO DA SILVA (CPF ***.123.456-**)"), titleBox.getChildren().addAll(title, sub);
new Label("Autoridade Certificadora: AC SERPRO RFB v5 (ICP-Brasil)") header.getChildren().addAll(SvgIcons.validateIcon(), titleBox);
);
root.getChildren().addAll(header, box); VBox resultBox = new VBox(12);
resultBox.getStyleClass().add("content-card");
resultBox.setPadding(new Insets(16));
Label docTitle = new Label("Documento: Relatorio_Financeiro_2026.pdf (Conforme ICP-Brasil)");
docTitle.getStyleClass().add("text-bold");
Label statusLbl = new Label("✓ 1 Assinatura Digital VÁLIDA detectada");
statusLbl.getStyleClass().addAll("status-badge", "status-badge-success", "text-bold");
VBox signerCard = new VBox(6);
signerCard.getStyleClass().add("certificate-card");
signerCard.setPadding(new Insets(12));
Label signerName = new Label("Assinante: CARLOS EDUARDO SANTOS:55443322100");
signerName.getStyleClass().add("text-bold");
Label signerAc = new Label("Autoridade Certificadora: AC SERPRO RFB v5");
signerAc.getStyleClass().add("text-muted");
Label signerDate = new Label("Data/Hora: 20/10/2026 15:42:10 (Carimbo do Tempo Validado)");
signerDate.getStyleClass().add("text-muted");
signerCard.getChildren().addAll(signerName, signerAc, signerDate);
resultBox.getChildren().addAll(docTitle, statusLbl, signerCard);
root.getChildren().addAll(header, resultBox);
return createScene(root, 720, 480); return createScene(root, 720, 480);
} }
private Scene createPromptSampleScene(ManagedCertificate cert, SigningContext ctx) { private Scene createPromptSampleScene(ManagedCertificate cert, SigningContext ctx) {
VBox root = new VBox(16); VBox root = new VBox(16);
root.setPadding(new Insets(22)); root.getStyleClass().add("dialog-container");
root.getStyleClass().addAll("dialog-container", "cyber-root"); root.setPadding(new Insets(24));
HBox header = new HBox(12); HBox header = new HBox(12);
header.setAlignment(Pos.CENTER_LEFT); header.setAlignment(Pos.CENTER_LEFT);
header.getChildren().addAll(
SvgIcons.cyberShieldLogo(24),
new Label("SOLICITAÇÃO DE ASSINATURA DIGITAL")
);
VBox box = new VBox(10); VBox titleBox = new VBox(2);
box.getStyleClass().add("dialog-box"); Label title = new Label("SOLICITAÇÃO DE ASSINATURA DIGITAL");
box.getChildren().addAll( title.getStyleClass().addAll("dialog-header-title", "text-bold");
new Label("Aplicação Solicitante: Portal de Assinaturas Web"), Label sub = new Label("Uma aplicação web autorizada requer sua confirmação para assinar um documento.");
new Label("Origem: https://assinador.empresa.com.br"), sub.getStyleClass().add("dialog-subtitle");
new Label("Documento: Contrato_Prestacao_Servicos_2026.pdf"), titleBox.getChildren().addAll(title, sub);
new Label("Algoritmo: " + ctx.algorithm()), header.getChildren().addAll(SvgIcons.cyberShieldLogo(24), titleBox);
new Label("Certificado: " + cert.commonName() + " [" + cert.type().getCode() + "]")
);
HBox actions = new HBox(12); VBox box = new VBox(14);
actions.setAlignment(Pos.CENTER_RIGHT); box.getStyleClass().add("content-card");
Button btnReject = new Button("Recusar"); box.setPadding(new Insets(16));
btnReject.getStyleClass().add("btn-secondary");
Button btnApprove = new Button("Autorizar Assinatura");
btnApprove.getStyleClass().add("btn-primary");
actions.getChildren().addAll(btnReject, btnApprove);
root.getChildren().addAll(header, box, actions); Label appLbl = new Label("Aplicação Solicitante: Portal de Assinaturas Web (https://assinador.empresa.com.br)");
return createScene(root, 620, 440); appLbl.getStyleClass().add("text-bold");
Label docLbl = new Label("Documento: Contrato_Prestacao_Servicos_2026.pdf (Hash SHA-256)");
docLbl.getStyleClass().add("text-muted");
VBox certNode = new VBox(6);
certNode.getStyleClass().add("certificate-card");
certNode.setPadding(new Insets(12));
Label certTitle = new Label("Certificado Digital Selecionado:");
certTitle.getStyleClass().addAll("text-bold", "text-muted");
Label certSubject = new Label(cert.commonName());
certSubject.getStyleClass().add("text-bold");
Label certEmissor = new Label("Emissor: " + cert.issuerDn());
certEmissor.getStyleClass().add("text-muted");
certNode.getChildren().addAll(certTitle, certSubject, certEmissor);
HBox btnBar = new HBox(12);
btnBar.setAlignment(Pos.CENTER_RIGHT);
Button btnRecusar = new Button("Recusar");
btnRecusar.getStyleClass().addAll("btn-ghost");
Button btnConfirmar = new Button("Autorizar e Assinar");
btnConfirmar.getStyleClass().addAll("btn-primary");
btnBar.getChildren().addAll(btnRecusar, btnConfirmar);
box.getChildren().addAll(appLbl, docLbl, certNode, btnBar);
root.getChildren().addAll(header, box);
return createScene(root, 700, 480);
} }
private Scene createSealsSampleScene() { private Scene createSealsSampleScene() {
VBox root = new VBox(16); VBox root = new VBox(16);
root.setPadding(new Insets(20)); root.getStyleClass().add("dialog-container");
root.getStyleClass().addAll("dialog-container", "cyber-root"); root.setPadding(new Insets(24));
HBox header = new HBox(12); HBox header = new HBox(12);
header.setAlignment(Pos.CENTER_LEFT); header.setAlignment(Pos.CENTER_LEFT);
header.getChildren().addAll(
SvgIcons.sealIcon(),
new Label("GERENCIAMENTO DE SELOS VISUAIS")
);
VBox box = new VBox(10); VBox titleBox = new VBox(2);
box.getStyleClass().add("dialog-box"); Label title = new Label("TEMAS E SELOS VISUAIS PAdES");
title.getStyleClass().addAll("dialog-header-title", "text-bold");
Label sub = new Label("Personalize a aparência dos carimbos visuais de assinatura digital em PDFs.");
sub.getStyleClass().add("dialog-subtitle");
titleBox.getChildren().addAll(title, sub);
header.getChildren().addAll(SvgIcons.sealIcon(), titleBox);
HBox box = new HBox(16);
box.getChildren().addAll( box.getChildren().addAll(
new Label("Selo Ativo: Padrão ICP-Brasil Institucional"), new Label("Visualização de Selos Padronizados ICP-Brasil"),
new Label("Dimensões: 200 x 60 mm • Posição: Canto inferior direito"), br.com.cybercore.signer.ui.components.Icons.sealIcpBrasil()
new Label("Elementos: Carimbo de Tempo + Brasão + Dados do Titular")
); );
root.getChildren().addAll(header, box); root.getChildren().addAll(header, box);
@@ -464,32 +468,26 @@ public class UiPreviewGenerator extends Application {
} }
private void captureExitDialogScreenshots(Stage stage) throws Exception { private void captureExitDialogScreenshots(Stage stage) throws Exception {
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) { ExitConfirmationDialog dialog = new ExitConfirmationDialog(null);
ThemeManager.setCurrentTheme(theme); Scene scene = dialog.getStage().getScene();
ExitConfirmationDialog dialog = new ExitConfirmationDialog(null); scene.getRoot().applyCss();
Scene scene = dialog.getStage().getScene(); scene.getRoot().layout();
scene.getRoot().applyCss();
scene.getRoot().layout();
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro"; saveNodeSnapshot(scene, new File(OUTPUT_DIR, "09_exit_dialog.png"));
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "09_exit_dialog_" + suffix + ".png")); saveNodeSnapshot(scene, new File(OUTPUT_DIR, "09_exit_dialog_claro.png"));
}
} }
private void capturePdfSignDialogScreenshots(Stage stage) throws Exception { private void capturePdfSignDialogScreenshots(Stage stage) throws Exception {
File samplePdf = getOrCreateSamplePdf(); File samplePdf = getOrCreateSamplePdf();
CertificateManager certManager = CertificateManager.createDefault(); CertificateManager certManager = CertificateManager.createDefault();
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) { PdfSignDialog dialog = new PdfSignDialog(null, certManager, samplePdf);
ThemeManager.setCurrentTheme(theme); Scene scene = dialog.getStage().getScene();
PdfSignDialog dialog = new PdfSignDialog(null, certManager, samplePdf); scene.getRoot().applyCss();
Scene scene = dialog.getStage().getScene(); scene.getRoot().layout();
scene.getRoot().applyCss();
scene.getRoot().layout();
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro"; saveNodeSnapshot(scene, new File(OUTPUT_DIR, "10_pdf_sign.png"));
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "10_pdf_sign_" + suffix + ".png")); saveNodeSnapshot(scene, new File(OUTPUT_DIR, "10_pdf_sign_claro.png"));
}
} }
private void capturePdfCertificateSignDialogScreenshots(Stage stage) throws Exception { private void capturePdfCertificateSignDialogScreenshots(Stage stage) throws Exception {
@@ -514,23 +512,20 @@ public class UiPreviewGenerator extends Application {
); );
br.com.cybercore.signer.core.pdf.PdfSigningService pdfSigningService = new br.com.cybercore.signer.core.pdf.PdfSigningService(certManager); br.com.cybercore.signer.core.pdf.PdfSigningService pdfSigningService = new br.com.cybercore.signer.core.pdf.PdfSigningService(certManager);
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) { PdfCertificateSignDialog dialog = new PdfCertificateSignDialog(
ThemeManager.setCurrentTheme(theme); null,
PdfCertificateSignDialog dialog = new PdfCertificateSignDialog( certManager,
null, pdfSigningService,
certManager, samplePdf,
pdfSigningService, config,
samplePdf, "Selo Padrão ICP-Brasil (Canto Inferior Direito)"
config, );
"Selo Padrão ICP-Brasil (Canto Inferior Direito)" Scene scene = dialog.getStage().getScene();
); scene.getRoot().applyCss();
Scene scene = dialog.getStage().getScene(); scene.getRoot().layout();
scene.getRoot().applyCss();
scene.getRoot().layout();
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro"; saveNodeSnapshot(scene, new File(OUTPUT_DIR, "11_pdf_cert_sign.png"));
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "11_pdf_cert_sign_" + suffix + ".png")); saveNodeSnapshot(scene, new File(OUTPUT_DIR, "11_pdf_cert_sign_claro.png"));
}
} }
private static File getOrCreateSamplePdf() throws Exception { private static File getOrCreateSamplePdf() throws Exception {
@@ -539,17 +534,17 @@ public class UiPreviewGenerator extends Application {
if (pdf.getParentFile() != null) { if (pdf.getParentFile() != null) {
pdf.getParentFile().mkdirs(); pdf.getParentFile().mkdirs();
} }
try (org.apache.pdfbox.pdmodel.PDDocument doc = new org.apache.pdfbox.pdmodel.PDDocument()) { try (org.apache.pdfbox.pdmodel.PDDocument document = new org.apache.pdfbox.pdmodel.PDDocument()) {
org.apache.pdfbox.pdmodel.PDPage page = new org.apache.pdfbox.pdmodel.PDPage(); org.apache.pdfbox.pdmodel.PDPage page = new org.apache.pdfbox.pdmodel.PDPage();
doc.addPage(page); document.addPage(page);
try (org.apache.pdfbox.pdmodel.PDPageContentStream stream = new org.apache.pdfbox.pdmodel.PDPageContentStream(doc, page)) { try (org.apache.pdfbox.pdmodel.PDPageContentStream cs = new org.apache.pdfbox.pdmodel.PDPageContentStream(document, page)) {
stream.beginText(); cs.beginText();
stream.setFont(new org.apache.pdfbox.pdmodel.font.PDType1Font(org.apache.pdfbox.pdmodel.font.Standard14Fonts.FontName.HELVETICA_BOLD), 16); cs.setFont(new org.apache.pdfbox.pdmodel.font.PDType1Font(org.apache.pdfbox.pdmodel.font.Standard14Fonts.FontName.HELVETICA_BOLD), 16);
stream.newLineAtOffset(50, 700); cs.newLineAtOffset(50, 700);
stream.showText("CyberCore Signer — Documento PDF de Demonstração"); cs.showText("CyberCore Signer - Documento de Teste de Assinatura");
stream.endText(); cs.endText();
} }
doc.save(pdf); document.save(pdf);
} }
} }
return pdf; return pdf;
@@ -557,15 +552,16 @@ public class UiPreviewGenerator extends Application {
private static void saveNodeSnapshot(Scene scene, File targetFile) throws Exception { private static void saveNodeSnapshot(Scene scene, File targetFile) throws Exception {
WritableImage image = scene.snapshot(null); WritableImage image = scene.snapshot(null);
int w = (int) image.getWidth(); int width = (int) image.getWidth();
int h = (int) image.getHeight(); int height = (int) image.getHeight();
BufferedImage bufferedImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
PixelReader reader = image.getPixelReader(); PixelReader reader = image.getPixelReader();
for (int y = 0; y < h; y++) { for (int y = 0; y < height; y++) {
for (int x = 0; x < w; x++) { for (int x = 0; x < width; x++) {
bufferedImage.setRGB(x, y, reader.getArgb(x, y)); bufferedImage.setRGB(x, y, reader.getArgb(x, y));
} }
} }
ImageIO.write(bufferedImage, "png", targetFile); ImageIO.write(bufferedImage, "png", targetFile);
System.out.println("Screenshot gerada: " + targetFile.getName());
} }
} }
@@ -33,7 +33,7 @@ class AssetManagerTest {
String[] iconNames = { String[] iconNames = {
"pdf", "xml", "file", "batch", "attached", "co-sign", "pdf", "xml", "file", "batch", "attached", "co-sign",
"timestamp", "ltv", "validate", "validate-timestamp", "exit", "timestamp", "ltv", "validate", "validate-timestamp", "exit",
"logs", "settings", "tutorial", "cert", "about", "sun", "moon", "logs", "settings", "tutorial", "cert", "about",
"shield", "seal", "seal-icp-brasil", "check", "error", "star", "close", "arrow-right", "shield", "seal", "seal-icp-brasil", "check", "error", "star", "close", "arrow-right",
"zoom-in", "zoom-out", "zoom-fit", "refresh" "zoom-in", "zoom-out", "zoom-fit", "refresh"
}; };
@@ -56,17 +56,10 @@ class AssetManagerTest {
} }
@Test @Test
@DisplayName("Deve repintar dinamicamente elementos currentColor ao mudar o tema") @DisplayName("Deve carregar ícones SVG com cor dinâmica sem lançar exceções")
void shouldRepaintCurrentColorDynamicallyOnThemeChange() { void shouldRenderDynamicSvgIcons() {
Node iconNode = br.com.cybercore.signer.ui.components.Icons.settings(24); Node iconNode = br.com.cybercore.signer.ui.components.Icons.settings(24);
assertThat(iconNode).isInstanceOf(javafx.scene.layout.Pane.class); assertThat(iconNode).isInstanceOf(javafx.scene.layout.Pane.class);
// Alterna o tema e garante que não lança erro
br.com.cybercore.signer.ui.theme.ThemeManager.setCurrentTheme(br.com.cybercore.signer.ui.theme.ThemeManager.AppTheme.DARK);
assertThat(br.com.cybercore.signer.ui.theme.ThemeManager.isDark()).isTrue();
br.com.cybercore.signer.ui.theme.ThemeManager.setCurrentTheme(br.com.cybercore.signer.ui.theme.ThemeManager.AppTheme.LIGHT);
assertThat(br.com.cybercore.signer.ui.theme.ThemeManager.isDark()).isFalse();
} }
@Test @Test
@@ -46,8 +46,8 @@ class DesignTokenGuardrailTest {
continue; continue;
} }
// Detecta entrada nos blocos de tokens .theme-light e .theme-dark // Detecta entrada no bloco canônico de tokens .root
if (trimmed.contains(".theme-light") || trimmed.contains(".theme-dark")) { if (trimmed.startsWith(".root") || trimmed.contains(".root ")) {
insideTokenBlock = true; insideTokenBlock = true;
} }
@@ -108,6 +108,51 @@ class DesignTokenGuardrailTest {
.isEmpty(); .isEmpty();
} }
@Test
@DisplayName("Código da UI e CSS não devem conter referências a .theme-dark, AppTheme ou isDark")
void shouldNotContainDarkThemeReferencesInUiCodeOrCss() throws Exception {
// 1. Valida theme.css e no-effects.css
String[] cssFiles = {"/styles/theme.css", "/styles/no-effects.css"};
for (String cssPath : cssFiles) {
InputStream stream = getClass().getResourceAsStream(cssPath);
if (stream != null) {
String content = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
assertThat(content)
.withFailMessage("Arquivo %s ainda contém referências a .theme-dark ou .theme-light!", cssPath)
.doesNotContain(".theme-dark")
.doesNotContain(".theme-light");
}
}
// 2. Valida fontes Java em src/main/java
java.nio.file.Path sourceRoot = java.nio.file.Files.exists(java.nio.file.Path.of("src/main/java"))
? java.nio.file.Path.of("src/main/java")
: java.nio.file.Path.of("modules/cybercore-ui/src/main/java");
List<String> violations = new ArrayList<>();
try (var stream = java.nio.file.Files.walk(sourceRoot)) {
List<java.nio.file.Path> javaFiles = stream
.filter(java.nio.file.Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.toList();
for (java.nio.file.Path file : javaFiles) {
List<String> lines = java.nio.file.Files.readAllLines(file, StandardCharsets.UTF_8);
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
if (line.contains("AppTheme") || line.contains("isDark(") || line.contains(".theme-dark")) {
violations.add(String.format("%s:%d -> %s", file.getFileName(), i + 1, line.trim()));
}
}
}
}
assertThat(violations)
.withFailMessage("Foram encontradas referências obsoletas a tema escuro no código da UI:\n%s",
String.join("\n", violations))
.isEmpty();
}
@Test @Test
@DisplayName("theme.css deve definir todos os tokens de cores semânticas primários e de superfície") @DisplayName("theme.css deve definir todos os tokens de cores semânticas primários e de superfície")
void shouldDefineAllRequiredSemanticColorTokens() throws Exception { void shouldDefineAllRequiredSemanticColorTokens() throws Exception {
@@ -248,8 +293,8 @@ class DesignTokenGuardrailTest {
} }
@Test @Test
@DisplayName("(c) Relação de contraste WCAG 2.1 deve ser >= 4.5:1 para texto normal nos temas claro e escuro") @DisplayName("(c) Relação de contraste WCAG 2.1 deve ser >= 4.5:1 para texto normal no tema único (.root)")
void shouldMeetWcagContrastRequirementsInBothThemes() throws Exception { void shouldMeetWcagContrastRequirementsInTheme() throws Exception {
InputStream stream = getClass().getResourceAsStream("/styles/theme.css"); InputStream stream = getClass().getResourceAsStream("/styles/theme.css");
assertThat(stream).isNotNull(); assertThat(stream).isNotNull();
@@ -258,14 +303,11 @@ class DesignTokenGuardrailTest {
content = new String(stream.readAllBytes(), StandardCharsets.UTF_8); content = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
} }
// Extrai mapa de tokens por tema (.theme-light e .theme-dark) // Extrai mapa de tokens definidos no bloco .root
java.util.Map<String, String> lightTokens = extractTokensFromBlock(content, ".theme-light"); java.util.Map<String, String> tokens = extractTokensFromBlock(content, ".root");
java.util.Map<String, String> darkTokens = extractTokensFromBlock(content, ".theme-dark"); assertThat(tokens).isNotEmpty();
assertThat(lightTokens).isNotEmpty(); // Pares a validar: (texto, fundo)
assertThat(darkTokens).isNotEmpty();
// Pares a validar em cada tema: (texto, fundo)
String[][] pairs = { String[][] pairs = {
{"-fx-color-text", "-fx-color-background"}, {"-fx-color-text", "-fx-color-background"},
{"-fx-color-text", "-fx-color-surface"}, {"-fx-color-text", "-fx-color-surface"},
@@ -281,24 +323,12 @@ class DesignTokenGuardrailTest {
String textToken = pair[0]; String textToken = pair[0];
String bgToken = pair[1]; String bgToken = pair[1];
// Tema Claro if (tokens.containsKey(textToken) && tokens.containsKey(bgToken)) {
if (lightTokens.containsKey(textToken) && lightTokens.containsKey(bgToken)) { String textColor = tokens.get(textToken);
String textColor = lightTokens.get(textToken); String bgColor = tokens.get(bgToken);
String bgColor = lightTokens.get(bgToken);
double ratio = calculateContrastRatio(textColor, bgColor); double ratio = calculateContrastRatio(textColor, bgColor);
if (ratio < 4.5) { if (ratio < 4.5) {
contrastFailures.add(String.format("Tema Claro: %s (%s) sobre %s (%s) = %.2f:1 (mínimo 4.5:1)", contrastFailures.add(String.format("%s (%s) sobre %s (%s) = %.2f:1 (mínimo 4.5:1)",
textToken, textColor, bgToken, bgColor, ratio));
}
}
// Tema Escuro
if (darkTokens.containsKey(textToken) && darkTokens.containsKey(bgToken)) {
String textColor = darkTokens.get(textToken);
String bgColor = darkTokens.get(bgToken);
double ratio = calculateContrastRatio(textColor, bgColor);
if (ratio < 4.5) {
contrastFailures.add(String.format("Tema Escuro: %s (%s) sobre %s (%s) = %.2f:1 (mínimo 4.5:1)",
textToken, textColor, bgToken, bgColor, ratio)); textToken, textColor, bgToken, bgColor, ratio));
} }
} }