/**
* CyberCore Signer - PdfSealDesigner
* Ferramenta visual interativa para criação, personalização e exportação
* de temas de selos de assinatura digital PAdES (ICP-Brasil).
*/
function escapeSvgText(str) {
if (str === null || str === undefined) return '';
return String(str)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function sanitizeLogoUrl(url) {
if (!url || typeof url !== 'string') return '';
const trimmed = url.trim();
if (/^data:image\/(png|jpeg|jpg|svg\+xml);base64,[A-Za-z0-9+/=]+$/i.test(trimmed)) {
return trimmed;
}
return '';
}
export class PdfSealDesigner {
/**
* @param {Object} options Configurações iniciais do designer.
* @param {HTMLElement|string} options.container Elemento onde renderizar o designer.
* @param {Object} [options.initialTheme] Configuração inicial de tema.
* @param {Function} [options.onChange] Callback chamado quando o tema for modificado: fn(theme).
*/
constructor(options = {}) {
this.container = typeof options.container === 'string'
? document.querySelector(options.container)
: options.container;
if (!this.container) {
throw new Error('Container DOM obrigatório para instanciar PdfSealDesigner.');
}
this.onChange = options.onChange || options.onConfigChange || null;
const rawInitial = options.initialTheme || options.initialConfig || {};
const normalizedInitial = {
name: rawInitial.name,
description: rawInitial.description,
width: rawInitial.width,
height: rawInitial.height,
primaryColor: rawInitial.primaryColor || rawInitial.primary_color,
secondaryColor: rawInitial.secondaryColor || rawInitial.secondary_color,
backgroundColor: rawInitial.backgroundColor || rawInitial.background_color,
borderColor: rawInitial.borderColor || rawInitial.border_color,
textColor: rawInitial.textColor || rawInitial.text_color,
iconStyle: rawInitial.iconStyle || rawInitial.icon_style,
customLogoBase64: rawInitial.customLogoBase64 || rawInitial.custom_logo_base64,
customHeader: rawInitial.customHeader || rawInitial.custom_header || rawInitial.title,
customFooter: rawInitial.customFooter || rawInitial.custom_footer,
customFieldLabel: rawInitial.customFieldLabel || rawInitial.custom_field_label,
customFieldValue: rawInitial.customFieldValue || rawInitial.custom_field_value,
defaultCpfMode: rawInitial.defaultCpfMode || rawInitial.default_cpf_mode,
defaultIncludeDateTime: rawInitial.defaultIncludeDateTime !== undefined ? rawInitial.defaultIncludeDateTime : rawInitial.default_include_date_time
};
// Remove chaves indefinidas para não sobrescrever os defaults
Object.keys(normalizedInitial).forEach(k => normalizedInitial[k] === undefined && delete normalizedInitial[k]);
// Estado do tema
this.theme = {
name: 'Meu Selo Personalizado',
description: 'Selo visual corporativo customizado via CyberCore Web SDK',
width: 320,
height: 82,
primaryColor: '#0d47a1',
secondaryColor: '#0284c7',
backgroundColor: '#ffffff',
borderColor: '#93c5fd',
textColor: '#0f172a',
iconStyle: 'shield',
customLogoBase64: null,
customHeader: 'DOCUMENTO ASSINADO DIGITALMENTE',
customFooter: 'Verifique a autenticidade da assinatura eletrônica em https://validar.iti.gov.br',
customFieldLabel: '',
customFieldValue: '',
defaultCpfMode: 'MASKED',
defaultIncludeDateTime: true,
...normalizedInitial
};
this._initDom();
this._attachEvents();
this._renderPreview();
}
_initDom() {
this.container.innerHTML = `
👁️ Pré-visualização em Alta Resolução (320x82 pt)
Renderização visual idêntica à estampada no PDFBox do agente desktop.
`;
this._injectStyles();
// Cache de elementos
this.root = this.container.querySelector('.cyber-designer-root');
this.previewBox = this.root.querySelector('.seal-live-preview-box');
this.jsonBox = this.root.querySelector('.theme-json-box code');
this.btnRemoveLogo = this.root.querySelector('.btn-remove-logo');
}
_injectStyles() {
if (document.getElementById('cyber-designer-styles')) return;
const style = document.createElement('style');
style.id = 'cyber-designer-styles';
style.textContent = `
.cyber-designer-root {
background: #0f172a;
border: 1px solid #24304f;
border-radius: 12px;
padding: 20px;
color: #f8fafc;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
max-width: 100%;
box-sizing: border-box;
overflow: hidden;
}
.designer-header {
margin-bottom: 20px;
border-bottom: 1px solid #1e293b;
padding-bottom: 12px;
}
.designer-header h3 {
font-size: 16px;
font-weight: 700;
color: #38bdf8;
}
.designer-header p {
font-size: 12px;
color: #94a3b8;
margin-top: 4px;
}
.designer-body {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 24px;
min-width: 0;
}
@media (max-width: 860px) {
.designer-body {
grid-template-columns: minmax(0, 1fr);
}
}
.form-section-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #38bdf8;
margin: 14px 0 8px 0;
border-top: 1px solid #1e293b;
padding-top: 10px;
}
.form-row {
margin-bottom: 10px;
display: flex;
flex-direction: column;
gap: 4px;
}
.form-row label {
font-size: 11px;
color: #cbd5e1;
}
.form-row input[type="text"], .form-row select {
background: #090d16;
border: 1px solid #24304f;
border-radius: 6px;
padding: 7px 10px;
color: #f8fafc;
font-size: 12px;
}
.form-row input[type="text"]:focus, .form-row select:focus {
border-color: #38bdf8;
outline: none;
}
.form-row-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.colors-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.color-picker-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.color-picker-item label {
font-size: 10px;
color: #94a3b8;
}
.color-input-wrap {
display: flex;
align-items: center;
gap: 6px;
background: #090d16;
border: 1px solid #24304f;
border-radius: 6px;
padding: 3px 6px;
}
.color-input-wrap input[type="color"] {
border: none;
width: 22px;
height: 22px;
background: transparent;
cursor: pointer;
}
.color-hex-text {
font-size: 10px;
font-family: monospace;
color: #cbd5e1;
}
.icon-selection-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.radio-icon-item {
display: flex;
align-items: center;
gap: 6px;
background: #090d16;
border: 1px solid #24304f;
border-radius: 6px;
padding: 6px 10px;
font-size: 11px;
cursor: pointer;
}
.logo-upload-row .logo-actions {
display: flex;
align-items: center;
gap: 8px;
}
.btn-designer-upload {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
background: #1e293b;
border: 1px solid #38bdf8;
border-radius: 6px;
color: #38bdf8;
font-size: 11px;
cursor: pointer;
}
.btn-remove-logo {
background: transparent;
border: 1px solid #f87171;
color: #f87171;
padding: 4px 8px;
border-radius: 6px;
font-size: 11px;
cursor: pointer;
}
.checkbox-row {
justify-content: flex-end;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
cursor: pointer;
margin-top: 18px;
}
.designer-preview-column {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
max-width: 100%;
}
.preview-card, .json-export-card {
background: #131c31;
border: 1px solid #24304f;
border-radius: 10px;
padding: 16px;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
}
.preview-card h4, .json-export-card h4 {
font-size: 12px;
font-weight: 700;
color: #cbd5e1;
margin-bottom: 12px;
}
.seal-live-preview-box {
background: #ffffff;
border-radius: 6px;
padding: 16px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
max-width: 340px;
margin: 0 auto;
box-sizing: border-box;
overflow: hidden;
}
.seal-live-preview-box svg {
max-width: 100%;
width: 100%;
height: auto;
display: block;
}
.preview-note {
display: block;
font-size: 10px;
color: #64748b;
margin-top: 8px;
text-align: center;
}
.json-export-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.btn-copy-theme-json {
background: #1e293b;
border: 1px solid #24304f;
color: #38bdf8;
padding: 3px 8px;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
}
.btn-copy-theme-json:hover {
background: #38bdf8;
color: #0f172a;
}
.theme-json-box {
background: #090d16;
border: 1px solid #1e293b;
border-radius: 6px;
padding: 10px;
font-family: 'JetBrains Mono', monospace;
font-size: 10.5px;
color: #38bdf8;
max-height: 180px;
overflow-y: auto;
overflow-x: hidden;
margin: 0;
white-space: pre-wrap;
word-break: break-all;
word-wrap: break-word;
max-width: 100%;
min-width: 0;
box-sizing: border-box;
}
.theme-json-box code {
white-space: pre-wrap;
word-break: break-all;
word-wrap: break-word;
display: block;
max-width: 100%;
}
`;
document.head.appendChild(style);
}
_attachEvents() {
const bindInput = (selector, key, formatFn = v => v) => {
this.root.querySelector(selector).addEventListener('input', e => {
this.theme[key] = formatFn(e.target.value);
this._onStateChange();
});
};
bindInput('.input-theme-name', 'name');
bindInput('.input-field-label', 'customFieldLabel');
bindInput('.input-field-value', 'customFieldValue');
// Cores
const bindColor = (selector, key, labelSelector) => {
const el = this.root.querySelector(selector);
const label = this.root.querySelector(labelSelector);
el.addEventListener('input', e => {
this.theme[key] = e.target.value;
label.textContent = e.target.value;
this._onStateChange();
});
};
bindColor('.color-primary', 'primaryColor', '.color-primary + .color-hex-text');
bindColor('.color-secondary', 'secondaryColor', '.color-secondary + .color-hex-text');
bindColor('.color-bg', 'backgroundColor', '.color-bg + .color-hex-text');
bindColor('.color-border', 'borderColor', '.color-border + .color-hex-text');
bindColor('.color-text', 'textColor', '.color-text + .color-hex-text');
// Ícones
this.root.querySelectorAll('input[name="iconStyle"]').forEach(radio => {
radio.addEventListener('change', e => {
if (e.target.checked) {
this.theme.iconStyle = e.target.value;
this._onStateChange();
}
});
});
// Upload de logo
const logoInput = this.root.querySelector('.designer-logo-file');
logoInput.addEventListener('change', async e => {
if (e.target.files.length) {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = ev => {
this.theme.customLogoBase64 = ev.target.result;
this.btnRemoveLogo.style.display = 'inline-block';
this._onStateChange();
};
reader.readAsDataURL(file);
}
});
this.btnRemoveLogo.addEventListener('click', () => {
this.theme.customLogoBase64 = null;
this.btnRemoveLogo.style.display = 'none';
logoInput.value = '';
this._onStateChange();
});
// LGPD CPF
this.root.querySelector('.select-cpf-mode').addEventListener('change', e => {
this.theme.defaultCpfMode = e.target.value;
this._onStateChange();
});
// Data e hora
this.root.querySelector('.chk-date-time').addEventListener('change', e => {
this.theme.defaultIncludeDateTime = e.target.checked;
this._onStateChange();
});
// Copiar JSON
this.root.querySelector('.btn-copy-theme-json').addEventListener('click', () => {
const config = this.getThemeConfig();
navigator.clipboard.writeText(JSON.stringify(config, null, 2));
const btn = this.root.querySelector('.btn-copy-theme-json');
btn.textContent = '✓ Copiado!';
setTimeout(() => (btn.textContent = 'Copiar JSON'), 1500);
});
}
_onStateChange() {
this._renderPreview();
if (this.onChange) {
this.onChange(this.getThemeConfig());
}
}
_renderPreview() {
const t = this.theme;
// CPF conforme a escolha
let docText = 'CPF: ***.456.789-**';
if (t.defaultCpfMode === 'FULL') docText = 'CPF: 123.456.789-00';
else if (t.defaultCpfMode === 'NONE') docText = '';
const dateText = t.defaultIncludeDateTime ? '11/10/2026 18:30:00' : '';
let extraText = '';
if (t.customFieldLabel || t.customFieldValue) {
const label = t.customFieldLabel ? `${t.customFieldLabel}: ` : '';
const val = t.customFieldValue || '';
extraText = `${label}${val}`.trim();
}
let iconGraphic = '';
const safeLogo = sanitizeLogoUrl(t.customLogoBase64);
if (safeLogo) {
iconGraphic = ``;
} else {
switch (t.iconStyle) {
case 'star':
iconGraphic = `
`;
break;
case 'crystal':
iconGraphic = `
`;
break;
case 'notary':
iconGraphic = `
`;
break;
case 'shield':
default:
iconGraphic = `
`;
break;
}
}
const svg = `
`;
this.previewBox.innerHTML = svg;
// Na visualização do código, trunca se o Base64 for gigante para preservar performance da UI
const displayConfig = { ...this.getThemeConfig() };
if (displayConfig.customLogoBase64 && displayConfig.customLogoBase64.length > 100) {
const lenKb = (displayConfig.customLogoBase64.length / 1024).toFixed(1);
displayConfig.customLogoBase64 = `${displayConfig.customLogoBase64.substring(0, 32)}... [${lenKb} KB]`;
}
this.jsonBox.textContent = JSON.stringify(displayConfig, null, 2);
}
/**
* Retorna a configuração do tema pronto para uso no payload seal_custom de assinatura.
*/
getThemeConfig() {
return {
name: this.theme.name,
description: this.theme.description,
width: this.theme.width,
height: this.theme.height,
primaryColor: this.theme.primaryColor,
secondaryColor: this.theme.secondaryColor,
backgroundColor: this.theme.backgroundColor,
borderColor: this.theme.borderColor,
textColor: this.theme.textColor,
iconStyle: this.theme.iconStyle,
customLogoBase64: this.theme.customLogoBase64,
customHeader: this.theme.customHeader,
customFooter: this.theme.customFooter,
customFieldLabel: this.theme.customFieldLabel,
customFieldValue: this.theme.customFieldValue,
defaultCpfMode: this.theme.defaultCpfMode,
defaultIncludeDateTime: this.theme.defaultIncludeDateTime
};
}
/**
* Alias conveniente para getThemeConfig().
*/
getConfig() {
return this.getThemeConfig();
}
/**
* Carrega um tema para edição.
*/
loadTheme(theme) {
this.theme = { ...this.theme, ...theme };
this._initDom();
this._attachEvents();
this._renderPreview();
}
}
export default PdfSealDesigner;