1901 lines
112 KiB
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/\"/g,""").replace(/'/g,"'");
|
|
}
|
|
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)">“</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>
|