fix(ui): resolve dark theme text contrast bug and eliminate inline color styles
This commit is contained in:
+1
-1
@@ -228,7 +228,7 @@ public class CyberCoreDesktopApplication extends Application {
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane(settingsView);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
|
||||
scrollPane.getStyleClass().add("scroll-transparent");
|
||||
VBox.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
|
||||
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
||||
|
||||
+5
-6
@@ -69,15 +69,14 @@ public class ApplicationApprovalDialog {
|
||||
Label originTitle = new Label("ORIGEM WEB (URL)");
|
||||
originTitle.getStyleClass().add("dialog-field-title");
|
||||
Label originValue = new Label(origin != null ? origin : "Origem desconhecida");
|
||||
originValue.setStyle("-fx-text-fill: -fx-accent; -fx-font-size: 13px; -fx-font-family: monospace;");
|
||||
originValue.getStyleClass().addAll("text-accent", "text-mono");
|
||||
originRow.getChildren().addAll(originTitle, originValue);
|
||||
|
||||
VBox idRow = new VBox(2);
|
||||
Label idTitle = new Label("IDENTIFICADOR DA APLICAÇÃO");
|
||||
idTitle.getStyleClass().add("dialog-field-title");
|
||||
Label idValue = new Label(applicationId);
|
||||
idValue.getStyleClass().add("dialog-field-value");
|
||||
idValue.setStyle("-fx-font-size: 12px; -fx-font-family: monospace;");
|
||||
idValue.getStyleClass().addAll("dialog-field-value", "text-mono", "text-small");
|
||||
idRow.getChildren().addAll(idTitle, idValue);
|
||||
|
||||
detailsBox.getChildren().addAll(appRow, originRow, idRow);
|
||||
@@ -88,21 +87,21 @@ public class ApplicationApprovalDialog {
|
||||
"instalados e solicitar assinaturas. Nenhuma assinatura será realizada sem a sua confirmação explícita na tela."
|
||||
);
|
||||
secWarning.setWrapText(true);
|
||||
secWarning.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 11px; -fx-line-spacing: 2px;");
|
||||
secWarning.getStyleClass().addAll("text-muted", "text-small");
|
||||
|
||||
// Botões de Ação
|
||||
HBox actionsRow = new HBox(12);
|
||||
actionsRow.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
Button btnReject = new Button("Recusar Acesso");
|
||||
btnReject.getStyleClass().add("btn-secondary");
|
||||
btnReject.getStyleClass().addAll("btn-danger-outline", "btn-dialog");
|
||||
btnReject.setOnAction(e -> {
|
||||
approvalFuture.complete(false);
|
||||
stage.close();
|
||||
});
|
||||
|
||||
Button btnAuthorize = new Button("Autorizar Conexão");
|
||||
btnAuthorize.getStyleClass().add("btn-primary");
|
||||
btnAuthorize.getStyleClass().addAll("btn-primary", "btn-dialog");
|
||||
btnAuthorize.setDefaultButton(true);
|
||||
btnAuthorize.setOnAction(e -> {
|
||||
approvalFuture.complete(true);
|
||||
|
||||
+3
-4
@@ -74,8 +74,7 @@ public class CertificateDetailsDialog {
|
||||
// Nome do Titular em destaque
|
||||
VBox titleBox = new VBox(4);
|
||||
Label nameLabel = new Label(cert.commonName() != null ? cert.commonName() : cert.alias());
|
||||
nameLabel.getStyleClass().add("cert-title");
|
||||
nameLabel.setStyle("-fx-font-size: 16px; -fx-font-weight: bold;");
|
||||
nameLabel.getStyleClass().addAll("dialog-header-title", "text-bold");
|
||||
|
||||
String docInfo = "";
|
||||
if (cert.cpf() != null) {
|
||||
@@ -121,7 +120,7 @@ public class CertificateDetailsDialog {
|
||||
|
||||
ScrollPane scroll = new ScrollPane(detailsBox);
|
||||
scroll.setFitToWidth(true);
|
||||
scroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
|
||||
scroll.getStyleClass().add("scroll-transparent");
|
||||
VBox.setVgrow(scroll, Priority.ALWAYS);
|
||||
|
||||
// Botões de Ação
|
||||
@@ -129,7 +128,7 @@ public class CertificateDetailsDialog {
|
||||
actionsRow.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
||||
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||
btnClose.getStyleClass().addAll("btn-ghost", "btn-dialog");
|
||||
btnClose.setPrefWidth(110);
|
||||
btnClose.setMinWidth(110);
|
||||
btnClose.setOnAction(e -> stage.close());
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ public final class CertificatesManagerDialog {
|
||||
// Container com Scroll
|
||||
ScrollPane scroll = new ScrollPane(certificatesContainer);
|
||||
scroll.setFitToWidth(true);
|
||||
scroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
|
||||
scroll.getStyleClass().add("scroll-transparent");
|
||||
VBox.setVgrow(scroll, Priority.ALWAYS);
|
||||
|
||||
refreshList();
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ public final class DiagnosticsView extends VBox {
|
||||
// Container de Itens
|
||||
ScrollPane scrollPane = new ScrollPane(itemsContainer);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
|
||||
scrollPane.getStyleClass().add("scroll-transparent");
|
||||
VBox.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
|
||||
getChildren().addAll(headerBox, actionsRow, scrollPane);
|
||||
|
||||
+8
-4
@@ -54,13 +54,13 @@ public final class ExitConfirmationDialog {
|
||||
|
||||
VBox titleBox = new VBox(4);
|
||||
Label title = new Label("Como você deseja proceder?");
|
||||
title.setStyle("-fx-font-size: 15px; -fx-font-weight: bold;");
|
||||
title.getStyleClass().addAll("dialog-header-title", "text-bold");
|
||||
|
||||
Label subtitle = new Label(
|
||||
"Você pode manter o CyberCore Signer ativo na bandeja do sistema\n" +
|
||||
"para atender assinaturas de aplicações web, ou encerrá-lo por completo."
|
||||
);
|
||||
subtitle.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;");
|
||||
subtitle.getStyleClass().addAll("dialog-subtitle", "text-muted");
|
||||
|
||||
titleBox.getChildren().addAll(title, subtitle);
|
||||
header.getChildren().addAll(SvgIcons.cyberShieldLogo(38), titleBox);
|
||||
@@ -70,7 +70,7 @@ public final class ExitConfirmationDialog {
|
||||
actionsRow.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
Button btnCancel = new Button("Cancelar");
|
||||
btnCancel.getStyleClass().addAll("btn-cancel", "btn-dialog");
|
||||
btnCancel.getStyleClass().addAll("btn-ghost", "btn-dialog");
|
||||
btnCancel.setOnAction(e -> {
|
||||
chosenAction = ExitChoice.CANCEL;
|
||||
stage.close();
|
||||
@@ -80,7 +80,7 @@ public final class ExitConfirmationDialog {
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
|
||||
Button btnExit = new Button("Encerrar Definitivamente");
|
||||
btnExit.getStyleClass().addAll("btn-danger", "btn-dialog");
|
||||
btnExit.getStyleClass().addAll("btn-danger-outline", "btn-dialog");
|
||||
btnExit.setOnAction(e -> {
|
||||
chosenAction = ExitChoice.EXIT_COMPLETELY;
|
||||
stage.close();
|
||||
@@ -104,6 +104,10 @@ public final class ExitConfirmationDialog {
|
||||
stage.setOnCloseRequest(e -> chosenAction = ExitChoice.CANCEL);
|
||||
}
|
||||
|
||||
public Stage getStage() {
|
||||
return stage;
|
||||
}
|
||||
|
||||
public ExitChoice showAndGetChoice() {
|
||||
stage.showAndWait();
|
||||
return chosenAction;
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ public final class MainDashboardView extends VBox {
|
||||
certsContainer.setPadding(new Insets(4, 0, 4, 0));
|
||||
ScrollPane scrollPane = new ScrollPane(certsContainer);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
|
||||
scrollPane.getStyleClass().add("scroll-transparent");
|
||||
VBox.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
|
||||
getChildren().addAll(statusBox, certsHeader, scrollPane);
|
||||
|
||||
+9
-6
@@ -259,7 +259,6 @@ public final class PdfCertificateSignDialog {
|
||||
|
||||
btnClearRememberedPin = new Button("Alterar PIN");
|
||||
btnClearRememberedPin.getStyleClass().addAll("btn-secondary", "btn-compact");
|
||||
btnClearRememberedPin.setStyle("-fx-font-size: 11px;");
|
||||
btnClearRememberedPin.setVisible(false);
|
||||
btnClearRememberedPin.setOnAction(e -> {
|
||||
ManagedCertificate sel = certCombo.getValue();
|
||||
@@ -270,13 +269,13 @@ public final class PdfCertificateSignDialog {
|
||||
});
|
||||
|
||||
lblPinNotice = new Label();
|
||||
lblPinNotice.setStyle("-fx-font-size: 11.5px;");
|
||||
lblPinNotice.getStyleClass().addAll("dialog-subtitle", "text-muted");
|
||||
lblPinNotice.setWrapText(true);
|
||||
HBox.setHgrow(lblPinNotice, Priority.ALWAYS);
|
||||
pinRow.getChildren().addAll(lblPinTitle, pinField, btnClearRememberedPin, lblPinNotice);
|
||||
|
||||
chkRememberPin = new CheckBox("Lembrar PIN/senha com segurança neste computador");
|
||||
chkRememberPin.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-text-primary;");
|
||||
chkRememberPin.getStyleClass().addAll("text-primary", "text-small");
|
||||
chkRememberPin.setSelected(true);
|
||||
|
||||
VBox tipBox = new VBox(2);
|
||||
@@ -300,11 +299,11 @@ public final class PdfCertificateSignDialog {
|
||||
progressIndicator.setVisible(false);
|
||||
|
||||
lblStatus = new Label();
|
||||
lblStatus.setStyle("-fx-font-size: 12px;");
|
||||
lblStatus.getStyleClass().addAll("text-muted", "text-small");
|
||||
HBox.setHgrow(lblStatus, Priority.ALWAYS);
|
||||
|
||||
btnCancel = new Button("Cancelar");
|
||||
btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||
btnCancel.getStyleClass().addAll("btn-ghost", "btn-dialog");
|
||||
btnCancel.setMinWidth(110);
|
||||
btnCancel.setOnAction(e -> stage.close());
|
||||
|
||||
@@ -468,7 +467,7 @@ public final class PdfCertificateSignDialog {
|
||||
setFormDisabled(true);
|
||||
progressIndicator.setVisible(true);
|
||||
lblStatus.setText("Assinando documento PDF com envelope PAdES...");
|
||||
lblStatus.setStyle("-fx-text-fill: -fx-accent;");
|
||||
lblStatus.getStyleClass().setAll("text-accent", "text-bold");
|
||||
|
||||
Task<File> signTask = new Task<>() {
|
||||
@Override
|
||||
@@ -586,4 +585,8 @@ public final class PdfCertificateSignDialog {
|
||||
stage.show();
|
||||
stage.toFront();
|
||||
}
|
||||
|
||||
public Stage getStage() {
|
||||
return stage;
|
||||
}
|
||||
}
|
||||
|
||||
+15
-11
@@ -228,9 +228,9 @@ public final class PdfSignDialog {
|
||||
|
||||
VBox docDetails = new VBox(2);
|
||||
lblDocName = new Label("Nenhum arquivo selecionado");
|
||||
lblDocName.setStyle("-fx-font-weight: bold; -fx-font-size: 12.5px; -fx-text-fill: -fx-text-primary;");
|
||||
lblDocName.getStyleClass().addAll("text-primary", "text-bold");
|
||||
lblDocMeta = new Label("Selecione um arquivo PDF para pré-visualização");
|
||||
lblDocMeta.setStyle("-fx-font-size: 11px; -fx-text-fill: -fx-text-secondary;");
|
||||
lblDocMeta.getStyleClass().addAll("text-muted", "text-small");
|
||||
docDetails.getChildren().addAll(lblDocName, lblDocMeta);
|
||||
|
||||
Region docSpacer = new Region();
|
||||
@@ -252,7 +252,7 @@ public final class PdfSignDialog {
|
||||
leftScroll.setPrefWidth(330);
|
||||
leftScroll.setMinWidth(310);
|
||||
leftScroll.setMaxWidth(360);
|
||||
leftScroll.setStyle("-fx-background-color: transparent; -fx-background: transparent; -fx-border-color: transparent;");
|
||||
leftScroll.getStyleClass().add("scroll-transparent");
|
||||
|
||||
VBox sealSection = buildSealOptionsSection();
|
||||
leftScroll.setContent(sealSection);
|
||||
@@ -269,7 +269,7 @@ public final class PdfSignDialog {
|
||||
footer.setPadding(new Insets(6, 0, 0, 0));
|
||||
|
||||
btnCancel = new Button("Cancelar");
|
||||
btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||
btnCancel.getStyleClass().addAll("btn-ghost", "btn-dialog");
|
||||
btnCancel.setMinWidth(110);
|
||||
btnCancel.setOnAction(e -> stage.close());
|
||||
|
||||
@@ -348,7 +348,7 @@ public final class PdfSignDialog {
|
||||
// Preview em miniatura do SVG
|
||||
VBox previewBox = new VBox(4);
|
||||
previewBox.setAlignment(Pos.CENTER);
|
||||
previewBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-padding: 8px;");
|
||||
previewBox.getStyleClass().add("surface-box");
|
||||
sealPreviewImageView = new ImageView();
|
||||
sealPreviewImageView.setPreserveRatio(true);
|
||||
sealPreviewImageView.setFitWidth(260);
|
||||
@@ -390,9 +390,9 @@ public final class PdfSignDialog {
|
||||
// Dica amigável de posicionamento livre
|
||||
VBox dragTipBox = new VBox(2);
|
||||
dragTipBox.setPadding(new Insets(8, 10, 8, 10));
|
||||
dragTipBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-background-radius: 6px;");
|
||||
dragTipBox.getStyleClass().add("surface-box");
|
||||
Label lblDragTip = new Label("💡 Arraste o selo livremente sobre a folha para posicioná-lo onde desejar.");
|
||||
lblDragTip.setStyle("-fx-font-size: 11px; -fx-text-fill: -fx-text-secondary;");
|
||||
lblDragTip.getStyleClass().addAll("text-muted", "text-small");
|
||||
lblDragTip.setWrapText(true);
|
||||
dragTipBox.getChildren().add(lblDragTip);
|
||||
|
||||
@@ -457,7 +457,7 @@ public final class PdfSignDialog {
|
||||
// Grupo de Zoom da Folha
|
||||
HBox zoomBox = new HBox(5);
|
||||
zoomBox.setAlignment(Pos.CENTER_LEFT);
|
||||
zoomBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-padding: 2px 6px;");
|
||||
zoomBox.getStyleClass().add("toolbar-box");
|
||||
|
||||
btnZoomOut = new Button();
|
||||
btnZoomOut.setGraphic(SvgIcons.zoomOutIcon(13));
|
||||
@@ -485,7 +485,7 @@ public final class PdfSignDialog {
|
||||
// Grupo de Tamanho do Selo
|
||||
HBox resizeToolBox = new HBox(6);
|
||||
resizeToolBox.setAlignment(Pos.CENTER_LEFT);
|
||||
resizeToolBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-padding: 2px 6px;");
|
||||
resizeToolBox.getStyleClass().add("toolbar-box");
|
||||
|
||||
btnToggleResize = new Button(" Redimensionar", SvgIcons.zoomFitIcon(13));
|
||||
btnToggleResize.getStyleClass().addAll("btn-secondary", "btn-compact");
|
||||
@@ -507,7 +507,7 @@ public final class PdfSignDialog {
|
||||
HBox.setHgrow(tbSpacer, Priority.ALWAYS);
|
||||
|
||||
lblPositionFeedback = new Label("Clique na folha para posicionar");
|
||||
lblPositionFeedback.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-accent; -fx-font-weight: bold;");
|
||||
lblPositionFeedback.getStyleClass().addAll("text-accent", "text-bold");
|
||||
|
||||
toolbar.getChildren().addAll(pageNavBox, zoomBox, resizeToolBox, tbSpacer, lblPositionFeedback);
|
||||
|
||||
@@ -516,7 +516,7 @@ public final class PdfSignDialog {
|
||||
sheetScroll.setFitToWidth(false);
|
||||
sheetScroll.setFitToHeight(false);
|
||||
sheetScroll.setPannable(false);
|
||||
sheetScroll.setStyle("-fx-background-color: transparent; -fx-background: transparent; -fx-border-color: transparent;");
|
||||
sheetScroll.getStyleClass().add("scroll-transparent");
|
||||
VBox.setVgrow(sheetScroll, Priority.ALWAYS);
|
||||
|
||||
// Permite atalho de Zoom com Ctrl + Rolagem do Mouse
|
||||
@@ -1249,4 +1249,8 @@ public final class PdfSignDialog {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
|
||||
public Stage getStage() {
|
||||
return stage;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -135,7 +135,7 @@ public final class SealDesignerDialog {
|
||||
ScrollPane scrollForm = new ScrollPane(formPane);
|
||||
scrollForm.setFitToWidth(true);
|
||||
VBox.setVgrow(scrollForm, Priority.ALWAYS);
|
||||
scrollForm.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
|
||||
scrollForm.getStyleClass().add("scroll-transparent");
|
||||
HBox.setHgrow(scrollForm, Priority.ALWAYS);
|
||||
|
||||
// 1. Identificação do Selo e Campos Adicionais
|
||||
|
||||
+1
-2
@@ -174,14 +174,13 @@ public class SignaturePromptDialog {
|
||||
if (isWindowsStore) {
|
||||
VBox winBox = new VBox(6);
|
||||
Label winTitle = new Label("🔐 Autenticação nativa pelo Windows / Driver do Token");
|
||||
winTitle.setStyle("-fx-text-fill: -fx-accent; -fx-font-weight: bold; -fx-font-size: 13px;");
|
||||
winTitle.getStyleClass().addAll("text-accent", "text-bold");
|
||||
|
||||
Label winDesc = new Label(
|
||||
"O PIN deste dispositivo é gerenciado diretamente pelo driver nativo (SafeSign / Windows Security). " +
|
||||
"Caso o token ainda não esteja autenticado, a janela segura do driver será aberta automaticamente."
|
||||
);
|
||||
winDesc.getStyleClass().add("dialog-subtitle");
|
||||
winDesc.setStyle("-fx-font-size: 12px;");
|
||||
winDesc.setWrapText(true);
|
||||
|
||||
winBox.getChildren().addAll(winTitle, winDesc);
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ public final class TutorialDialog {
|
||||
|
||||
ScrollPane scroll = new ScrollPane(content);
|
||||
scroll.setFitToWidth(true);
|
||||
scroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
|
||||
scroll.getStyleClass().add("scroll-transparent");
|
||||
VBox.setVgrow(scroll, Priority.ALWAYS);
|
||||
|
||||
HBox footer = new HBox();
|
||||
|
||||
+3
-3
@@ -194,7 +194,7 @@ public final class XmlSignDialog {
|
||||
pinField.setPrefWidth(260);
|
||||
|
||||
lblPinNotice = new Label();
|
||||
lblPinNotice.setStyle("-fx-font-size: 11px;");
|
||||
lblPinNotice.getStyleClass().addAll("dialog-subtitle", "text-muted");
|
||||
pinRow.getChildren().addAll(lblPin, pinField, lblPinNotice);
|
||||
|
||||
certSection.getChildren().addAll(lblCertTitle, certCombo, pinRow);
|
||||
@@ -289,11 +289,11 @@ public final class XmlSignDialog {
|
||||
progressIndicator.setVisible(false);
|
||||
|
||||
lblStatus = new Label();
|
||||
lblStatus.setStyle("-fx-font-size: 12px;");
|
||||
lblStatus.getStyleClass().addAll("text-muted", "text-small");
|
||||
HBox.setHgrow(lblStatus, Priority.ALWAYS);
|
||||
|
||||
btnCancel = new Button("Cancelar");
|
||||
btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||
btnCancel.getStyleClass().addAll("btn-ghost", "btn-dialog");
|
||||
btnCancel.setOnAction(e -> stage.close());
|
||||
|
||||
btnSign = new Button("Assinar Documento XML");
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
.root {
|
||||
-fx-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-fx-font-size: 13px;
|
||||
-fx-text-primary: -fx-color-text;
|
||||
-fx-text-secondary: -fx-color-text-muted;
|
||||
-fx-bg-primary: -fx-color-surface;
|
||||
-fx-border-light: -fx-color-border;
|
||||
-fx-accent: -fx-color-primary;
|
||||
}
|
||||
|
||||
/* --- Tema Claro (Light Theme) --- */
|
||||
@@ -67,6 +72,13 @@
|
||||
-fx-background: -fx-color-background;
|
||||
-fx-control-inner-background: -fx-color-surface;
|
||||
-fx-text-fill: -fx-color-text;
|
||||
|
||||
/* Tokens semânticos complementares e aliases de compatibilidade */
|
||||
-fx-text-primary: -fx-color-text;
|
||||
-fx-text-secondary: -fx-color-text-muted;
|
||||
-fx-bg-primary: -fx-color-surface;
|
||||
-fx-border-light: -fx-color-border-subtle;
|
||||
-fx-accent: -fx-color-primary;
|
||||
}
|
||||
|
||||
/* --- Tema Escuro (Dark Theme) --- */
|
||||
@@ -124,6 +136,13 @@
|
||||
-fx-background: -fx-color-background;
|
||||
-fx-control-inner-background: -fx-color-surface;
|
||||
-fx-text-fill: -fx-color-text;
|
||||
|
||||
/* Tokens semânticos complementares e aliases de compatibilidade */
|
||||
-fx-text-primary: -fx-color-text;
|
||||
-fx-text-secondary: -fx-color-text-muted;
|
||||
-fx-bg-primary: -fx-color-surface;
|
||||
-fx-border-light: -fx-color-border;
|
||||
-fx-accent: -fx-color-primary;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
@@ -356,41 +375,128 @@
|
||||
|
||||
/* Utilitários semânticos */
|
||||
.text-primary {
|
||||
-fx-text-fill: -fx-color-text;
|
||||
}
|
||||
|
||||
.text-secondary,
|
||||
.text-muted {
|
||||
-fx-text-fill: -fx-color-text-muted;
|
||||
}
|
||||
|
||||
.text-accent {
|
||||
-fx-text-fill: -fx-color-primary;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
.text-success,
|
||||
.status-ok {
|
||||
-fx-text-fill: -fx-color-success;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
.text-warning,
|
||||
.status-warn {
|
||||
-fx-text-fill: -fx-color-warning;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
.text-danger,
|
||||
.status-error {
|
||||
-fx-text-fill: -fx-color-error;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
-fx-text-fill: -fx-color-text-muted;
|
||||
}
|
||||
|
||||
.text-bold {
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.text-small {
|
||||
-fx-font-size: 11px;
|
||||
}
|
||||
|
||||
.text-mono {
|
||||
-fx-font-family: "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.surface-box {
|
||||
-fx-background-color: -fx-color-surface;
|
||||
-fx-border-color: -fx-color-border-subtle;
|
||||
-fx-border-width: 1px;
|
||||
-fx-border-radius: 6px;
|
||||
-fx-background-radius: 6px;
|
||||
-fx-padding: 8px;
|
||||
}
|
||||
|
||||
.toolbar-box {
|
||||
-fx-background-color: -fx-color-surface;
|
||||
-fx-border-color: -fx-color-border-subtle;
|
||||
-fx-border-width: 1px;
|
||||
-fx-border-radius: 6px;
|
||||
-fx-background-radius: 6px;
|
||||
-fx-padding: 2px 6px;
|
||||
}
|
||||
|
||||
.scroll-transparent,
|
||||
.scroll-transparent .viewport {
|
||||
-fx-background: transparent;
|
||||
-fx-background-color: transparent;
|
||||
-fx-border-color: transparent;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
5. BOTÕES E CONTROLES DE AÇÃO
|
||||
========================================================================== */
|
||||
|
||||
/* 5.1 Regras Base e Tamanhos */
|
||||
.button,
|
||||
.btn-primary,
|
||||
.btn-secondary,
|
||||
.btn-danger,
|
||||
.btn-danger-outline,
|
||||
.btn-ghost,
|
||||
.btn-toolbar,
|
||||
.btn-cancel {
|
||||
-fx-background-radius: 6px;
|
||||
-fx-border-radius: 6px;
|
||||
-fx-cursor: hand;
|
||||
-fx-alignment: CENTER;
|
||||
-fx-pref-height: 34px;
|
||||
-fx-min-height: 34px;
|
||||
-fx-font-size: 13px;
|
||||
-fx-padding: 0 16px;
|
||||
}
|
||||
|
||||
/* Tamanho Compacto (28 px) */
|
||||
.btn-compact,
|
||||
.btn-sm {
|
||||
-fx-pref-height: 28px;
|
||||
-fx-min-height: 28px;
|
||||
-fx-font-size: 11.5px;
|
||||
-fx-padding: 0 10px;
|
||||
}
|
||||
|
||||
/* Variante Botão de Ícone Apenas */
|
||||
.btn-icon-only {
|
||||
-fx-pref-width: 34px;
|
||||
-fx-min-width: 34px;
|
||||
-fx-pref-height: 34px;
|
||||
-fx-min-height: 34px;
|
||||
-fx-padding: 0;
|
||||
-fx-alignment: CENTER;
|
||||
}
|
||||
|
||||
.btn-icon-only.btn-compact,
|
||||
.btn-icon-only.btn-sm {
|
||||
-fx-pref-width: 28px;
|
||||
-fx-min-width: 28px;
|
||||
-fx-pref-height: 28px;
|
||||
-fx-min-height: 28px;
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
/* 5.2 Variante Primary (Ação Principal) */
|
||||
.btn-primary {
|
||||
-fx-background-color: -fx-color-primary;
|
||||
-fx-border-color: transparent;
|
||||
-fx-border-width: 1px;
|
||||
-fx-text-fill: -fx-color-text-inverse;
|
||||
-fx-font-size: 13px;
|
||||
-fx-font-weight: bold;
|
||||
-fx-padding: 8px 16px;
|
||||
-fx-background-radius: 6px;
|
||||
-fx-cursor: hand;
|
||||
-fx-effect: dropshadow(three-pass-box, -fx-color-shadow, 4, 0, 0, 1);
|
||||
}
|
||||
|
||||
@@ -402,17 +508,23 @@
|
||||
-fx-background-color: -fx-color-primary-active;
|
||||
}
|
||||
|
||||
.btn-primary:focused {
|
||||
-fx-border-color: -fx-color-focus;
|
||||
-fx-border-width: 1.5px;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
-fx-opacity: 0.55;
|
||||
-fx-cursor: default;
|
||||
}
|
||||
|
||||
/* 5.3 Variante Secondary (Ação Neutra) */
|
||||
.btn-secondary {
|
||||
-fx-background-color: -fx-color-surface;
|
||||
-fx-border-color: -fx-color-border;
|
||||
-fx-border-width: 1px;
|
||||
-fx-text-fill: -fx-color-text;
|
||||
-fx-font-size: 13px;
|
||||
-fx-font-weight: bold;
|
||||
-fx-padding: 8px 16px;
|
||||
-fx-background-radius: 6px;
|
||||
-fx-border-radius: 6px;
|
||||
-fx-cursor: hand;
|
||||
-fx-font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
@@ -424,34 +536,114 @@
|
||||
-fx-background-color: -fx-color-surface-active;
|
||||
}
|
||||
|
||||
.btn-secondary:focused {
|
||||
-fx-border-color: -fx-color-focus;
|
||||
-fx-border-width: 1.5px;
|
||||
}
|
||||
|
||||
.btn-secondary:disabled {
|
||||
-fx-opacity: 0.55;
|
||||
-fx-cursor: default;
|
||||
}
|
||||
|
||||
/* 5.4 Variante Danger (Ação Destrutiva Sólida) */
|
||||
.btn-danger {
|
||||
-fx-background-color: -fx-color-error;
|
||||
-fx-border-color: transparent;
|
||||
-fx-border-width: 1px;
|
||||
-fx-text-fill: -fx-color-text-inverse;
|
||||
-fx-font-size: 13px;
|
||||
-fx-font-weight: bold;
|
||||
-fx-padding: 8px 16px;
|
||||
-fx-background-radius: 6px;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
-fx-background-color: -fx-color-error-hover;
|
||||
}
|
||||
|
||||
.btn-danger-sm {
|
||||
-fx-background-color: transparent;
|
||||
-fx-border-color: -fx-color-error-border;
|
||||
-fx-border-radius: 4px;
|
||||
-fx-background-radius: 4px;
|
||||
-fx-text-fill: -fx-color-error;
|
||||
-fx-font-size: 11px;
|
||||
-fx-font-weight: bold;
|
||||
-fx-padding: 4px 8px;
|
||||
-fx-cursor: hand;
|
||||
.btn-danger:pressed {
|
||||
-fx-background-color: -fx-color-error-hover;
|
||||
-fx-opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-danger:focused {
|
||||
-fx-border-color: -fx-color-error-border;
|
||||
-fx-border-width: 1.5px;
|
||||
}
|
||||
|
||||
.btn-danger:disabled {
|
||||
-fx-opacity: 0.55;
|
||||
-fx-cursor: default;
|
||||
}
|
||||
|
||||
/* 5.5 Variante Danger Outline (Ação Destrutiva em Diálogos de Confirmação) */
|
||||
.btn-danger-outline,
|
||||
.btn-danger-sm {
|
||||
-fx-background-color: transparent;
|
||||
-fx-border-color: -fx-color-error;
|
||||
-fx-border-width: 1.2px;
|
||||
-fx-text-fill: -fx-color-error;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-danger-outline:hover,
|
||||
.btn-danger-sm:hover {
|
||||
-fx-background-color: -fx-color-error-subtle;
|
||||
-fx-border-color: -fx-color-error-hover;
|
||||
}
|
||||
|
||||
.btn-danger-outline:pressed,
|
||||
.btn-danger-sm:pressed {
|
||||
-fx-background-color: -fx-color-error;
|
||||
-fx-text-fill: -fx-color-text-inverse;
|
||||
}
|
||||
|
||||
.btn-danger-outline:focused,
|
||||
.btn-danger-sm:focused {
|
||||
-fx-border-color: -fx-color-error;
|
||||
-fx-border-width: 1.5px;
|
||||
}
|
||||
|
||||
.btn-danger-outline:disabled,
|
||||
.btn-danger-sm:disabled {
|
||||
-fx-opacity: 0.55;
|
||||
-fx-cursor: default;
|
||||
}
|
||||
|
||||
/* 5.6 Variante Ghost / Toolbar / Cancel */
|
||||
.btn-ghost,
|
||||
.btn-toolbar,
|
||||
.btn-cancel {
|
||||
-fx-background-color: transparent;
|
||||
-fx-border-color: transparent;
|
||||
-fx-border-width: 1px;
|
||||
-fx-text-fill: -fx-color-text;
|
||||
-fx-font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-ghost:hover,
|
||||
.btn-toolbar:hover,
|
||||
.btn-cancel:hover {
|
||||
-fx-background-color: -fx-color-surface-hover;
|
||||
-fx-border-color: -fx-color-border-subtle;
|
||||
}
|
||||
|
||||
.btn-ghost:pressed,
|
||||
.btn-toolbar:pressed,
|
||||
.btn-cancel:pressed {
|
||||
-fx-background-color: -fx-color-surface-active;
|
||||
}
|
||||
|
||||
.btn-ghost:focused,
|
||||
.btn-toolbar:focused,
|
||||
.btn-cancel:focused {
|
||||
-fx-border-color: -fx-color-focus;
|
||||
-fx-border-width: 1.5px;
|
||||
}
|
||||
|
||||
.btn-ghost:disabled,
|
||||
.btn-toolbar:disabled,
|
||||
.btn-cancel:disabled {
|
||||
-fx-opacity: 0.55;
|
||||
-fx-cursor: default;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
|
||||
+107
@@ -6,12 +6,18 @@ import br.com.cybercore.signer.core.domain.CertificateStatus;
|
||||
import br.com.cybercore.signer.core.domain.CertificateType;
|
||||
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||
import br.com.cybercore.signer.core.domain.SigningContext;
|
||||
import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode;
|
||||
import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry;
|
||||
import br.com.cybercore.signer.core.provider.CertificateManager;
|
||||
import br.com.cybercore.signer.ui.components.CertificateCardNode;
|
||||
import br.com.cybercore.signer.ui.components.SvgIcons;
|
||||
import br.com.cybercore.signer.ui.theme.ThemeManager;
|
||||
import br.com.cybercore.signer.ui.view.AboutView;
|
||||
import br.com.cybercore.signer.ui.view.DiagnosticsView;
|
||||
import br.com.cybercore.signer.ui.view.ExitConfirmationDialog;
|
||||
import br.com.cybercore.signer.ui.view.MainAppView;
|
||||
import br.com.cybercore.signer.ui.view.PdfCertificateSignDialog;
|
||||
import br.com.cybercore.signer.ui.view.PdfSignDialog;
|
||||
import br.com.cybercore.signer.ui.view.SealManagementDialog;
|
||||
import br.com.cybercore.signer.ui.view.SettingsView;
|
||||
import br.com.cybercore.signer.ui.view.SignaturePromptDialog;
|
||||
@@ -77,6 +83,15 @@ public class UiPreviewGenerator extends Application {
|
||||
// 8. Sobre
|
||||
captureAboutScreenshots(primaryStage);
|
||||
|
||||
// 9. Diálogo de Encerramento / Bandeja
|
||||
captureExitDialogScreenshots(primaryStage);
|
||||
|
||||
// 10. Diálogo de Posicionamento e Assinatura PDF (PAdES)
|
||||
capturePdfSignDialogScreenshots(primaryStage);
|
||||
|
||||
// 11. Diálogo de Seleção de Certificado e PIN do PDF
|
||||
capturePdfCertificateSignDialogScreenshots(primaryStage);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
@@ -448,6 +463,98 @@ public class UiPreviewGenerator extends Application {
|
||||
return createScene(root, 680, 460);
|
||||
}
|
||||
|
||||
private void captureExitDialogScreenshots(Stage stage) throws Exception {
|
||||
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) {
|
||||
ThemeManager.setCurrentTheme(theme);
|
||||
ExitConfirmationDialog dialog = new ExitConfirmationDialog(null);
|
||||
Scene scene = dialog.getStage().getScene();
|
||||
scene.getRoot().applyCss();
|
||||
scene.getRoot().layout();
|
||||
|
||||
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro";
|
||||
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "09_exit_dialog_" + suffix + ".png"));
|
||||
}
|
||||
}
|
||||
|
||||
private void capturePdfSignDialogScreenshots(Stage stage) throws Exception {
|
||||
File samplePdf = getOrCreateSamplePdf();
|
||||
CertificateManager certManager = CertificateManager.createDefault();
|
||||
|
||||
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) {
|
||||
ThemeManager.setCurrentTheme(theme);
|
||||
PdfSignDialog dialog = new PdfSignDialog(null, certManager, samplePdf);
|
||||
Scene scene = dialog.getStage().getScene();
|
||||
scene.getRoot().applyCss();
|
||||
scene.getRoot().layout();
|
||||
|
||||
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro";
|
||||
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "10_pdf_sign_" + suffix + ".png"));
|
||||
}
|
||||
}
|
||||
|
||||
private void capturePdfCertificateSignDialogScreenshots(Stage stage) throws Exception {
|
||||
File samplePdf = getOrCreateSamplePdf();
|
||||
CertificateManager certManager = CertificateManager.createDefault();
|
||||
|
||||
PdfCertificateSignDialog.SealConfig config = new PdfCertificateSignDialog.SealConfig(
|
||||
true,
|
||||
PdfSealThemeRegistry.getDefaultTheme(),
|
||||
CpfDisplayMode.MASKED,
|
||||
true,
|
||||
"Assinado via CyberCore Signer Desktop",
|
||||
PdfSignDialog.PositionOption.BOTTOM_RIGHT,
|
||||
PdfSignDialog.PageOption.LAST_PAGE,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"Assinatura Digital ICP-Brasil",
|
||||
"Brasília, DF",
|
||||
1.0
|
||||
);
|
||||
|
||||
br.com.cybercore.signer.core.pdf.PdfSigningService pdfSigningService = new br.com.cybercore.signer.core.pdf.PdfSigningService(certManager);
|
||||
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) {
|
||||
ThemeManager.setCurrentTheme(theme);
|
||||
PdfCertificateSignDialog dialog = new PdfCertificateSignDialog(
|
||||
null,
|
||||
certManager,
|
||||
pdfSigningService,
|
||||
samplePdf,
|
||||
config,
|
||||
"Selo Padrão ICP-Brasil (Canto Inferior Direito)"
|
||||
);
|
||||
Scene scene = dialog.getStage().getScene();
|
||||
scene.getRoot().applyCss();
|
||||
scene.getRoot().layout();
|
||||
|
||||
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro";
|
||||
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "11_pdf_cert_sign_" + suffix + ".png"));
|
||||
}
|
||||
}
|
||||
|
||||
private static File getOrCreateSamplePdf() throws Exception {
|
||||
File pdf = new File("target/sample-test.pdf");
|
||||
if (!pdf.exists()) {
|
||||
if (pdf.getParentFile() != null) {
|
||||
pdf.getParentFile().mkdirs();
|
||||
}
|
||||
try (org.apache.pdfbox.pdmodel.PDDocument doc = new org.apache.pdfbox.pdmodel.PDDocument()) {
|
||||
org.apache.pdfbox.pdmodel.PDPage page = new org.apache.pdfbox.pdmodel.PDPage();
|
||||
doc.addPage(page);
|
||||
try (org.apache.pdfbox.pdmodel.PDPageContentStream stream = new org.apache.pdfbox.pdmodel.PDPageContentStream(doc, page)) {
|
||||
stream.beginText();
|
||||
stream.setFont(new org.apache.pdfbox.pdmodel.font.PDType1Font(org.apache.pdfbox.pdmodel.font.Standard14Fonts.FontName.HELVETICA_BOLD), 16);
|
||||
stream.newLineAtOffset(50, 700);
|
||||
stream.showText("CyberCore Signer — Documento PDF de Demonstração");
|
||||
stream.endText();
|
||||
}
|
||||
doc.save(pdf);
|
||||
}
|
||||
}
|
||||
return pdf;
|
||||
}
|
||||
|
||||
private static void saveNodeSnapshot(Scene scene, File targetFile) throws Exception {
|
||||
WritableImage image = scene.snapshot(null);
|
||||
int w = (int) image.getWidth();
|
||||
|
||||
+219
@@ -140,4 +140,223 @@ class DesignTokenGuardrailTest {
|
||||
.contains(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("(a) Código Java não deve conter cores ou propriedades de cor inline em setStyle()")
|
||||
void shouldNotHaveInlineColorStylesInJavaFiles() throws Exception {
|
||||
java.nio.file.Path sourceRoot = java.nio.file.Files.exists(java.nio.file.Path.of("src/main/java"))
|
||||
? java.nio.file.Path.of("src/main/java")
|
||||
: java.nio.file.Path.of("modules/cybercore-ui/src/main/java");
|
||||
|
||||
assertThat(java.nio.file.Files.exists(sourceRoot))
|
||||
.withFailMessage("Diretório fonte não encontrado: %s", sourceRoot)
|
||||
.isTrue();
|
||||
|
||||
Pattern colorInSetStylePattern = Pattern.compile(
|
||||
"\\.setStyle\\(.*(?:"
|
||||
+ "-fx-text-fill"
|
||||
+ "|-fx-background-color"
|
||||
+ "|-fx-border-color"
|
||||
+ "|-fx-fill"
|
||||
+ "|#[0-9a-fA-F]{3,8}"
|
||||
+ "|rgb\\s*\\("
|
||||
+ "|rgba\\s*\\("
|
||||
+ "|-fx-accent"
|
||||
+ "|-fx-text-primary"
|
||||
+ "|-fx-text-secondary"
|
||||
+ "|-fx-bg-primary"
|
||||
+ "|-fx-border-light"
|
||||
+ "|-fx-color-"
|
||||
+ ").*\\)"
|
||||
);
|
||||
|
||||
List<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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user