feat: add SVG asset management, icons, and UI component enhancements

This commit is contained in:
Claudecio Martins
2026-10-11 12:27:47 -03:00
parent 3fab68c118
commit 176acf6f40
39 changed files with 869 additions and 134 deletions
@@ -5,8 +5,14 @@ import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Paint;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Polygon;
@@ -17,7 +23,6 @@ import javafx.scene.shape.Shape;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.transform.Scale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
@@ -26,19 +31,20 @@ import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
/**
* Gerenciador centralizado de recursos visuais (ícones e imagens).
* Permite que o usuário customize ou substitua facilmente qualquer ícone (SVG/PNG) ou imagem (PNG/JPG)
* Permite que o usuário customize ou substitua facilmente qualquer ícone ou imagem (SVG/PNG)
* do CyberCore Signer simplesmente colocando os arquivos na pasta 'assets/'
* na raiz do projeto ou no diretório ~/.cybercore/assets/.
*/
@@ -121,7 +127,7 @@ public final class AssetManager {
}
/**
* Retorna um Node de ícone customizado (SVG ou PNG) ou invoca o fallback caso não exista imagem customizada.
* Retorna um Node de ícone customizado (prioritariamente SVG, com fallback para PNG ou supplier vetorial).
*/
public static Node getIconOrDefault(String iconName, double size, Supplier<Node> fallbackSupplier) {
if (iconName != null && !iconName.isBlank()) {
@@ -140,7 +146,20 @@ public final class AssetManager {
return userSvg.get();
}
// 3. Se não houver SVG, tenta imagem bitmap (.png, .jpg) nas pastas externas
// 3. Procura SVG no Classpath interno
String resSvgPath = "/assets/icons/" + cleanName + ".svg";
InputStream resStream = AssetManager.class.getResourceAsStream(resSvgPath);
if (resStream != null) {
try (resStream) {
Node node = parseSvgToNode(resStream, size, size);
if (node != null) {
return node;
}
} catch (Exception ignored) {
}
}
// 4. Se não houver SVG, tenta imagem bitmap (.png, .jpg)
Optional<Image> customImg = loadImage("icons", cleanName, size, size);
if (customImg.isPresent() && !customImg.get().isError()) {
ImageView iv = new ImageView(customImg.get());
@@ -155,28 +174,76 @@ public final class AssetManager {
}
/**
* Retorna a imagem do logotipo do sistema ou o fallback vetorial.
* Retorna uma imagem institucional ou elemento gráfico (prioritariamente SVG, com fallback para PNG ou supplier).
*/
public static Node getLogoOrDefault(double width, double height, Supplier<Node> fallbackSupplier) {
// Tenta carregar app-logo bitmap (.png) primeiro
Optional<Image> customLogo = loadImage("images", "app-logo", width, height);
if (customLogo.isPresent() && !customLogo.get().isError()) {
ImageView iv = new ImageView(customLogo.get());
iv.setFitWidth(width);
iv.setFitHeight(height);
iv.setPreserveRatio(true);
return iv;
}
public static Node getImageOrDefault(String imageName, double width, double height, Supplier<Node> fallbackSupplier) {
if (imageName != null && !imageName.isBlank()) {
String cleanName = cleanBaseName(imageName);
// Tenta carregar SVG de app-logo se existir
Optional<Node> svgLogo = loadSvgIconFromPath(Path.of("assets", "images", "app-logo.svg"), height);
if (svgLogo.isPresent()) {
return svgLogo.get();
// 1. Prioridade para SVG em ./assets/images/
Path localSvg = Path.of("assets", "images", cleanName + ".svg");
if (Files.isRegularFile(localSvg)) {
try (InputStream in = new FileInputStream(localSvg.toFile())) {
Node node = parseSvgToNode(in, width, height);
if (node != null) {
logger.debug("Carregada imagem SVG local: {}", localSvg);
return node;
}
} catch (Exception e) {
logger.warn("Falha ao ler imagem SVG local {}: {}", localSvg, e.getMessage());
}
}
// 2. Procura SVG em ~/.cybercore/assets/images/
String userHome = System.getProperty("user.home", ".");
Path userSvg = Path.of(userHome, ".cybercore", "assets", "images", cleanName + ".svg");
if (Files.isRegularFile(userSvg)) {
try (InputStream in = new FileInputStream(userSvg.toFile())) {
Node node = parseSvgToNode(in, width, height);
if (node != null) {
logger.debug("Carregada imagem SVG do usuário: {}", userSvg);
return node;
}
} catch (Exception e) {
logger.warn("Falha ao ler imagem SVG do usuário {}: {}", userSvg, e.getMessage());
}
}
// 3. Procura SVG no Classpath interno
String resPath = "/assets/images/" + cleanName + ".svg";
InputStream resStream = AssetManager.class.getResourceAsStream(resPath);
if (resStream != null) {
try (resStream) {
Node node = parseSvgToNode(resStream, width, height);
if (node != null) {
return node;
}
} catch (Exception e) {
logger.warn("Falha ao ler recurso interno SVG {}: {}", resPath, e.getMessage());
}
}
// 4. Fallback para imagem bitmap (.png, .jpg)
Optional<Image> customImg = loadImage("images", cleanName, width, height);
if (customImg.isPresent() && !customImg.get().isError()) {
ImageView iv = new ImageView(customImg.get());
iv.setFitWidth(width);
iv.setFitHeight(height);
iv.setPreserveRatio(true);
return iv;
}
}
return fallbackSupplier != null ? fallbackSupplier.get() : new ImageView();
}
/**
* Retorna a imagem do logotipo do sistema (SVG ou bitmap).
*/
public static Node getLogoOrDefault(double width, double height, Supplier<Node> fallbackSupplier) {
return getImageOrDefault("app-logo", width, height, fallbackSupplier);
}
private static String cleanBaseName(String name) {
String clean = name;
int dotIdx = clean.lastIndexOf('.');
@@ -189,7 +256,7 @@ public final class AssetManager {
private static Optional<Node> loadSvgIconFromPath(Path path, double size) {
if (Files.isRegularFile(path)) {
try (InputStream in = new FileInputStream(path.toFile())) {
Node node = parseSvgToNode(in, size);
Node node = parseSvgToNode(in, size, size);
if (node != null) {
logger.debug("Carregado ícone SVG customizado de: {}", path);
return Optional.of(node);
@@ -202,9 +269,16 @@ public final class AssetManager {
}
/**
* Parser analítico leve de SVG padrão para nós gráficos JavaFX com preservação geométrica exata.
* Parser analítico de SVG para nós gráficos JavaFX com suporte a dimensões quadradas.
*/
public static Node parseSvgToNode(InputStream in, double targetSize) {
return parseSvgToNode(in, targetSize, targetSize);
}
/**
* Parser analítico completo de SVG para nós gráficos JavaFX com dimensões proporcionais e suporte a defs/gradientes.
*/
public static Node parseSvgToNode(InputStream in, double targetWidth, double targetHeight) {
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(false);
@@ -249,27 +323,34 @@ public final class AssetManager {
if (vbWidth <= 0) vbWidth = 48;
if (vbHeight <= 0) vbHeight = 48;
Group contentGroup = new Group();
renderChildren(svg, contentGroup);
Map<String, Paint> defsMap = new HashMap<>();
parseDefs(svg, defsMap);
double maxDim = Math.max(vbWidth, vbHeight);
double scale = (targetSize * 0.90) / maxDim;
Group contentGroup = new Group();
renderChildren(svg, contentGroup, defsMap);
double scaleX = targetWidth / vbWidth;
double scaleY = targetHeight / vbHeight;
double scale = Math.min(scaleX, scaleY);
if (scale <= 0) scale = 1.0;
double drawnW = vbWidth * scale;
double drawnH = vbHeight * scale;
double tx = (targetSize - drawnW) / 2.0 - (vbMinX * scale);
double ty = (targetSize - drawnH) / 2.0 - (vbMinY * scale);
double tx = (targetWidth - drawnW) / 2.0 - (vbMinX * scale);
double ty = (targetHeight - drawnH) / 2.0 - (vbMinY * scale);
contentGroup.setManaged(false);
contentGroup.setLayoutX(0);
contentGroup.setLayoutY(0);
contentGroup.getTransforms().setAll(
new javafx.scene.transform.Translate(tx, ty),
new javafx.scene.transform.Scale(scale, scale, 0, 0)
);
StackPane container = new StackPane(contentGroup);
container.setPrefSize(targetSize, targetSize);
container.setMaxSize(targetSize, targetSize);
container.setMinSize(targetSize, targetSize);
container.setAlignment(Pos.CENTER);
Pane container = new Pane(contentGroup);
container.setPrefSize(targetWidth, targetHeight);
container.setMaxSize(targetWidth, targetHeight);
container.setMinSize(targetWidth, targetHeight);
return container;
} catch (Exception e) {
@@ -278,12 +359,100 @@ public final class AssetManager {
}
}
private static void renderChildren(Element parent, Group targetGroup) {
private static void parseDefs(Element root, Map<String, Paint> defsMap) {
NodeList defsList = root.getElementsByTagName("defs");
for (int i = 0; i < defsList.getLength(); i++) {
org.w3c.dom.Node defNode = defsList.item(i);
if (defNode instanceof Element defsEl) {
NodeList children = defsEl.getChildNodes();
for (int j = 0; j < children.getLength(); j++) {
org.w3c.dom.Node c = children.item(j);
if (c instanceof Element gradEl) {
String id = gradEl.getAttribute("id");
if (!id.isBlank()) {
Paint paint = createPaintFromGradient(gradEl);
if (paint != null) {
defsMap.put(id, paint);
}
}
}
}
}
}
}
private static Paint createPaintFromGradient(Element gradEl) {
String tag = gradEl.getTagName().toLowerCase();
List<Stop> stops = new ArrayList<>();
NodeList stopNodes = gradEl.getElementsByTagName("stop");
for (int i = 0; i < stopNodes.getLength(); i++) {
org.w3c.dom.Node s = stopNodes.item(i);
if (s instanceof Element stopEl) {
double offset = parseOffset(stopEl.getAttribute("offset"));
String colorStr = stopEl.getAttribute("stop-color");
double opacity = parseDouble(stopEl.getAttribute("stop-opacity"), 1.0);
Color baseColor = parseColor(colorStr, Color.BLACK);
Color finalColor = new Color(baseColor.getRed(), baseColor.getGreen(), baseColor.getBlue(), baseColor.getOpacity() * opacity);
stops.add(new Stop(offset, finalColor));
}
}
if (stops.isEmpty()) {
stops.add(new Stop(0, Color.web("#0288d1")));
stops.add(new Stop(1, Color.web("#01579b")));
}
if ("lineargradient".equals(tag)) {
double x1 = parseCoordinate(gradEl.getAttribute("x1"), 0);
double y1 = parseCoordinate(gradEl.getAttribute("y1"), 0);
double x2 = parseCoordinate(gradEl.getAttribute("x2"), 1);
double y2 = parseCoordinate(gradEl.getAttribute("y2"), 1);
return new LinearGradient(x1, y1, x2, y2, true, CycleMethod.NO_CYCLE, stops);
} else if ("radialgradient".equals(tag)) {
double cx = parseCoordinate(gradEl.getAttribute("cx"), 0.5);
double cy = parseCoordinate(gradEl.getAttribute("cy"), 0.5);
double r = parseCoordinate(gradEl.getAttribute("r"), 0.5);
return new RadialGradient(0, 0, cx, cy, r, true, CycleMethod.NO_CYCLE, stops);
}
return null;
}
private static double parseOffset(String str) {
if (str == null || str.isBlank()) return 0;
String s = str.trim();
try {
if (s.endsWith("%")) {
return Double.parseDouble(s.substring(0, s.length() - 1)) / 100.0;
}
return Double.parseDouble(s);
} catch (NumberFormatException e) {
return 0;
}
}
private static double parseCoordinate(String str, double def) {
if (str == null || str.isBlank()) return def;
String s = str.trim();
try {
if (s.endsWith("%")) {
return Double.parseDouble(s.substring(0, s.length() - 1)) / 100.0;
}
return Double.parseDouble(s);
} catch (NumberFormatException e) {
return def;
}
}
private static void renderChildren(Element parent, Group targetGroup, Map<String, Paint> defsMap) {
NodeList children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
org.w3c.dom.Node n = children.item(i);
if (n instanceof Element el) {
Node jfxNode = createElementNode(el);
if ("defs".equalsIgnoreCase(el.getTagName())) {
continue;
}
Node jfxNode = createElementNode(el, defsMap);
if (jfxNode != null) {
targetGroup.getChildren().add(jfxNode);
}
@@ -291,7 +460,7 @@ public final class AssetManager {
}
}
private static Node createElementNode(Element el) {
private static Node createElementNode(Element el, Map<String, Paint> defsMap) {
String tagName = el.getTagName().toLowerCase();
Shape shape = null;
@@ -350,12 +519,12 @@ public final class AssetManager {
double x = parseDouble(el.getAttribute("x"), 0);
double y = parseDouble(el.getAttribute("y"), 0);
String content = el.getTextContent();
Text t = new Text(x, y, content);
double fs = parseDouble(el.getAttribute("font-size"), 10);
Text t = new Text(x, y, content != null ? content.trim() : "");
double fs = parseDouble(el.getAttribute("font-size"), 12);
String fw = el.getAttribute("font-weight");
t.setFont(Font.font("Segoe UI", "bold".equalsIgnoreCase(fw) ? FontWeight.BOLD : FontWeight.NORMAL, fs));
if (el.hasAttribute("fill")) {
t.setFill(parseColor(el.getAttribute("fill"), Color.BLACK));
t.setFill(parsePaint(el.getAttribute("fill"), defsMap, Color.BLACK));
}
if ("middle".equalsIgnoreCase(el.getAttribute("text-anchor"))) {
t.setTextOrigin(javafx.geometry.VPos.CENTER);
@@ -364,38 +533,40 @@ public final class AssetManager {
}
case "g" -> {
Group g = new Group();
renderChildren(el, g);
renderChildren(el, g, defsMap);
if (el.hasAttribute("stroke")) {
Color strokeCol = parseColor(el.getAttribute("stroke"), null);
Paint strokePaint = parsePaint(el.getAttribute("stroke"), defsMap, null);
double sw = parseDouble(el.getAttribute("stroke-width"), 1.0);
for (Node child : g.getChildren()) {
if (child instanceof Shape s) {
if (strokeCol != null) s.setStroke(strokeCol);
if (strokePaint != null) s.setStroke(strokePaint);
s.setStrokeWidth(sw);
}
}
}
applyElementTransform(el, g);
return g;
}
}
if (shape != null) {
applyShapeStyles(el, shape);
applyShapeStyles(el, shape, defsMap);
applyElementTransform(el, shape);
return shape;
}
return null;
}
private static void applyShapeStyles(Element el, Shape shape) {
private static void applyShapeStyles(Element el, Shape shape, Map<String, Paint> defsMap) {
if (el.hasAttribute("fill")) {
shape.setFill(parseColor(el.getAttribute("fill"), Color.TRANSPARENT));
shape.setFill(parsePaint(el.getAttribute("fill"), defsMap, Color.TRANSPARENT));
} else if (shape instanceof Line) {
shape.setFill(Color.TRANSPARENT);
}
if (el.hasAttribute("stroke")) {
shape.setStroke(parseColor(el.getAttribute("stroke"), Color.TRANSPARENT));
shape.setStroke(parsePaint(el.getAttribute("stroke"), defsMap, Color.TRANSPARENT));
}
if (el.hasAttribute("stroke-width")) {
@@ -409,6 +580,22 @@ public final class AssetManager {
}
}
private static Paint parsePaint(String str, Map<String, Paint> defsMap, Paint fallback) {
if (str == null || str.isBlank() || "none".equalsIgnoreCase(str)) {
return Color.TRANSPARENT;
}
String trimmed = str.trim();
if (trimmed.startsWith("url(#") && trimmed.endsWith(")")) {
String id = trimmed.substring(5, trimmed.length() - 1).trim();
Paint defPaint = defsMap.get(id);
if (defPaint != null) {
return defPaint;
}
return Color.web("#0288d1");
}
return parseColor(trimmed, fallback instanceof Color ? (Color) fallback : Color.BLACK);
}
private static void parsePoints(String ptsStr, java.util.List<Double> pointsList) {
String[] tokens = ptsStr.trim().split("[,\\s]+");
for (String t : tokens) {
@@ -446,4 +633,52 @@ public final class AssetManager {
return defaultValue;
}
}
private static void applyElementTransform(Element el, Node node) {
if (!el.hasAttribute("transform")) return;
String tr = el.getAttribute("transform").trim();
if (tr.isBlank()) return;
try {
if (tr.contains("rotate(")) {
int start = tr.indexOf("rotate(") + 7;
int close = tr.indexOf(')', start);
if (close > 0) {
String[] args = tr.substring(start, close).trim().split("[,\\s]+");
if (args.length >= 3) {
double deg = parseDouble(args[0], 0);
double px = parseDouble(args[1], 0);
double py = parseDouble(args[2], 0);
node.getTransforms().add(new javafx.scene.transform.Rotate(deg, px, py));
} else if (args.length >= 1) {
double deg = parseDouble(args[0], 0);
node.getTransforms().add(new javafx.scene.transform.Rotate(deg));
}
}
}
if (tr.contains("translate(")) {
int start = tr.indexOf("translate(") + 10;
int close = tr.indexOf(')', start);
if (close > 0) {
String[] args = tr.substring(start, close).trim().split("[,\\s]+");
double tx = parseDouble(args[0], 0);
double ty = args.length > 1 ? parseDouble(args[1], 0) : 0;
node.getTransforms().add(new javafx.scene.transform.Translate(tx, ty));
}
}
if (tr.contains("scale(")) {
int start = tr.indexOf("scale(") + 6;
int close = tr.indexOf(')', start);
if (close > 0) {
String[] args = tr.substring(start, close).trim().split("[,\\s]+");
double sx = parseDouble(args[0], 1.0);
double sy = args.length > 1 ? parseDouble(args[1], sx) : sx;
node.getTransforms().add(new javafx.scene.transform.Scale(sx, sy));
}
}
} catch (Exception ignored) {
}
}
}
@@ -709,6 +709,10 @@ public final class SvgIcons {
}
public static Node closeIcon(double size) {
return AssetManager.getIconOrDefault("close", size, () -> buildCloseIcon(size));
}
public static Node buildCloseIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
@@ -738,6 +742,10 @@ public final class SvgIcons {
}
public static Node arrowRightIcon(double size, String colorHex) {
return AssetManager.getIconOrDefault("arrow-right", size, () -> buildArrowRightIcon(size, colorHex));
}
public static Node buildArrowRightIcon(double size, String colorHex) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
@@ -764,6 +772,10 @@ public final class SvgIcons {
* Ícones vetoriais de Zoom In (+), Zoom Out (-) e Ajustar à Tela.
*/
public static Node zoomInIcon(double size) {
return AssetManager.getIconOrDefault("zoom-in", size, () -> buildZoomInIcon(size));
}
public static Node buildZoomInIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
@@ -786,6 +798,10 @@ public final class SvgIcons {
}
public static Node zoomOutIcon(double size) {
return AssetManager.getIconOrDefault("zoom-out", size, () -> buildZoomOutIcon(size));
}
public static Node buildZoomOutIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
@@ -808,6 +824,10 @@ public final class SvgIcons {
}
public static Node zoomFitIcon(double size) {
return AssetManager.getIconOrDefault("zoom-fit", size, () -> buildZoomFitIcon(size));
}
public static Node buildZoomFitIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
@@ -828,4 +848,33 @@ public final class SvgIcons {
box.getChildren().add(path);
return box;
}
/**
* Ícone de Atualização / Sincronização.
*/
public static Node refreshIcon(double size) {
return AssetManager.getIconOrDefault("refresh", size, () -> buildRefreshIcon(size));
}
public static Node buildRefreshIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
box.setMinSize(size, size);
box.setAlignment(Pos.CENTER);
SVGPath path = new SVGPath();
path.setContent("M 20 4 L 20 9 L 15 9 M 4 20 L 4 15 L 9 15 M 5.8 9 A 7 7 0 0 1 18.5 7.5 L 20 9 M 4 15 L 5.5 16.5 A 7 7 0 0 0 18.2 15");
path.setStroke(Color.web("#0284c7"));
path.setStrokeWidth(1.8);
path.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
path.setFill(Color.TRANSPARENT);
double scale = size / 24.0;
path.setScaleX(scale);
path.setScaleY(scale);
box.getChildren().add(path);
return box;
}
}
@@ -1,12 +1,15 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import br.com.cybercore.signer.ui.assets.AssetManager;
import br.com.cybercore.signer.ui.components.SvgIcons;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
/**
* Painel informativo Sobre o CyberCore Signer.
* Painel informativo Sobre o CyberCore Signer com exibição de identidade visual SVG e dados do agente.
*/
@SuppressWarnings("this-escape")
public final class AboutView extends VBox {
@@ -16,13 +19,12 @@ public final class AboutView extends VBox {
}
private void initView() {
setSpacing(16);
setSpacing(14);
setAlignment(Pos.CENTER);
getStyleClass().add("content-pane");
Label brandLabel = new Label(CyberCoreUiConstants.APP_NAME);
brandLabel.getStyleClass().add("brand-title");
brandLabel.setStyle("-fx-font-size: 24px;");
// Logotipo oficial vetorial SVG (app-logo.svg) com fallback para cristal 3D
Node logoNode = AssetManager.getLogoOrDefault(320, 80, () -> SvgIcons.createAppCrystalLogo(54));
Label versionLabel = new Label("Versão " + CyberCoreUiConstants.APP_VERSION + " • CCSP/1");
versionLabel.getStyleClass().add("status-badge-active");
@@ -44,9 +46,9 @@ public final class AboutView extends VBox {
detailsBox.getChildren().add(lbl);
}
Label copyLabel = new Label("© 2026 CyberCore. Todos os direitos reservados.");
Label copyLabel = new Label("© 2026 CyberCore Systems. Todos os direitos reservados.");
copyLabel.getStyleClass().add("dialog-subtitle");
getChildren().addAll(brandLabel, versionLabel, descLabel, detailsBox, copyLabel);
getChildren().addAll(logoNode, versionLabel, descLabel, detailsBox, copyLabel);
}
}
@@ -39,7 +39,10 @@ public class ApplicationApprovalDialog {
root.setPadding(new Insets(24));
root.getStyleClass().add("dialog-container");
// Cabeçalho
// Cabeçalho com Ícone SVG de Escudo
HBox header = new HBox(14);
header.setAlignment(Pos.CENTER_LEFT);
VBox headerBox = new VBox(4);
Label titleLabel = new Label("NOVA APLICAÇÃO WEB SOLICITA ACESSO");
titleLabel.getStyleClass().add("dialog-header");
@@ -48,6 +51,8 @@ public class ApplicationApprovalDialog {
subtitleLabel.getStyleClass().add("dialog-subtitle");
headerBox.getChildren().addAll(titleLabel, subtitleLabel);
header.getChildren().addAll(br.com.cybercore.signer.ui.components.SvgIcons.cyberShieldLogo(34), headerBox);
// Painel de Detalhes da Aplicação
VBox detailsBox = new VBox(10);
detailsBox.getStyleClass().add("dialog-box");
@@ -106,7 +111,7 @@ public class ApplicationApprovalDialog {
actionsRow.getChildren().addAll(btnReject, btnAuthorize);
root.getChildren().addAll(headerBox, detailsBox, secWarning, actionsRow);
root.getChildren().addAll(header, detailsBox, secWarning, actionsRow);
Scene scene = new Scene(root);
ThemeManager.applyTheme(scene);
@@ -77,7 +77,7 @@ public final class CertificatesManagerDialog {
btnAddA1.getStyleClass().add("btn-primary");
btnAddA1.setOnAction(e -> handleImportA1());
Button btnRefresh = new Button("Atualizar Lista");
Button btnRefresh = new Button(" Atualizar Lista", SvgIcons.refreshIcon(14));
btnRefresh.getStyleClass().add("btn-secondary");
btnRefresh.setOnAction(e -> refreshList());
@@ -93,7 +93,7 @@ public final class CertificatesManagerDialog {
HBox footer = new HBox();
footer.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button("Fechar");
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-secondary");
btnClose.setOnAction(e -> stage.close());
footer.getChildren().add(btnClose);
@@ -13,6 +13,7 @@ import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
@@ -204,14 +205,20 @@ public final class MainAppView {
private void updateThemeToggleButton() {
boolean dark = ThemeManager.isDark();
HBox content = new HBox(6);
HBox content = new HBox(8);
content.setAlignment(Pos.CENTER);
Node icon = dark ? SvgIcons.sunIcon(15) : SvgIcons.moonIcon(15);
StackPane iconBox = new StackPane(icon);
iconBox.setPrefSize(18, 18);
iconBox.setMinSize(18, 18);
iconBox.setMaxSize(18, 18);
iconBox.setAlignment(Pos.CENTER);
Label label = new Label(dark ? "Modo Claro" : "Modo Escuro");
label.setStyle("-fx-font-size: 11.5px; -fx-font-weight: bold;");
content.getChildren().addAll(icon, label);
content.getChildren().addAll(iconBox, label);
themeToggleBtn.setGraphic(content);
}
@@ -223,16 +230,22 @@ public final class MainAppView {
btn.setPrefHeight(68);
btn.setMinHeight(68);
HBox content = new HBox(10);
HBox content = new HBox(12);
content.setAlignment(Pos.CENTER_LEFT);
Label lbl = new Label(text);
lbl.getStyleClass().addAll("cyber-action-text", "serpro-action-text");
lbl.setWrapText(true);
lbl.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(lbl, Priority.ALWAYS);
if (icon != null) {
content.getChildren().addAll(icon, lbl);
StackPane iconSlot = new StackPane(icon);
iconSlot.setPrefSize(40, 40);
iconSlot.setMinSize(40, 40);
iconSlot.setMaxSize(40, 40);
iconSlot.setAlignment(Pos.CENTER);
content.getChildren().addAll(iconSlot, lbl);
} else {
content.getChildren().add(lbl);
}
@@ -251,16 +264,22 @@ public final class MainAppView {
btn.setMinHeight(42);
btn.setMaxWidth(Double.MAX_VALUE);
HBox content = new HBox(10);
HBox content = new HBox(12);
content.setAlignment(Pos.CENTER_LEFT);
Label lbl = new Label(text);
lbl.getStyleClass().addAll("cyber-side-text", "serpro-side-text");
lbl.setWrapText(true);
lbl.setWrapText(false);
lbl.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(lbl, Priority.ALWAYS);
if (icon != null) {
content.getChildren().addAll(icon, lbl);
StackPane iconSlot = new StackPane(icon);
iconSlot.setPrefSize(26, 26);
iconSlot.setMinSize(26, 26);
iconSlot.setMaxSize(26, 26);
iconSlot.setAlignment(Pos.CENTER);
content.getChildren().addAll(iconSlot, lbl);
} else {
content.getChildren().add(lbl);
}
@@ -488,7 +488,7 @@ public final class PdfSignDialog {
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;");
btnToggleResize = new Button("📏 Redimensionar");
btnToggleResize = new Button(" Redimensionar", SvgIcons.zoomFitIcon(13));
btnToggleResize.getStyleClass().addAll("btn-secondary", "btn-compact");
btnToggleResize.setTooltip(new Tooltip("Ativar/desativar alça interativa para alterar o tamanho do selo"));
btnToggleResize.setOnAction(e -> toggleResizeMode());
@@ -496,7 +496,7 @@ public final class PdfSignDialog {
lblSealScaleIndicator = new Label("100%");
lblSealScaleIndicator.setStyle("-fx-font-size: 11px; -fx-font-weight: bold; -fx-min-width: 36px; -fx-alignment: center;");
btnResetSealScale = new Button("↺ 100%");
btnResetSealScale = new Button(" 100%", SvgIcons.refreshIcon(12));
btnResetSealScale.getStyleClass().addAll("btn-secondary", "btn-compact");
btnResetSealScale.setTooltip(new Tooltip("Restaurar tamanho padrão original (100%)"));
btnResetSealScale.setVisible(false);
@@ -507,7 +507,7 @@ public final class PdfSignDialog {
Region tbSpacer = new Region();
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;");
toolbar.getChildren().addAll(pageNavBox, zoomBox, resizeToolBox, tbSpacer, lblPositionFeedback);
@@ -5,6 +5,7 @@ import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext;
import br.com.cybercore.signer.core.security.PinSessionManager;
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -60,13 +61,23 @@ public class SignaturePromptDialog {
root.getStyleClass().add("dialog-container");
root.setPrefWidth(540);
Label header = new Label(isAuthentication ? "🔑 SOLICITAÇÃO DE LOGIN DIGITAL" : "SOLICITAÇÃO DE ASSINATURA");
HBox headerBox = new HBox(12);
headerBox.setAlignment(Pos.CENTER_LEFT);
VBox titleBox = new VBox(2);
Label header = new Label(isAuthentication ? "SOLICITAÇÃO DE LOGIN DIGITAL" : "SOLICITAÇÃO DE ASSINATURA");
header.getStyleClass().add("dialog-header");
Label desc = new Label(isAuthentication
? "Uma aplicação web está solicitando autenticação de identidade com seu Certificado Digital ICP-Brasil."
: "Uma aplicação web está solicitando a assinatura digital de um documento.");
desc.getStyleClass().add("brand-subtitle");
titleBox.getChildren().addAll(header, desc);
headerBox.getChildren().addAll(
isAuthentication ? SvgIcons.certificatesIcon() : SvgIcons.cyberShieldLogo(26),
titleBox
);
// Caixa de Detalhes
VBox detailsBox = new VBox(12);
@@ -231,7 +242,7 @@ public class SignaturePromptDialog {
actionsRow.getChildren().addAll(btnCancel, btnConfirm);
root.getChildren().addAll(header, desc, detailsBox, validationLabel, actionsRow);
root.getChildren().addAll(headerBox, detailsBox, validationLabel, actionsRow);
Scene scene = new Scene(root);
ThemeManager.applyTheme(scene);
@@ -1,6 +1,6 @@
# Pasta de Ícones (SVG) e Imagens (PNG) — CyberCore Signer
# Recursos Visuais (Ícones e Imagens em SVG) — CyberCore Signer
Esta pasta é o repositório oficial onde você pode visualizar, customizar e substituir livremente os **ícones em SVG** e as **imagens em PNG** do **CyberCore Signer**.
Esta pasta é o repositório oficial onde você pode visualizar, customizar e substituir livremente todos os **ícones em SVG** e as **imagens em SVG** do **CyberCore Signer**.
---
@@ -31,13 +31,23 @@ assets/
│ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais
│ ├── check.svg # Ícone Checkmark de sucesso e validação
│ ├── error.svg # Ícone de Erro / Alerta em vermelho
│ └── star.svg # Ícone de Estrela notarial / favorita
│ ├── star.svg # Ícone de Estrela notarial / favorita
│ ├── close.svg # Ícone Fechar / Cancelar (cruz geométrica moderna)
│ ├── arrow-right.svg # Ícone Avançar / Seta direcional
│ ├── zoom-in.svg # Ícone Zoom In (+)
│ ├── zoom-out.svg # Ícone Zoom Out (-)
│ ├── zoom-fit.svg # Ícone Ajustar à Tela
│ └── refresh.svg # Ícone Atualizar / Recarregar listas e status
│
└── images/ # Imagens de alta fidelidade em PNG com canal alfa (transparência)
├── app-icon.png # Ícone oficial do app (256x256 px - Diamante 3D azul Serpro)
├── app-logo.png # Logotipo horizontal oficial (480x120 px - CyberCore SIGNER)
├── brand-banner.png # Banner de cabeçalho e marca (600x160 px - Serpro Blue)
└── default-seal.png # Selo de assinatura padrão ICP-Brasil (640x164 px)
└── images/ # Imagens vetoriais SVG editáveis (com cópias PNG de fallback)
├── app-icon.svg # Ícone oficial do app em SVG (Cristal/Diamante 3D azul)
├── app-icon.png # Fallback em PNG (256x256 px)
├── app-logo.svg # Logotipo horizontal oficial em SVG (CyberCore SIGNER)
├── app-logo.png # Fallback em PNG (480x120 px)
├── brand-banner.svg # Banner de cabeçalho e marca em SVG (600x160 px)
├── brand-banner.png # Fallback em PNG (600x160 px)
├── default-seal.svg # Selo de assinatura padrão ICP-Brasil em SVG (640x164 px)
└── default-seal.png # Fallback em PNG (640x164 px)
```
---
@@ -45,20 +55,21 @@ assets/
## 🎨 Como Customizar Qualquer Ícone ou Imagem
1. **Ícones:**
- Estão no formato `.svg` padrão.
- Podem ser editados diretamente no seu editor de código (VS Code, etc.) ou em editores vetoriais como **Inkscape, Figma ou Illustrator**.
- Se preferir colocar uma imagem bitmap para um ícone específico, você também pode colocar um `.png` (ex: `pdf.png`). O sistema prioriza `.svg` e aceita `.png` com fallback transparente.
- Todos estão no formato `.svg` padrão da W3C.
- Podem ser abertos e editados diretamente em editores de texto/código (VS Code) ou ferramentas vetoriais como **Inkscape, Figma, Adobe Illustrator**.
- Se preferir usar um arquivo bitmap para algum ícone específico, basta salvar um `.png` correspondente na pasta (ex: `pdf.png`). O sistema detecta e renderiza automaticamente.
2. **Imagens do Sistema:**
- Estão no formato `.png` com canal alfa de 32 bits (fundo transparente).
- Você pode substituir `app-logo.png`, `app-icon.png`, `brand-banner.png` ou `default-seal.png` pelo logotipo e identidade visual da sua instituição ou empresa.
- Tanto o logotipo horizontal (`app-logo.svg`), o ícone do sistema (`app-icon.svg`), o banner institucional (`brand-banner.svg`) e o selo notarial padrão (`default-seal.svg`) agora são arquivos **SVG vetoriais editáveis**.
- Você pode alterar cores, tipografias, brasões e dimensões diretamente nos arquivos SVG.
- O `AssetManager` renderiza os arquivos SVG preservando fidelidade geométrica e suportando gradientes e alinhamentos.
3. **Ordem de Prioridade e Hot-Reloading:**
Quando a aplicação busca um ícone ou imagem, a resolução ocorre na ordem:
1. **Pasta Física Local (`./assets/icons/` e `./assets/images/`)**: Qualquer alteração feita aqui entra em vigor imediatamente ao abrir a janela ou reiniciar a aplicação, sem necessidade de recompilar!
2. **Pasta do Usuário (`~/.cybercore/assets/`)**: Configurações específicas por usuário do sistema operacional.
3. **Resources Empacotados no JAR (`/assets/`)**: Cópia empacotada internamente no `.jar`.
4. **Fallback Vetorial Nativo (`SvgIcons`)**: Garantia de que a interface nunca fique em branco caso algum arquivo seja excluído.
3. **Ordem de Resolução e Carregamento:**
Ao requisitar qualquer elemento gráfico, a aplicação segue a ordem de prioridade:
1. **Pasta Física Local (`./assets/icons/` e `./assets/images/`)**: Arquivos `.svg` editados aqui têm prioridade máxima e entram em vigor imediatamente sem necessidade de recompilar.
2. **Pasta do Usuário (`~/.cybercore/assets/`)**: Para customizações no diretório pessoal do usuário.
3. **Recursos Empacotados no JAR (`/assets/`)**: SVGs e imagens internas empacotadas no pacote de distribuição.
4. **Fallback Vetorial Nativo (`SvgIcons`)**: Garantia de que a interface nunca fique desprovida de grafismos caso algum arquivo seja removido.
---
@@ -1,5 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<circle cx="24" cy="24" r="20" fill="#0288d1"/>
<circle cx="24" cy="14" r="3" fill="#ffffff"/>
<line x1="24" y1="21" x2="24" y2="34" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round"/>
</svg>
<!-- Círculo azul e letra 'i' informativa centrada em (24, 24) -->
<circle cx="24" cy="24" r="18" fill="#0288d1"/>
<circle cx="24" cy="15" r="2.5" fill="#ffffff"/>
<line x1="24" y1="21" x2="24" y2="33" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 291 B

After

Width:  |  Height:  |  Size: 362 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M 8 5 L 15 12 L 8 19" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 221 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M 6 6 L 18 18 M 18 6 L 6 18" fill="none" stroke="#64748b" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 228 B

@@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Lua crescente ciano/índigo -->
<path d="M 40 26.5 A 18 18 0 1 1 21.5 8 A 14 14 0 0 0 40 26.5 Z" fill="#38bdf8"/>
<circle cx="36" cy="12" r="1.5" fill="#818cf8"/>
<circle cx="42" cy="18" r="1" fill="#818cf8"/>
<!-- Lua crescente ciano/índigo perfeitamente centrada em (24, 24) -->
<path d="M 24 8 A 16 16 0 1 0 40 24 A 14 14 0 0 1 24 8 Z" fill="#38bdf8"/>
<circle cx="34" cy="14" r="1.8" fill="#818cf8"/>
<circle cx="38" cy="22" r="1.2" fill="#818cf8"/>
</svg>

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 344 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M 20 4 L 20 9 L 15 9" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M 4 20 L 4 15 L 9 15" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M 5.8 9 A 7 7 0 0 1 18.5 7.5 L 20 9 M 4 15 L 5.5 16.5 A 7 7 0 0 0 18.2 15" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 510 B

@@ -1,8 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Engrenagem simétrica de 8 dentes centrada em (24, 24) -->
<path d="M 21 4 L 27 4 L 28 10 C 30 10.8 31.8 11.8 33.4 13.2 L 39 10 L 43 14 L 39.8 19.6 C 41.2 21.2 42.2 23 43 25 L 49 26 L 49 32 L 43 33 C 42.2 35 41.2 36.8 39.8 38.4 L 43 44 L 39 48 L 33.4 44.8 C 31.8 46.2 30 47.2 28 48 L 27 54 L 21 54 L 20 48 C 18 47.2 16.2 46.2 14.6 44.8 L 9 48 L 5 44 L 8.2 38.4 C 6.8 36.8 5.8 35 5 33 L -1 32 L -1 26 L 5 25 C 5.8 23 6.8 21.2 8.2 19.6 L 5 14 L 9 10 L 14.6 13.2 C 16.2 11.8 18 10.8 20 10 Z" transform="scale(0.8) translate(6, 4)" fill="#0288d1"/>
<!-- Círculo externo da engrenagem -->
<circle cx="24" cy="24" r="14" fill="#0288d1"/>
<!-- Furo central vazado -->
<circle cx="24" cy="24" r="6" fill="#ffffff"/>
<!-- Engrenagem corporativa simétrica centrada em (24, 24) -->
<path fill="#0288d1" fill-rule="evenodd" d="M 21.5 5 L 26.5 5 L 27.2 10.2 C 29 10.9 30.7 11.9 32.2 13.1 L 36.6 10.6 L 40.1 14.1 L 37.6 18.5 C 38.8 20 39.8 21.7 40.5 23.5 L 45.7 24.2 L 45.7 29.2 L 40.5 29.9 C 39.8 31.7 38.8 33.4 37.6 34.9 L 40.1 39.3 L 36.6 42.8 L 32.2 40.3 C 30.7 41.5 29 42.5 27.2 43.2 L 26.5 48.4 L 21.5 48.4 L 20.8 43.2 C 19 42.5 17.3 41.5 15.8 40.3 L 11.4 42.8 L 7.9 39.3 L 10.4 34.9 C 9.2 33.4 8.2 31.7 7.5 29.9 L 2.3 29.2 L 2.3 24.2 L 7.5 23.5 C 8.2 21.7 9.2 20 10.4 18.5 L 7.9 14.1 L 11.4 10.6 L 15.8 13.1 C 17.3 11.9 19 10.9 20.8 10.2 Z M 24 17.5 A 6.5 6.5 0 1 0 24 30.5 A 6.5 6.5 0 1 0 24 17.5 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 817 B

After

Width:  |  Height:  |  Size: 784 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M 4 9 L 4 4 L 9 4 M 15 4 L 20 4 L 20 9 M 20 15 L 20 20 L 15 20 M 9 20 L 4 20 L 4 15" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 284 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<circle cx="10" cy="10" r="7" fill="none" stroke="#0284c7" stroke-width="2.2"/>
<path d="M 15 15 L 21 21" fill="none" stroke="#0284c7" stroke-width="2.5" stroke-linecap="round"/>
<path d="M 10 7 L 10 13 M 7 10 L 13 10" fill="none" stroke="#0284c7" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 387 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<circle cx="10" cy="10" r="7" fill="none" stroke="#0284c7" stroke-width="2.2"/>
<path d="M 15 15 L 21 21" fill="none" stroke="#0284c7" stroke-width="2.5" stroke-linecap="round"/>
<path d="M 7 10 L 13 10" fill="none" stroke="#0284c7" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 372 B

@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<defs>
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1976d2"/>
<stop offset="45%" stop-color="#0d47a1"/>
<stop offset="100%" stop-color="#01579b"/>
</linearGradient>
<linearGradient id="topFacetGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.9"/>
<stop offset="100%" stop-color="#80d8ff" stop-opacity="0.55"/>
</linearGradient>
<linearGradient id="leftFacetGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#40c4ff" stop-opacity="0.75"/>
<stop offset="100%" stop-color="#0288d1" stop-opacity="0.35"/>
</linearGradient>
<linearGradient id="rightFacetGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#e1f5fe" stop-opacity="0.85"/>
<stop offset="100%" stop-color="#29b6f6" stop-opacity="0.45"/>
</linearGradient>
<radialGradient id="centerGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.95"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Fundo com cantos arredondados -->
<rect x="8" y="8" width="240" height="240" rx="68" ry="68" fill="url(#bgGrad)"/>
<rect x="10" y="10" width="236" height="236" rx="66" ry="66" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-opacity="0.25"/>
<!-- Facetas 3D do Cristal / Diamante Azul -->
<polygon points="128,46 210,113 128,143 46,113" fill="url(#topFacetGrad)"/>
<polygon points="46,113 128,143 128,215 72,179" fill="url(#leftFacetGrad)"/>
<polygon points="210,113 128,143 128,215 184,179" fill="url(#rightFacetGrad)"/>
<!-- Brilho estelar central -->
<circle cx="128" cy="128" r="38" fill="url(#centerGlow)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 120" width="480" height="120">
<defs>
<linearGradient id="logoIconBg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1976d2"/>
<stop offset="50%" stop-color="#0d47a1"/>
<stop offset="100%" stop-color="#01579b"/>
</linearGradient>
<linearGradient id="logoTopFacet" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.95"/>
<stop offset="100%" stop-color="#80d8ff" stop-opacity="0.6"/>
</linearGradient>
<linearGradient id="logoLeftFacet" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#40c4ff" stop-opacity="0.8"/>
<stop offset="100%" stop-color="#0288d1" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="logoRightFacet" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#e1f5fe" stop-opacity="0.9"/>
<stop offset="100%" stop-color="#29b6f6" stop-opacity="0.5"/>
</linearGradient>
</defs>
<!-- Container do Ícone Cristal 3D -->
<rect x="14" y="16" width="88" height="88" rx="24" ry="24" fill="url(#logoIconBg)"/>
<rect x="16" y="18" width="84" height="84" rx="22" ry="22" fill="none" stroke="#ffffff" stroke-width="1.5" stroke-opacity="0.25"/>
<!-- Facetas Cristalinas (cx=58, cy=60, sz=63) -->
<polygon points="58,32 86,55 58,66 30,55" fill="url(#logoTopFacet)"/>
<polygon points="30,55 58,66 58,92 39,79" fill="url(#logoLeftFacet)"/>
<polygon points="86,55 58,66 58,92 77,79" fill="url(#logoRightFacet)"/>
<circle cx="58" cy="60" r="14" fill="#ffffff" fill-opacity="0.3"/>
<!-- Tipografia Oficial "CyberCore" -->
<text x="124" y="62" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="42" font-weight="bold" fill="#0a2540">Cyber<tspan fill="#0288d1">Core</tspan></text>
<!-- Badge "SIGNER" -->
<rect x="124" y="76" width="94" height="22" rx="6" ry="6" fill="#0288d1"/>
<text x="139" y="92" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" fill="#ffffff" letter-spacing="1">SIGNER</text>
<!-- Subtítulo ICP-Brasil -->
<text x="230" y="92" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="normal" fill="#64748b">ICP-Brasil • Assinador Digital Seguro</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 160" width="600" height="160">
<defs>
<linearGradient id="bannerBgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0d47a1"/>
<stop offset="60%" stop-color="#0284c7"/>
<stop offset="100%" stop-color="#0369a1"/>
</linearGradient>
<linearGradient id="bannerIconGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1e88e5"/>
<stop offset="100%" stop-color="#01579b"/>
</linearGradient>
</defs>
<!-- Fundo do Banner -->
<rect x="0" y="0" width="600" height="160" rx="16" ry="16" fill="url(#bannerBgGrad)"/>
<!-- Formas geométricas de segurança no fundo -->
<circle cx="480" cy="50" r="110" fill="#ffffff" fill-opacity="0.08"/>
<circle cx="540" cy="120" r="80" fill="#ffffff" fill-opacity="0.06"/>
<circle cx="380" cy="-20" r="90" fill="#ffffff" fill-opacity="0.04"/>
<!-- Ícone Cristal à esquerda -->
<rect x="30" y="35" width="90" height="90" rx="22" ry="22" fill="url(#bannerIconGrad)"/>
<rect x="33" y="38" width="84" height="84" rx="19" ry="19" fill="none" stroke="#ffffff" stroke-width="2" stroke-opacity="0.3"/>
<polygon points="75,52 101,80 75,108 49,80" fill="#ffffff" fill-opacity="0.9"/>
<polygon points="75,52 75,108 49,80" fill="#80d8ff" fill-opacity="0.5"/>
<!-- Textos do Banner -->
<text x="148" y="74" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="34" font-weight="bold" fill="#ffffff">CyberCore Signer</text>
<text x="148" y="104" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="16" font-weight="normal" fill="#e0f2fe">Agente de Assinatura Digital ICP-Brasil A1 &amp; A3</text>
<text x="148" y="128" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="bold" fill="#38bdf8" letter-spacing="1">PADES • XADES • CADES • TIMESTAMPS • CCSP/1</text>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 164" width="640" height="164">
<defs>
<linearGradient id="badgeGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#0288d1"/>
<stop offset="100%" stop-color="#01579b"/>
</linearGradient>
</defs>
<!-- Fundo do Selo -->
<rect x="2" y="2" width="636" height="160" rx="12" ry="12" fill="#f8fafc" stroke="#0288d1" stroke-width="2.5"/>
<!-- Brasão Notarial no lado esquerdo -->
<rect x="18" y="16" width="130" height="132" rx="10" ry="10" fill="url(#badgeGrad)"/>
<!-- Textos do Brasão -->
<text x="83" y="44" font-family="'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="#ffffff" text-anchor="middle" letter-spacing="0.5">ASSINADO</text>
<text x="83" y="60" font-family="'Segoe UI', Roboto, sans-serif" font-size="11" font-weight="bold" fill="#ffffff" text-anchor="middle" letter-spacing="0.5">DIGITALMENTE</text>
<!-- Estrela dourada -->
<circle cx="83" cy="85" r="14" fill="#fbbf24"/>
<text x="83" y="91" font-family="'Segoe UI', sans-serif" font-size="16" font-weight="bold" fill="#ffffff" text-anchor="middle">★</text>
<text x="83" y="126" font-family="'Segoe UI', Roboto, sans-serif" font-size="10" font-weight="bold" fill="#e0f2fe" text-anchor="middle" letter-spacing="1">ICP-BRASIL</text>
<!-- Linha divisória vertical -->
<line x1="166" y1="16" x2="166" y2="148" stroke="#cfd8dc" stroke-width="1.5"/>
<!-- Informações da Assinatura -->
<text x="184" y="36" font-family="'Segoe UI', Roboto, sans-serif" font-size="11" font-weight="bold" fill="#64748b" letter-spacing="0.5">DOCUMENTO ASSINADO DIGITALMENTE POR:</text>
<text x="184" y="60" font-family="'Segoe UI', Roboto, sans-serif" font-size="17" font-weight="bold" fill="#0a2540">NOME DO TITULAR DO CERTIFICADO</text>
<text x="184" y="82" font-family="'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="normal" fill="#334155">CPF: ***.000.000-**</text>
<text x="184" y="102" font-family="'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="normal" fill="#334155">Data e Hora: 11/10/2026 12:00:00 -03:00</text>
<text x="184" y="128" font-family="'Segoe UI', Roboto, sans-serif" font-size="11" font-style="italic" fill="#0288d1">A conformidade com a ICP-Brasil pode ser verificada no ITI.</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,58 @@
package br.com.cybercore.signer.ui.assets;
import br.com.cybercore.signer.ui.components.SvgIcons;
import javafx.scene.Node;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
import static org.assertj.core.api.Assertions.assertThat;
class AssetManagerTest {
@Test
@DisplayName("Deve carregar imagens institucionais em SVG do classpath ou pasta de assets")
void shouldLoadSvgImages() {
Node logo = AssetManager.getImageOrDefault("app-logo", 480, 120, null);
assertThat(logo).isNotNull();
Node banner = AssetManager.getImageOrDefault("brand-banner", 600, 160, null);
assertThat(banner).isNotNull();
Node icon = AssetManager.getImageOrDefault("app-icon", 256, 256, null);
assertThat(icon).isNotNull();
Node seal = AssetManager.getImageOrDefault("default-seal", 640, 164, null);
assertThat(seal).isNotNull();
}
@Test
@DisplayName("Deve carregar ícones SVG dinâmicos através do AssetManager")
void shouldLoadSvgIcons() {
String[] iconNames = {
"pdf", "xml", "file", "batch", "attached", "co-sign",
"timestamp", "ltv", "validate", "validate-timestamp", "exit",
"logs", "settings", "tutorial", "cert", "about", "sun", "moon",
"shield", "seal", "check", "error", "star", "close", "arrow-right",
"zoom-in", "zoom-out", "zoom-fit", "refresh"
};
for (String name : iconNames) {
Node iconNode = AssetManager.getIconOrDefault(name, 24, null);
assertThat(iconNode).withFailMessage("Ícone SVG não encontrado ou nulo: %s", name).isNotNull();
}
}
@Test
@DisplayName("Deve instanciar nós de ícones via fábrica SvgIcons com fallback")
void shouldCreateNodesViaSvgIcons() {
assertThat(SvgIcons.closeIcon(16)).isNotNull();
assertThat(SvgIcons.arrowRightIcon(16)).isNotNull();
assertThat(SvgIcons.zoomInIcon(16)).isNotNull();
assertThat(SvgIcons.zoomOutIcon(16)).isNotNull();
assertThat(SvgIcons.zoomFitIcon(16)).isNotNull();
assertThat(SvgIcons.refreshIcon(16)).isNotNull();
assertThat(SvgIcons.createAppCrystalLogo(48)).isNotNull();
}
}