feat(customizer): add pro visual presets, social styles, and responsive map controls
This commit is contained in:
877
demo/templates/customizer_webflow.html
Normal file
877
demo/templates/customizer_webflow.html
Normal file
@@ -0,0 +1,877 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Customizer New - {{ site_name }}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Oswald:wght@400;600;700&family=Source+Serif+4:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #e4dccb;
|
||||
--bg-2: #f3ecde;
|
||||
--panel: #fff8eb;
|
||||
--panel-2: #f2e7cd;
|
||||
--canvas: #ece3d1;
|
||||
--text: #1f1b17;
|
||||
--muted: #6e6458;
|
||||
--accent: #9b2f16;
|
||||
--accent-2: #2d5e4c;
|
||||
--border: rgba(31, 27, 23, 0.14);
|
||||
--shadow-soft: 0 12px 28px rgba(31, 27, 23, 0.14);
|
||||
--shadow-panel: 0 20px 42px rgba(31, 27, 23, 0.16);
|
||||
--fx-blur: 8px;
|
||||
--fx-radius: 12px;
|
||||
--topbar-a: rgba(45, 94, 76, 0.96);
|
||||
--topbar-b: rgba(31, 27, 23, 0.88);
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: "Source Serif 4", serif;
|
||||
color: var(--text);
|
||||
background: radial-gradient(circle at 10% 0%, #f7f1e2 0%, var(--bg) 46%, #d6c6a5 100%);
|
||||
}
|
||||
.topbar {
|
||||
height: 52px; display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: linear-gradient(120deg, var(--topbar-a), var(--topbar-b));
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
.brand {
|
||||
font-family: "Oswald", sans-serif;
|
||||
letter-spacing: 0.04em;
|
||||
font-size: 1.02rem;
|
||||
font-weight: 700;
|
||||
color: #f7f1e2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid rgba(255, 248, 235, 0.7);
|
||||
background: rgba(255, 248, 235, 0.15);
|
||||
color: #f7f1e2;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: transform 140ms ease, box-shadow 140ms ease;
|
||||
}
|
||||
.btn:hover { transform: translateY(-2px); box-shadow: 0 8px 16px rgba(31, 27, 23, 0.26); }
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 16px rgba(155, 47, 22, 0.3);
|
||||
}
|
||||
.layout { display: grid; grid-template-columns: 260px 1fr 260px; height: calc(100vh - 52px); }
|
||||
.side {
|
||||
background: linear-gradient(160deg, var(--panel), #f5ebd4);
|
||||
border-right: 1px solid var(--border);
|
||||
overflow: auto;
|
||||
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.4);
|
||||
backdrop-filter: blur(var(--fx-blur));
|
||||
-webkit-backdrop-filter: blur(var(--fx-blur));
|
||||
}
|
||||
.side.right { border-right: 0; border-left: 1px solid var(--border); }
|
||||
.section { padding: 12px; }
|
||||
.section-title {
|
||||
font-family: "Oswald", sans-serif;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.list-item {
|
||||
padding: 8px 10px;
|
||||
background: rgba(255, 255, 255, 0.58);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--fx-radius);
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
box-shadow: 0 6px 14px rgba(31, 27, 23, 0.08);
|
||||
backdrop-filter: blur(var(--fx-blur));
|
||||
-webkit-backdrop-filter: blur(var(--fx-blur));
|
||||
}
|
||||
.list-item.active {
|
||||
border-color: rgba(45, 94, 76, 0.45);
|
||||
box-shadow: 0 10px 16px rgba(45, 94, 76, 0.12);
|
||||
}
|
||||
.list-actions button {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
.canvas {
|
||||
background: radial-gradient(circle at 30% 0%, #f4ead6 0%, var(--canvas) 60%, #ddcfb1 100%);
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
.preview {
|
||||
background: #fff;
|
||||
border-radius: calc(var(--fx-radius) + 4px);
|
||||
border: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
min-height: 80vh;
|
||||
box-shadow: var(--shadow-panel);
|
||||
backdrop-filter: blur(var(--fx-blur));
|
||||
-webkit-backdrop-filter: blur(var(--fx-blur));
|
||||
}
|
||||
.preview iframe { width: 100%; height: 80vh; border: 0; display: block; }
|
||||
.field { margin-bottom: 10px; }
|
||||
.field label { display: block; font-size: 11px; color: var(--muted); margin-bottom: 4px; }
|
||||
.field input, .field textarea, .field select {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
}
|
||||
.hint { font-size: 11px; color: var(--muted); }
|
||||
.template-box {
|
||||
padding: 10px;
|
||||
border-radius: var(--fx-radius);
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(255, 255, 255, 0.56);
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 8px 18px rgba(31, 27, 23, 0.1);
|
||||
backdrop-filter: blur(var(--fx-blur));
|
||||
-webkit-backdrop-filter: blur(var(--fx-blur));
|
||||
}
|
||||
.preset-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.preset-chip {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.preset-chip.active {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.field input[type="range"] {
|
||||
padding: 0;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
body.compact .side { display: none; }
|
||||
body.compact .layout { grid-template-columns: 1fr; }
|
||||
body.compact .canvas { padding: 0; }
|
||||
body.compact .preview { min-height: calc(100vh - 52px); border-radius: 0; border: 0; }
|
||||
.toggle-panels {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgba(255, 248, 235, 0.7);
|
||||
background: rgba(255, 248, 235, 0.15);
|
||||
color: #f7f1e2;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: transform 140ms ease, box-shadow 140ms ease;
|
||||
}
|
||||
.toggle-panels:hover { transform: translateY(-2px); box-shadow: 0 8px 16px rgba(31, 27, 23, 0.26); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="topbar">
|
||||
<div class="brand"><i class="fas fa-layer-group"></i> Customizer New</div>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<button class="toggle-panels" onclick="togglePanels()">Paneles</button>
|
||||
<button class="btn" onclick="reloadPreview()">Refrescar preview</button>
|
||||
<button class="btn btn-primary" onclick="saveState()">Guardar</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<aside class="side">
|
||||
<div class="section">
|
||||
<div class="section-title">Páginas</div>
|
||||
<div id="pagesList"></div>
|
||||
<button class="btn btn-primary" style="width:100%;margin-top:6px;" onclick="loadDemo()">Cargar demo</button>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title">Capas (Secciones)</div>
|
||||
<div id="sectionsList"></div>
|
||||
<button class="btn" style="width:100%;margin-top:6px;" onclick="addSection()">Agregar sección</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="canvas">
|
||||
<div class="preview">
|
||||
<iframe id="previewFrame" src="/api/customizer/preview-frame/{{ site_id }}?page=home&edit=1&t={{ security_hash }}"></iframe>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<aside class="side right">
|
||||
<div class="section">
|
||||
<div class="section-title">Sistema Visual Pro</div>
|
||||
<div id="visualPanel"></div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title">Propiedades</div>
|
||||
<div class="hint">Edicion simple para plantillas</div>
|
||||
<div id="propsPanel"></div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const SITE_ID = {{ site_id }};
|
||||
const THEME = "{{ theme }}";
|
||||
const VISUAL_PRESETS = {
|
||||
tierra_editorial: {
|
||||
name: 'Tierra Editorial',
|
||||
bg: '#e4dccb',
|
||||
bg2: '#f3ecde',
|
||||
panel: '#fff8eb',
|
||||
text: '#1f1b17',
|
||||
muted: '#6e6458',
|
||||
accent: '#9b2f16',
|
||||
accent2: '#2d5e4c',
|
||||
blur: 8,
|
||||
radius: 12,
|
||||
depth: 38,
|
||||
glow: 16,
|
||||
map_ratio: '16 / 9',
|
||||
map_h_desktop: 360,
|
||||
map_h_tablet: 280,
|
||||
map_h_mobile: 220,
|
||||
social_style: 'pill'
|
||||
},
|
||||
glass_aurora: {
|
||||
name: 'Glass Aurora',
|
||||
bg: '#dde6ff',
|
||||
bg2: '#eafcff',
|
||||
panel: '#f7fbff',
|
||||
text: '#1c2433',
|
||||
muted: '#5a6a86',
|
||||
accent: '#6b5bff',
|
||||
accent2: '#3ecfc2',
|
||||
blur: 18,
|
||||
radius: 18,
|
||||
depth: 56,
|
||||
glow: 42,
|
||||
map_ratio: '16 / 9',
|
||||
map_h_desktop: 380,
|
||||
map_h_tablet: 300,
|
||||
map_h_mobile: 240,
|
||||
social_style: 'glass'
|
||||
},
|
||||
ice_minimal: {
|
||||
name: 'Ice Minimal',
|
||||
bg: '#e9eef5',
|
||||
bg2: '#f5f8fc',
|
||||
panel: '#ffffff',
|
||||
text: '#172133',
|
||||
muted: '#667389',
|
||||
accent: '#3a7bf7',
|
||||
accent2: '#2fba99',
|
||||
blur: 10,
|
||||
radius: 14,
|
||||
depth: 34,
|
||||
glow: 10,
|
||||
map_ratio: '4 / 3',
|
||||
map_h_desktop: 340,
|
||||
map_h_tablet: 280,
|
||||
map_h_mobile: 220,
|
||||
social_style: 'minimal'
|
||||
},
|
||||
noir_pro: {
|
||||
name: 'Noir Pro',
|
||||
bg: '#131620',
|
||||
bg2: '#1b2030',
|
||||
panel: '#1f2535',
|
||||
text: '#eff3ff',
|
||||
muted: '#9eabc7',
|
||||
accent: '#5f8dff',
|
||||
accent2: '#52d7be',
|
||||
blur: 12,
|
||||
radius: 12,
|
||||
depth: 62,
|
||||
glow: 26,
|
||||
map_ratio: '16 / 10',
|
||||
map_h_desktop: 360,
|
||||
map_h_tablet: 290,
|
||||
map_h_mobile: 230,
|
||||
social_style: 'card'
|
||||
}
|
||||
};
|
||||
let state = {
|
||||
content: {{ content | tojson | safe }},
|
||||
blocks: {{ content.get('blocks', []) | tojson | safe }},
|
||||
settings: {{ content.get('settings', {}) | tojson | safe }},
|
||||
pages: [
|
||||
{ id: 'home', title: 'Inicio' },
|
||||
{ id: 'servicios', title: 'Servicios' },
|
||||
{ id: 'proyectos', title: 'Proyectos' },
|
||||
{ id: 'contacto', title: 'Contacto' }
|
||||
],
|
||||
currentPage: 'home',
|
||||
selectedIndex: null
|
||||
};
|
||||
let visualSaveTimer = null;
|
||||
|
||||
function applyCompactMode() {
|
||||
if (THEME === 'streaming-vix') {
|
||||
document.body.classList.add('compact');
|
||||
}
|
||||
}
|
||||
|
||||
function togglePanels() {
|
||||
document.body.classList.toggle('compact');
|
||||
}
|
||||
|
||||
function normalizeBlocks() {
|
||||
state.blocks = (state.blocks || []).map(b => {
|
||||
if (!b || typeof b !== 'object') return b;
|
||||
if (!b.page) b.page = 'home';
|
||||
if (!b.data) b.data = {};
|
||||
return b;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeVisualSettings() {
|
||||
if (!state.settings) state.settings = {};
|
||||
const basePreset = VISUAL_PRESETS.tierra_editorial;
|
||||
const current = state.settings.visual || {};
|
||||
state.settings.visual = {
|
||||
preset: current.preset || 'tierra_editorial',
|
||||
...basePreset,
|
||||
...current
|
||||
};
|
||||
if (!VISUAL_PRESETS[state.settings.visual.preset]) {
|
||||
state.settings.visual.preset = 'tierra_editorial';
|
||||
}
|
||||
}
|
||||
|
||||
function renderVisualPanel() {
|
||||
const panel = document.getElementById('visualPanel');
|
||||
if (!panel) return;
|
||||
const v = state.settings.visual || VISUAL_PRESETS.tierra_editorial;
|
||||
const chips = Object.entries(VISUAL_PRESETS).map(([key, preset]) => `
|
||||
<button class="preset-chip ${v.preset === key ? 'active' : ''}" onclick="applyVisualPreset('${key}')">${preset.name}</button>
|
||||
`).join('');
|
||||
|
||||
panel.innerHTML = `
|
||||
<div class="template-box">
|
||||
<div class="preset-grid">${chips}</div>
|
||||
${colorVisualTpl('accent', 'Acento', v.accent)}
|
||||
${colorVisualTpl('accent2', 'Acento 2', v.accent2)}
|
||||
${rangeVisualTpl('blur', 'Glass Blur', v.blur, 0, 28, 1)}
|
||||
${rangeVisualTpl('radius', 'Radio', v.radius, 6, 28, 1)}
|
||||
${rangeVisualTpl('depth', 'Profundidad', v.depth, 0, 100, 1)}
|
||||
${rangeVisualTpl('glow', 'Glow', v.glow, 0, 80, 1)}
|
||||
${selectVisualTpl('social_style', 'Estilo Redes', v.social_style, [
|
||||
{ value: 'pill', label: 'Pill' },
|
||||
{ value: 'glass', label: 'Glass' },
|
||||
{ value: 'card', label: 'Card' },
|
||||
{ value: 'minimal', label: 'Minimal' }
|
||||
])}
|
||||
${selectVisualTpl('map_ratio', 'Ratio Mapa', v.map_ratio, [
|
||||
{ value: '16 / 9', label: '16:9' },
|
||||
{ value: '4 / 3', label: '4:3' },
|
||||
{ value: '1 / 1', label: '1:1' },
|
||||
{ value: '21 / 9', label: '21:9' },
|
||||
{ value: 'auto', label: 'Auto' }
|
||||
])}
|
||||
${rangeVisualTpl('map_h_desktop', 'Mapa Desktop', v.map_h_desktop, 220, 700, 10)}
|
||||
${rangeVisualTpl('map_h_tablet', 'Mapa Tablet', v.map_h_tablet, 180, 540, 10)}
|
||||
${rangeVisualTpl('map_h_mobile', 'Mapa Móvil', v.map_h_mobile, 140, 420, 10)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function colorVisualTpl(id, label, value) {
|
||||
return `<div class="field"><label>${label}</label><input type="color" value="${value}" oninput="updateVisual('${id}', this.value, true)" onchange="updateVisual('${id}', this.value, false)"></div>`;
|
||||
}
|
||||
|
||||
function rangeVisualTpl(id, label, value, min, max, step) {
|
||||
return `<div class="field"><label>${label}: <strong>${value}</strong></label><input type="range" min="${min}" max="${max}" step="${step}" value="${value}" oninput="updateVisual('${id}', this.value, true)" onchange="updateVisual('${id}', this.value, false)"></div>`;
|
||||
}
|
||||
|
||||
function selectVisualTpl(id, label, selected, options) {
|
||||
const opts = options.map(opt => `<option value="${opt.value}" ${selected === opt.value ? 'selected' : ''}>${opt.label}</option>`).join('');
|
||||
return `<div class="field"><label>${label}</label><select oninput="updateVisual('${id}', this.value, true)" onchange="updateVisual('${id}', this.value, false)">${opts}</select></div>`;
|
||||
}
|
||||
|
||||
function applyVisualPreset(presetKey) {
|
||||
const preset = VISUAL_PRESETS[presetKey];
|
||||
if (!preset) return;
|
||||
state.settings.visual = { preset: presetKey, ...preset };
|
||||
applyEditorVisual();
|
||||
applyPreviewVisual();
|
||||
renderVisualPanel();
|
||||
saveState();
|
||||
}
|
||||
|
||||
function updateVisual(key, value, live) {
|
||||
const v = state.settings.visual || {};
|
||||
if (['blur', 'radius', 'depth', 'glow', 'map_h_desktop', 'map_h_tablet', 'map_h_mobile'].includes(key)) {
|
||||
v[key] = Number(value);
|
||||
} else {
|
||||
v[key] = value;
|
||||
}
|
||||
state.settings.visual = v;
|
||||
applyEditorVisual();
|
||||
applyPreviewVisual();
|
||||
if (!live) {
|
||||
renderVisualPanel();
|
||||
saveState();
|
||||
return;
|
||||
}
|
||||
if (visualSaveTimer) clearTimeout(visualSaveTimer);
|
||||
visualSaveTimer = setTimeout(() => saveState(), 700);
|
||||
}
|
||||
|
||||
function hexToRgba(hex, alpha) {
|
||||
if (!hex || typeof hex !== 'string') return `rgba(0,0,0,${alpha})`;
|
||||
const clean = hex.replace('#', '');
|
||||
const full = clean.length === 3 ? clean.split('').map(ch => ch + ch).join('') : clean;
|
||||
const num = parseInt(full, 16);
|
||||
if (Number.isNaN(num)) return `rgba(0,0,0,${alpha})`;
|
||||
const r = (num >> 16) & 255;
|
||||
const g = (num >> 8) & 255;
|
||||
const b = num & 255;
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
function applyEditorVisual() {
|
||||
const v = state.settings.visual || VISUAL_PRESETS.tierra_editorial;
|
||||
const root = document.documentElement;
|
||||
const depth = Number(v.depth || 40);
|
||||
const blur = Number(v.blur || 8);
|
||||
const radius = Number(v.radius || 12);
|
||||
const glow = Number(v.glow || 0);
|
||||
root.style.setProperty('--bg', v.bg);
|
||||
root.style.setProperty('--bg-2', v.bg2);
|
||||
root.style.setProperty('--panel', v.panel);
|
||||
root.style.setProperty('--panel-2', v.panel);
|
||||
root.style.setProperty('--canvas', v.bg2);
|
||||
root.style.setProperty('--text', v.text);
|
||||
root.style.setProperty('--muted', v.muted);
|
||||
root.style.setProperty('--accent', v.accent);
|
||||
root.style.setProperty('--accent-2', v.accent2);
|
||||
root.style.setProperty('--border', hexToRgba(v.text, 0.14));
|
||||
root.style.setProperty('--fx-blur', `${blur}px`);
|
||||
root.style.setProperty('--fx-radius', `${radius}px`);
|
||||
root.style.setProperty('--shadow-soft', `0 12px 28px ${hexToRgba(v.text, 0.08 + depth / 600)}`);
|
||||
root.style.setProperty('--shadow-panel', `0 22px 46px ${hexToRgba(v.text, 0.12 + depth / 500)}`);
|
||||
root.style.setProperty('--topbar-a', hexToRgba(v.accent2, 0.94));
|
||||
root.style.setProperty('--topbar-b', hexToRgba(v.text, 0.9));
|
||||
if (glow > 0) {
|
||||
root.style.setProperty('--shadow-soft', `0 12px 28px ${hexToRgba(v.text, 0.08 + depth / 600)}, 0 0 ${Math.round(glow / 3)}px ${hexToRgba(v.accent, 0.18)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function applyPreviewVisual() {
|
||||
const frame = document.getElementById('previewFrame');
|
||||
if (!frame || !frame.contentDocument) return;
|
||||
const doc = frame.contentDocument;
|
||||
const v = state.settings.visual || VISUAL_PRESETS.tierra_editorial;
|
||||
const styleId = 'gk-customizer-visual';
|
||||
let styleEl = doc.getElementById(styleId);
|
||||
if (!styleEl) {
|
||||
styleEl = doc.createElement('style');
|
||||
styleEl.id = styleId;
|
||||
doc.head.appendChild(styleEl);
|
||||
}
|
||||
const blur = Number(v.blur || 8);
|
||||
const radius = Number(v.radius || 12);
|
||||
const glow = Number(v.glow || 0);
|
||||
const font = (state.settings && state.settings.font_family) ? state.settings.font_family : 'Source Serif 4';
|
||||
const mapRatio = v.map_ratio || '16 / 9';
|
||||
const mapDesktop = Number(v.map_h_desktop || 360);
|
||||
const mapTablet = Number(v.map_h_tablet || 280);
|
||||
const mapMobile = Number(v.map_h_mobile || 220);
|
||||
const socialStyle = v.social_style || 'pill';
|
||||
const socialBg = socialStyle === 'minimal'
|
||||
? 'transparent'
|
||||
: socialStyle === 'card'
|
||||
? hexToRgba(v.panel, 0.92)
|
||||
: hexToRgba(v.panel, 0.72);
|
||||
const socialBorder = socialStyle === 'minimal'
|
||||
? hexToRgba(v.text, 0.26)
|
||||
: hexToRgba(v.text, 0.14);
|
||||
const socialShadow = socialStyle === 'card'
|
||||
? `0 14px 26px ${hexToRgba(v.text, 0.18)}`
|
||||
: socialStyle === 'glass'
|
||||
? `0 10px 18px ${hexToRgba(v.text, 0.15)}, 0 0 ${Math.max(6, Math.round(glow / 2))}px ${hexToRgba(v.accent, 0.2)}`
|
||||
: `0 8px 14px ${hexToRgba(v.text, 0.12)}`;
|
||||
styleEl.textContent = `
|
||||
:root {
|
||||
--gk-accent: ${v.accent};
|
||||
--gk-accent-2: ${v.accent2};
|
||||
--gk-text: ${v.text};
|
||||
--gk-muted: ${v.muted};
|
||||
}
|
||||
body {
|
||||
background: radial-gradient(circle at 15% 0%, ${v.bg2} 0%, ${v.bg} 56%, ${v.panel} 100%) !important;
|
||||
color: ${v.text} !important;
|
||||
font-family: '${font}', system-ui, sans-serif !important;
|
||||
}
|
||||
:where(section, article, .card, .panel, .tile, .feature, .service-item, .pricing-card, .contact-card, .box) {
|
||||
background: ${hexToRgba(v.panel, 0.72)} !important;
|
||||
border: 1px solid ${hexToRgba(v.text, 0.14)} !important;
|
||||
border-radius: ${radius}px !important;
|
||||
box-shadow: 0 16px 28px ${hexToRgba(v.text, 0.14)}${glow > 0 ? `, 0 0 ${Math.round(glow / 2)}px ${hexToRgba(v.accent, 0.22)}` : ''} !important;
|
||||
backdrop-filter: blur(${blur}px) !important;
|
||||
-webkit-backdrop-filter: blur(${blur}px) !important;
|
||||
}
|
||||
:where(button, .btn, a.btn, input, textarea, select) {
|
||||
border-radius: ${Math.max(8, radius - 2)}px !important;
|
||||
}
|
||||
:where(a, .link-accent, h1, h2, h3) {
|
||||
color: inherit;
|
||||
}
|
||||
:where(.btn-primary, button.primary, a.primary) {
|
||||
background: linear-gradient(120deg, ${v.accent}, ${v.accent2}) !important;
|
||||
color: #fff !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
:where(.redes-sociales a, .social-icon, .social-btn, [data-social], .social-link) {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
gap: 8px !important;
|
||||
background: ${socialBg} !important;
|
||||
border: 1px solid ${socialBorder} !important;
|
||||
color: ${v.text} !important;
|
||||
border-radius: ${Math.max(10, radius)}px !important;
|
||||
padding: 8px 12px !important;
|
||||
box-shadow: ${socialShadow} !important;
|
||||
text-decoration: none !important;
|
||||
backdrop-filter: blur(${Math.max(0, blur - 2)}px) !important;
|
||||
-webkit-backdrop-filter: blur(${Math.max(0, blur - 2)}px) !important;
|
||||
transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease !important;
|
||||
}
|
||||
:where(.redes-sociales a, .social-icon, .social-btn, [data-social], .social-link):hover {
|
||||
transform: translateY(-2px) !important;
|
||||
border-color: ${hexToRgba(v.accent, 0.5)} !important;
|
||||
box-shadow: 0 14px 24px ${hexToRgba(v.text, 0.2)}, 0 0 ${Math.max(8, Math.round(glow / 2))}px ${hexToRgba(v.accent, 0.24)} !important;
|
||||
}
|
||||
:where(.mapa-container iframe, iframe.map-frame, [class*="map"] iframe, [data-block-type="map"] iframe) {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
border: 0 !important;
|
||||
border-radius: ${Math.max(10, radius)}px !important;
|
||||
box-shadow: 0 12px 22px ${hexToRgba(v.text, 0.16)} !important;
|
||||
display: block !important;
|
||||
${mapRatio === 'auto' ? `height: ${mapDesktop}px !important;` : `height: auto !important; aspect-ratio: ${mapRatio} !important;`}
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
:where(.mapa-container iframe, iframe.map-frame, [class*="map"] iframe, [data-block-type="map"] iframe) {
|
||||
${mapRatio === 'auto' ? `height: ${mapTablet}px !important;` : `min-height: ${mapTablet}px !important;`}
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
:where(.mapa-container iframe, iframe.map-frame, [class*="map"] iframe, [data-block-type="map"] iframe) {
|
||||
${mapRatio === 'auto' ? `height: ${mapMobile}px !important;` : `min-height: ${mapMobile}px !important;`}
|
||||
}
|
||||
}
|
||||
`;
|
||||
applyPerBlockMapSizing(doc, mapRatio, mapDesktop);
|
||||
}
|
||||
|
||||
function applyPerBlockMapSizing(doc, fallbackRatio, fallbackHeight) {
|
||||
const mapBlocks = (state.blocks || []).filter(b => (b.page || 'home') === state.currentPage && (b.type === 'map' || b.type === 'mapa'));
|
||||
if (!mapBlocks.length) return;
|
||||
const mapIframes = doc.querySelectorAll('.mapa-container iframe, iframe.map-frame, [class*="map"] iframe, [data-block-type="map"] iframe');
|
||||
if (!mapIframes.length) return;
|
||||
|
||||
mapIframes.forEach((iframe, i) => {
|
||||
const data = (mapBlocks[i] && mapBlocks[i].data) ? mapBlocks[i].data : {};
|
||||
const ratio = data.ratio || fallbackRatio || '16 / 9';
|
||||
const height = Number(data.height || fallbackHeight || 360);
|
||||
if (ratio && ratio !== 'auto') {
|
||||
iframe.style.aspectRatio = ratio;
|
||||
iframe.style.height = 'auto';
|
||||
iframe.style.minHeight = `${Math.max(160, Math.min(height, 800))}px`;
|
||||
} else {
|
||||
iframe.style.height = `${Math.max(160, Math.min(height, 800))}px`;
|
||||
iframe.style.aspectRatio = '';
|
||||
iframe.style.minHeight = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getSections() {
|
||||
const sections = [];
|
||||
let current = null;
|
||||
state.blocks.forEach((block, idx) => {
|
||||
if ((block.page || 'home') !== state.currentPage) return;
|
||||
if (block.type === 'heading') {
|
||||
if (current) current.endIdx = idx - 1;
|
||||
current = { title: block.data.text || 'Sección', startIdx: idx, endIdx: idx, idx };
|
||||
sections.push(current);
|
||||
} else if (!current) {
|
||||
current = { title: 'Sección', startIdx: idx, endIdx: idx, idx };
|
||||
sections.push(current);
|
||||
} else {
|
||||
current.endIdx = idx;
|
||||
}
|
||||
});
|
||||
return sections;
|
||||
}
|
||||
|
||||
function renderPages() {
|
||||
const list = document.getElementById('pagesList');
|
||||
list.innerHTML = state.pages.map(p => `
|
||||
<div class="list-item ${p.id === state.currentPage ? 'active' : ''}" onclick="setPage('${p.id}')">
|
||||
<span>${p.title}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderSections() {
|
||||
const list = document.getElementById('sectionsList');
|
||||
const sections = getSections();
|
||||
if (sections.length === 0) {
|
||||
list.innerHTML = '<div class="list-item">Sin secciones</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = sections.map((s, i) => `
|
||||
<div class="list-item ${state.selectedIndex === s.idx ? 'active' : ''}" onclick="selectBlock(${s.idx})">
|
||||
<span>${s.title}</span>
|
||||
<div class="list-actions">
|
||||
<button onclick="moveSection(${i}, -1); event.stopPropagation()">↑</button>
|
||||
<button onclick="moveSection(${i}, 1); event.stopPropagation()">↓</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderProps() {
|
||||
const panel = document.getElementById('propsPanel');
|
||||
let html = '';
|
||||
if (THEME === 'streaming-vix') {
|
||||
const c = state.content || {};
|
||||
html += `<div class="template-box">
|
||||
<div class="section-title">Template Streaming</div>
|
||||
${fieldTpl('hero_title', 'Titulo principal', c.hero_title || 'Bienvenido')}
|
||||
${textareaTpl('hero_description', 'Descripcion', c.hero_description || '')}
|
||||
${fieldTpl('cta_primary_text', 'CTA principal', c.cta_primary_text || 'Empezar ahora')}
|
||||
${fieldTpl('cta_primary_url', 'URL CTA principal', c.cta_primary_url || '#cta')}
|
||||
${fieldTpl('cta_secondary_text', 'CTA secundario', c.cta_secondary_text || 'Ver catalogo')}
|
||||
${fieldTpl('cta_secondary_url', 'URL CTA secundario', c.cta_secondary_url || '#showcase')}
|
||||
${textareaTpl('benefits', 'Beneficios (uno por linea)', (c.benefits || []).join('\n'))}
|
||||
${textareaTpl('showcase', 'Contenido destacado (uno por linea)', (c.showcase || []).join('\n'))}
|
||||
${fieldTpl('footer_text', 'Footer', c.footer_text || '')}
|
||||
</div>`;
|
||||
}
|
||||
const idx = state.selectedIndex;
|
||||
if (idx !== null && state.blocks[idx]) {
|
||||
const b = state.blocks[idx];
|
||||
html += `<div class="template-box"><div class="section-title">Bloque</div>`;
|
||||
if (b.type === 'heading') {
|
||||
html += field('text', 'Titulo', b.data.text || '');
|
||||
html += field('color', 'Color', b.data.color || '#0f172a');
|
||||
} else if (b.type === 'paragraph') {
|
||||
html += textarea('text', 'Contenido', b.data.text || '');
|
||||
} else if (b.type === 'image') {
|
||||
html += field('url', 'URL Imagen', b.data.url || '');
|
||||
} else if (b.type === 'map' || b.type === 'mapa') {
|
||||
html += field('url', 'URL Embed', b.data.url || b.data.embed || '');
|
||||
html += field('address', 'Dirección', b.data.address || '');
|
||||
html += selectField('ratio', 'Ratio', b.data.ratio || '16 / 9', [
|
||||
{ value: '16 / 9', label: '16:9' },
|
||||
{ value: '4 / 3', label: '4:3' },
|
||||
{ value: '1 / 1', label: '1:1' },
|
||||
{ value: '21 / 9', label: '21:9' },
|
||||
{ value: 'auto', label: 'Auto' }
|
||||
]);
|
||||
html += field('height', 'Altura (px)', b.data.height || '');
|
||||
} else if (b.type === 'list') {
|
||||
html += field('title', 'Titulo', b.data.title || '');
|
||||
html += textarea('items', 'Items (uno por linea)', (b.data.items || []).join('\n'));
|
||||
} else {
|
||||
html += textarea('json', 'JSON', JSON.stringify(b.data || {}, null, 2));
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
panel.innerHTML = html || '<div class="hint">Selecciona un bloque o edita el template.</div>';
|
||||
}
|
||||
|
||||
function field(id, label, value) {
|
||||
return `<div class="field"><label>${label}</label><input id="field_${id}" value="${value}" oninput="updateField('${id}')"></div>`;
|
||||
}
|
||||
|
||||
function textarea(id, label, value) {
|
||||
return `<div class="field"><label>${label}</label><textarea id="field_${id}" rows="5" oninput="updateField('${id}')">${value}</textarea></div>`;
|
||||
}
|
||||
function selectField(id, label, selected, options) {
|
||||
const opts = options.map(opt => `<option value="${opt.value}" ${selected === opt.value ? 'selected' : ''}>${opt.label}</option>`).join('');
|
||||
return `<div class="field"><label>${label}</label><select id="field_${id}" oninput="updateField('${id}')" onchange="updateField('${id}')">${opts}</select></div>`;
|
||||
}
|
||||
|
||||
function fieldTpl(id, label, value) {
|
||||
return `<div class="field"><label>${label}</label><input id="tpl_${id}" value="${value}" oninput="updateTemplate('${id}')"></div>`;
|
||||
}
|
||||
|
||||
function textareaTpl(id, label, value) {
|
||||
return `<div class="field"><label>${label}</label><textarea id="tpl_${id}" rows="4" oninput="updateTemplate('${id}')">${value}</textarea></div>`;
|
||||
}
|
||||
|
||||
function updateField(id) {
|
||||
const b = state.blocks[state.selectedIndex];
|
||||
if (!b) return;
|
||||
if (id === 'items') {
|
||||
b.data.items = document.getElementById('field_items').value.split('\\n').filter(Boolean);
|
||||
} else if (id === 'json') {
|
||||
try { b.data = JSON.parse(document.getElementById('field_json').value); } catch (e) { return; }
|
||||
} else if (id === 'url' && (b.type === 'map' || b.type === 'mapa')) {
|
||||
const val = document.getElementById('field_url').value;
|
||||
b.data.url = val;
|
||||
b.data.embed = val;
|
||||
} else if (id === 'height' && (b.type === 'map' || b.type === 'mapa')) {
|
||||
const raw = document.getElementById('field_height').value;
|
||||
const n = Number(raw);
|
||||
b.data.height = Number.isFinite(n) && n > 0 ? n : '';
|
||||
} else {
|
||||
b.data[id] = document.getElementById('field_' + id).value;
|
||||
}
|
||||
saveState();
|
||||
}
|
||||
|
||||
function updateTemplate(id) {
|
||||
if (!state.content) state.content = {};
|
||||
if (id === 'benefits' || id === 'showcase') {
|
||||
const raw = document.getElementById('tpl_' + id).value;
|
||||
state.content[id] = raw.split('\n').filter(Boolean);
|
||||
} else {
|
||||
state.content[id] = document.getElementById('tpl_' + id).value;
|
||||
}
|
||||
saveState();
|
||||
}
|
||||
|
||||
function selectBlock(idx) {
|
||||
state.selectedIndex = idx;
|
||||
renderSections();
|
||||
renderProps();
|
||||
}
|
||||
|
||||
function setPage(id) {
|
||||
state.currentPage = id;
|
||||
state.selectedIndex = null;
|
||||
renderPages();
|
||||
renderSections();
|
||||
renderProps();
|
||||
reloadPreview();
|
||||
}
|
||||
|
||||
function moveSection(idx, dir) {
|
||||
const sections = getSections();
|
||||
const newIdx = idx + dir;
|
||||
if (newIdx < 0 || newIdx >= sections.length) return;
|
||||
const order = sections.map((_, i) => i);
|
||||
const temp = order[idx];
|
||||
order[idx] = order[newIdx];
|
||||
order[newIdx] = temp;
|
||||
reorderSections(order);
|
||||
}
|
||||
|
||||
function reorderSections(order) {
|
||||
const sections = getSections();
|
||||
if (sections.length === 0) return;
|
||||
const removed = new Set();
|
||||
const reordered = [];
|
||||
order.forEach(i => {
|
||||
const sec = sections[i];
|
||||
for (let x = sec.startIdx; x <= sec.endIdx; x++) {
|
||||
reordered.push(state.blocks[x]);
|
||||
removed.add(x);
|
||||
}
|
||||
});
|
||||
const firstStart = sections[0].startIdx;
|
||||
const before = [];
|
||||
const after = [];
|
||||
state.blocks.forEach((b, i) => {
|
||||
if (removed.has(i)) return;
|
||||
if (i < firstStart) before.push(b);
|
||||
else after.push(b);
|
||||
});
|
||||
state.blocks = before.concat(reordered, after);
|
||||
renderSections();
|
||||
saveState();
|
||||
}
|
||||
|
||||
function addSection() {
|
||||
const title = prompt('Título de la sección', 'Nueva sección');
|
||||
if (!title) return;
|
||||
state.blocks.push({ id: Math.random(), type: 'heading', colSpan: 4, page: state.currentPage, data: { text: title, color: '#0f172a' } });
|
||||
state.blocks.push({ id: Math.random(), type: 'paragraph', colSpan: 4, page: state.currentPage, data: { text: 'Contenido de la sección...' } });
|
||||
renderSections();
|
||||
saveState();
|
||||
}
|
||||
|
||||
function loadDemo() {
|
||||
const demo = [
|
||||
{ id: 1, type: 'heading', colSpan: 4, page: 'home', data: { text: 'Construcción de Viviendas', color: '#0f172a' } },
|
||||
{ id: 2, type: 'paragraph', colSpan: 4, page: 'home', data: { text: 'Diseño, planificación y obra llave en mano.' } },
|
||||
{ id: 3, type: 'heading', colSpan: 4, page: 'home', data: { text: 'Servicios', color: '#0f172a' } },
|
||||
{ id: 4, type: 'list', colSpan: 4, page: 'home', data: { title: 'Servicios', items: ['Proyecto', 'Obra nueva', 'Reformas', 'Permisos'] } },
|
||||
{ id: 5, type: 'heading', colSpan: 4, page: 'home', data: { text: 'Testimonios', color: '#0f172a' } },
|
||||
{ id: 6, type: 'list', colSpan: 4, page: 'home', data: { title: 'Opiniones', items: ['Excelente calidad', 'Buen trato', 'Recomendado'] } }
|
||||
];
|
||||
state.blocks = demo;
|
||||
renderSections();
|
||||
saveState();
|
||||
}
|
||||
|
||||
async function saveState() {
|
||||
if (!state.content) state.content = {};
|
||||
state.content.settings = state.settings || {};
|
||||
state.content.blocks = state.blocks || [];
|
||||
await fetch('/api/customizer/save', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
site_id: SITE_ID,
|
||||
content: state.content
|
||||
})
|
||||
});
|
||||
reloadPreview();
|
||||
}
|
||||
|
||||
function reloadPreview() {
|
||||
const frame = document.getElementById('previewFrame');
|
||||
frame.src = `/api/customizer/preview-frame/${SITE_ID}?page=${state.currentPage}&edit=1&t=${Date.now()}`;
|
||||
}
|
||||
|
||||
applyCompactMode();
|
||||
if (!state.content) state.content = {};
|
||||
normalizeVisualSettings();
|
||||
normalizeBlocks();
|
||||
applyEditorVisual();
|
||||
const previewFrame = document.getElementById('previewFrame');
|
||||
if (previewFrame) {
|
||||
previewFrame.addEventListener('load', applyPreviewVisual);
|
||||
}
|
||||
renderPages();
|
||||
renderSections();
|
||||
renderVisualPanel();
|
||||
renderProps();
|
||||
applyPreviewVisual();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user