fix(ui): resolve dark theme text contrast bug and eliminate inline color styles
CI Pipeline / Build & Verify (Linux x86_64) (push) Canceled after 0s
CI Pipeline / Build & Verify (Windows) (push) Canceled after 0s

This commit is contained in:
Claudecio Martins
2026-10-20 20:52:08 -03:00
parent caaa1de8a0
commit 4010a9bba5
35 changed files with 678 additions and 73 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

+79
View File
@@ -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: <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.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.
@@ -228,7 +228,7 @@ public class CyberCoreDesktopApplication extends Application {
ScrollPane scrollPane = new ScrollPane(settingsView); ScrollPane scrollPane = new ScrollPane(settingsView);
scrollPane.setFitToWidth(true); scrollPane.setFitToWidth(true);
scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); scrollPane.getStyleClass().add("scroll-transparent");
VBox.setVgrow(scrollPane, Priority.ALWAYS); VBox.setVgrow(scrollPane, Priority.ALWAYS);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13)); Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
@@ -69,15 +69,14 @@ public class ApplicationApprovalDialog {
Label originTitle = new Label("ORIGEM WEB (URL)"); Label originTitle = new Label("ORIGEM WEB (URL)");
originTitle.getStyleClass().add("dialog-field-title"); originTitle.getStyleClass().add("dialog-field-title");
Label originValue = new Label(origin != null ? origin : "Origem desconhecida"); 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); originRow.getChildren().addAll(originTitle, originValue);
VBox idRow = new VBox(2); VBox idRow = new VBox(2);
Label idTitle = new Label("IDENTIFICADOR DA APLICAÇÃO"); Label idTitle = new Label("IDENTIFICADOR DA APLICAÇÃO");
idTitle.getStyleClass().add("dialog-field-title"); idTitle.getStyleClass().add("dialog-field-title");
Label idValue = new Label(applicationId); Label idValue = new Label(applicationId);
idValue.getStyleClass().add("dialog-field-value"); idValue.getStyleClass().addAll("dialog-field-value", "text-mono", "text-small");
idValue.setStyle("-fx-font-size: 12px; -fx-font-family: monospace;");
idRow.getChildren().addAll(idTitle, idValue); idRow.getChildren().addAll(idTitle, idValue);
detailsBox.getChildren().addAll(appRow, originRow, idRow); 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." "instalados e solicitar assinaturas. Nenhuma assinatura será realizada sem a sua confirmação explícita na tela."
); );
secWarning.setWrapText(true); 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 // Botões de Ação
HBox actionsRow = new HBox(12); HBox actionsRow = new HBox(12);
actionsRow.setAlignment(Pos.CENTER_RIGHT); actionsRow.setAlignment(Pos.CENTER_RIGHT);
Button btnReject = new Button("Recusar Acesso"); Button btnReject = new Button("Recusar Acesso");
btnReject.getStyleClass().add("btn-secondary"); btnReject.getStyleClass().addAll("btn-danger-outline", "btn-dialog");
btnReject.setOnAction(e -> { btnReject.setOnAction(e -> {
approvalFuture.complete(false); approvalFuture.complete(false);
stage.close(); stage.close();
}); });
Button btnAuthorize = new Button("Autorizar Conexão"); Button btnAuthorize = new Button("Autorizar Conexão");
btnAuthorize.getStyleClass().add("btn-primary"); btnAuthorize.getStyleClass().addAll("btn-primary", "btn-dialog");
btnAuthorize.setDefaultButton(true); btnAuthorize.setDefaultButton(true);
btnAuthorize.setOnAction(e -> { btnAuthorize.setOnAction(e -> {
approvalFuture.complete(true); approvalFuture.complete(true);
@@ -74,8 +74,7 @@ public class CertificateDetailsDialog {
// Nome do Titular em destaque // Nome do Titular em destaque
VBox titleBox = new VBox(4); VBox titleBox = new VBox(4);
Label nameLabel = new Label(cert.commonName() != null ? cert.commonName() : cert.alias()); Label nameLabel = new Label(cert.commonName() != null ? cert.commonName() : cert.alias());
nameLabel.getStyleClass().add("cert-title"); nameLabel.getStyleClass().addAll("dialog-header-title", "text-bold");
nameLabel.setStyle("-fx-font-size: 16px; -fx-font-weight: bold;");
String docInfo = ""; String docInfo = "";
if (cert.cpf() != null) { if (cert.cpf() != null) {
@@ -121,7 +120,7 @@ public class CertificateDetailsDialog {
ScrollPane scroll = new ScrollPane(detailsBox); ScrollPane scroll = new ScrollPane(detailsBox);
scroll.setFitToWidth(true); scroll.setFitToWidth(true);
scroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); scroll.getStyleClass().add("scroll-transparent");
VBox.setVgrow(scroll, Priority.ALWAYS); VBox.setVgrow(scroll, Priority.ALWAYS);
// Botões de Ação // Botões de Ação
@@ -129,7 +128,7 @@ public class CertificateDetailsDialog {
actionsRow.setAlignment(Pos.CENTER_RIGHT); actionsRow.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13)); 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.setPrefWidth(110);
btnClose.setMinWidth(110); btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close()); btnClose.setOnAction(e -> stage.close());
@@ -86,7 +86,7 @@ public final class CertificatesManagerDialog {
// Container com Scroll // Container com Scroll
ScrollPane scroll = new ScrollPane(certificatesContainer); ScrollPane scroll = new ScrollPane(certificatesContainer);
scroll.setFitToWidth(true); scroll.setFitToWidth(true);
scroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); scroll.getStyleClass().add("scroll-transparent");
VBox.setVgrow(scroll, Priority.ALWAYS); VBox.setVgrow(scroll, Priority.ALWAYS);
refreshList(); refreshList();
@@ -69,7 +69,7 @@ public final class DiagnosticsView extends VBox {
// Container de Itens // Container de Itens
ScrollPane scrollPane = new ScrollPane(itemsContainer); ScrollPane scrollPane = new ScrollPane(itemsContainer);
scrollPane.setFitToWidth(true); scrollPane.setFitToWidth(true);
scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); scrollPane.getStyleClass().add("scroll-transparent");
VBox.setVgrow(scrollPane, Priority.ALWAYS); VBox.setVgrow(scrollPane, Priority.ALWAYS);
getChildren().addAll(headerBox, actionsRow, scrollPane); getChildren().addAll(headerBox, actionsRow, scrollPane);
@@ -54,13 +54,13 @@ public final class ExitConfirmationDialog {
VBox titleBox = new VBox(4); VBox titleBox = new VBox(4);
Label title = new Label("Como você deseja proceder?"); 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( Label subtitle = new Label(
"Você pode manter o CyberCore Signer ativo na bandeja do sistema\n" + "Você pode manter o CyberCore Signer ativo na bandeja do sistema\n" +
"para atender assinaturas de aplicações web, ou encerrá-lo por completo." "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); titleBox.getChildren().addAll(title, subtitle);
header.getChildren().addAll(SvgIcons.cyberShieldLogo(38), titleBox); header.getChildren().addAll(SvgIcons.cyberShieldLogo(38), titleBox);
@@ -70,7 +70,7 @@ public final class ExitConfirmationDialog {
actionsRow.setAlignment(Pos.CENTER_RIGHT); actionsRow.setAlignment(Pos.CENTER_RIGHT);
Button btnCancel = new Button("Cancelar"); Button btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().addAll("btn-cancel", "btn-dialog"); btnCancel.getStyleClass().addAll("btn-ghost", "btn-dialog");
btnCancel.setOnAction(e -> { btnCancel.setOnAction(e -> {
chosenAction = ExitChoice.CANCEL; chosenAction = ExitChoice.CANCEL;
stage.close(); stage.close();
@@ -80,7 +80,7 @@ public final class ExitConfirmationDialog {
HBox.setHgrow(spacer, Priority.ALWAYS); HBox.setHgrow(spacer, Priority.ALWAYS);
Button btnExit = new Button("Encerrar Definitivamente"); Button btnExit = new Button("Encerrar Definitivamente");
btnExit.getStyleClass().addAll("btn-danger", "btn-dialog"); btnExit.getStyleClass().addAll("btn-danger-outline", "btn-dialog");
btnExit.setOnAction(e -> { btnExit.setOnAction(e -> {
chosenAction = ExitChoice.EXIT_COMPLETELY; chosenAction = ExitChoice.EXIT_COMPLETELY;
stage.close(); stage.close();
@@ -104,6 +104,10 @@ public final class ExitConfirmationDialog {
stage.setOnCloseRequest(e -> chosenAction = ExitChoice.CANCEL); stage.setOnCloseRequest(e -> chosenAction = ExitChoice.CANCEL);
} }
public Stage getStage() {
return stage;
}
public ExitChoice showAndGetChoice() { public ExitChoice showAndGetChoice() {
stage.showAndWait(); stage.showAndWait();
return chosenAction; return chosenAction;
@@ -83,7 +83,7 @@ public final class MainDashboardView extends VBox {
certsContainer.setPadding(new Insets(4, 0, 4, 0)); certsContainer.setPadding(new Insets(4, 0, 4, 0));
ScrollPane scrollPane = new ScrollPane(certsContainer); ScrollPane scrollPane = new ScrollPane(certsContainer);
scrollPane.setFitToWidth(true); scrollPane.setFitToWidth(true);
scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); scrollPane.getStyleClass().add("scroll-transparent");
VBox.setVgrow(scrollPane, Priority.ALWAYS); VBox.setVgrow(scrollPane, Priority.ALWAYS);
getChildren().addAll(statusBox, certsHeader, scrollPane); getChildren().addAll(statusBox, certsHeader, scrollPane);
@@ -259,7 +259,6 @@ public final class PdfCertificateSignDialog {
btnClearRememberedPin = new Button("Alterar PIN"); btnClearRememberedPin = new Button("Alterar PIN");
btnClearRememberedPin.getStyleClass().addAll("btn-secondary", "btn-compact"); btnClearRememberedPin.getStyleClass().addAll("btn-secondary", "btn-compact");
btnClearRememberedPin.setStyle("-fx-font-size: 11px;");
btnClearRememberedPin.setVisible(false); btnClearRememberedPin.setVisible(false);
btnClearRememberedPin.setOnAction(e -> { btnClearRememberedPin.setOnAction(e -> {
ManagedCertificate sel = certCombo.getValue(); ManagedCertificate sel = certCombo.getValue();
@@ -270,13 +269,13 @@ public final class PdfCertificateSignDialog {
}); });
lblPinNotice = new Label(); lblPinNotice = new Label();
lblPinNotice.setStyle("-fx-font-size: 11.5px;"); lblPinNotice.getStyleClass().addAll("dialog-subtitle", "text-muted");
lblPinNotice.setWrapText(true); lblPinNotice.setWrapText(true);
HBox.setHgrow(lblPinNotice, Priority.ALWAYS); HBox.setHgrow(lblPinNotice, Priority.ALWAYS);
pinRow.getChildren().addAll(lblPinTitle, pinField, btnClearRememberedPin, lblPinNotice); pinRow.getChildren().addAll(lblPinTitle, pinField, btnClearRememberedPin, lblPinNotice);
chkRememberPin = new CheckBox("Lembrar PIN/senha com segurança neste computador"); 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); chkRememberPin.setSelected(true);
VBox tipBox = new VBox(2); VBox tipBox = new VBox(2);
@@ -300,11 +299,11 @@ public final class PdfCertificateSignDialog {
progressIndicator.setVisible(false); progressIndicator.setVisible(false);
lblStatus = new Label(); lblStatus = new Label();
lblStatus.setStyle("-fx-font-size: 12px;"); lblStatus.getStyleClass().addAll("text-muted", "text-small");
HBox.setHgrow(lblStatus, Priority.ALWAYS); HBox.setHgrow(lblStatus, Priority.ALWAYS);
btnCancel = new Button("Cancelar"); btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog"); btnCancel.getStyleClass().addAll("btn-ghost", "btn-dialog");
btnCancel.setMinWidth(110); btnCancel.setMinWidth(110);
btnCancel.setOnAction(e -> stage.close()); btnCancel.setOnAction(e -> stage.close());
@@ -468,7 +467,7 @@ public final class PdfCertificateSignDialog {
setFormDisabled(true); setFormDisabled(true);
progressIndicator.setVisible(true); progressIndicator.setVisible(true);
lblStatus.setText("Assinando documento PDF com envelope PAdES..."); lblStatus.setText("Assinando documento PDF com envelope PAdES...");
lblStatus.setStyle("-fx-text-fill: -fx-accent;"); lblStatus.getStyleClass().setAll("text-accent", "text-bold");
Task<File> signTask = new Task<>() { Task<File> signTask = new Task<>() {
@Override @Override
@@ -586,4 +585,8 @@ public final class PdfCertificateSignDialog {
stage.show(); stage.show();
stage.toFront(); stage.toFront();
} }
public Stage getStage() {
return stage;
}
} }
@@ -228,9 +228,9 @@ public final class PdfSignDialog {
VBox docDetails = new VBox(2); VBox docDetails = new VBox(2);
lblDocName = new Label("Nenhum arquivo selecionado"); 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 = 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); docDetails.getChildren().addAll(lblDocName, lblDocMeta);
Region docSpacer = new Region(); Region docSpacer = new Region();
@@ -252,7 +252,7 @@ public final class PdfSignDialog {
leftScroll.setPrefWidth(330); leftScroll.setPrefWidth(330);
leftScroll.setMinWidth(310); leftScroll.setMinWidth(310);
leftScroll.setMaxWidth(360); leftScroll.setMaxWidth(360);
leftScroll.setStyle("-fx-background-color: transparent; -fx-background: transparent; -fx-border-color: transparent;"); leftScroll.getStyleClass().add("scroll-transparent");
VBox sealSection = buildSealOptionsSection(); VBox sealSection = buildSealOptionsSection();
leftScroll.setContent(sealSection); leftScroll.setContent(sealSection);
@@ -269,7 +269,7 @@ public final class PdfSignDialog {
footer.setPadding(new Insets(6, 0, 0, 0)); footer.setPadding(new Insets(6, 0, 0, 0));
btnCancel = new Button("Cancelar"); btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog"); btnCancel.getStyleClass().addAll("btn-ghost", "btn-dialog");
btnCancel.setMinWidth(110); btnCancel.setMinWidth(110);
btnCancel.setOnAction(e -> stage.close()); btnCancel.setOnAction(e -> stage.close());
@@ -348,7 +348,7 @@ public final class PdfSignDialog {
// Preview em miniatura do SVG // Preview em miniatura do SVG
VBox previewBox = new VBox(4); VBox previewBox = new VBox(4);
previewBox.setAlignment(Pos.CENTER); 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 = new ImageView();
sealPreviewImageView.setPreserveRatio(true); sealPreviewImageView.setPreserveRatio(true);
sealPreviewImageView.setFitWidth(260); sealPreviewImageView.setFitWidth(260);
@@ -390,9 +390,9 @@ public final class PdfSignDialog {
// Dica amigável de posicionamento livre // Dica amigável de posicionamento livre
VBox dragTipBox = new VBox(2); VBox dragTipBox = new VBox(2);
dragTipBox.setPadding(new Insets(8, 10, 8, 10)); 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."); 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); lblDragTip.setWrapText(true);
dragTipBox.getChildren().add(lblDragTip); dragTipBox.getChildren().add(lblDragTip);
@@ -457,7 +457,7 @@ public final class PdfSignDialog {
// Grupo de Zoom da Folha // Grupo de Zoom da Folha
HBox zoomBox = new HBox(5); HBox zoomBox = new HBox(5);
zoomBox.setAlignment(Pos.CENTER_LEFT); 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 = new Button();
btnZoomOut.setGraphic(SvgIcons.zoomOutIcon(13)); btnZoomOut.setGraphic(SvgIcons.zoomOutIcon(13));
@@ -485,7 +485,7 @@ public final class PdfSignDialog {
// Grupo de Tamanho do Selo // Grupo de Tamanho do Selo
HBox resizeToolBox = new HBox(6); HBox resizeToolBox = new HBox(6);
resizeToolBox.setAlignment(Pos.CENTER_LEFT); 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 = new Button(" Redimensionar", SvgIcons.zoomFitIcon(13));
btnToggleResize.getStyleClass().addAll("btn-secondary", "btn-compact"); btnToggleResize.getStyleClass().addAll("btn-secondary", "btn-compact");
@@ -507,7 +507,7 @@ public final class PdfSignDialog {
HBox.setHgrow(tbSpacer, Priority.ALWAYS); HBox.setHgrow(tbSpacer, Priority.ALWAYS);
lblPositionFeedback = new Label("Clique na folha para posicionar"); 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); toolbar.getChildren().addAll(pageNavBox, zoomBox, resizeToolBox, tbSpacer, lblPositionFeedback);
@@ -516,7 +516,7 @@ public final class PdfSignDialog {
sheetScroll.setFitToWidth(false); sheetScroll.setFitToWidth(false);
sheetScroll.setFitToHeight(false); sheetScroll.setFitToHeight(false);
sheetScroll.setPannable(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); VBox.setVgrow(sheetScroll, Priority.ALWAYS);
// Permite atalho de Zoom com Ctrl + Rolagem do Mouse // Permite atalho de Zoom com Ctrl + Rolagem do Mouse
@@ -1249,4 +1249,8 @@ public final class PdfSignDialog {
return label; return label;
} }
} }
public Stage getStage() {
return stage;
}
} }
@@ -135,7 +135,7 @@ public final class SealDesignerDialog {
ScrollPane scrollForm = new ScrollPane(formPane); ScrollPane scrollForm = new ScrollPane(formPane);
scrollForm.setFitToWidth(true); scrollForm.setFitToWidth(true);
VBox.setVgrow(scrollForm, Priority.ALWAYS); VBox.setVgrow(scrollForm, Priority.ALWAYS);
scrollForm.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); scrollForm.getStyleClass().add("scroll-transparent");
HBox.setHgrow(scrollForm, Priority.ALWAYS); HBox.setHgrow(scrollForm, Priority.ALWAYS);
// 1. Identificação do Selo e Campos Adicionais // 1. Identificação do Selo e Campos Adicionais
@@ -174,14 +174,13 @@ public class SignaturePromptDialog {
if (isWindowsStore) { if (isWindowsStore) {
VBox winBox = new VBox(6); VBox winBox = new VBox(6);
Label winTitle = new Label("🔐 Autenticação nativa pelo Windows / Driver do Token"); 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( Label winDesc = new Label(
"O PIN deste dispositivo é gerenciado diretamente pelo driver nativo (SafeSign / Windows Security). " + "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." "Caso o token ainda não esteja autenticado, a janela segura do driver será aberta automaticamente."
); );
winDesc.getStyleClass().add("dialog-subtitle"); winDesc.getStyleClass().add("dialog-subtitle");
winDesc.setStyle("-fx-font-size: 12px;");
winDesc.setWrapText(true); winDesc.setWrapText(true);
winBox.getChildren().addAll(winTitle, winDesc); winBox.getChildren().addAll(winTitle, winDesc);
@@ -71,7 +71,7 @@ public final class TutorialDialog {
ScrollPane scroll = new ScrollPane(content); ScrollPane scroll = new ScrollPane(content);
scroll.setFitToWidth(true); scroll.setFitToWidth(true);
scroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); scroll.getStyleClass().add("scroll-transparent");
VBox.setVgrow(scroll, Priority.ALWAYS); VBox.setVgrow(scroll, Priority.ALWAYS);
HBox footer = new HBox(); HBox footer = new HBox();
@@ -194,7 +194,7 @@ public final class XmlSignDialog {
pinField.setPrefWidth(260); pinField.setPrefWidth(260);
lblPinNotice = new Label(); lblPinNotice = new Label();
lblPinNotice.setStyle("-fx-font-size: 11px;"); lblPinNotice.getStyleClass().addAll("dialog-subtitle", "text-muted");
pinRow.getChildren().addAll(lblPin, pinField, lblPinNotice); pinRow.getChildren().addAll(lblPin, pinField, lblPinNotice);
certSection.getChildren().addAll(lblCertTitle, certCombo, pinRow); certSection.getChildren().addAll(lblCertTitle, certCombo, pinRow);
@@ -289,11 +289,11 @@ public final class XmlSignDialog {
progressIndicator.setVisible(false); progressIndicator.setVisible(false);
lblStatus = new Label(); lblStatus = new Label();
lblStatus.setStyle("-fx-font-size: 12px;"); lblStatus.getStyleClass().addAll("text-muted", "text-small");
HBox.setHgrow(lblStatus, Priority.ALWAYS); HBox.setHgrow(lblStatus, Priority.ALWAYS);
btnCancel = new Button("Cancelar"); btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog"); btnCancel.getStyleClass().addAll("btn-ghost", "btn-dialog");
btnCancel.setOnAction(e -> stage.close()); btnCancel.setOnAction(e -> stage.close());
btnSign = new Button("Assinar Documento XML"); btnSign = new Button("Assinar Documento XML");
@@ -10,6 +10,11 @@
.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) --- */ /* --- Tema Claro (Light Theme) --- */
@@ -67,6 +72,13 @@
-fx-background: -fx-color-background; -fx-background: -fx-color-background;
-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 */
-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) --- */ /* --- Tema Escuro (Dark Theme) --- */
@@ -124,6 +136,13 @@
-fx-background: -fx-color-background; -fx-background: -fx-color-background;
-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 */
-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 */ /* Utilitários semânticos */
.text-primary { .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; -fx-text-fill: -fx-color-primary;
} }
.text-success { .text-success,
.status-ok {
-fx-text-fill: -fx-color-success; -fx-text-fill: -fx-color-success;
} }
.text-warning { .text-warning,
.status-warn {
-fx-text-fill: -fx-color-warning; -fx-text-fill: -fx-color-warning;
} }
.text-danger { .text-danger,
.status-error {
-fx-text-fill: -fx-color-error; -fx-text-fill: -fx-color-error;
} }
.text-muted {
-fx-text-fill: -fx-color-text-muted;
}
.text-bold { .text-bold {
-fx-font-weight: 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. 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 { .btn-primary {
-fx-background-color: -fx-color-primary; -fx-background-color: -fx-color-primary;
-fx-border-color: transparent;
-fx-border-width: 1px;
-fx-text-fill: -fx-color-text-inverse; -fx-text-fill: -fx-color-text-inverse;
-fx-font-size: 13px;
-fx-font-weight: bold; -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); -fx-effect: dropshadow(three-pass-box, -fx-color-shadow, 4, 0, 0, 1);
} }
@@ -402,17 +508,23 @@
-fx-background-color: -fx-color-primary-active; -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 { .btn-secondary {
-fx-background-color: -fx-color-surface; -fx-background-color: -fx-color-surface;
-fx-border-color: -fx-color-border; -fx-border-color: -fx-color-border;
-fx-border-width: 1px; -fx-border-width: 1px;
-fx-text-fill: -fx-color-text; -fx-text-fill: -fx-color-text;
-fx-font-size: 13px; -fx-font-weight: 500;
-fx-font-weight: bold;
-fx-padding: 8px 16px;
-fx-background-radius: 6px;
-fx-border-radius: 6px;
-fx-cursor: hand;
} }
.btn-secondary:hover { .btn-secondary:hover {
@@ -424,34 +536,114 @@
-fx-background-color: -fx-color-surface-active; -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 { .btn-danger {
-fx-background-color: -fx-color-error; -fx-background-color: -fx-color-error;
-fx-border-color: transparent;
-fx-border-width: 1px;
-fx-text-fill: -fx-color-text-inverse; -fx-text-fill: -fx-color-text-inverse;
-fx-font-size: 13px;
-fx-font-weight: bold; -fx-font-weight: bold;
-fx-padding: 8px 16px;
-fx-background-radius: 6px;
-fx-cursor: hand;
} }
.btn-danger:hover { .btn-danger:hover {
-fx-background-color: -fx-color-error-hover; -fx-background-color: -fx-color-error-hover;
} }
.btn-danger-sm { .btn-danger:pressed {
-fx-background-color: transparent; -fx-background-color: -fx-color-error-hover;
-fx-border-color: -fx-color-error-border; -fx-opacity: 0.9;
-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: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 { .btn-danger-sm:hover {
-fx-background-color: -fx-color-error-subtle; -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;
} }
/* ========================================================================== /* ==========================================================================
@@ -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.CertificateType;
import br.com.cybercore.signer.core.domain.ManagedCertificate; import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext; 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.CertificateCardNode;
import br.com.cybercore.signer.ui.components.SvgIcons; import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager; import br.com.cybercore.signer.ui.theme.ThemeManager;
import br.com.cybercore.signer.ui.view.AboutView; import br.com.cybercore.signer.ui.view.AboutView;
import br.com.cybercore.signer.ui.view.DiagnosticsView; 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.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.SealManagementDialog;
import br.com.cybercore.signer.ui.view.SettingsView; import br.com.cybercore.signer.ui.view.SettingsView;
import br.com.cybercore.signer.ui.view.SignaturePromptDialog; import br.com.cybercore.signer.ui.view.SignaturePromptDialog;
@@ -77,6 +83,15 @@ public class UiPreviewGenerator extends Application {
// 8. Sobre // 8. Sobre
captureAboutScreenshots(primaryStage); 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) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
@@ -448,6 +463,98 @@ public class UiPreviewGenerator extends Application {
return createScene(root, 680, 460); 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 { 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 w = (int) image.getWidth();
@@ -140,4 +140,223 @@ class DesignTokenGuardrailTest {
.contains(token); .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<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("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<String> 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<String> 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<String, String> lightTokens = extractTokensFromBlock(content, ".theme-light");
java.util.Map<String, String> 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<String> 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<String, String> extractTokensFromBlock(String css, String blockSelector) {
java.util.Map<String, String> 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);
}
} }