fix(ui): resolve dark theme text contrast bug and eliminate inline color styles
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 47 KiB |
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||