Files
gkachele-saas/elementor/templates/elementor_builder.html

1901 lines
112 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GKACHELE Builder</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700&family=DM+Sans:wght@400;500;700&family=Space+Grotesk:wght@400;600;700&family=Outfit:wght@400;500;700&family=Sora:wght@400;600;700&family=Poppins:wght@400;500;700&family=Playfair+Display:wght@400;600;700&family=IBM+Plex+Sans:wght@400;500;700&family=Merriweather:wght@400;700&display=swap" rel="stylesheet">
<style>
:root { --bg:#0b0f16; --panel:#121826; --panel2:#1a2234; --text:#e5e7eb; --muted:#8a93a5; --accent:#59d9c8; --border:#263043; --space-1:6px; --space-2:10px; --space-3:14px; --space-4:18px; --space-5:24px; --radius-sm:10px; --radius-md:14px; --radius-lg:18px; --shadow-soft:0 8px 18px rgba(15,23,42,.06); --shadow-lift:0 16px 36px rgba(15,23,42,.12); }
*{box-sizing:border-box}
body{margin:0;font-family:Manrope,system-ui,sans-serif;background:radial-gradient(circle at top left,#101725,#080b12 65%);color:var(--text)}
.app{display:grid;grid-template-columns:240px 1fr 280px;min-height:100vh}
.sidebar,.inspector{background:var(--panel);padding:16px;border-right:1px solid var(--border)}
.inspector{border-right:none;border-left:1px solid var(--border)}
.brand{display:flex;align-items:center;gap:8px;font-weight:700;margin-bottom:12px}
.dot{width:10px;height:10px;border-radius:50%;background:linear-gradient(135deg,#59d9c8,#7aa7ff)}
.section-title{font-size:11px;text-transform:uppercase;letter-spacing:1.2px;color:var(--muted);margin:12px 0 8px}
.block-item{background:var(--panel2);border:1px solid var(--border);padding:8px 10px;border-radius:10px;margin-bottom:8px;cursor:grab}
.main{padding:10px}
.topbar{display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap}
.save-status{font-size:12px;color:var(--muted);min-width:150px;text-align:right}
.save-status.ok{color:#34d399}
.save-status.error{color:#f87171}
.save-status.busy{color:#fbbf24}
.btn{background:var(--accent);color:#09121a;border:0;padding:8px 12px;border-radius:999px;font-weight:700;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease}
.btn:hover{transform:translateY(-1px);box-shadow:0 10px 22px rgba(15,23,42,.15)}
.btn.secondary{background:transparent;color:var(--text);border:1px solid var(--border)}
.btn.icon{width:34px;height:34px;padding:0;display:flex;align-items:center;justify-content:center}
.btn.active{border-color:var(--accent);box-shadow:0 0 0 2px rgba(89,217,200,.15)}
.preview-shell{background:#0f1520;border:1px solid #222b3a;border-radius:18px;padding:0;width:100%;max-width:none;margin:0 auto}
body.ub24 .preview-shell{background:#eef1f6;border-color:#e5e7eb}
body.ub24 .apple-bar{display:none}
body.ub24 .preview-shell{padding:0;border-radius:12px}
body.ub24 .apple{border-radius:12px;border:1px solid #e5e7eb}
body.ub24 .canvas{padding:24px;background:#f8fafc}
body.ub24 .block{border:1px solid #e5e7eb;box-shadow:0 4px 14px rgba(15,23,42,.06)}
body.ub24 .block.selected{border-color:#2563eb}
body.ub24 .block-actions, body.ub24 .resize-handle, body.ub24 .scroll-btn{display:none !important}
body.ub24 #btnAlign, body.ub24 #btnBack{display:none}
body.ub24 .topbar .top-meta{display:none}
body.ub24 .block-item{display:flex;align-items:center;gap:8px}
body.ub24 .block-item:before{content:"*";color:#6b7280;font-size:12px}
body.ub24 .inspector .section-title{margin-top:0}
body.ub24 .inspector .acc{background:#f9fafb;border-color:#e5e7eb}
body.ub24 .inspector label{color:#6b7280}
.apple{background:var(--site-bg,#f6f7fb);color:var(--site-text,#0b0c10);font-family:var(--site-font-body,Manrope),system-ui,sans-serif;border-radius:14px;border:1px solid #d1d5db;overflow:hidden}
.apple h1,.apple h2,.apple h3,.apple h4{font-family:var(--site-font-heading,Manrope),system-ui,sans-serif}
.apple-bar{background:#f8fafc;border-bottom:1px solid #e2e8f0;padding:8px 12px;font-size:12px;color:#334155;display:flex;align-items:center;gap:6px}
.apple-dot{width:9px;height:9px;border-radius:50%}
.red{background:#f87171}.yellow{background:#fbbf24}.green{background:#4ade80}
.canvas{min-height:700px;padding:18px;background:#f6f7fb;transition:background .6s ease,color .4s ease; color:var(--site-text,#0b0c10)}
.block{background:var(--site-card,#fff);border-radius:var(--radius-md);padding:var(--space-3);margin-bottom:var(--space-4);border:1px solid transparent;box-shadow:0 12px 30px rgba(15,23,42,.08);transition:transform .25s ease,box-shadow .25s ease,border-color .25s ease;animation:fadeUp .25s ease;color:var(--site-text,#0b0c10);position:relative;will-change:transform;cursor:grab}
.block.dragging,.block.resizing{transition:none;cursor:grabbing}
body.dragging{user-select:none}
.block:hover{transform:translateY(-2px);box-shadow:0 20px 46px rgba(15,23,42,.14)}
.block.selected{border-color:#7aa7ff}
.empty{padding:32px;border:1px dashed #cbd5e1;border-radius:12px;text-align:center;color:#64748b;background:#fff}
.drop{height:8px;border-radius:6px;background:rgba(122,167,255,.4);margin:6px 0}
@keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
label{font-size:12px;color:var(--muted)}
input,textarea,select{width:100%;background:#0f172a;border:1px solid #1f2937;color:var(--text);padding:8px;border-radius:10px;font-family:inherit}
input[type="color"]{height:36px;padding:4px;background:#0f172a;border-radius:10px}
.top-meta{font-size:12px;color:var(--muted)}
.top-select{background:#0f172a;border:1px solid #1f2937;color:var(--text);padding:6px 8px;border-radius:10px}
.dropzone{background:#0f172a;border:1px dashed #2b364a;color:var(--muted);padding:10px;border-radius:10px;text-align:center;cursor:pointer}
.scroll-btn{position:absolute;right:18px;bottom:18px;width:36px;height:36px;border-radius:999px;border:1px solid #d1d5db;background:#ffffff;box-shadow:0 10px 24px rgba(15,23,42,.15);cursor:pointer}
.social-icons{display:flex;flex-wrap:wrap;gap:10px}
.social-btn{display:flex;align-items:center;gap:6px;background:var(--site-card);border:1px solid #e5e7eb;padding:6px 10px;border-radius:999px;color:var(--site-text);text-decoration:none;font-size:13px;transition:transform .18s ease,box-shadow .18s ease,background .2s ease,border-color .2s ease}
.social-btn i{transition:transform .18s ease,filter .18s ease}
.social-btn:hover{transform:translateY(-2px);box-shadow:0 12px 24px rgba(15,23,42,.16)}
.social-btn:hover i{transform:scale(1.06);filter:drop-shadow(0 6px 14px rgba(15,23,42,.18))}
.social-style-circle .social-btn{width:38px;height:38px;padding:0;justify-content:center;border-radius:999px}
.social-style-circle .social-btn span{display:none}
.social-style-outline .social-btn{background:transparent;border:1px dashed #cbd5e1}
.social-style-minimal .social-btn{background:transparent;border:0;padding:0}
.social-style-minimal .social-btn i{background:transparent}
.social-style-solid .social-btn{background:var(--site-primary);border-color:var(--site-primary);color:#0b0f16}
.social-style-solid .social-btn i{color:#0b0f16 !important}
.inline-drop{background:#e2e8f0;border:1px dashed #cbd5e1;border-radius:12px;padding:18px;text-align:center;color:#64748b;cursor:pointer}
.image-wrap{position:relative;border-radius:12px;overflow:hidden}
.image-overlay{position:absolute;left:12px;right:12px;bottom:12px;padding:8px 10px;border-radius:10px;background:rgba(15,23,42,.55);backdrop-filter:blur(8px);color:#fff;font-size:13px;line-height:1.3}
.image-overlay.top{top:12px;bottom:auto}
.gallery-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px}
.gallery-slot{min-height:90px;border-radius:10px;overflow:hidden}
.gallery-slot img{width:100%;height:100%;object-fit:cover;display:block}
.calendar-card{background:var(--site-card);border:1px solid #e5e7eb;border-radius:14px;padding:14px}
.calendar-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.calendar-title{font-weight:700}
.calendar-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:6px}
.calendar-dow{font-size:11px;color:var(--site-muted);text-transform:uppercase;letter-spacing:.6px;text-align:center}
.calendar-day{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:10px 0;text-align:center;font-size:12px;color:var(--site-text)}
.calendar-day.muted{color:#94a3b8;background:#f1f5f9}
.calendar-day.today{border-color:var(--site-primary);box-shadow:0 0 0 2px rgba(89,217,200,.15)}
.hero-pro{display:grid;gap:var(--space-4)}
.hero-layout{display:grid;grid-template-columns:minmax(260px,1fr) minmax(300px,.92fr);gap:var(--space-5);align-items:stretch}
.hero-copy{display:grid;gap:var(--space-3);align-content:center}
.hero-media{min-height:320px;border-radius:var(--radius-lg);overflow:hidden;border:1px solid #dbe3ee;background:linear-gradient(130deg,#e8eef6,#dce6f1);box-shadow:var(--shadow-soft)}
.hero-media img{width:100%;height:100%;object-fit:cover;display:block}
.hero-media-empty{height:100%;display:flex;align-items:center;justify-content:center;padding:var(--space-4);text-align:center;font-size:13px;color:var(--site-muted)}
.hero-actions{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}
.hero-kicker{text-transform:uppercase;font-size:11px;letter-spacing:2px;color:var(--site-primary);font-weight:700}
.hero-pro h2.editable{margin:0;line-height:1.08;font-size:clamp(30px,4.5vw,58px);font-weight:700;max-width:16ch}
.hero-pro p.editable{margin:0;max-width:58ch;color:var(--site-muted);font-size:clamp(15px,1.6vw,19px);line-height:1.55}
.hero-cta{display:inline-flex;align-items:center;justify-content:center;padding:11px 18px;border-radius:999px;background:var(--site-primary);color:#0b0f16;text-decoration:none;font-weight:700;line-height:1}
.hero-cta-secondary{display:inline-flex;align-items:center;justify-content:center;padding:10px 16px;border-radius:999px;border:1px solid #dbe3ee;background:transparent;color:var(--site-text);text-decoration:none;font-weight:600;line-height:1}
.feature-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px}
.feature-pill{background:var(--site-card);padding:12px;border-radius:12px;text-align:center;font-weight:600;border:1px solid #e5e7eb}
.block .cards-grid{margin-top:10px}
.cards-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}
.card-pro{background:var(--site-card);padding:16px;border-radius:14px;border:1px solid #e5e7eb;box-shadow:0 8px 18px rgba(15,23,42,.06)}
.card-pro-title{font-weight:700;margin-bottom:6px}
.card-pro-desc{color:var(--site-muted);font-size:13px;line-height:1.5}
.contact-pro{display:grid;grid-template-columns:minmax(220px,1fr) minmax(260px,1.15fr);gap:14px}
.contact-card{background:var(--site-card);border:1px solid #e2e8f0;border-radius:14px;padding:14px}
.contact-card h4{margin:0 0 10px;font-size:15px;font-weight:700}
.contact-list{display:grid;gap:8px}
.contact-item{display:grid;grid-template-columns:28px 1fr;gap:10px;align-items:start}
.contact-item i{width:28px;height:28px;border-radius:8px;background:rgba(89,217,200,.15);color:var(--site-primary);display:flex;align-items:center;justify-content:center;font-size:13px}
.contact-item-label{font-size:11px;text-transform:uppercase;letter-spacing:.7px;color:var(--site-muted);line-height:1.2}
.contact-item-value{font-size:14px;color:var(--site-text);line-height:1.35;word-break:break-word}
.contact-item-value a{color:inherit;text-decoration:none}
.contact-form{display:grid;gap:8px}
.contact-form input,.contact-form textarea{width:100%;border:1px solid #dbe3ee;background:#f8fafc;color:#0f172a;border-radius:10px;padding:10px 11px}
.contact-form textarea{min-height:116px;resize:vertical}
.contact-send{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:0;border-radius:10px;padding:10px 14px;background:var(--site-primary);color:#0b0f16;font-weight:700;cursor:pointer}
.contact-send i{font-size:12px}
@media (max-width:860px){.contact-pro{grid-template-columns:1fr}}
.site-nav{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);padding:10px 12px;border:1px solid #dde4ef;border-radius:var(--radius-md);background:rgba(255,255,255,.72);backdrop-filter:blur(8px)}
.site-brand{display:flex;align-items:center;gap:10px;font-weight:800;letter-spacing:.2px;min-width:0}
.site-brand img{height:28px;width:auto;border-radius:8px;border:1px solid #dbe3ee}
.site-brand span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.site-nav-links{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:center}
.site-nav-link{display:inline-flex;align-items:center;height:32px;padding:0 12px;border-radius:999px;border:1px solid transparent;text-decoration:none;font-size:13px;color:var(--site-text);transition:background .2s ease,border-color .2s ease,transform .2s ease}
.site-nav-link:hover{background:#f1f5f9;border-color:#dbe3ee;transform:translateY(-1px)}
.site-nav-cta{display:inline-flex;align-items:center;justify-content:center;padding:9px 14px;border-radius:999px;background:var(--site-primary);color:#0b0f16;text-decoration:none;font-weight:700;white-space:nowrap}
.menu-empty{font-size:12px;color:var(--site-muted)}
.menu-inline{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.menu-accordion{display:none;border:1px solid #e5e7eb;border-radius:var(--radius-md);padding:8px 10px;background:var(--site-card);width:100%}
.menu-accordion summary{list-style:none;cursor:pointer;font-weight:600}
.menu-accordion summary::-webkit-details-marker{display:none}
.menu-links{display:flex;flex-direction:column;gap:8px;margin-top:8px}
.menu-accordion summary::after{content:"v";float:right;color:var(--site-muted)}
.float-whatsapp{position:fixed;right:22px;bottom:22px;width:52px;height:52px;border-radius:999px;background:#25d366;color:#fff;display:flex;align-items:center;justify-content:center;box-shadow:0 14px 30px rgba(0,0,0,.22);z-index:999;text-decoration:none}
.float-whatsapp:hover{transform:translateY(-2px)}
@media (max-width:980px){
.menu-inline{display:none}
.menu-accordion{display:block}
}
.preview-shell.size-phone .menu-inline,
.preview-shell.size-tablet .menu-inline{display:none}
.preview-shell.size-phone .menu-accordion,
.preview-shell.size-tablet .menu-accordion{display:block}
.preview-shell.size-phone .hero-layout,
.preview-shell.size-tablet .hero-layout{grid-template-columns:1fr}
.preview-shell.size-phone .hero-media,
.preview-shell.size-tablet .hero-media{min-height:220px}
@keyframes slowGradient{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
.free-drag a{pointer-events:none}
.block-actions{position:absolute;top:8px;right:8px;display:flex;gap:6px}
.block-actions button{border:0;background:#0f172a;color:#e2e8f0;border-radius:8px;padding:4px 6px;cursor:pointer;font-size:12px}
.block-actions button:hover{background:#1f2937}
.preview-mode .block-actions,
.preview-mode .resize-handle,
.preview-mode .scroll-btn{display:none !important}
body.preview-mode .app{grid-template-columns:1fr !important}
body.preview-mode .sidebar,
body.preview-mode .inspector,
body.preview-mode .topbar{display:none !important}
body.preview-mode .main{padding:0 !important}
body.preview-mode .preview-shell{
max-width:100% !important;
border:0 !important;
border-radius:0 !important;
padding:0 !important;
margin:0 !important;
background:transparent !important;
}
body.preview-mode .apple{
border:0 !important;
border-radius:0 !important;
min-height:100vh;
}
body.preview-mode .canvas{
min-height:100vh !important;
padding:0 !important;
}
.resize-handle{position:absolute;right:8px;bottom:8px;width:14px;height:14px;border-right:2px solid #94a3b8;border-bottom:2px solid #94a3b8;cursor:se-resize;opacity:.75}
.resize-handle.edge{width:10px;height:10px;border:none;background:rgba(148,163,184,.35);border-radius:4px;opacity:.9}
.resize-handle.e{top:50%;right:-4px;transform:translateY(-50%);cursor:e-resize}
.resize-handle.s{left:50%;bottom:-4px;transform:translateX(-50%);cursor:s-resize}
.resize-handle.w{top:50%;left:-4px;transform:translateY(-50%);cursor:w-resize}
.resize-handle.n{left:50%;top:-4px;transform:translateX(-50%);cursor:n-resize}
.resize-handle.ne{top:-4px;right:-4px;cursor:ne-resize}
.resize-handle.nw{top:-4px;left:-4px;cursor:nw-resize}
.resize-handle.se{bottom:-4px;right:-4px;cursor:se-resize}
.resize-handle.sw{bottom:-4px;left:-4px;cursor:sw-resize}
.snap-guide{position:absolute;background:rgba(89,217,200,.6);pointer-events:none;z-index:1}
.snap-guide.v{width:2px;top:0;bottom:0}
.snap-guide.h{height:2px;left:0;right:0}
.resize-handle:hover{opacity:1}
textarea{min-height:70px}
.row{margin-bottom:8px}
details.acc{border:1px solid var(--border);border-radius:12px;padding:8px 10px;margin-bottom:10px;background:#0f172a}
details.acc summary{list-style:none;cursor:pointer;font-weight:600}
details.acc summary::-webkit-details-marker{display:none}
details.acc summary:after{content:"v";float:right;color:var(--muted)}
details.acc[open] summary:after{content:"^"}
.acc-body{margin-top:8px}
.danger{background:transparent;border:1px solid #f87171;color:#f87171;padding:8px;border-radius:999px;width:100%;cursor:pointer}
@media (max-width:1100px){.app{grid-template-columns:220px 1fr}.inspector{display:none}}
.editable{outline:none;cursor:text;border-bottom:1px dashed transparent}
.editable:focus{border-bottom-color:var(--site-primary)}
.editable:empty:before{content:attr(data-placeholder);color:var(--site-muted)}
/* UB24 (base44-like) skin */
body.ub24{background:#eef1f6;color:#0b0f16}
body.ub24 .app{grid-template-columns:260px 1fr 320px}
body.ub24 .sidebar, body.ub24 .inspector{background:#ffffff;border-color:#e5e7eb;color:#0b0f16}
body.ub24 .section-title{color:#6b7280}
body.ub24 .block-item{background:#f3f4f6;border-color:#e5e7eb;color:#0b0f16}
body.ub24 .topbar{background:#ffffff;border:1px solid #e5e7eb;border-radius:12px;padding:10px}
body.ub24 .preview-shell{background:#f8fafc;border-color:#e5e7eb}
body.ub24 .apple{border-color:#e5e7eb}
body.ub24 .block{box-shadow:0 4px 14px rgba(15,23,42,.06)}
body.ub24 .btn{background:#2563eb;color:#fff}
body.ub24 .btn.secondary{background:#fff;color:#0b0f16;border-color:#e5e7eb}
body.ub24 input, body.ub24 textarea, body.ub24 select{background:#f9fafb;color:#0b0f16;border-color:#e5e7eb}
</style>
</head>
<body class="{{ 'ub24' if builder_mode == 'ub24' else '' }}">
<div class="app">
<aside class="sidebar">
<div class="brand"><span class="dot"></span>{{ 'Page Builder' if builder_mode == 'ub24' else 'GKACHELE Builder' }}</div>
<div class="section-title">{{ 'Componentes' if builder_mode == 'ub24' else 'Arrastrar' }}</div>
<div id="blockList">
<div class="block-item" draggable="true" data-type="menu" data-multi="false">Menu superior</div>
<div class="block-item" draggable="true" data-type="text" data-multi="true">Texto</div>
<div class="block-item" draggable="true" data-type="image" data-multi="true">Imagen</div>
<div class="block-item" draggable="true" data-type="features" data-multi="true">Caracteristicas</div>
<div class="block-item" draggable="true" data-type="gallery" data-multi="true">Galeria</div>
<div class="block-item" draggable="true" data-type="cards" data-multi="true">Tarjetas</div>
<div class="block-item" draggable="true" data-type="iconlist" data-multi="true">Iconos</div>
<div class="block-item" draggable="true" data-type="contact" data-multi="false">Contacto</div>
<div class="block-item" draggable="true" data-type="map" data-multi="false">Mapa</div>
<div class="block-item" draggable="true" data-type="button" data-multi="true">Boton</div>
<div class="block-item" draggable="true" data-type="video" data-multi="true">Video</div>
<div class="block-item" draggable="true" data-type="social" data-multi="false">Redes</div>
<div class="block-item" draggable="true" data-type="review" data-multi="true">Resena</div>
<div class="block-item" draggable="true" data-type="calendar" data-multi="false">Calendario</div>
</div>
</aside>
<main class="main">
<div class="topbar">
<div style="font-weight:600">{{ 'Nueva pagina' if builder_mode == 'ub24' else 'Pagina Apple' }}</div>
<div style="color:var(--muted);font-size:12px">Arrastra bloques desde la izquierda.</div>
<div class="top-meta" id="blockCount">Bloques: 0 (Ilimitado)</div>
<div style="margin-left:auto;display:flex;gap:8px;align-items:center">
{% if builder_mode == 'ub24' %}
<select id="pageSelect" class="top-select">
<option value="home">Inicio</option>
<option value="servicios">Servicios</option>
<option value="galeria">Galeria</option>
<option value="contacto">Contacto</option>
</select>
{% endif %}
<select id="templateSelect" class="top-select">
<option value="">Plantillas</option>
<option value="servicios">Servicios</option>
<option value="industrial">Industrial</option>
<option value="restaurante">Restaurante</option>
<option value="streaming">Streaming</option>
</select>
<button class="btn secondary" id="btnBack">Atras</button>
<button class="btn secondary" id="btnPreview">Vista previa</button>
<button class="btn secondary" id="btnFullPage">Completo</button>
<button class="btn secondary" id="btnTheme">Claro</button>
<button class="btn secondary icon" id="btnSizePhone" title="Movil"><i class="fa-solid fa-mobile-screen-button"></i></button>
<button class="btn secondary icon" id="btnSizeTablet" title="Tablet"><i class="fa-solid fa-tablet-screen-button"></i></button>
<button class="btn secondary icon active" id="btnSizeDesktop" title="Web"><i class="fa-solid fa-display"></i></button>
<button class="btn secondary" id="btnFreeDrag">Modo libre</button>
<button class="btn secondary" id="btnAlign">Alinear</button>
<button class="btn" id="btnSave">Publicar</button>
<div class="save-status" id="saveStatus">Listo</div>
</div>
</div>
<div class="preview-shell">
<div class="apple">
<div class="apple-bar">
<span class="apple-dot red"></span><span class="apple-dot yellow"></span><span class="apple-dot green"></span>
</div>
<div class="canvas" id="previewCanvas"></div>
</div>
</div>
</main>
<aside class="inspector">
{% if builder_mode == 'ub24' %}
<div class="section-title" style="margin-top:0">Tema Global</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px">
<button class="btn secondary" style="border-radius:10px;padding:6px 10px">Moderno</button>
<button class="btn secondary" style="border-radius:10px;padding:6px 10px">Oscuro</button>
<button class="btn secondary" style="border-radius:10px;padding:6px 10px">Natural</button>
</div>
{% endif %}
<details class="acc" open>
<summary>Bloque</summary>
<div class="acc-body" id="inspectorPanel" style="color:var(--muted);font-size:13px">Selecciona un bloque para editarlo.</div>
</details>
<details class="acc" open>
<summary>Marca</summary>
<div class="acc-body">
<div class="row"><label>Nombre del sitio</label><input id="siteNameInput" type="text"></div>
<div class="row">
<label>Logo (arrastrar imagen)</label>
<div class="dropzone" id="logoDrop">Suelta imagen o click</div>
<input id="logoFileInput" type="file" accept="image/*" hidden>
</div>
</div>
</details>
<details class="acc" open>
<summary>Colores</summary>
<div class="acc-body">
<div class="row"><label>Color principal</label><input id="primaryColorInput" type="color"></div>
<div class="row"><label>Fondo base</label><input id="bgColorInput" type="color"></div>
<div class="row"><label>Fondo secundario</label><input id="bgColor2Input" type="color"></div>
<div class="row"><label>Usar gradiente</label><input id="bgGradientToggle" type="checkbox"></div>
<div class="row"><label>Color texto</label><input id="textColorInput" type="color"></div>
<div class="row"><label>Color secundario</label><input id="mutedColorInput" type="color"></div>
</div>
</details>
<details class="acc">
<summary>Tipografias</summary>
<div class="acc-body">
<div class="row"><label>Tipografia texto</label>
<select id="fontBodySelect">
<option value="Manrope">Manrope</option>
<option value="DM Sans">DM Sans</option>
<option value="Space Grotesk">Space Grotesk</option>
<option value="Outfit">Outfit</option>
<option value="Sora">Sora</option>
<option value="Poppins">Poppins</option>
<option value="IBM Plex Sans">IBM Plex Sans</option>
<option value="Merriweather">Merriweather</option>
</select>
</div>
<div class="row"><label>Tipografia titulos</label>
<select id="fontHeadingSelect">
<option value="Manrope">Manrope</option>
<option value="DM Sans">DM Sans</option>
<option value="Space Grotesk">Space Grotesk</option>
<option value="Outfit">Outfit</option>
<option value="Sora">Sora</option>
<option value="Poppins">Poppins</option>
<option value="Playfair Display">Playfair Display</option>
<option value="Merriweather">Merriweather</option>
</select>
</div>
</div>
</details>
<details class="acc">
<summary>Fondos</summary>
<div class="acc-body">
<div class="row">
<label>Fondo (arrastrar imagen)</label>
<div class="dropzone" id="bgDrop">Suelta imagen o click</div>
<input id="bgFileInput" type="file" accept="image/*" hidden>
</div>
<div class="row"><label>Video fondo (URL)</label><input id="bgVideoInput" type="text" placeholder="https://..."></div>
<div class="row"><label>Fondo animado (URL GIF)</label><input id="bgAnimInput" type="text" placeholder="https://..."></div>
<div class="row"><label>Animacion de fondo</label>
<select id="bgMotionSelect">
<option value="none">Sin animacion</option>
<option value="slow">Gradiente animado</option>
</select>
</div>
</div>
</details>
<details class="acc">
<summary>Layout</summary>
<div class="acc-body">
<div class="row"><label>Animaciones</label><input id="animToggle" type="checkbox"></div>
</div>
</details>
</aside>
</div>
<script>
const SITE_ID = {{ site_id }};
const SITE_SLUG = "{{ slug }}";
const SERVER_CONTENT = {{ content|tojson }};
const BUILDER_MODE = "{{ builder_mode or 'default' }}";
const FULL_PAGE_MODE = new URLSearchParams(window.location.search).get("full") === "1";
const defaultSettings = {
site_name: "{{ slug }}",
primary_color: "#59d9c8",
bg_color: "#f6f7fb",
text_color: "#0b0c10",
muted_color: "#6b7280",
font_body: "Manrope",
font_heading: "Manrope",
free_drag: false,
bg_color2: "#e9eef5",
bg_gradient: false,
animations: true,
theme: "light",
bg_motion: "none",
bg_anim_url: "",
logo_url: "",
bg_image_url: "",
bg_video_url: ""
};
const templates = {
servicios: {
settings: { primary_color: '#1d4ed8', bg_color: '#f6f7fb', text_color: '#0b0c10', muted_color: '#6b7280', font_body: 'Manrope', font_heading: 'Manrope', bg_gradient: true, bg_color2: '#e9eef5' },
blocks: [
{ id: makeId(), type: 'menu', data: defaultData('menu') },
{ id: makeId(), type: 'hero', data: { title: 'Servicios profesionales para tu negocio', subtitle: 'Crecemos contigo con soluciones claras y resultados medibles.', button_text: 'Cotizar ahora', button_url: '#contacto', image_url: '' } },
{ id: makeId(), type: 'features', data: { title: 'Beneficios', items: ['Rapido','Profesional','Confiable'] } },
{ id: makeId(), type: 'cards', data: { title: 'Servicios', items: ['Consultoria|Estrategia y ejecucion','Marketing|Crecimiento real','Soporte|Atencion prioritaria'] } },
{ id: makeId(), type: 'gallery', data: { title: 'Proyectos', images: ['','',''], captions: ['','',''], fit: 'cover' } },
{ id: makeId(), type: 'contact', data: { title: 'Hablemos', email: '', phone: '', address: '' } },
{ id: makeId(), type: 'social', data: defaultData('social') }
]
},
industrial: {
settings: { primary_color: '#f97316', bg_color: '#f8fafc', text_color: '#0b0c10', muted_color: '#6b7280', font_body: 'IBM Plex Sans', font_heading: 'Space Grotesk', bg_gradient: false },
blocks: [
{ id: makeId(), type: 'menu', data: defaultData('menu') },
{ id: makeId(), type: 'hero', data: { title: 'Soluciones industriales confiables', subtitle: 'Calidad, seguridad y cumplimiento en cada proyecto.', button_text: 'Solicitar presupuesto', button_url: '#contacto', image_url: '' } },
{ id: makeId(), type: 'iconlist', data: { title: 'Diferenciales', items: ['Certificados|Normas y seguridad','Experiencia|Equipos expertos','Entrega|Plazos claros'] } },
{ id: makeId(), type: 'features', data: { title: 'Servicios', items: ['Mantenimiento','Montajes','Ingenieria'] } },
{ id: makeId(), type: 'gallery', data: { title: 'Proyectos', images: ['','',''], captions: ['','',''], fit: 'cover' } },
{ id: makeId(), type: 'contact', data: { title: 'Contacto', email: '', phone: '', address: '' } }
]
},
restaurante: {
settings: { primary_color: '#ef4444', bg_color: '#fff7ed', text_color: '#0b0c10', muted_color: '#6b7280', font_body: 'Poppins', font_heading: 'Playfair Display', bg_gradient: false },
blocks: [
{ id: makeId(), type: 'menu', data: defaultData('menu') },
{ id: makeId(), type: 'hero', data: { title: 'Sabores que enamoran', subtitle: 'Cocina artesanal, ambiente unico y atencion cercana.', button_text: 'Reservar', button_url: '#contacto', image_url: '' } },
{ id: makeId(), type: 'gallery', data: { title: 'Platos destacados', images: ['','',''], captions: ['','',''], fit: 'cover' } },
{ id: makeId(), type: 'cards', data: { title: 'Especialidades', items: ['Entradas|Frescas y ligeras','Platos fuertes|Hechos al momento','Postres|Dulce final'] } },
{ id: makeId(), type: 'review', data: { title: 'Rese?as', name: 'Cliente feliz', text: 'Excelente sabor y servicio impecable.', rating: 5, style: 'card' } },
{ id: makeId(), type: 'contact', data: { title: 'Reservas', email: '', phone: '', address: '' } },
{ id: makeId(), type: 'map', data: { title: 'Ubicacion', address: '' } }
]
},
streaming: {
settings: { primary_color: '#4f8cff', bg_color: '#0b0f16', text_color: '#e7ebf0', muted_color: '#98a3b6', font_body: 'DM Sans', font_heading: 'Space Grotesk', bg_gradient: true, bg_color2: '#111827' },
blocks: [
{ id: makeId(), type: 'menu', data: defaultData('menu') },
{ id: makeId(), type: 'hero', data: { title: 'Tu marca, tu estilo.', subtitle: 'Dise?o sin limites con contenido que impacta desde el primer segundo.', button_text: 'Empezar ahora', button_url: '#contacto', image_url: '' } },
{ id: makeId(), type: 'features', data: { title: 'Beneficios', items: ['Catalogo ordenado','Experiencia inmersiva','Conversion clara'] } },
{ id: makeId(), type: 'gallery', data: { title: 'Contenido destacado', images: ['','',''], captions: ['','',''], fit: 'cover' } },
{ id: makeId(), type: 'button', data: { text: 'Crear mi cuenta', url: '#contacto', style: 'primary', size: 'lg' } },
{ id: makeId(), type: 'contact', data: { title: 'Contacto', email: '', phone: '', address: '' } }
]
}
};
const state = {
settings: { ...defaultSettings, ...((SERVER_CONTENT && SERVER_CONTENT.settings) ? SERVER_CONTENT.settings : {}) },
blocks: Array.isArray(SERVER_CONTENT && SERVER_CONTENT.blocks) ? SERVER_CONTENT.blocks : []
};
let selectedBlockId = null;
let currentPage = "home";
let dropIndicator = null;
let isDraggingFree = false;
let dragStart = { x: 0, y: 0, left: 0, top: 0, id: null };
let isResizingFree = false;
let resizeStart = { x: 0, y: 0, width: 0, height: 0, canvasWidth: 1, id: null, handle: "se", aspect: 1 };
let snapGuides = [];
let dragRaf = 0;
let resizeRaf = 0;
let pendingMove = null;
let pendingResize = null;
let previewStateBefore = null;
let isSaving = false;
function makeId(){ return "block_" + Date.now() + "_" + Math.floor(Math.random()*1000); }
function getDefaultPos(){
const base = 20;
const gap = 120;
const i = state.blocks.length;
return { x: base + (i % 2) * 220, y: base + Math.floor(i / 2) * gap };
}
function defaultData(type){
switch(type){
case "menu": return { title:"Menu", items:[], menu_mode:"both", full_width:true };
case "hero": return { title:"Tu propuesta de valor", subtitle:"Explica en una frase por que elegirte.", button_text:"Contactar", button_url:"#contacto", image_url:"" };
case "text": return { text:"Describe tu negocio aqui." };
case "image": return { url:"", alt:"", caption:"", fit:"cover", overlay_text:"" };
case "features": return { title:"Beneficios", items:["Rapido","Profesional","Confiable"] };
case "gallery": return { title:"Proyectos", images:["","",""], captions:["","",""], fit:"cover" };
case "cards": return { title:"Servicios", items:["Titulo 1|Texto breve","Titulo 2|Texto breve","Titulo 3|Texto breve"] };
case "iconlist": return { title:"Diferenciales", items:["Rapido|Ahorra tiempo","Seguro|Datos protegidos","Soporte|Respuesta rapida"] };
case "contact": return { title:"Hablemos", email:"", phone:"", address:"" };
case "map": return { title:"Ubicacion", address:"" };
case "button": return { text:"Accion", url:"#", style:"primary", size:"md"};
case "social": return { instagram:"", facebook:"", whatsapp:"", tiktok:"", youtube:"", icon_size:18, icon_color:"#0b0c10", show_text:true, icon_style:"pill" };
case "video": return { url:"", description:"" };
case "review": return { title:"Reseña destacada", name:"Cliente feliz", text:"Excelente servicio y resultados profesionales.", rating:5, style:"card" };
case "calendar": return { title:"Agenda una cita", note:"Disponible en plan premium. Proximamente.", embed_url:"" };
default: return {};
}
}
function repairMojibake(text){
const raw = String(text || "");
if (!/[ÃÂ]/.test(raw)) return raw;
try { return decodeURIComponent(escape(raw)); } catch(_e) { return raw; }
}
function escapeHtml(str){
const safe = repairMojibake(str);
return String(safe).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/\"/g,"&quot;").replace(/'/g,"&#039;");
}
function editable(tag, field, text, placeholder, multiline, style){
const ph = placeholder ? ` data-placeholder="${escapeHtml(placeholder)}"` : ` data-placeholder=""`;
const ml = multiline ? ` data-multiline="true"` : "";
const st = style ? ` style="${style}"` : "";
return `<${tag} class="editable" data-field="${escapeHtml(field)}"${ph}${ml} contenteditable="true"${st}>${escapeHtml(text||"")}</${tag}>`;
}
function setBlockField(block, field, value){
if (!block || !field) return;
if (!block.data) block.data = {};
const clean = String(value ?? "").replace(/\s+$/,"");
if (field.startsWith("items.")){
const parts = field.split(".");
const idx = Number(parts[1] || 0);
const sub = parts[2] || "";
const arr = Array.isArray(block.data.items) ? block.data.items : [];
while (arr.length <= idx) arr.push("");
if (!sub){
arr[idx] = clean;
} else {
const raw = String(arr[idx] || "");
const split = raw.split("|");
const title = sub === "title" ? clean : (split[0] || "");
const desc = sub === "desc" ? clean : (split[1] || "");
arr[idx] = `${title}|${desc}`;
}
block.data.items = arr;
return;
}
if (field.startsWith("captions.")){
const parts = field.split(".");
const idx = Number(parts[1] || 0);
const arr = Array.isArray(block.data.captions) ? block.data.captions : [];
while (arr.length <= idx) arr.push("");
arr[idx] = clean;
block.data.captions = arr;
return;
}
block.data[field] = clean;
}
function normalizeLink(url){
if (!url) return "";
const trimmed = String(url).trim();
if (!trimmed) return "";
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
if (trimmed.startsWith("mailto:") || trimmed.startsWith("tel:")) return trimmed;
return "https://" + trimmed;
}
function normalizeVideoUrl(url){
if (!url) return "";
const u=url.trim();
if (u.includes("youtube.com/shorts/")){ const id=u.split("youtube.com/shorts/")[1].split(/[?&]/)[0]; return "https://www.youtube.com/embed/"+id; }
if (u.includes("youtube.com/watch")){ const id=new URL(u).searchParams.get("v"); return id? "https://www.youtube.com/embed/"+id : u; }
if (u.includes("youtu.be/")){ const id=u.split("youtu.be/")[1].split(/[?&]/)[0]; return "https://www.youtube.com/embed/"+id; }
return u;
}
function buildCalendarHtml(){
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const monthNames = ["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"];
const dow = ["Lun","Mar","Mie","Jue","Vie","Sab","Dom"];
const first = new Date(year, month, 1);
const last = new Date(year, month + 1, 0);
const startIdx = (first.getDay() + 6) % 7; // Monday start
const daysInMonth = last.getDate();
const cells = [];
for (let i = 0; i < startIdx; i++){ cells.push({ d:"", muted:true }); }
for (let d = 1; d <= daysInMonth; d++){ cells.push({ d, muted:false, today: d === now.getDate() }); }
while (cells.length % 7 !== 0){ cells.push({ d:"", muted:true }); }
const head = `<div class="calendar-head"><div class="calendar-title">${monthNames[month]} ${year}</div><div style="font-size:12px;color:var(--site-muted)">Agenda</div></div>`;
const dowRow = dow.map(d=>`<div class="calendar-dow">${d}</div>`).join("");
const grid = cells.map(c=>`<div class="calendar-day ${c.muted ? "muted" : ""} ${c.today ? "today" : ""}">${c.d || ""}</div>`).join("");
return `<div class="calendar-card">${head}<div class="calendar-grid">${dowRow}${grid}</div></div>`;
}
function readFile(file, cb){
const reader = new FileReader();
reader.onload = () => cb(reader.result);
reader.readAsDataURL(file);
}
function bindDrop(dropEl, inputEl, setter, labelLoaded){
if (!dropEl || !inputEl) return;
dropEl.addEventListener("click",()=>inputEl.click());
dropEl.addEventListener("dragover",(e)=>{ e.preventDefault(); dropEl.style.borderColor = "#59d9c8"; });
dropEl.addEventListener("dragleave",()=>{ dropEl.style.borderColor = "#2b364a"; });
dropEl.addEventListener("drop",(e)=>{
e.preventDefault();
dropEl.style.borderColor = "#2b364a";
const file = e.dataTransfer.files && e.dataTransfer.files[0];
if (file){ readFile(file, (data)=>{ setter(data); dropEl.textContent = labelLoaded || "Imagen cargada"; renderPreview(); }); }
});
inputEl.addEventListener("change",()=>{
const file = inputEl.files && inputEl.files[0];
if (file){ readFile(file, (data)=>{ setter(data); dropEl.textContent = labelLoaded || "Imagen cargada"; renderPreview(); }); }
});
}
function bindInlineImageDrop(container, block){
const inline = container.querySelector("[data-inline-image]");
if (!inline) return;
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.hidden = true;
container.appendChild(input);
const setImage = (dataUrl)=>{
block.data.url = dataUrl;
const urlInput = document.getElementById("imageUrl");
if (urlInput) urlInput.value = dataUrl;
renderPreview();
};
inline.addEventListener("click",(e)=>{ e.stopPropagation(); input.click(); });
inline.addEventListener("dragover",(e)=>{ e.preventDefault(); e.stopPropagation(); inline.style.borderColor = "#59d9c8"; });
inline.addEventListener("dragleave",()=>{ inline.style.borderColor = "#cbd5e1"; });
inline.addEventListener("drop",(e)=>{
e.preventDefault(); e.stopPropagation(); inline.style.borderColor = "#cbd5e1";
const file = e.dataTransfer.files && e.dataTransfer.files[0];
if (file){ readFile(file, setImage); }
});
input.addEventListener("change",()=>{
const file = input.files && input.files[0];
if (file){ readFile(file, setImage); }
});
}
function bindInlineVideoDrop(container, block){
const inline = container.querySelector("[data-inline-video]");
if (!inline) return;
const input = document.createElement("input");
input.type = "file";
input.accept = "video/*";
input.hidden = true;
container.appendChild(input);
const setVideo = (dataUrl)=>{
block.data.url = dataUrl;
const urlInput = document.getElementById("videoUrl");
if (urlInput) urlInput.value = dataUrl;
renderPreview();
};
inline.addEventListener("click",(e)=>{ e.stopPropagation(); input.click(); });
inline.addEventListener("dragover",(e)=>{ e.preventDefault(); e.stopPropagation(); inline.style.borderColor = "#59d9c8"; });
inline.addEventListener("dragleave",()=>{ inline.style.borderColor = "#cbd5e1"; });
inline.addEventListener("drop",(e)=>{
e.preventDefault(); e.stopPropagation(); inline.style.borderColor = "#cbd5e1";
const file = e.dataTransfer.files && e.dataTransfer.files[0];
if (file){ readFile(file, setVideo); }
});
input.addEventListener("change",()=>{
const file = input.files && input.files[0];
if (file){ readFile(file, setVideo); }
});
}
function bindGalleryDrops(container, block){
const slots = container.querySelectorAll("[data-gallery-index]");
if (!slots.length) return;
slots.forEach((slot)=>{
const idx = Number(slot.getAttribute("data-gallery-index") || 0);
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.hidden = true;
slot.appendChild(input);
const setImage = (dataUrl)=>{
const imgs = Array.isArray(block.data.images) ? block.data.images : [];
while (imgs.length < 3) imgs.push("");
imgs[idx] = dataUrl;
block.data.images = imgs;
const textarea = document.getElementById("galleryImages");
if (textarea) textarea.value = imgs.join("\n");
renderPreview();
};
slot.addEventListener("click",(e)=>{ e.stopPropagation(); input.click(); });
slot.addEventListener("dragover",(e)=>{ e.preventDefault(); e.stopPropagation(); slot.style.outline = "2px dashed #59d9c8"; });
slot.addEventListener("dragleave",()=>{ slot.style.outline = "none"; });
slot.addEventListener("drop",(e)=>{
e.preventDefault(); e.stopPropagation(); slot.style.outline = "none";
const file = e.dataTransfer.files && e.dataTransfer.files[0];
if (file){ readFile(file, setImage); }
});
input.addEventListener("change",()=>{
const file = input.files && input.files[0];
if (file){ readFile(file, setImage); }
});
});
}
function renderBlockHtml(block){
if (block.type==="menu"){
const items = state.blocks
.filter(b=>b.type!=="menu")
.map((b, i)=>{
const title = (b.data && (b.data.title || b.data.text || b.type)) || b.type;
return { id: b.id, label: String(title).slice(0, 24), index: i+1 };
});
const links = items.map(it=>`<a class="site-nav-link" href="#${it.id}">${escapeHtml(it.label)}</a>`).join("");
const mobileLinks = items.map(it=>`<a class="site-nav-link" href="#${it.id}">${escapeHtml(it.label)}</a>`).join("");
const logo = state.settings.logo_url ? `<img src="${escapeHtml(state.settings.logo_url)}" alt="Logo" />` : "";
const mode = (block.data?.menu_mode || "both").toLowerCase();
const showInline = mode === "both" || mode === "inline";
const showAccordion = mode === "both" || mode === "accordion";
return `<div class="site-nav">
<div class="site-brand">${logo}<span>${escapeHtml(state.settings.site_name||"GKACHELE")}</span></div>
<div class="menu-inline site-nav-links" style="${showInline ? "" : "display:none"}">${links}</div>
<details class="menu-accordion" style="${showAccordion ? "" : "display:none"}">
<summary>Menu</summary>
<div class="menu-links">${mobileLinks}</div>
</details>
</div>`;
}
if (block.type==="hero"){
const image = block.data.image_url ? `<img src="${escapeHtml(block.data.image_url)}" alt="Hero image">` : `<div class="hero-media-empty">Agrega imagen en "Imagen URL" para completar el hero.</div>`;
return `<div class="hero-pro hero-layout">
<div class="hero-copy">
<div class="hero-kicker">${escapeHtml(state.settings.site_name||"GKACHELE")}</div>
${editable("h2","title",block.data.title,"Titulo",false,"")}
${editable("p","subtitle",block.data.subtitle,"Subtitulo",true,"")}
<div class="hero-actions">
<a href="${escapeHtml(block.data.button_url||"#")}" class="editable hero-cta" data-field="button_text" data-placeholder="Boton" contenteditable="true">${escapeHtml(block.data.button_text)}</a>
<a href="#servicios" class="hero-cta-secondary">Ver servicios</a>
</div>
</div>
<div class="hero-media">${image}</div>
</div>`;
}
if (block.type==="text"){ return editable("p","text",block.data.text,"Escribe aqui...",true,"margin:0;color:var(--site-text)"); }
if (block.type==="image"){
const fit = block.data.fit || "cover";
if (block.data.url){
const cap = (BUILDER_MODE === "ub24")
? editable("div","caption",block.data.caption,"Descripcion",true,"margin-top:6px;color:var(--site-muted);font-size:12px")
: (block.data.caption ? `<div style="margin-top:6px;color:var(--site-muted);font-size:12px">${escapeHtml(block.data.caption)}</div>` : "");
const overlay = editable("div","overlay_text",block.data.overlay_text,"Texto sobre imagen",true,`margin:0;${block.data.overlay_text ? "" : ""}`);
return `<div><div class="image-wrap gallery-slot" data-inline-image="true"><img src="${escapeHtml(block.data.url)}" alt="${escapeHtml(block.data.alt)}" style="object-fit:${escapeHtml(fit)}"><div class="image-overlay">${overlay}</div></div>${cap}</div>`;
}
return `<div class="inline-drop" data-inline-image="true">Suelta imagen o click</div>`;
}
if (block.type==="features"){
const items = Array.isArray(block.data.items)?block.data.items:[];
return `${editable("h3","title",block.data.title,"Titulo",false,"margin:0 0 10px")}<div class="feature-grid">${items.map((i,idx)=>`<div class="editable feature-pill" data-field="items.${idx}" data-placeholder="Item" contenteditable="true">${escapeHtml(i)}</div>`).join("")}</div>`;
}
if (block.type==="gallery"){
const images = Array.isArray(block.data.images)?block.data.images:[];
const captions = Array.isArray(block.data.captions)?block.data.captions:[];
const fit = block.data.fit || "cover";
const slots = [0,1,2].map((i)=>images[i] || "");
return `<h3 style="margin:0 0 10px">${escapeHtml(block.data.title)}</h3><div class="gallery-grid">${slots.map((img,i)=>img?`<div><div class="gallery-slot" data-gallery-index="${i}"><img src="${escapeHtml(img)}" style="object-fit:${escapeHtml(fit)}"></div>${BUILDER_MODE === "ub24" ? editable("div",`captions.${i}`,captions[i]||"","Descripcion",true,"margin-top:4px;color:var(--site-muted);font-size:12px") : `<div style="margin-top:4px;color:var(--site-muted);font-size:12px">${escapeHtml(captions[i]||"")}</div>`}</div>`:`<div class="gallery-slot" data-gallery-index="${i}"><div class="inline-drop">Suelta imagen o click</div></div>`).join("")}</div>`;
}
if (block.type==="cards"){
const items = Array.isArray(block.data.items)?block.data.items:[];
return `${editable("h3","title",block.data.title,"Titulo",false,"")}<div class="cards-grid">${items.map((raw,idx)=>{const parts=String(raw).split("|");const t=parts[0]||"";const d=parts[1]||"";return `<div class="card-pro"><div class="editable card-pro-title" data-field="items.${idx}.title" data-placeholder="Titulo" contenteditable="true">${escapeHtml(t)}</div><div class="editable card-pro-desc" data-field="items.${idx}.desc" data-placeholder="Descripcion" contenteditable="true">${escapeHtml(d)}</div></div>`;}).join("")}</div>`;
}
if (block.type==="iconlist"){
const items = Array.isArray(block.data.items)?block.data.items:[];
return `${editable("h3","title",block.data.title,"Titulo",false,"margin:0 0 10px")}<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px">${items.map((raw,idx)=>{const parts=String(raw).split("|");const t=parts[0]||"";const d=parts[1]||"";const letter=(t||"I").trim().slice(0,1).toUpperCase();return `<div style="display:flex;gap:10px;align-items:flex-start;background:var(--site-card);padding:12px;border-radius:12px"><div style="width:34px;height:34px;border-radius:10px;background:rgba(89,217,200,.2);display:flex;align-items:center;justify-content:center;font-weight:700;color:var(--site-primary)">${escapeHtml(letter)}</div><div><div class="editable" data-field="items.${idx}.title" data-placeholder="Titulo" contenteditable="true" style="font-weight:600">${escapeHtml(t)}</div><div class="editable" data-field="items.${idx}.desc" data-placeholder="Descripcion" contenteditable="true" style="color:var(--site-muted);font-size:13px">${escapeHtml(d)}</div></div></div>`;}).join("")}</div>`;
}
if (block.type==="contact"){
const emailVal = escapeHtml(block.data.email || "");
const phoneVal = escapeHtml(block.data.phone || "");
const addressVal = escapeHtml(block.data.address || "");
const email = emailVal ? `<a href="mailto:${emailVal}">${emailVal}</a>` : "Sin correo configurado";
const phone = phoneVal ? `<a href="tel:${phoneVal}">${phoneVal}</a>` : "Sin telefono configurado";
const address = addressVal || "Sin direccion configurada";
return `
${editable("h3","title",block.data.title,"Contacto",false,"margin:0 0 12px;font-size:clamp(24px,2.4vw,34px);line-height:1.1")}
<div class="contact-pro">
<div class="contact-card">
<h4>Canales directos</h4>
<div class="contact-list">
<div class="contact-item">
<i class="fa-solid fa-envelope"></i>
<div>
<div class="contact-item-label">Email</div>
<div class="contact-item-value">${email}</div>
</div>
</div>
<div class="contact-item">
<i class="fa-solid fa-phone"></i>
<div>
<div class="contact-item-label">Telefono</div>
<div class="contact-item-value">${phone}</div>
</div>
</div>
<div class="contact-item">
<i class="fa-solid fa-location-dot"></i>
<div>
<div class="contact-item-label">Direccion</div>
<div class="contact-item-value">${address}</div>
</div>
</div>
</div>
</div>
<div class="contact-card">
<h4>Escribenos</h4>
<div class="contact-form">
<input placeholder="Nombre completo">
<input placeholder="Email de contacto">
<textarea placeholder="Cuéntanos brevemente en qué te ayudamos"></textarea>
<button class="contact-send"><i class="fa-solid fa-paper-plane"></i>Enviar consulta</button>
</div>
</div>
</div>
`;
}
if (block.type==="button"){
const style = (block.data.style || "primary").toLowerCase();
const size = (block.data.size || "md").toLowerCase();
const pad = size === "lg" ? "12px 18px" : size === "sm" ? "6px 12px" : "8px 14px";
let bg = "var(--site-primary)";
let color = "#0b0f16";
let border = "1px solid transparent";
if (style === "outline"){ bg = "transparent"; color = "var(--site-text)"; border = "1px solid var(--site-text)"; }
if (style === "ghost"){ bg = "transparent"; color = "var(--site-text)"; border = "1px dashed #cbd5e1"; }
return `<a href="${escapeHtml(block.data.url)}" class="editable" data-field="text" data-placeholder="Boton" contenteditable="true" style="display:inline-block;padding:${pad};border-radius:999px;background:${bg};color:${color};text-decoration:none;font-weight:600;border:${border};transition:transform .2s ease,box-shadow .2s ease">${escapeHtml(block.data.text)}</a>`;
}
if (block.type==="social"){
const icons = {
instagram:"fa-brands fa-instagram",
facebook:"fa-brands fa-facebook",
whatsapp:"fa-brands fa-whatsapp",
tiktok:"fa-brands fa-tiktok",
youtube:"fa-brands fa-youtube",
email:"fa-solid fa-envelope",
web:"fa-solid fa-globe",
threads:"fa-brands fa-threads"
};
const size = Math.max(14, Math.min(36, Number(block.data?.icon_size || 18)));
const iconColor = block.data?.icon_color || "var(--site-text)";
const showText = block.data?.show_text !== false;
const style = (block.data?.icon_style || "pill").toLowerCase();
const mainKeys = ["whatsapp","instagram","facebook","tiktok","youtube"];
const items = mainKeys.map(k=>[k, (block.data||{})[k]]).filter(([,v])=>v);
const linkFor = (k,v)=>{
if (k==="email") return "mailto:" + String(v).trim();
if (k==="whatsapp"){
const digits = String(v).replace(/\D/g,"");
return digits ? "https://wa.me/" + digits : normalizeLink(v);
}
return normalizeLink(v);
};
return `<h3 style="margin:0 0 10px">Redes</h3><div class="social-icons social-style-${escapeHtml(style)}">${items.map(([k,v],idx)=>`<a class="social-btn" href="${escapeHtml(linkFor(k,v)||"#")}" target="_blank" rel="noreferrer" style="font-size:${size}px"><i class="${icons[k]||'fa-solid fa-circle'}" style="font-size:${size}px;color:${escapeHtml(iconColor)}"></i>${showText ? `<span class="editable" data-field="${escapeHtml(k)}" data-placeholder="${escapeHtml(k)}" contenteditable="true" style="font-size:12px">${escapeHtml(v)}</span>` : ""}</a>`).join("")}</div>`;
}
if (block.type==="map"){
const q = encodeURIComponent(block.data.address || "");
const src = q ? `https://www.google.com/maps?q=${q}&z=15&output=embed` : "";
return `<h3 style="margin:0 0 10px">${escapeHtml(block.data.title||"Ubicacion")}</h3>` + (src ? `<iframe title="Mapa" src="${src}" loading="lazy" referrerpolicy="no-referrer-when-downgrade" style="width:100%;height:260px;border:0;border-radius:12px"></iframe><div style="margin-top:6px;color:var(--site-muted);font-size:12px">${escapeHtml(block.data.address||"")}</div>` : `<div style="background:#e2e8f0;border-radius:12px;padding:20px;text-align:center;color:var(--site-muted)">Ingresa una direccion</div>`);
}
if (block.type==="video"){
const embed = normalizeVideoUrl(block.data.url);
const desc = block.data.description ? `<div style="margin-top:8px;color:var(--site-muted);font-size:12px">${escapeHtml(block.data.description)}</div>` : "";
if (embed){
if (embed.startsWith("data:video/")){
return `<video src="${escapeHtml(embed)}" style="width:100%;border-radius:12px" controls></video>${desc}`;
}
return `<div style="position:relative;padding-top:56.25%;border-radius:12px;overflow:hidden"><iframe src="${escapeHtml(embed)}" style="position:absolute;inset:0;width:100%;height:100%;border:0" allowfullscreen></iframe></div>${desc}`;
}
return `<div class="inline-drop" data-inline-video="true">Suelta video o click</div>`;
}
if (block.type==="review"){
const rating = Math.max(1, Math.min(5, Number(block.data.rating || 5)));
const stars = Array.from({length:5}).map((_,i)=>`<i class="${i<rating ? "fa-solid fa-star" : "fa-regular fa-star"}" style="color:#fbbf24"></i>`).join("");
const style = (block.data.style || "card").toLowerCase();
if (style === "quote"){
return `${editable("h3","title",block.data.title||"Resena","Resena",false,"margin:0 0 10px")}<div style="background:var(--site-card);border:1px solid #e5e7eb;border-radius:12px;padding:16px"><div style="font-size:24px;line-height:1;color:var(--site-primary)">&ldquo;</div><div class="editable" data-field="text" data-placeholder="Texto" contenteditable="true" style="font-style:italic;margin:6px 0">${escapeHtml(block.data.text||"")}</div><div style="display:flex;align-items:center;gap:8px;margin-top:10px"><div>${stars}</div><div class="editable" data-field="name" data-placeholder="Nombre" contenteditable="true" style="font-weight:600">${escapeHtml(block.data.name||"")}</div></div></div>`;
}
if (style === "compact"){
return `${editable("h3","title",block.data.title||"Resena","Resena",false,"margin:0 0 10px")}<div style="display:flex;align-items:center;gap:10px;background:var(--site-card);border:1px solid #e5e7eb;border-radius:12px;padding:12px"><div>${stars}</div><div><div class="editable" data-field="name" data-placeholder="Nombre" contenteditable="true" style="font-weight:600">${escapeHtml(block.data.name||"")}</div><div class="editable" data-field="text" data-placeholder="Texto" contenteditable="true" style="color:var(--site-muted);font-size:13px">${escapeHtml(block.data.text||"")}</div></div></div>`;
}
return `${editable("h3","title",block.data.title||"Resena","Resena",false,"margin:0 0 10px")}<div style="background:var(--site-card);border:1px solid #e5e7eb;border-radius:12px;padding:14px"><div style="margin-bottom:6px">${stars}</div><div class="editable" data-field="text" data-placeholder="Texto" contenteditable="true" style="font-size:14px">${escapeHtml(block.data.text||"")}</div><div class="editable" data-field="name" data-placeholder="Nombre" contenteditable="true" style="margin-top:10px;font-weight:600">${escapeHtml(block.data.name||"")}</div></div>`;
}
if (block.type==="calendar"){
const embed = block.data.embed_url ? normalizeLink(block.data.embed_url) : "";
if (embed){
return `<h3 style="margin:0 0 10px">${escapeHtml(block.data.title||"Calendario")}</h3><div style="border-radius:12px;overflow:hidden;border:1px solid #e5e7eb"><iframe title="Calendario" src="${escapeHtml(embed)}" style="width:100%;height:420px;border:0"></iframe></div>`;
}
return `<h3 style="margin:0 0 10px">${escapeHtml(block.data.title||"Calendario")}</h3>` + buildCalendarHtml() + `<div style="margin-top:8px;color:var(--site-muted);font-size:12px">${escapeHtml(block.data.note||"")}</div>`;
}
return `<div>${escapeHtml(block.type)}</div>`;
}
function updateTopbar(){
const countEl = document.getElementById("blockCount");
if (!countEl) return;
countEl.textContent = `Bloques: ${state.blocks.length} (Ilimitado)`;
}
function updateBlockList(){
const used = new Set(state.blocks.map(b=>b.type));
document.querySelectorAll("#blockList .block-item").forEach((item)=>{
const type = item.dataset.type;
if (used.has(type)) item.style.display = "none";
else item.style.display = "";
});
}
function wireJumpSelect(){ return; }
function applySiteTheme(){
const shell=document.querySelector(".apple");
if (!shell) return;
const s=state.settings;
shell.style.setProperty("--site-primary", s.primary_color || "#59d9c8");
shell.style.setProperty("--site-bg", s.bg_color || "#f6f7fb");
shell.style.setProperty("--site-text", s.text_color || "#0b0c10");
shell.style.setProperty("--site-muted", s.muted_color || "#6b7280");
shell.style.setProperty("--site-card", s.theme === "dark" ? "#0f172a" : "#ffffff");
shell.style.setProperty("--site-font-body", s.font_body || "Manrope");
shell.style.setProperty("--site-font-heading", s.font_heading || "Manrope");
}
function renderPreview(){
const canvas = document.getElementById("previewCanvas");
canvas.innerHTML = "";
canvas.classList.toggle("free-drag", !!state.settings.free_drag);
applySiteTheme();
if (state.settings.bg_anim_url){
canvas.style.background = `url('${state.settings.bg_anim_url}') center/cover no-repeat`;
canvas.style.backgroundSize = "cover";
} else if (state.settings.bg_image_url){
canvas.style.background = `url('${state.settings.bg_image_url}') center/cover no-repeat`;
} else if (state.settings.bg_gradient){
canvas.style.background = `linear-gradient(135deg, ${state.settings.bg_color || "#f6f7fb"} 0%, ${state.settings.bg_color2 || "#e9eef5"} 100%)`;
} else {
canvas.style.background = (state.settings.bg_color || "#f6f7fb");
}
if (state.settings.bg_motion === "slow" && !state.settings.bg_anim_url){
canvas.style.backgroundSize = "200% 200%";
canvas.style.animation = "slowGradient 12s ease infinite";
} else {
canvas.style.animation = "";
}
canvas.style.position = "relative";
if (state.settings.bg_video_url){
const video = document.createElement("video");
video.src = state.settings.bg_video_url;
video.autoplay = true;
video.muted = true;
video.loop = true;
video.playsInline = true;
video.style.position = "absolute";
video.style.inset = "0";
video.style.width = "100%";
video.style.height = "100%";
video.style.objectFit = "cover";
canvas.style.position = "relative";
canvas.appendChild(video);
}
const inner = document.createElement("div");
inner.style.position = "relative";
inner.style.zIndex = "1";
if (!state.settings.free_drag){
inner.style.display = "flex";
inner.style.flexWrap = "wrap";
inner.style.gap = "16px";
inner.style.width = "100%";
}
canvas.appendChild(inner);
if (!state.blocks.length){ inner.innerHTML = '<div class="empty">Arrastra bloques para empezar.</div>'; return; }
const visibleBlocks = (BUILDER_MODE === "ub24")
? state.blocks.filter(b=>((b.page || "home") === currentPage))
: state.blocks;
visibleBlocks.forEach((block)=>{
const el = document.createElement("div");
el.className = "block";
if (!state.settings.free_drag){ el.setAttribute("draggable","true"); }
el.dataset.blockId = block.id;
el.id = block.id;
if (state.settings.free_drag){
const pos = block.pos || getDefaultPos();
block.pos = pos;
el.style.position = "absolute";
el.style.left = "0px";
el.style.top = "0px";
el.style.transform = `translate3d(${pos.x}px, ${pos.y}px, 0)`;
const defaultWidth = block.type === "menu" ? 90 : 70;
const w = Math.max(30, Math.min(100, Number(block.data?.width || defaultWidth)));
el.style.width = `${w}%`;
const h = Number(block.data?.height || 0);
if (h > 60){ el.style.height = h + "px"; }
el.style.cursor = "grab";
} else {
el.style.transform = "";
const isFullWidth = block.type === "menu" || !!block.data?.full_width;
if (isFullWidth){
const w = Math.max(30, Math.min(100, Number(block.data?.width || 100)));
el.style.width = `${w}%`;
el.style.flex = "0 0 100%";
} else {
el.style.width = "calc(50% - 8px)";
el.style.flex = "0 0 calc(50% - 8px)";
}
}
el.innerHTML = renderBlockHtml(block);
if (block.type === "image"){ bindInlineImageDrop(el, block); }
if (block.type === "gallery"){ bindGalleryDrops(el, block); }
if (block.type === "video"){ bindInlineVideoDrop(el, block); }
if (BUILDER_MODE !== "ub24"){
const actions = document.createElement("div");
actions.className = "block-actions";
const del = document.createElement("button");
del.innerHTML = '<i class="fa-solid fa-trash"></i>';
del.addEventListener("click",(e)=>{
e.stopPropagation();
state.blocks = state.blocks.filter(b=>b.id!==block.id);
if (selectedBlockId === block.id) selectedBlockId = null;
renderInspector(); renderPreview();
});
actions.appendChild(del);
el.appendChild(actions);
}
if (block.id === selectedBlockId) el.classList.add("selected");
el.addEventListener("click",(e)=>{
if (e.target && (e.target.closest("details") || e.target.closest("summary") || e.target.closest("a"))) return;
e.stopPropagation();
if (e.target && e.target.closest(".editable")){
selectedBlockId=block.id; renderInspector();
return;
}
selectedBlockId=block.id; renderInspector(); renderPreview();
});
if (!state.settings.free_drag){
el.addEventListener("dragstart",(e)=>{ e.dataTransfer.setData("text/block-id", block.id); e.dataTransfer.effectAllowed="move"; });
el.addEventListener("dragend",()=>removeDrop());
} else {
el.addEventListener("mousedown",(e)=>{
if (e.target && (e.target.closest(".resize-handle") || e.target.closest(".editable"))) return;
startFreeDrag(e, block);
});
el.addEventListener("touchstart",(e)=>{
if (e.target && (e.target.closest(".resize-handle") || e.target.closest(".editable"))) return;
startFreeDrag(e, block);
}, { passive: false });
}
if (BUILDER_MODE !== "ub24"){
const handles = ["n","e","s","w","ne","nw","se","sw"];
handles.forEach((h)=>{
const handle = document.createElement("div");
handle.className = "resize-handle edge " + h;
handle.setAttribute("data-handle", h);
handle.addEventListener("mousedown",(e)=>startFreeResize(e, block));
handle.addEventListener("touchstart",(e)=>startFreeResize(e, block), { passive: false });
el.appendChild(handle);
});
}
if (!state.settings.animations){ el.style.animation = "none"; }
inner.appendChild(el);
});
if (state.settings.free_drag){
let maxBottom = 700;
inner.querySelectorAll(".block").forEach((node)=>{
const n = node;
maxBottom = Math.max(maxBottom, n.offsetTop + n.offsetHeight + 120);
});
canvas.style.minHeight = maxBottom + "px";
} else {
canvas.style.minHeight = "700px";
}
const scrollBtn = document.createElement("button");
scrollBtn.className = "scroll-btn";
scrollBtn.innerHTML = '<i class="fa-solid fa-arrow-up"></i>';
scrollBtn.addEventListener("click",()=>{ window.scrollTo({ top: 0, behavior: "smooth" }); });
canvas.appendChild(scrollBtn);
const whatsappBlock = state.blocks.find(b=>b.type==="social" && b.data && b.data.whatsapp);
if (whatsappBlock && whatsappBlock.data && whatsappBlock.data.whatsapp){
const digits = String(whatsappBlock.data.whatsapp).replace(/\\D/g,"");
const link = digits ? "https://wa.me/" + digits : normalizeLink(whatsappBlock.data.whatsapp);
if (link){
const wa = document.createElement("a");
wa.href = link;
wa.target = "_blank";
wa.rel = "noreferrer";
wa.className = "float-whatsapp";
wa.innerHTML = '<i class="fa-brands fa-whatsapp"></i>';
canvas.appendChild(wa);
}
}
updateBlockList();
updateTopbar();
}
function removeDrop(){ if (dropIndicator && dropIndicator.parentNode) dropIndicator.parentNode.removeChild(dropIndicator); dropIndicator=null; }
function resolveFullWidthByDropX(container, clientX){
if (state.settings.free_drag) return null;
const rect = container.getBoundingClientRect();
if (!rect || !rect.width) return null;
const ratio = (clientX - rect.left) / rect.width;
return ratio > 0.33 && ratio < 0.67;
}
function getDropIndex(container,y){
const blocks=[...container.querySelectorAll(".block")];
for (let i=0;i<blocks.length;i++){ const r=blocks[i].getBoundingClientRect(); if (y < r.top + r.height/2) return i; }
return blocks.length;
}
function showDrop(container,index){
removeDrop();
dropIndicator=document.createElement("div");
dropIndicator.className="drop";
const blocks=container.querySelectorAll(".block");
if (index>=blocks.length) container.appendChild(dropIndicator); else container.insertBefore(dropIndicator, blocks[index]);
}
function addBlock(type,index=state.blocks.length){
const b={ id: makeId(), type, data: defaultData(type) };
if (!state.settings.free_drag && type !== "menu"){
b.data.full_width = false;
}
if (BUILDER_MODE === "ub24"){ b.page = currentPage; }
if (state.settings.free_drag){ b.pos = getDefaultPos(); }
state.blocks.splice(index,0,b); selectedBlockId=b.id; renderInspector(); renderPreview();
}
function moveBlock(id,toIndex,opts={}){
const from=state.blocks.findIndex(b=>b.id===id);
if (from<0) return;
const [b]=state.blocks.splice(from,1);
state.blocks.splice(toIndex,0,b);
if (opts && typeof opts.fullWidth === "boolean" && b.type !== "menu"){
b.data = b.data || {};
b.data.full_width = opts.fullWidth;
}
renderPreview();
}
function renderInspector(){
const panel=document.getElementById("inspectorPanel");
const block=state.blocks.find(b=>b.id===selectedBlockId);
if (!block){ panel.textContent="Selecciona un bloque para editarlo."; return; }
let html = `<div style="font-weight:600;margin-bottom:6px;text-transform:capitalize">${block.type}</div>`;
const data=block.data||{};
const input=(label,id,val)=>`<div class="row"><label>${label}</label><input id="${id}" type="text" value="${escapeHtml(val||"")}"></div>`;
const widthVal = Math.max(30, Math.min(100, Number(data.width || 100)));
html += `<div class="row"><label>Ancho bloque (%)</label><input id="blockWidth" type="range" min="30" max="100" value="${widthVal}"><div id="blockWidthValue" style="margin-top:4px;color:var(--muted);font-size:12px">${widthVal}%</div></div>`;
if (!state.settings.free_drag && block.type !== "menu"){
html += `<div class="row"><label>Ancho completo</label><input id="blockFullWidth" type="checkbox" ${data.full_width ? "checked" : ""}></div>`;
}
if (block.type==="menu"){
html+=input("Titulo","menuTitle",data.title);
const mm = escapeHtml(data.menu_mode || "both");
html+=`<div class="row"><label>Modo menu</label><select id="menuMode"><option value="both" ${mm==="both"?"selected":""}>Ambos</option><option value="inline" ${mm==="inline"?"selected":""}>Horizontal</option><option value="accordion" ${mm==="accordion"?"selected":""}>Acordeon</option></select></div>`;
} else if (block.type==="hero"){
html+=input("Titulo","heroTitle",data.title);
html+=input("Subtitulo","heroSubtitle",data.subtitle);
html+=input("Texto boton","heroBtnText",data.button_text);
html+=input("URL boton","heroBtnUrl",data.button_url);
html+=input("Imagen URL","heroImage",data.image_url);
} else if (block.type==="text"){
html+=`<div class="row"><label>Texto</label><textarea id="textBlock">${escapeHtml(data.text||"")}</textarea></div>`;
} else if (block.type==="image"){
html+=input("Imagen URL","imageUrl",data.url);
html+=input("Alt","imageAlt",data.alt);
html+=input("Descripcion","imageCaption",data.caption);
html+=input("Texto sobre imagen","imageOverlay",data.overlay_text);
const fitVal = escapeHtml(data.fit || "cover");
html+=`<div class="row"><label>Fit</label><select id="imageFit"><option value="cover" ${fitVal==="cover"?"selected":""}>Cover</option><option value="contain" ${fitVal==="contain"?"selected":""}>Contain</option></select></div>`;
html+=`<div class="row"><label>Imagen (arrastrar)</label><div class="dropzone" id="imageDrop">${data.url ? "Imagen cargada" : "Suelta imagen o click"}</div><input id="imageFile" type="file" accept="image/*" hidden></div>`;
} else if (block.type==="features"){
html+=input("Titulo","featuresTitle",data.title);
html+=`<div class="row"><label>Items (una linea)</label><textarea id="featuresItems">${escapeHtml((data.items||[]).join("\\n"))}</textarea></div>`;
} else if (block.type==="gallery"){
html+=input("Titulo","galleryTitle",data.title);
const fitVal = escapeHtml(data.fit || "cover");
html+=`<div class="row"><label>Fit</label><select id="galleryFit"><option value="cover" ${fitVal==="cover"?"selected":""}>Cover</option><option value="contain" ${fitVal==="contain"?"selected":""}>Contain</option></select></div>`;
html+=`<div class="row"><label>Subir imagenes (3)</label></div>`;
[0,1,2].forEach((i)=>{
const has = (data.images||[])[i];
html+=`<div class="row"><div class="dropzone" id="galleryDrop${i}">${has ? "Imagen cargada" : "Suelta imagen o click"}</div><input id="galleryFile${i}" type="file" accept="image/*" hidden></div>`;
});
html+=`<div class="row"><label>Imagenes (una linea)</label><textarea id="galleryImages">${escapeHtml((data.images||[]).join("\\n"))}</textarea></div>`;
html+=`<div class="row"><label>Descripciones (una linea)</label><textarea id="galleryCaptions">${escapeHtml((data.captions||[]).join("\\n"))}</textarea></div>`;
} else if (block.type==="cards"){
html+=input("Titulo","cardsTitle",data.title);
html+=`<div class="row"><label>Tarjetas (Titulo|Texto por linea)</label><textarea id="cardsItems">${escapeHtml((data.items||[]).join("\\n"))}</textarea></div>`;
} else if (block.type==="iconlist"){
html+=input("Titulo","iconTitle",data.title);
html+=`<div class="row"><label>Items (Titulo|Texto por linea)</label><textarea id="iconItems">${escapeHtml((data.items||[]).join("\\n"))}</textarea></div>`;
} else if (block.type==="contact"){
html+=input("Titulo","contactTitle",data.title);
html+=input("Email","contactEmail",data.email);
html+=input("Telefono","contactPhone",data.phone);
html+=input("Direccion","contactAddress",data.address);
} else if (block.type==="button"){
html+=input("Texto","buttonText",data.text);
html+=input("URL","buttonUrl",data.url);
const btnStyle = escapeHtml(data.style || "primary");
html+=`<div class="row"><label>Estilo</label><select id="buttonStyle"><option value="primary" ${btnStyle==="primary"?"selected":""}>Primario</option><option value="outline" ${btnStyle==="outline"?"selected":""}>Outline</option><option value="ghost" ${btnStyle==="ghost"?"selected":""}>Ghost</option></select></div>`;
const btnSize = escapeHtml(data.size || "md");
html+=`<div class="row"><label>Tamano</label><select id="buttonSize"><option value="sm" ${btnSize==="sm"?"selected":""}>Pequeno</option><option value="md" ${btnSize==="md"?"selected":""}>Medio</option><option value="lg" ${btnSize==="lg"?"selected":""}>Grande</option></select></div>`;
} else if (block.type==="social"){
["whatsapp","instagram","facebook","tiktok","youtube"].forEach((k)=>{ html+=input(k,"social_"+k,data[k]); });
const sizeVal = Number(data.icon_size || 18);
html+=`<div class="row"><label>Tamano iconos</label><input id="socialIconSize" type="range" min="14" max="36" value="${Math.max(14, Math.min(36, sizeVal))}"></div>`;
html+=`<div class="row"><label>Color iconos</label><input id="socialIconColor" type="color" value="${escapeHtml(data.icon_color || "#0b0c10")}"></div>`;
const styleVal = escapeHtml(data.icon_style || "pill");
html+=`<div class="row"><label>Estilo iconos</label><select id="socialIconStyle"><option value="pill" ${styleVal==="pill"?"selected":""}>Pill</option><option value="circle" ${styleVal==="circle"?"selected":""}>Circulo</option><option value="outline" ${styleVal==="outline"?"selected":""}>Outline</option><option value="minimal" ${styleVal==="minimal"?"selected":""}>Minimal</option><option value="solid" ${styleVal==="solid"?"selected":""}>Solid</option></select></div>`;
html+=`<div class="row"><label>Mostrar texto</label><input id="socialShowText" type="checkbox" ${data.show_text !== false ? "checked" : ""}></div>`;
} else if (block.type==="video"){
html+=input("URL video","videoUrl",data.url);
html+=input("Descripcion","videoDesc",data.description);
html+=`<div class="row"><label>Video (arrastrar)</label><div class="dropzone" id="videoDrop">${data.url ? "Video cargado" : "Suelta video o click"}</div><input id="videoFile" type="file" accept="video/*" hidden></div>`;
} else if (block.type==="map"){
html+=input("Titulo","mapTitle",data.title);
html+=input("Direccion","mapAddress",data.address);
} else if (block.type==="review"){
html+=input("Titulo","reviewTitle",data.title);
html+=input("Nombre","reviewName",data.name);
html+=`<div class="row"><label>Texto</label><textarea id="reviewText">${escapeHtml(data.text||"")}</textarea></div>`;
html+=`<div class="row"><label>Rating (1-5)</label><input id="reviewRating" type="number" min="1" max="5" value="${Math.max(1, Math.min(5, Number(data.rating||5)))}"></div>`;
const reviewStyle = escapeHtml(data.style || "card");
html+=`<div class="row"><label>Estilo</label><select id="reviewStyle"><option value="card" ${reviewStyle==="card"?"selected":""}>Card</option><option value="quote" ${reviewStyle==="quote"?"selected":""}>Quote</option><option value="compact" ${reviewStyle==="compact"?"selected":""}>Compacto</option></select></div>`;
} else if (block.type==="calendar"){
html+=input("Titulo","calendarTitle",data.title);
html+=input("Embed URL","calendarEmbed",data.embed_url);
html+=`<div class="row"><label>Nota</label><textarea id="calendarNote">${escapeHtml(data.note||"")}</textarea></div>`;
}
html+=`<button class="danger" id="deleteBlockBtn">Eliminar bloque</button>`;
panel.innerHTML=html;
if (block.type==="image"){
const drop = document.getElementById("imageDrop");
const file = document.getElementById("imageFile");
bindDrop(drop, file, (dataUrl)=>{ block.data.url = dataUrl; if (document.getElementById("imageUrl")) document.getElementById("imageUrl").value = dataUrl; }, "Imagen cargada");
}
if (block.type==="video"){
const drop = document.getElementById("videoDrop");
const file = document.getElementById("videoFile");
bindDrop(drop, file, (dataUrl)=>{ block.data.url = dataUrl; if (document.getElementById("videoUrl")) document.getElementById("videoUrl").value = dataUrl; }, "Video cargado");
}
if (block.type==="gallery"){
[0,1,2].forEach((i)=>{
const drop = document.getElementById(`galleryDrop${i}`);
const file = document.getElementById(`galleryFile${i}`);
bindDrop(drop, file, (dataUrl)=>{
const imgs = Array.isArray(block.data.images) ? block.data.images : [];
while (imgs.length < 3) imgs.push("");
imgs[i] = dataUrl;
block.data.images = imgs;
const textarea = document.getElementById("galleryImages");
if (textarea) textarea.value = imgs.join("\n");
}, "Imagen cargada");
});
}
panel.querySelectorAll("input,textarea,select").forEach((el)=>{
el.addEventListener("input",()=>applyInspector(block));
el.addEventListener("change",()=>applyInspector(block));
});
const widthEl = document.getElementById("blockWidth");
const widthValueEl = document.getElementById("blockWidthValue");
if (widthEl && widthValueEl){
const updateWidthLabel = ()=>{ widthValueEl.textContent = `${widthEl.value}%`; };
widthEl.addEventListener("input", updateWidthLabel);
updateWidthLabel();
}
document.getElementById("deleteBlockBtn").addEventListener("click",()=>{ state.blocks=state.blocks.filter(b=>b.id!==block.id); selectedBlockId=null; renderInspector(); renderPreview(); });
}
function applyInspector(block){
const widthEl = document.getElementById("blockWidth");
if (widthEl){
block.data.width = Math.max(30, Math.min(100, Number(widthEl.value || 100)));
}
const fullWidthEl = document.getElementById("blockFullWidth");
if (fullWidthEl && block.type !== "menu"){
block.data.full_width = !!fullWidthEl.checked;
}
if (block.type==="menu"){
block.data.title=document.getElementById("menuTitle").value;
const mm = document.getElementById("menuMode");
if (mm){ block.data.menu_mode = mm.value || "both"; }
}
else if (block.type==="hero"){
block.data.title=document.getElementById("heroTitle").value;
block.data.subtitle=document.getElementById("heroSubtitle").value;
block.data.button_text=document.getElementById("heroBtnText").value;
block.data.button_url=document.getElementById("heroBtnUrl").value;
block.data.image_url=document.getElementById("heroImage").value;
} else if (block.type==="text"){ block.data.text=document.getElementById("textBlock").value; }
else if (block.type==="image"){
block.data.url=document.getElementById("imageUrl").value;
block.data.alt=document.getElementById("imageAlt").value;
const cap = document.getElementById("imageCaption");
if (cap){ block.data.caption = cap.value; }
const overlay = document.getElementById("imageOverlay");
if (overlay){ block.data.overlay_text = overlay.value; }
const fit = document.getElementById("imageFit");
if (fit){ block.data.fit = fit.value || "cover"; }
}
else if (block.type==="features"){ block.data.title=document.getElementById("featuresTitle").value; block.data.items=document.getElementById("featuresItems").value.split("\\n").filter(Boolean); }
else if (block.type==="gallery"){
block.data.title=document.getElementById("galleryTitle").value;
block.data.images=document.getElementById("galleryImages").value.split("\\n");
const caps = document.getElementById("galleryCaptions");
if (caps){ block.data.captions = caps.value.split("\\n"); }
const fit = document.getElementById("galleryFit");
if (fit){ block.data.fit = fit.value || "cover"; }
}
else if (block.type==="cards"){ block.data.title=document.getElementById("cardsTitle").value; block.data.items=document.getElementById("cardsItems").value.split("\\n").filter(Boolean); }
else if (block.type==="iconlist"){ block.data.title=document.getElementById("iconTitle").value; block.data.items=document.getElementById("iconItems").value.split("\\n").filter(Boolean); }
else if (block.type==="contact"){ block.data.title=document.getElementById("contactTitle").value; block.data.email=document.getElementById("contactEmail").value; block.data.phone=document.getElementById("contactPhone").value; block.data.address=document.getElementById("contactAddress").value; }
else if (block.type==="button"){
block.data.text=document.getElementById("buttonText").value;
block.data.url=document.getElementById("buttonUrl").value;
const bs = document.getElementById("buttonStyle");
if (bs){ block.data.style = bs.value || "primary"; }
const bz = document.getElementById("buttonSize");
if (bz){ block.data.size = bz.value || "md"; }
}
else if (block.type==="social"){
["whatsapp","instagram","facebook","tiktok","youtube"].forEach(k=>{ block.data[k]=document.getElementById("social_"+k).value; });
const sizeEl = document.getElementById("socialIconSize");
if (sizeEl){ block.data.icon_size = Number(sizeEl.value || 18); }
const colorEl = document.getElementById("socialIconColor");
if (colorEl){ block.data.icon_color = colorEl.value || "#0b0c10"; }
const styleEl = document.getElementById("socialIconStyle");
if (styleEl){ block.data.icon_style = styleEl.value || "pill"; }
const showEl = document.getElementById("socialShowText");
if (showEl){ block.data.show_text = !!showEl.checked; }
}
else if (block.type==="video"){
block.data.url=document.getElementById("videoUrl").value;
const vd = document.getElementById("videoDesc");
if (vd){ block.data.description = vd.value; }
}
else if (block.type==="map"){
block.data.title=document.getElementById("mapTitle").value;
block.data.address=document.getElementById("mapAddress").value;
} else if (block.type==="review"){
block.data.title=document.getElementById("reviewTitle").value;
block.data.name=document.getElementById("reviewName").value;
block.data.text=document.getElementById("reviewText").value;
block.data.rating=Number(document.getElementById("reviewRating").value || 5);
const styleEl = document.getElementById("reviewStyle");
if (styleEl){ block.data.style = styleEl.value || "card"; }
} else if (block.type==="calendar"){
block.data.title=document.getElementById("calendarTitle").value;
block.data.note=document.getElementById("calendarNote").value;
const calEmbed = document.getElementById("calendarEmbed");
if (calEmbed){ block.data.embed_url = calEmbed.value; }
}
renderPreview();
}
function wireSidebar(){
document.querySelectorAll(".block-item").forEach((item)=>{
item.addEventListener("dragstart",(e)=>{ e.dataTransfer.setData("text/block-type", item.dataset.type); e.dataTransfer.effectAllowed="copy"; });
});
}
function wirePreviewDrop(){
const canvas=document.getElementById("previewCanvas");
canvas.addEventListener("dragover",(e)=>{
e.preventDefault();
if (!state.settings.free_drag){
const container=canvas.querySelector("div")||canvas;
const index=getDropIndex(container,e.clientY);
showDrop(container,index);
}
});
canvas.addEventListener("drop",(e)=>{
e.preventDefault();
const id=e.dataTransfer.getData("text/block-id");
const type=e.dataTransfer.getData("text/block-type");
if (id && !state.settings.free_drag){
const container=canvas.querySelector("div")||canvas;
const index=getDropIndex(container,e.clientY);
const fullWidth = resolveFullWidthByDropX(container, e.clientX);
moveBlock(id,index,{ fullWidth });
} else if (type){
const b={ id: makeId(), type, data: defaultData(type) };
if (state.settings.free_drag){
const rect = canvas.getBoundingClientRect();
b.pos = { x: Math.max(0, e.clientX - rect.left - 20), y: Math.max(0, e.clientY - rect.top - 20) };
} else {
const container=canvas.querySelector("div")||canvas;
const fullWidth = resolveFullWidthByDropX(container, e.clientX);
if (typeof fullWidth === "boolean" && type !== "menu"){
b.data.full_width = fullWidth;
}
}
state.blocks.push(b);
selectedBlockId=b.id;
renderInspector(); renderPreview();
}
removeDrop();
});
}
function wireInlineEditing(){
const canvas = document.getElementById("previewCanvas");
if (!canvas) return;
canvas.addEventListener("input",(e)=>{
const el = e.target.closest(".editable");
if (!el) return;
const blockEl = el.closest(".block");
if (!blockEl) return;
const block = state.blocks.find(b=>b.id===blockEl.dataset.blockId);
if (!block) return;
const field = el.getAttribute("data-field") || "";
setBlockField(block, field, el.textContent);
});
canvas.addEventListener("keydown",(e)=>{
const el = e.target.closest(".editable");
if (!el) return;
if (e.key === "Enter" && !el.dataset.multiline){
e.preventDefault();
el.blur();
}
});
canvas.addEventListener("click",(e)=>{
const el = e.target.closest(".editable");
if (!el) return;
if (el.tagName === "A") e.preventDefault();
e.stopPropagation();
});
canvas.addEventListener("focusin",(e)=>{
const el = e.target.closest(".editable");
if (!el) return;
const blockEl = el.closest(".block");
if (blockEl){
selectedBlockId = blockEl.dataset.blockId;
renderInspector();
}
});
}
function snap(val){ return Math.round(val / 10) * 10; }
function getPoint(e){
if (e.touches && e.touches[0]) return { x: e.touches[0].clientX, y: e.touches[0].clientY };
if (e.changedTouches && e.changedTouches[0]) return { x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY };
return { x: e.clientX, y: e.clientY };
}
function clearGuides(){
snapGuides.forEach(g=>g.remove());
snapGuides = [];
}
function scheduleBlockMove(id, x, y){
pendingMove = { id, x, y };
if (dragRaf) return;
dragRaf = requestAnimationFrame(()=>{
dragRaf = 0;
const job = pendingMove;
if (!job) return;
const el = document.getElementById(job.id);
if (el){
el.style.transform = `translate3d(${job.x}px, ${job.y}px, 0)`;
}
});
}
function scheduleBlockResize(id, x, y, widthPct, heightPx){
pendingResize = { id, x, y, widthPct, heightPx };
if (resizeRaf) return;
resizeRaf = requestAnimationFrame(()=>{
resizeRaf = 0;
const job = pendingResize;
if (!job) return;
const el = document.getElementById(job.id);
if (el){
el.style.transform = `translate3d(${job.x}px, ${job.y}px, 0)`;
el.style.width = `calc(${job.widthPct}% - 8px)`;
if (job.heightPx > 60) el.style.height = job.heightPx + "px";
}
});
}
function addGuide(type, pos){
const g = document.createElement("div");
g.className = "snap-guide " + type;
if (type === "v"){ g.style.left = pos + "px"; }
if (type === "h"){ g.style.top = pos + "px"; }
const canvas = document.getElementById("previewCanvas");
canvas.appendChild(g);
snapGuides.push(g);
}
function startFreeDrag(e, block){
e.preventDefault();
const p = getPoint(e);
const el = document.getElementById(block.id);
const rect = el ? el.getBoundingClientRect() : { width: 120, height: 80 };
isDraggingFree = true;
dragStart = { x: p.x, y: p.y, left: block.pos?.x || 0, top: block.pos?.y || 0, id: block.id, width: rect.width, height: rect.height };
if (el){ el.classList.add("dragging"); }
document.body.classList.add("dragging");
document.addEventListener("mousemove", onFreeDragMove, { passive: false });
document.addEventListener("mouseup", endFreeDrag);
document.addEventListener("touchmove", onFreeDragMove, { passive: false });
document.addEventListener("touchend", endFreeDrag);
}
function onFreeDragMove(e){
if (!isDraggingFree) return;
e.preventDefault();
const p = getPoint(e);
const block = state.blocks.find(b=>b.id===dragStart.id);
if (!block) return;
const dx = p.x - dragStart.x;
const dy = p.y - dragStart.y;
const canvas = document.getElementById("previewCanvas");
const rect = canvas.getBoundingClientRect();
const maxX = Math.max(0, rect.width - (dragStart.width || 120));
const maxY = Math.max(0, rect.height - (dragStart.height || 80));
const x = Math.max(0, Math.min(maxX, snap(dragStart.left + dx)));
const y = Math.max(0, Math.min(maxY, snap(dragStart.top + dy)));
block.pos = { x, y };
clearGuides();
addGuide("v", x);
addGuide("h", y);
scheduleBlockMove(block.id, x, y);
}
function endFreeDrag(){
isDraggingFree = false;
clearGuides();
const el = document.getElementById(dragStart.id);
if (el){ el.classList.remove("dragging"); }
document.body.classList.remove("dragging");
document.removeEventListener("mousemove", onFreeDragMove);
document.removeEventListener("mouseup", endFreeDrag);
document.removeEventListener("touchmove", onFreeDragMove);
document.removeEventListener("touchend", endFreeDrag);
}
function startFreeResize(e, block){
e.preventDefault();
e.stopPropagation();
const p = getPoint(e);
const el = e.target.closest(".block");
const canvas = document.getElementById("previewCanvas");
if (!el || !canvas) return;
const rect = el.getBoundingClientRect();
const canvasRect = canvas.getBoundingClientRect();
const handle = e.target.getAttribute("data-handle") || "se";
const aspect = rect.width / Math.max(1, rect.height);
isResizingFree = true;
resizeStart = { x: p.x, y: p.y, width: rect.width, height: rect.height, canvasWidth: canvasRect.width || 1, id: block.id, free: state.settings.free_drag, handle, aspect, left: block.pos?.x || 0, top: block.pos?.y || 0 };
document.addEventListener("mousemove", onFreeResizeMove, { passive: false });
document.addEventListener("mouseup", endFreeResize);
document.addEventListener("touchmove", onFreeResizeMove, { passive: false });
document.addEventListener("touchend", endFreeResize);
if (el){ el.classList.add("resizing"); }
}
function onFreeResizeMove(e){
if (!isResizingFree) return;
e.preventDefault();
const p = getPoint(e);
const block = state.blocks.find(b=>b.id===resizeStart.id);
if (!block) return;
const dx = p.x - resizeStart.x;
const dy = p.y - resizeStart.y;
let newWidthPx = resizeStart.width;
let newHeightPx = resizeStart.height;
let newX = resizeStart.left;
let newY = resizeStart.top;
const h = resizeStart.handle;
if (h.includes("e")) newWidthPx = resizeStart.width + dx;
if (h.includes("w")) { newWidthPx = resizeStart.width - dx; newX = resizeStart.left + dx; }
if (h.includes("s")) newHeightPx = resizeStart.height + dy;
if (h.includes("n")) { newHeightPx = resizeStart.height - dy; newY = resizeStart.top + dy; }
if (e.shiftKey){
const ratio = resizeStart.aspect || 1;
if (Math.abs(dx) > Math.abs(dy)) newHeightPx = newWidthPx / ratio;
else newWidthPx = newHeightPx * ratio;
}
newWidthPx = Math.max(120, newWidthPx);
newHeightPx = Math.max(80, newHeightPx);
const canvas = document.getElementById("previewCanvas");
const rect = canvas.getBoundingClientRect();
const maxX = Math.max(0, rect.width - newWidthPx);
const maxY = Math.max(0, rect.height - newHeightPx);
newX = Math.max(0, Math.min(maxX, newX));
newY = Math.max(0, Math.min(maxY, newY));
const widthPct = Math.max(20, Math.min(100, Math.round((newWidthPx / resizeStart.canvasWidth) * 100)));
block.data.width = widthPct;
block.data.height = Math.round(newHeightPx);
block.pos = { x: snap(newX), y: snap(newY) };
clearGuides();
addGuide("v", snap(block.pos.x || 0));
addGuide("h", snap(block.pos.y || 0));
scheduleBlockResize(block.id, block.pos.x, block.pos.y, widthPct, block.data.height);
}
function endFreeResize(){
isResizingFree = false;
clearGuides();
const el = document.getElementById(resizeStart.id);
if (el){ el.classList.remove("resizing"); }
document.removeEventListener("mousemove", onFreeResizeMove);
document.removeEventListener("mouseup", endFreeResize);
document.removeEventListener("touchmove", onFreeResizeMove);
document.removeEventListener("touchend", endFreeResize);
}
function wireSettings(){
const s=state.settings;
const siteName=document.getElementById("siteNameInput");
const primary=document.getElementById("primaryColorInput");
const bgColor=document.getElementById("bgColorInput");
const bgColor2=document.getElementById("bgColor2Input");
const bgGradient=document.getElementById("bgGradientToggle");
const textColor=document.getElementById("textColorInput");
const mutedColor=document.getElementById("mutedColorInput");
const fontBody=document.getElementById("fontBodySelect");
const fontHeading=document.getElementById("fontHeadingSelect");
const bgMotion=document.getElementById("bgMotionSelect");
const logoDrop=document.getElementById("logoDrop");
const logoFile=document.getElementById("logoFileInput");
const bgDrop=document.getElementById("bgDrop");
const bgFile=document.getElementById("bgFileInput");
const bgVideo=document.getElementById("bgVideoInput");
const bgAnim=document.getElementById("bgAnimInput");
const animToggle=document.getElementById("animToggle");
siteName.value=s.site_name||""; primary.value=s.primary_color||"#59d9c8";
bgColor.value=s.bg_color||"#f6f7fb"; bgColor2.value=s.bg_color2||"#e9eef5"; bgGradient.checked=!!s.bg_gradient;
bgMotion.value=s.bg_motion||"none";
textColor.value=s.text_color||"#0b0c10"; mutedColor.value=s.muted_color||"#6b7280";
fontBody.value=s.font_body||"Manrope"; fontHeading.value=s.font_heading||"Manrope";
logoDrop.textContent = s.logo_url ? "Logo cargado" : "Suelta imagen o click";
bgDrop.textContent = s.bg_image_url ? "Fondo cargado" : "Suelta imagen o click";
bgVideo.value=s.bg_video_url||"";
bgAnim.value=s.bg_anim_url||"";
animToggle.checked = s.animations !== false;
siteName.addEventListener("input",()=>{ s.site_name=siteName.value; renderPreview(); });
primary.addEventListener("input",()=>{ s.primary_color=primary.value; renderPreview(); });
bgColor.addEventListener("input",()=>{ s.bg_color=bgColor.value; renderPreview(); });
bgColor2.addEventListener("input",()=>{ s.bg_color2=bgColor2.value; renderPreview(); });
bgGradient.addEventListener("change",()=>{ s.bg_gradient=bgGradient.checked; renderPreview(); });
bgMotion.addEventListener("change",()=>{ s.bg_motion=bgMotion.value; renderPreview(); });
textColor.addEventListener("input",()=>{ s.text_color=textColor.value; renderPreview(); });
mutedColor.addEventListener("input",()=>{ s.muted_color=mutedColor.value; renderPreview(); });
fontBody.addEventListener("change",()=>{ s.font_body=fontBody.value; renderPreview(); });
fontHeading.addEventListener("change",()=>{ s.font_heading=fontHeading.value; renderPreview(); });
bgVideo.addEventListener("input",()=>{ s.bg_video_url=bgVideo.value; renderPreview(); });
bgAnim.addEventListener("input",()=>{ s.bg_anim_url=bgAnim.value; renderPreview(); });
animToggle.addEventListener("change",()=>{ s.animations=animToggle.checked; renderPreview(); });
bindDrop(logoDrop, logoFile, (data)=>{ s.logo_url = data; }, "Logo cargado");
bindDrop(bgDrop, bgFile, (data)=>{ s.bg_image_url = data; }, "Fondo cargado");
}
function wireFreeDragToggle(){
const btn = document.getElementById("btnFreeDrag");
if (!btn) return;
const applyLabel = ()=>{
btn.textContent = state.settings.free_drag ? "Modo libre: ON" : "Modo libre";
btn.classList.toggle("active", !!state.settings.free_drag);
};
btn.addEventListener("click", ()=>{
state.settings.free_drag = !state.settings.free_drag;
if (state.settings.free_drag){
state.blocks.forEach((b)=>{
if (!b.pos) b.pos = getDefaultPos();
});
}
applyLabel();
renderPreview();
});
applyLabel();
}
function wirePreviewSize(){
const shell=document.querySelector(".preview-shell");
if (!shell) return;
const setActive=(btnId)=>{
["btnSizePhone","btnSizeTablet","btnSizeDesktop"].forEach(id=>{
const b=document.getElementById(id);
if (b) b.classList.toggle("active", id===btnId);
});
};
const setShellClass=(cls)=>{
shell.classList.remove("size-phone","size-tablet","size-desktop");
shell.classList.add(cls);
};
const setSize=(w, btnId)=>{
const before = shell.getBoundingClientRect().width || 1;
shell.style.maxWidth = w;
setActive(btnId);
setShellClass(btnId==="btnSizePhone" ? "size-phone" : btnId==="btnSizeTablet" ? "size-tablet" : "size-desktop");
setTimeout(()=>{
const after = shell.getBoundingClientRect().width || before;
if (state.settings.free_drag && after !== before){
const ratio = after / before;
state.blocks.forEach(b=>{
if (b.pos){
b.pos = { x: Math.round(b.pos.x * ratio), y: Math.round(b.pos.y * ratio) };
}
});
renderPreview();
}
}, 30);
};
document.getElementById("btnSizePhone").addEventListener("click",()=>setSize("520px","btnSizePhone"));
document.getElementById("btnSizeTablet").addEventListener("click",()=>setSize("820px","btnSizeTablet"));
document.getElementById("btnSizeDesktop").addEventListener("click",()=>setSize("100%","btnSizeDesktop"));
setShellClass("size-desktop");
}
function wireThemeToggle(){
const btn = document.getElementById("btnTheme");
if (!btn) return;
const s = state.settings;
const applyLabel = ()=>{ btn.textContent = s.theme === "dark" ? "Oscuro" : "Claro"; };
const setTheme = (mode)=>{
s.theme = mode;
if (mode === "dark"){
s.bg_color = "#0b0f16";
s.bg_color2 = "#111827";
s.text_color = "#e5e7eb";
s.muted_color = "#94a3b8";
s.bg_gradient = true;
} else {
s.bg_color = "#f6f7fb";
s.bg_color2 = "#e9eef5";
s.text_color = "#0b0c10";
s.muted_color = "#6b7280";
s.bg_gradient = false;
}
renderPreview();
const bgColor=document.getElementById("bgColorInput");
const bgColor2=document.getElementById("bgColor2Input");
const textColor=document.getElementById("textColorInput");
const mutedColor=document.getElementById("mutedColorInput");
const bgGradient=document.getElementById("bgGradientToggle");
if (bgColor) bgColor.value = s.bg_color;
if (bgColor2) bgColor2.value = s.bg_color2;
if (textColor) textColor.value = s.text_color;
if (mutedColor) mutedColor.value = s.muted_color;
if (bgGradient) bgGradient.checked = !!s.bg_gradient;
};
applyLabel();
btn.addEventListener("click",()=>{
setTheme(s.theme === "dark" ? "light" : "dark");
applyLabel();
});
}
function alignBlocks(){
if (!state.settings.free_drag) return;
let y = 20;
const x = 20;
state.blocks.forEach((b)=>{
b.pos = { x, y };
y += 120;
});
renderPreview();
}
function setSaveStatus(msg, kind=""){
const status = document.getElementById("saveStatus");
if (!status) return;
status.textContent = msg;
status.className = `save-status${kind ? ` ${kind}` : ""}`;
}
function normalizeLoadedBlocks(blocks){
if (!Array.isArray(blocks)) return [];
return blocks
.filter((b)=>b && typeof b === "object")
.map((b)=>({
...b,
id: b.id || makeId(),
data: (b.data && typeof b.data === "object") ? b.data : {}
}));
}
async function saveContent(){
if (isSaving) return;
isSaving = true;
const btn = document.getElementById("btnSave");
if (btn){
btn.disabled = true;
btn.textContent = "Publicando...";
}
setSaveStatus("Guardando cambios...", "busy");
const payload = {
site_id: SITE_ID,
publish: true,
content: { ...SERVER_CONTENT, settings: state.settings, blocks: state.blocks }
};
try{
const res = await fetch("/api/elementor/save",{
method:"POST",
headers:{ "Content-Type":"application/json" },
body: JSON.stringify(payload)
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.error || "save failed");
setSaveStatus("Publicado", "ok");
} catch(err){
console.error(err);
setSaveStatus("Error al publicar", "error");
} finally {
isSaving = false;
if (btn){
btn.disabled = false;
btn.textContent = "Publicar";
}
}
}
function init(){
// By default we keep section flow layout for stable full-page composition.
state.settings.free_drag = false;
state.blocks = normalizeLoadedBlocks(state.blocks);
if (BUILDER_MODE === "ub24"){
state.blocks.forEach(b=>{ if (!b.page) b.page = "home"; });
}
if (BUILDER_MODE === "ub24"){
state.settings.free_drag = false;
}
selectedBlockId = null;
wireSidebar(); wirePreviewDrop(); wireInlineEditing(); wireSettings(); wireFreeDragToggle(); wireJumpSelect(); wirePreviewSize(); wireThemeToggle();
const backBtn = document.getElementById("btnBack");
if (backBtn){ backBtn.addEventListener("click",()=>{ window.history.back(); }); }
const previewBtn = document.getElementById("btnPreview");
if (previewBtn){
previewBtn.addEventListener("click",()=>{
document.body.classList.toggle("preview-mode");
const isPreview = document.body.classList.contains("preview-mode");
const sidebar = document.querySelector(".sidebar");
const inspector = document.querySelector(".inspector");
const main = document.querySelector(".main");
const shell = document.querySelector(".preview-shell");
const activeSizeBtn = document.querySelector("#btnSizePhone.active, #btnSizeTablet.active, #btnSizeDesktop.active");
const activeSizeId = activeSizeBtn ? activeSizeBtn.id : "btnSizeDesktop";
if (isPreview && shell){
previewStateBefore = {
maxWidth: shell.style.maxWidth || "",
margin: shell.style.margin || "",
activeSizeId
};
}
if (sidebar) sidebar.style.display = isPreview ? "none" : "";
if (inspector) inspector.style.display = isPreview ? "none" : "";
if (main) main.style.padding = isPreview ? "0" : "";
if (shell){
if (isPreview){
shell.style.maxWidth = "100%";
shell.style.margin = "0";
shell.classList.remove("size-phone","size-tablet","size-desktop");
shell.classList.add("size-desktop");
["btnSizePhone","btnSizeTablet","btnSizeDesktop"].forEach(id=>{
const b=document.getElementById(id);
if (b) b.classList.toggle("active", id==="btnSizeDesktop");
});
} else if (previewStateBefore){
shell.style.maxWidth = previewStateBefore.maxWidth;
shell.style.margin = previewStateBefore.margin;
shell.classList.remove("size-phone","size-tablet","size-desktop");
const map = { btnSizePhone: "size-phone", btnSizeTablet: "size-tablet", btnSizeDesktop: "size-desktop" };
shell.classList.add(map[previewStateBefore.activeSizeId] || "size-desktop");
["btnSizePhone","btnSizeTablet","btnSizeDesktop"].forEach(id=>{
const b=document.getElementById(id);
if (b) b.classList.toggle("active", id===previewStateBefore.activeSizeId);
});
}
}
window.scrollTo({ top: 0, behavior: "smooth" });
return;
});
}
const fullPageBtn = document.getElementById("btnFullPage");
if (fullPageBtn){
fullPageBtn.addEventListener("click",()=>{
const url = `${window.location.pathname}?full=1`;
window.location.href = url;
});
}
if (FULL_PAGE_MODE && !document.body.classList.contains("preview-mode")){
document.body.classList.add("preview-mode");
const shell = document.querySelector(".preview-shell");
const main = document.querySelector(".main");
if (main) main.style.padding = "0";
if (shell){
shell.style.maxWidth = "100%";
shell.style.margin = "0";
shell.classList.remove("size-phone","size-tablet","size-desktop");
shell.classList.add("size-desktop");
}
}
const pageSelect = document.getElementById("pageSelect");
if (pageSelect){
pageSelect.addEventListener("change",()=>{
currentPage = pageSelect.value || "home";
renderPreview();
});
}
const templateSelect = document.getElementById("templateSelect");
const applyTemplate = (key)=>{
if (!key || !templates[key]) return;
const t = templates[key];
state.settings = { ...state.settings, ...t.settings };
state.blocks = t.blocks.map(b=>({ ...b, id: makeId(), page: (BUILDER_MODE==="ub24" ? "home" : b.page) }));
selectedBlockId = null;
renderInspector(); renderPreview();
wireSettings();
};
if (templateSelect){
templateSelect.addEventListener("change", ()=>{
applyTemplate(templateSelect.value);
});
}
const alignBtn = document.getElementById("btnAlign");
if (alignBtn){ alignBtn.addEventListener("click",alignBlocks); }
document.getElementById("previewCanvas").addEventListener("click",()=>{ selectedBlockId=null; renderInspector(); renderPreview(); });
document.getElementById("btnSave").addEventListener("click",saveContent);
renderInspector(); renderPreview();
}
init();
</script>
</body>
</html>