diff --git a/docs/screenshots/01_hub_claro.png b/docs/screenshots/01_hub_claro.png index f61c4b6..247d606 100644 Binary files a/docs/screenshots/01_hub_claro.png and b/docs/screenshots/01_hub_claro.png differ diff --git a/docs/screenshots/01_hub_escuro.png b/docs/screenshots/01_hub_escuro.png index 39e452e..c349b6d 100644 Binary files a/docs/screenshots/01_hub_escuro.png and b/docs/screenshots/01_hub_escuro.png differ diff --git a/docs/screenshots/01_layout_claro.png b/docs/screenshots/01_layout_claro.png index f61c4b6..247d606 100644 Binary files a/docs/screenshots/01_layout_claro.png and b/docs/screenshots/01_layout_claro.png differ diff --git a/docs/screenshots/02_diagnostico.png b/docs/screenshots/02_diagnostico.png index 7b13108..aa3b4ee 100644 Binary files a/docs/screenshots/02_diagnostico.png and b/docs/screenshots/02_diagnostico.png differ diff --git a/docs/screenshots/02_diagnostico_claro.png b/docs/screenshots/02_diagnostico_claro.png index 7b13108..aa3b4ee 100644 Binary files a/docs/screenshots/02_diagnostico_claro.png and b/docs/screenshots/02_diagnostico_claro.png differ diff --git a/docs/screenshots/02_diagnostico_escuro.png b/docs/screenshots/02_diagnostico_escuro.png index 305b9b2..02cd446 100644 Binary files a/docs/screenshots/02_diagnostico_escuro.png and b/docs/screenshots/02_diagnostico_escuro.png differ diff --git a/docs/screenshots/02_layout_escuro.png b/docs/screenshots/02_layout_escuro.png index 39e452e..c349b6d 100644 Binary files a/docs/screenshots/02_layout_escuro.png and b/docs/screenshots/02_layout_escuro.png differ diff --git a/docs/screenshots/03_configuracoes.png b/docs/screenshots/03_configuracoes.png index 25f5e97..3a8a638 100644 Binary files a/docs/screenshots/03_configuracoes.png and b/docs/screenshots/03_configuracoes.png differ diff --git a/docs/screenshots/03_configuracoes_claro.png b/docs/screenshots/03_configuracoes_claro.png index 25f5e97..3a8a638 100644 Binary files a/docs/screenshots/03_configuracoes_claro.png and b/docs/screenshots/03_configuracoes_claro.png differ diff --git a/docs/screenshots/03_configuracoes_escuro.png b/docs/screenshots/03_configuracoes_escuro.png index 27ce454..2e34e21 100644 Binary files a/docs/screenshots/03_configuracoes_escuro.png and b/docs/screenshots/03_configuracoes_escuro.png differ diff --git a/docs/screenshots/06_solicitacao_claro.png b/docs/screenshots/06_solicitacao_claro.png index 8fbb38a..dd6c751 100644 Binary files a/docs/screenshots/06_solicitacao_claro.png and b/docs/screenshots/06_solicitacao_claro.png differ diff --git a/docs/screenshots/06_solicitacao_escuro.png b/docs/screenshots/06_solicitacao_escuro.png index c1d03d6..b603710 100644 Binary files a/docs/screenshots/06_solicitacao_escuro.png and b/docs/screenshots/06_solicitacao_escuro.png differ diff --git a/docs/screenshots/09_exit_dialog_claro.png b/docs/screenshots/09_exit_dialog_claro.png new file mode 100644 index 0000000..d60c35c Binary files /dev/null and b/docs/screenshots/09_exit_dialog_claro.png differ diff --git a/docs/screenshots/09_exit_dialog_escuro.png b/docs/screenshots/09_exit_dialog_escuro.png new file mode 100644 index 0000000..9dd46f0 Binary files /dev/null and b/docs/screenshots/09_exit_dialog_escuro.png differ diff --git a/docs/screenshots/10_pdf_sign_claro.png b/docs/screenshots/10_pdf_sign_claro.png new file mode 100644 index 0000000..45d2a0c Binary files /dev/null and b/docs/screenshots/10_pdf_sign_claro.png differ diff --git a/docs/screenshots/10_pdf_sign_escuro.png b/docs/screenshots/10_pdf_sign_escuro.png new file mode 100644 index 0000000..92f6367 Binary files /dev/null and b/docs/screenshots/10_pdf_sign_escuro.png differ diff --git a/docs/screenshots/11_pdf_cert_sign_claro.png b/docs/screenshots/11_pdf_cert_sign_claro.png new file mode 100644 index 0000000..a2e5def Binary files /dev/null and b/docs/screenshots/11_pdf_cert_sign_claro.png differ diff --git a/docs/screenshots/11_pdf_cert_sign_escuro.png b/docs/screenshots/11_pdf_cert_sign_escuro.png new file mode 100644 index 0000000..488742e Binary files /dev/null and b/docs/screenshots/11_pdf_cert_sign_escuro.png differ diff --git a/docs/ui-troubleshooting.md b/docs/ui-troubleshooting.md new file mode 100644 index 0000000..a23b384 --- /dev/null +++ b/docs/ui-troubleshooting.md @@ -0,0 +1,79 @@ +# 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. + +--- + +## 1. O Problema: Texto Preto sobre Fundo Escuro (Contraste ~1.1:1) + +### 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`). + +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. + +--- + +## 2. Investigação Técnica e Causa Raiz + +### 2.1 Lookups Inexistentes no CSS +Ao inspecionar o código-fonte Java dessas telas, verificou-se o uso de estilos inline via `setStyle(...)`: +```java +subtitle.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;"); +lblDocName.setStyle("-fx-font-weight: bold; -fx-font-size: 12.5px; -fx-text-fill: -fx-text-primary;"); +lblDocMeta.setStyle("-fx-font-size: 11px; -fx-text-fill: -fx-text-secondary;"); +previewBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; ..."); +chkRememberPin.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-text-primary;"); +``` + +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: `), 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.1 Definição Completa de Tokens e Aliases no `theme.css` +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 { + -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; +} +``` + +### 3.2 Eliminação de Cores Inline +É terminantemente proibido utilizar `setStyle(...)` com propriedades que afetem cores: +- 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`). + +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. +- `.text-accent`: Destaque com a cor primária da marca. +- `.surface-box`, `.toolbar-box`: Caixas de superfície neutra e barras de ferramentas. + +### 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. diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java index 1db2a18..d6d0a68 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java @@ -228,7 +228,7 @@ public class CyberCoreDesktopApplication extends Application { ScrollPane scrollPane = new ScrollPane(settingsView); scrollPane.setFitToWidth(true); - scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + scrollPane.getStyleClass().add("scroll-transparent"); VBox.setVgrow(scrollPane, Priority.ALWAYS); Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13)); diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/ApplicationApprovalDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/ApplicationApprovalDialog.java index 151a495..7bab0a0 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/ApplicationApprovalDialog.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/ApplicationApprovalDialog.java @@ -69,15 +69,14 @@ public class ApplicationApprovalDialog { Label originTitle = new Label("ORIGEM WEB (URL)"); originTitle.getStyleClass().add("dialog-field-title"); Label originValue = new Label(origin != null ? origin : "Origem desconhecida"); - originValue.setStyle("-fx-text-fill: -fx-accent; -fx-font-size: 13px; -fx-font-family: monospace;"); + originValue.getStyleClass().addAll("text-accent", "text-mono"); originRow.getChildren().addAll(originTitle, originValue); VBox idRow = new VBox(2); Label idTitle = new Label("IDENTIFICADOR DA APLICAÇÃO"); idTitle.getStyleClass().add("dialog-field-title"); Label idValue = new Label(applicationId); - idValue.getStyleClass().add("dialog-field-value"); - idValue.setStyle("-fx-font-size: 12px; -fx-font-family: monospace;"); + idValue.getStyleClass().addAll("dialog-field-value", "text-mono", "text-small"); idRow.getChildren().addAll(idTitle, idValue); detailsBox.getChildren().addAll(appRow, originRow, idRow); @@ -88,21 +87,21 @@ public class ApplicationApprovalDialog { "instalados e solicitar assinaturas. Nenhuma assinatura será realizada sem a sua confirmação explícita na tela." ); secWarning.setWrapText(true); - secWarning.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 11px; -fx-line-spacing: 2px;"); + secWarning.getStyleClass().addAll("text-muted", "text-small"); // Botões de Ação HBox actionsRow = new HBox(12); actionsRow.setAlignment(Pos.CENTER_RIGHT); Button btnReject = new Button("Recusar Acesso"); - btnReject.getStyleClass().add("btn-secondary"); + btnReject.getStyleClass().addAll("btn-danger-outline", "btn-dialog"); btnReject.setOnAction(e -> { approvalFuture.complete(false); stage.close(); }); Button btnAuthorize = new Button("Autorizar Conexão"); - btnAuthorize.getStyleClass().add("btn-primary"); + btnAuthorize.getStyleClass().addAll("btn-primary", "btn-dialog"); btnAuthorize.setDefaultButton(true); btnAuthorize.setOnAction(e -> { approvalFuture.complete(true); diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificateDetailsDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificateDetailsDialog.java index fc7c311..501a939 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificateDetailsDialog.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificateDetailsDialog.java @@ -74,8 +74,7 @@ public class CertificateDetailsDialog { // Nome do Titular em destaque VBox titleBox = new VBox(4); Label nameLabel = new Label(cert.commonName() != null ? cert.commonName() : cert.alias()); - nameLabel.getStyleClass().add("cert-title"); - nameLabel.setStyle("-fx-font-size: 16px; -fx-font-weight: bold;"); + nameLabel.getStyleClass().addAll("dialog-header-title", "text-bold"); String docInfo = ""; if (cert.cpf() != null) { @@ -121,7 +120,7 @@ public class CertificateDetailsDialog { ScrollPane scroll = new ScrollPane(detailsBox); scroll.setFitToWidth(true); - scroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + scroll.getStyleClass().add("scroll-transparent"); VBox.setVgrow(scroll, Priority.ALWAYS); // Botões de Ação @@ -129,7 +128,7 @@ public class CertificateDetailsDialog { actionsRow.setAlignment(Pos.CENTER_RIGHT); Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13)); - btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog"); + btnClose.getStyleClass().addAll("btn-ghost", "btn-dialog"); btnClose.setPrefWidth(110); btnClose.setMinWidth(110); btnClose.setOnAction(e -> stage.close()); diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificatesManagerDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificatesManagerDialog.java index c0968f1..33c61d7 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificatesManagerDialog.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificatesManagerDialog.java @@ -86,7 +86,7 @@ public final class CertificatesManagerDialog { // Container com Scroll ScrollPane scroll = new ScrollPane(certificatesContainer); scroll.setFitToWidth(true); - scroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + scroll.getStyleClass().add("scroll-transparent"); VBox.setVgrow(scroll, Priority.ALWAYS); refreshList(); diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/DiagnosticsView.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/DiagnosticsView.java index 0efa7f1..55ef9c3 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/DiagnosticsView.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/DiagnosticsView.java @@ -69,7 +69,7 @@ public final class DiagnosticsView extends VBox { // Container de Itens ScrollPane scrollPane = new ScrollPane(itemsContainer); scrollPane.setFitToWidth(true); - scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + scrollPane.getStyleClass().add("scroll-transparent"); VBox.setVgrow(scrollPane, Priority.ALWAYS); getChildren().addAll(headerBox, actionsRow, scrollPane); diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/ExitConfirmationDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/ExitConfirmationDialog.java index 0af15f8..b8ef87d 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/ExitConfirmationDialog.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/ExitConfirmationDialog.java @@ -54,13 +54,13 @@ public final class ExitConfirmationDialog { VBox titleBox = new VBox(4); Label title = new Label("Como você deseja proceder?"); - title.setStyle("-fx-font-size: 15px; -fx-font-weight: bold;"); + title.getStyleClass().addAll("dialog-header-title", "text-bold"); Label subtitle = new Label( "Você pode manter o CyberCore Signer ativo na bandeja do sistema\n" + "para atender assinaturas de aplicações web, ou encerrá-lo por completo." ); - subtitle.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;"); + subtitle.getStyleClass().addAll("dialog-subtitle", "text-muted"); titleBox.getChildren().addAll(title, subtitle); header.getChildren().addAll(SvgIcons.cyberShieldLogo(38), titleBox); @@ -70,7 +70,7 @@ public final class ExitConfirmationDialog { actionsRow.setAlignment(Pos.CENTER_RIGHT); Button btnCancel = new Button("Cancelar"); - btnCancel.getStyleClass().addAll("btn-cancel", "btn-dialog"); + btnCancel.getStyleClass().addAll("btn-ghost", "btn-dialog"); btnCancel.setOnAction(e -> { chosenAction = ExitChoice.CANCEL; stage.close(); @@ -80,7 +80,7 @@ public final class ExitConfirmationDialog { HBox.setHgrow(spacer, Priority.ALWAYS); Button btnExit = new Button("Encerrar Definitivamente"); - btnExit.getStyleClass().addAll("btn-danger", "btn-dialog"); + btnExit.getStyleClass().addAll("btn-danger-outline", "btn-dialog"); btnExit.setOnAction(e -> { chosenAction = ExitChoice.EXIT_COMPLETELY; stage.close(); @@ -104,6 +104,10 @@ public final class ExitConfirmationDialog { stage.setOnCloseRequest(e -> chosenAction = ExitChoice.CANCEL); } + public Stage getStage() { + return stage; + } + public ExitChoice showAndGetChoice() { stage.showAndWait(); return chosenAction; diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/MainDashboardView.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/MainDashboardView.java index c4cfcaf..fa0e30e 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/MainDashboardView.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/MainDashboardView.java @@ -83,7 +83,7 @@ public final class MainDashboardView extends VBox { certsContainer.setPadding(new Insets(4, 0, 4, 0)); ScrollPane scrollPane = new ScrollPane(certsContainer); scrollPane.setFitToWidth(true); - scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + scrollPane.getStyleClass().add("scroll-transparent"); VBox.setVgrow(scrollPane, Priority.ALWAYS); getChildren().addAll(statusBox, certsHeader, scrollPane); diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfCertificateSignDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfCertificateSignDialog.java index 1f269d7..0736470 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfCertificateSignDialog.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfCertificateSignDialog.java @@ -259,7 +259,6 @@ public final class PdfCertificateSignDialog { btnClearRememberedPin = new Button("Alterar PIN"); btnClearRememberedPin.getStyleClass().addAll("btn-secondary", "btn-compact"); - btnClearRememberedPin.setStyle("-fx-font-size: 11px;"); btnClearRememberedPin.setVisible(false); btnClearRememberedPin.setOnAction(e -> { ManagedCertificate sel = certCombo.getValue(); @@ -270,13 +269,13 @@ public final class PdfCertificateSignDialog { }); lblPinNotice = new Label(); - lblPinNotice.setStyle("-fx-font-size: 11.5px;"); + lblPinNotice.getStyleClass().addAll("dialog-subtitle", "text-muted"); lblPinNotice.setWrapText(true); HBox.setHgrow(lblPinNotice, Priority.ALWAYS); pinRow.getChildren().addAll(lblPinTitle, pinField, btnClearRememberedPin, lblPinNotice); chkRememberPin = new CheckBox("Lembrar PIN/senha com segurança neste computador"); - chkRememberPin.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-text-primary;"); + chkRememberPin.getStyleClass().addAll("text-primary", "text-small"); chkRememberPin.setSelected(true); VBox tipBox = new VBox(2); @@ -300,11 +299,11 @@ public final class PdfCertificateSignDialog { progressIndicator.setVisible(false); lblStatus = new Label(); - lblStatus.setStyle("-fx-font-size: 12px;"); + lblStatus.getStyleClass().addAll("text-muted", "text-small"); HBox.setHgrow(lblStatus, Priority.ALWAYS); btnCancel = new Button("Cancelar"); - btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog"); + btnCancel.getStyleClass().addAll("btn-ghost", "btn-dialog"); btnCancel.setMinWidth(110); btnCancel.setOnAction(e -> stage.close()); @@ -468,7 +467,7 @@ public final class PdfCertificateSignDialog { setFormDisabled(true); progressIndicator.setVisible(true); lblStatus.setText("Assinando documento PDF com envelope PAdES..."); - lblStatus.setStyle("-fx-text-fill: -fx-accent;"); + lblStatus.getStyleClass().setAll("text-accent", "text-bold"); Task signTask = new Task<>() { @Override @@ -586,4 +585,8 @@ public final class PdfCertificateSignDialog { stage.show(); stage.toFront(); } + + public Stage getStage() { + return stage; + } } diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfSignDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfSignDialog.java index 8bef6bf..2199120 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfSignDialog.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfSignDialog.java @@ -228,9 +228,9 @@ public final class PdfSignDialog { VBox docDetails = new VBox(2); lblDocName = new Label("Nenhum arquivo selecionado"); - lblDocName.setStyle("-fx-font-weight: bold; -fx-font-size: 12.5px; -fx-text-fill: -fx-text-primary;"); + lblDocName.getStyleClass().addAll("text-primary", "text-bold"); lblDocMeta = new Label("Selecione um arquivo PDF para pré-visualização"); - lblDocMeta.setStyle("-fx-font-size: 11px; -fx-text-fill: -fx-text-secondary;"); + lblDocMeta.getStyleClass().addAll("text-muted", "text-small"); docDetails.getChildren().addAll(lblDocName, lblDocMeta); Region docSpacer = new Region(); @@ -252,7 +252,7 @@ public final class PdfSignDialog { leftScroll.setPrefWidth(330); leftScroll.setMinWidth(310); leftScroll.setMaxWidth(360); - leftScroll.setStyle("-fx-background-color: transparent; -fx-background: transparent; -fx-border-color: transparent;"); + leftScroll.getStyleClass().add("scroll-transparent"); VBox sealSection = buildSealOptionsSection(); leftScroll.setContent(sealSection); @@ -269,7 +269,7 @@ public final class PdfSignDialog { footer.setPadding(new Insets(6, 0, 0, 0)); btnCancel = new Button("Cancelar"); - btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog"); + btnCancel.getStyleClass().addAll("btn-ghost", "btn-dialog"); btnCancel.setMinWidth(110); btnCancel.setOnAction(e -> stage.close()); @@ -348,7 +348,7 @@ public final class PdfSignDialog { // Preview em miniatura do SVG VBox previewBox = new VBox(4); previewBox.setAlignment(Pos.CENTER); - previewBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-padding: 8px;"); + previewBox.getStyleClass().add("surface-box"); sealPreviewImageView = new ImageView(); sealPreviewImageView.setPreserveRatio(true); sealPreviewImageView.setFitWidth(260); @@ -390,9 +390,9 @@ public final class PdfSignDialog { // Dica amigável de posicionamento livre VBox dragTipBox = new VBox(2); dragTipBox.setPadding(new Insets(8, 10, 8, 10)); - dragTipBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-background-radius: 6px;"); + dragTipBox.getStyleClass().add("surface-box"); Label lblDragTip = new Label("💡 Arraste o selo livremente sobre a folha para posicioná-lo onde desejar."); - lblDragTip.setStyle("-fx-font-size: 11px; -fx-text-fill: -fx-text-secondary;"); + lblDragTip.getStyleClass().addAll("text-muted", "text-small"); lblDragTip.setWrapText(true); dragTipBox.getChildren().add(lblDragTip); @@ -457,7 +457,7 @@ public final class PdfSignDialog { // Grupo de Zoom da Folha HBox zoomBox = new HBox(5); zoomBox.setAlignment(Pos.CENTER_LEFT); - zoomBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-padding: 2px 6px;"); + zoomBox.getStyleClass().add("toolbar-box"); btnZoomOut = new Button(); btnZoomOut.setGraphic(SvgIcons.zoomOutIcon(13)); @@ -485,7 +485,7 @@ public final class PdfSignDialog { // Grupo de Tamanho do Selo HBox resizeToolBox = new HBox(6); resizeToolBox.setAlignment(Pos.CENTER_LEFT); - resizeToolBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-padding: 2px 6px;"); + resizeToolBox.getStyleClass().add("toolbar-box"); btnToggleResize = new Button(" Redimensionar", SvgIcons.zoomFitIcon(13)); btnToggleResize.getStyleClass().addAll("btn-secondary", "btn-compact"); @@ -507,7 +507,7 @@ public final class PdfSignDialog { HBox.setHgrow(tbSpacer, Priority.ALWAYS); lblPositionFeedback = new Label("Clique na folha para posicionar"); - lblPositionFeedback.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-accent; -fx-font-weight: bold;"); + lblPositionFeedback.getStyleClass().addAll("text-accent", "text-bold"); toolbar.getChildren().addAll(pageNavBox, zoomBox, resizeToolBox, tbSpacer, lblPositionFeedback); @@ -516,7 +516,7 @@ public final class PdfSignDialog { sheetScroll.setFitToWidth(false); sheetScroll.setFitToHeight(false); sheetScroll.setPannable(false); - sheetScroll.setStyle("-fx-background-color: transparent; -fx-background: transparent; -fx-border-color: transparent;"); + sheetScroll.getStyleClass().add("scroll-transparent"); VBox.setVgrow(sheetScroll, Priority.ALWAYS); // Permite atalho de Zoom com Ctrl + Rolagem do Mouse @@ -1249,4 +1249,8 @@ public final class PdfSignDialog { return label; } } + + public Stage getStage() { + return stage; + } } diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SealDesignerDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SealDesignerDialog.java index 638d721..6975387 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SealDesignerDialog.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SealDesignerDialog.java @@ -135,7 +135,7 @@ public final class SealDesignerDialog { ScrollPane scrollForm = new ScrollPane(formPane); scrollForm.setFitToWidth(true); VBox.setVgrow(scrollForm, Priority.ALWAYS); - scrollForm.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + scrollForm.getStyleClass().add("scroll-transparent"); HBox.setHgrow(scrollForm, Priority.ALWAYS); // 1. Identificação do Selo e Campos Adicionais diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SignaturePromptDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SignaturePromptDialog.java index 4a01a1c..0cc8490 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SignaturePromptDialog.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SignaturePromptDialog.java @@ -174,14 +174,13 @@ public class SignaturePromptDialog { if (isWindowsStore) { VBox winBox = new VBox(6); Label winTitle = new Label("🔐 Autenticação nativa pelo Windows / Driver do Token"); - winTitle.setStyle("-fx-text-fill: -fx-accent; -fx-font-weight: bold; -fx-font-size: 13px;"); + winTitle.getStyleClass().addAll("text-accent", "text-bold"); Label winDesc = new Label( "O PIN deste dispositivo é gerenciado diretamente pelo driver nativo (SafeSign / Windows Security). " + "Caso o token ainda não esteja autenticado, a janela segura do driver será aberta automaticamente." ); winDesc.getStyleClass().add("dialog-subtitle"); - winDesc.setStyle("-fx-font-size: 12px;"); winDesc.setWrapText(true); winBox.getChildren().addAll(winTitle, winDesc); diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/TutorialDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/TutorialDialog.java index 0792d5c..0b980ec 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/TutorialDialog.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/TutorialDialog.java @@ -71,7 +71,7 @@ public final class TutorialDialog { ScrollPane scroll = new ScrollPane(content); scroll.setFitToWidth(true); - scroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + scroll.getStyleClass().add("scroll-transparent"); VBox.setVgrow(scroll, Priority.ALWAYS); HBox footer = new HBox(); diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/XmlSignDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/XmlSignDialog.java index ad3a41a..cbaa48a 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/XmlSignDialog.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/XmlSignDialog.java @@ -194,7 +194,7 @@ public final class XmlSignDialog { pinField.setPrefWidth(260); lblPinNotice = new Label(); - lblPinNotice.setStyle("-fx-font-size: 11px;"); + lblPinNotice.getStyleClass().addAll("dialog-subtitle", "text-muted"); pinRow.getChildren().addAll(lblPin, pinField, lblPinNotice); certSection.getChildren().addAll(lblCertTitle, certCombo, pinRow); @@ -289,11 +289,11 @@ public final class XmlSignDialog { progressIndicator.setVisible(false); lblStatus = new Label(); - lblStatus.setStyle("-fx-font-size: 12px;"); + lblStatus.getStyleClass().addAll("text-muted", "text-small"); HBox.setHgrow(lblStatus, Priority.ALWAYS); btnCancel = new Button("Cancelar"); - btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog"); + btnCancel.getStyleClass().addAll("btn-ghost", "btn-dialog"); btnCancel.setOnAction(e -> stage.close()); btnSign = new Button("Assinar Documento XML"); diff --git a/modules/cybercore-ui/src/main/resources/styles/theme.css b/modules/cybercore-ui/src/main/resources/styles/theme.css index 17f0fac..fbb5cd3 100644 --- a/modules/cybercore-ui/src/main/resources/styles/theme.css +++ b/modules/cybercore-ui/src/main/resources/styles/theme.css @@ -10,6 +10,11 @@ .root { -fx-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; -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) --- */ @@ -67,6 +72,13 @@ -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-subtle; + -fx-accent: -fx-color-primary; } /* --- Tema Escuro (Dark Theme) --- */ @@ -124,6 +136,13 @@ -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; } /* ========================================================================== @@ -356,41 +375,128 @@ /* Utilitários semânticos */ .text-primary { + -fx-text-fill: -fx-color-text; +} + +.text-secondary, +.text-muted { + -fx-text-fill: -fx-color-text-muted; +} + +.text-accent { -fx-text-fill: -fx-color-primary; } -.text-success { +.text-success, +.status-ok { -fx-text-fill: -fx-color-success; } -.text-warning { +.text-warning, +.status-warn { -fx-text-fill: -fx-color-warning; } -.text-danger { +.text-danger, +.status-error { -fx-text-fill: -fx-color-error; } -.text-muted { - -fx-text-fill: -fx-color-text-muted; -} - .text-bold { -fx-font-weight: bold; } +.text-small { + -fx-font-size: 11px; +} + +.text-mono { + -fx-font-family: "Liberation Mono", "Courier New", monospace; +} + +.surface-box { + -fx-background-color: -fx-color-surface; + -fx-border-color: -fx-color-border-subtle; + -fx-border-width: 1px; + -fx-border-radius: 6px; + -fx-background-radius: 6px; + -fx-padding: 8px; +} + +.toolbar-box { + -fx-background-color: -fx-color-surface; + -fx-border-color: -fx-color-border-subtle; + -fx-border-width: 1px; + -fx-border-radius: 6px; + -fx-background-radius: 6px; + -fx-padding: 2px 6px; +} + +.scroll-transparent, +.scroll-transparent .viewport { + -fx-background: transparent; + -fx-background-color: transparent; + -fx-border-color: transparent; +} + /* ========================================================================== 5. BOTÕES E CONTROLES DE AÇÃO ========================================================================== */ +/* 5.1 Regras Base e Tamanhos */ +.button, +.btn-primary, +.btn-secondary, +.btn-danger, +.btn-danger-outline, +.btn-ghost, +.btn-toolbar, +.btn-cancel { + -fx-background-radius: 6px; + -fx-border-radius: 6px; + -fx-cursor: hand; + -fx-alignment: CENTER; + -fx-pref-height: 34px; + -fx-min-height: 34px; + -fx-font-size: 13px; + -fx-padding: 0 16px; +} + +/* Tamanho Compacto (28 px) */ +.btn-compact, +.btn-sm { + -fx-pref-height: 28px; + -fx-min-height: 28px; + -fx-font-size: 11.5px; + -fx-padding: 0 10px; +} + +/* Variante Botão de Ícone Apenas */ +.btn-icon-only { + -fx-pref-width: 34px; + -fx-min-width: 34px; + -fx-pref-height: 34px; + -fx-min-height: 34px; + -fx-padding: 0; + -fx-alignment: CENTER; +} + +.btn-icon-only.btn-compact, +.btn-icon-only.btn-sm { + -fx-pref-width: 28px; + -fx-min-width: 28px; + -fx-pref-height: 28px; + -fx-min-height: 28px; + -fx-padding: 0; +} + +/* 5.2 Variante Primary (Ação Principal) */ .btn-primary { -fx-background-color: -fx-color-primary; + -fx-border-color: transparent; + -fx-border-width: 1px; -fx-text-fill: -fx-color-text-inverse; - -fx-font-size: 13px; -fx-font-weight: bold; - -fx-padding: 8px 16px; - -fx-background-radius: 6px; - -fx-cursor: hand; -fx-effect: dropshadow(three-pass-box, -fx-color-shadow, 4, 0, 0, 1); } @@ -402,17 +508,23 @@ -fx-background-color: -fx-color-primary-active; } +.btn-primary:focused { + -fx-border-color: -fx-color-focus; + -fx-border-width: 1.5px; +} + +.btn-primary:disabled { + -fx-opacity: 0.55; + -fx-cursor: default; +} + +/* 5.3 Variante Secondary (Ação Neutra) */ .btn-secondary { -fx-background-color: -fx-color-surface; -fx-border-color: -fx-color-border; -fx-border-width: 1px; -fx-text-fill: -fx-color-text; - -fx-font-size: 13px; - -fx-font-weight: bold; - -fx-padding: 8px 16px; - -fx-background-radius: 6px; - -fx-border-radius: 6px; - -fx-cursor: hand; + -fx-font-weight: 500; } .btn-secondary:hover { @@ -424,34 +536,114 @@ -fx-background-color: -fx-color-surface-active; } +.btn-secondary:focused { + -fx-border-color: -fx-color-focus; + -fx-border-width: 1.5px; +} + +.btn-secondary:disabled { + -fx-opacity: 0.55; + -fx-cursor: default; +} + +/* 5.4 Variante Danger (Ação Destrutiva Sólida) */ .btn-danger { -fx-background-color: -fx-color-error; + -fx-border-color: transparent; + -fx-border-width: 1px; -fx-text-fill: -fx-color-text-inverse; - -fx-font-size: 13px; -fx-font-weight: bold; - -fx-padding: 8px 16px; - -fx-background-radius: 6px; - -fx-cursor: hand; } .btn-danger:hover { -fx-background-color: -fx-color-error-hover; } -.btn-danger-sm { - -fx-background-color: transparent; - -fx-border-color: -fx-color-error-border; - -fx-border-radius: 4px; - -fx-background-radius: 4px; - -fx-text-fill: -fx-color-error; - -fx-font-size: 11px; - -fx-font-weight: bold; - -fx-padding: 4px 8px; - -fx-cursor: hand; +.btn-danger:pressed { + -fx-background-color: -fx-color-error-hover; + -fx-opacity: 0.9; } +.btn-danger:focused { + -fx-border-color: -fx-color-error-border; + -fx-border-width: 1.5px; +} + +.btn-danger:disabled { + -fx-opacity: 0.55; + -fx-cursor: default; +} + +/* 5.5 Variante Danger Outline (Ação Destrutiva em Diálogos de Confirmação) */ +.btn-danger-outline, +.btn-danger-sm { + -fx-background-color: transparent; + -fx-border-color: -fx-color-error; + -fx-border-width: 1.2px; + -fx-text-fill: -fx-color-error; + -fx-font-weight: bold; +} + +.btn-danger-outline:hover, .btn-danger-sm:hover { -fx-background-color: -fx-color-error-subtle; + -fx-border-color: -fx-color-error-hover; +} + +.btn-danger-outline:pressed, +.btn-danger-sm:pressed { + -fx-background-color: -fx-color-error; + -fx-text-fill: -fx-color-text-inverse; +} + +.btn-danger-outline:focused, +.btn-danger-sm:focused { + -fx-border-color: -fx-color-error; + -fx-border-width: 1.5px; +} + +.btn-danger-outline:disabled, +.btn-danger-sm:disabled { + -fx-opacity: 0.55; + -fx-cursor: default; +} + +/* 5.6 Variante Ghost / Toolbar / Cancel */ +.btn-ghost, +.btn-toolbar, +.btn-cancel { + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-border-width: 1px; + -fx-text-fill: -fx-color-text; + -fx-font-weight: 500; +} + +.btn-ghost:hover, +.btn-toolbar:hover, +.btn-cancel:hover { + -fx-background-color: -fx-color-surface-hover; + -fx-border-color: -fx-color-border-subtle; +} + +.btn-ghost:pressed, +.btn-toolbar:pressed, +.btn-cancel:pressed { + -fx-background-color: -fx-color-surface-active; +} + +.btn-ghost:focused, +.btn-toolbar:focused, +.btn-cancel:focused { + -fx-border-color: -fx-color-focus; + -fx-border-width: 1.5px; +} + +.btn-ghost:disabled, +.btn-toolbar:disabled, +.btn-cancel:disabled { + -fx-opacity: 0.55; + -fx-cursor: default; } /* ========================================================================== diff --git a/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/UiPreviewGenerator.java b/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/UiPreviewGenerator.java index e6b2ba7..6856202 100644 --- a/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/UiPreviewGenerator.java +++ b/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/UiPreviewGenerator.java @@ -6,12 +6,18 @@ import br.com.cybercore.signer.core.domain.CertificateStatus; import br.com.cybercore.signer.core.domain.CertificateType; import br.com.cybercore.signer.core.domain.ManagedCertificate; import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode; +import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry; +import br.com.cybercore.signer.core.provider.CertificateManager; import br.com.cybercore.signer.ui.components.CertificateCardNode; import br.com.cybercore.signer.ui.components.SvgIcons; import br.com.cybercore.signer.ui.theme.ThemeManager; import br.com.cybercore.signer.ui.view.AboutView; import br.com.cybercore.signer.ui.view.DiagnosticsView; +import br.com.cybercore.signer.ui.view.ExitConfirmationDialog; import br.com.cybercore.signer.ui.view.MainAppView; +import br.com.cybercore.signer.ui.view.PdfCertificateSignDialog; +import br.com.cybercore.signer.ui.view.PdfSignDialog; import br.com.cybercore.signer.ui.view.SealManagementDialog; import br.com.cybercore.signer.ui.view.SettingsView; import br.com.cybercore.signer.ui.view.SignaturePromptDialog; @@ -77,6 +83,15 @@ public class UiPreviewGenerator extends Application { // 8. Sobre captureAboutScreenshots(primaryStage); + // 9. Diálogo de Encerramento / Bandeja + captureExitDialogScreenshots(primaryStage); + + // 10. Diálogo de Posicionamento e Assinatura PDF (PAdES) + capturePdfSignDialogScreenshots(primaryStage); + + // 11. Diálogo de Seleção de Certificado e PIN do PDF + capturePdfCertificateSignDialogScreenshots(primaryStage); + } catch (Exception e) { e.printStackTrace(); } finally { @@ -448,6 +463,98 @@ public class UiPreviewGenerator extends Application { return createScene(root, 680, 460); } + private void captureExitDialogScreenshots(Stage stage) throws Exception { + for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) { + ThemeManager.setCurrentTheme(theme); + ExitConfirmationDialog dialog = new ExitConfirmationDialog(null); + Scene scene = dialog.getStage().getScene(); + scene.getRoot().applyCss(); + scene.getRoot().layout(); + + String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro"; + saveNodeSnapshot(scene, new File(OUTPUT_DIR, "09_exit_dialog_" + suffix + ".png")); + } + } + + private void capturePdfSignDialogScreenshots(Stage stage) throws Exception { + File samplePdf = getOrCreateSamplePdf(); + CertificateManager certManager = CertificateManager.createDefault(); + + for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) { + ThemeManager.setCurrentTheme(theme); + PdfSignDialog dialog = new PdfSignDialog(null, certManager, samplePdf); + Scene scene = dialog.getStage().getScene(); + scene.getRoot().applyCss(); + scene.getRoot().layout(); + + String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro"; + saveNodeSnapshot(scene, new File(OUTPUT_DIR, "10_pdf_sign_" + suffix + ".png")); + } + } + + private void capturePdfCertificateSignDialogScreenshots(Stage stage) throws Exception { + File samplePdf = getOrCreateSamplePdf(); + CertificateManager certManager = CertificateManager.createDefault(); + + PdfCertificateSignDialog.SealConfig config = new PdfCertificateSignDialog.SealConfig( + true, + PdfSealThemeRegistry.getDefaultTheme(), + CpfDisplayMode.MASKED, + true, + "Assinado via CyberCore Signer Desktop", + PdfSignDialog.PositionOption.BOTTOM_RIGHT, + PdfSignDialog.PageOption.LAST_PAGE, + false, + null, + null, + null, + "Assinatura Digital ICP-Brasil", + "Brasília, DF", + 1.0 + ); + + br.com.cybercore.signer.core.pdf.PdfSigningService pdfSigningService = new br.com.cybercore.signer.core.pdf.PdfSigningService(certManager); + for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) { + ThemeManager.setCurrentTheme(theme); + PdfCertificateSignDialog dialog = new PdfCertificateSignDialog( + null, + certManager, + pdfSigningService, + samplePdf, + config, + "Selo Padrão ICP-Brasil (Canto Inferior Direito)" + ); + Scene scene = dialog.getStage().getScene(); + scene.getRoot().applyCss(); + scene.getRoot().layout(); + + String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro"; + saveNodeSnapshot(scene, new File(OUTPUT_DIR, "11_pdf_cert_sign_" + suffix + ".png")); + } + } + + private static File getOrCreateSamplePdf() throws Exception { + File pdf = new File("target/sample-test.pdf"); + if (!pdf.exists()) { + if (pdf.getParentFile() != null) { + pdf.getParentFile().mkdirs(); + } + try (org.apache.pdfbox.pdmodel.PDDocument doc = new org.apache.pdfbox.pdmodel.PDDocument()) { + org.apache.pdfbox.pdmodel.PDPage page = new org.apache.pdfbox.pdmodel.PDPage(); + doc.addPage(page); + try (org.apache.pdfbox.pdmodel.PDPageContentStream stream = new org.apache.pdfbox.pdmodel.PDPageContentStream(doc, page)) { + stream.beginText(); + stream.setFont(new org.apache.pdfbox.pdmodel.font.PDType1Font(org.apache.pdfbox.pdmodel.font.Standard14Fonts.FontName.HELVETICA_BOLD), 16); + stream.newLineAtOffset(50, 700); + stream.showText("CyberCore Signer — Documento PDF de Demonstração"); + stream.endText(); + } + doc.save(pdf); + } + } + return pdf; + } + private static void saveNodeSnapshot(Scene scene, File targetFile) throws Exception { WritableImage image = scene.snapshot(null); int w = (int) image.getWidth(); diff --git a/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/theme/DesignTokenGuardrailTest.java b/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/theme/DesignTokenGuardrailTest.java index 6e7289c..e37e5fb 100644 --- a/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/theme/DesignTokenGuardrailTest.java +++ b/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/theme/DesignTokenGuardrailTest.java @@ -140,4 +140,223 @@ class DesignTokenGuardrailTest { .contains(token); } } + + @Test + @DisplayName("(a) Código Java não deve conter cores ou propriedades de cor inline em setStyle()") + void shouldNotHaveInlineColorStylesInJavaFiles() throws Exception { + 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"); + + assertThat(java.nio.file.Files.exists(sourceRoot)) + .withFailMessage("Diretório fonte não encontrado: %s", sourceRoot) + .isTrue(); + + Pattern colorInSetStylePattern = Pattern.compile( + "\\.setStyle\\(.*(?:" + + "-fx-text-fill" + + "|-fx-background-color" + + "|-fx-border-color" + + "|-fx-fill" + + "|#[0-9a-fA-F]{3,8}" + + "|rgb\\s*\\(" + + "|rgba\\s*\\(" + + "|-fx-accent" + + "|-fx-text-primary" + + "|-fx-text-secondary" + + "|-fx-bg-primary" + + "|-fx-border-light" + + "|-fx-color-" + + ").*\\)" + ); + + List violations = new ArrayList<>(); + + try (var stream = java.nio.file.Files.walk(sourceRoot)) { + List javaFiles = stream + .filter(java.nio.file.Files::isRegularFile) + .filter(p -> p.toString().endsWith(".java")) + .toList(); + + for (java.nio.file.Path file : javaFiles) { + List 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("setStyle") && colorInSetStylePattern.matcher(line).find()) { + violations.add(String.format("%s:%d -> %s", file.getFileName(), i + 1, line.trim())); + } + } + } + } + + assertThat(violations) + .withFailMessage("Foram encontradas cores inline proibidas em setStyle() no código Java:\n%s", + String.join("\n", violations)) + .isEmpty(); + } + + @Test + @DisplayName("(b) theme.css deve definir todos os lookups -fx-* usados e tokens essenciais nos temas claro e escuro") + void shouldDefineAllLookupsUsedInCss() throws Exception { + InputStream stream = getClass().getResourceAsStream("/styles/theme.css"); + assertThat(stream).isNotNull(); + + String content; + try (stream) { + content = new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } + + String[] essentialTokens = { + "-fx-text-primary", + "-fx-text-secondary", + "-fx-bg-primary", + "-fx-border-light", + "-fx-accent" + }; + + // Verifica presença dos tokens essenciais + for (String token : essentialTokens) { + assertThat(content) + .withFailMessage("Lookup essencial ausente nas definições do theme.css: %s", token) + .contains(token + ":"); + } + + // Extrai tokens definidos em .theme-light e .theme-dark + Pattern tokenDefPattern = Pattern.compile("(-fx-[a-zA-Z0-9_-]+)\\s*:"); + java.util.Set declaredTokens = new java.util.HashSet<>(); + Matcher defMatcher = tokenDefPattern.matcher(content); + while (defMatcher.find()) { + declaredTokens.add(defMatcher.group(1)); + } + + // Lookups usados como valores de propriedade de cor (ex: -fx-text-fill: -fx-text-primary;) + Pattern lookupUsagePattern = Pattern.compile(":\\s*(-fx-[a-zA-Z0-9_-]+)\\s*;"); + Matcher usageMatcher = lookupUsagePattern.matcher(content); + List missingTokens = new ArrayList<>(); + + while (usageMatcher.find()) { + String usedToken = usageMatcher.group(1); + // Ignora propriedades estruturais que porventura comecem com -fx- + if (!declaredTokens.contains(usedToken) && !usedToken.startsWith("-fx-border-style")) { + missingTokens.add(usedToken); + } + } + + assertThat(missingTokens) + .withFailMessage("Tokens/lookups usados como valor em theme.css mas não declarados: %s", missingTokens) + .isEmpty(); + } + + @Test + @DisplayName("(c) Relação de contraste WCAG 2.1 deve ser >= 4.5:1 para texto normal nos temas claro e escuro") + void shouldMeetWcagContrastRequirementsInBothThemes() throws Exception { + InputStream stream = getClass().getResourceAsStream("/styles/theme.css"); + assertThat(stream).isNotNull(); + + String content; + try (stream) { + content = new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } + + // Extrai mapa de tokens por tema (.theme-light e .theme-dark) + java.util.Map lightTokens = extractTokensFromBlock(content, ".theme-light"); + java.util.Map darkTokens = extractTokensFromBlock(content, ".theme-dark"); + + assertThat(lightTokens).isNotEmpty(); + assertThat(darkTokens).isNotEmpty(); + + // Pares a validar em cada tema: (texto, fundo) + String[][] pairs = { + {"-fx-color-text", "-fx-color-background"}, + {"-fx-color-text", "-fx-color-surface"}, + {"-fx-color-text-muted", "-fx-color-surface"}, + {"-fx-color-text-muted", "-fx-color-background"}, + {"-fx-text-primary", "-fx-bg-primary"}, + {"-fx-text-secondary", "-fx-bg-primary"} + }; + + List contrastFailures = new ArrayList<>(); + + for (String[] pair : pairs) { + String textToken = pair[0]; + String bgToken = pair[1]; + + // Tema Claro + if (lightTokens.containsKey(textToken) && lightTokens.containsKey(bgToken)) { + String textColor = lightTokens.get(textToken); + String bgColor = lightTokens.get(bgToken); + double ratio = calculateContrastRatio(textColor, bgColor); + if (ratio < 4.5) { + contrastFailures.add(String.format("Tema Claro: %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)); + } + } + } + + assertThat(contrastFailures) + .withFailMessage("Falha nos requisitos de contraste WCAG 2.1 (mínimo 4.5:1):\n%s", + String.join("\n", contrastFailures)) + .isEmpty(); + } + + private static java.util.Map extractTokensFromBlock(String css, String blockSelector) { + java.util.Map tokens = new java.util.HashMap<>(); + int startIndex = css.indexOf(blockSelector); + if (startIndex == -1) return tokens; + + int openBrace = css.indexOf('{', startIndex); + if (openBrace == -1) return tokens; + + int closeBrace = css.indexOf('}', openBrace); + if (closeBrace == -1) return tokens; + + String blockBody = css.substring(openBrace + 1, closeBrace); + Pattern linePattern = Pattern.compile("(-fx-[a-zA-Z0-9_-]+)\\s*:\\s*(#[0-9a-fA-F]{3,8})\\s*;"); + Matcher m = linePattern.matcher(blockBody); + while (m.find()) { + tokens.put(m.group(1), m.group(2)); + } + return tokens; + } + + private static double calculateContrastRatio(String hexColor1, String hexColor2) { + double l1 = relativeLuminance(hexColor1); + double l2 = relativeLuminance(hexColor2); + double lighter = Math.max(l1, l2); + double darker = Math.min(l1, l2); + return (lighter + 0.05) / (darker + 0.05); + } + + private static double relativeLuminance(String hex) { + String clean = hex.startsWith("#") ? hex.substring(1) : hex; + if (clean.length() == 3) { + clean = "" + clean.charAt(0) + clean.charAt(0) + + clean.charAt(1) + clean.charAt(1) + + clean.charAt(2) + clean.charAt(2); + } + int r = Integer.parseInt(clean.substring(0, 2), 16); + int g = Integer.parseInt(clean.substring(2, 4), 16); + int b = Integer.parseInt(clean.substring(4, 6), 16); + + double rs = srgbLinear(r / 255.0); + double gs = srgbLinear(g / 255.0); + double bs = srgbLinear(b / 255.0); + + return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs; + } + + private static double srgbLinear(double channel) { + return (channel <= 0.04045) ? (channel / 12.92) : Math.pow((channel + 0.055) / 1.055, 2.4); + } }