3153 lines
201 KiB
HTML
3153 lines
201 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&family=Oswald:wght@400;600;700&family=Source+Serif+4:wght@400;500;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); --motion-fast:160ms; --motion-base:240ms; --motion-slow:420ms; --ease-standard:cubic-bezier(.22,.61,.36,1); --ease-emphasis:cubic-bezier(.2,.8,.2,1); --block-hover-lift:-4px; --block-shadow-rest:0 12px 30px rgba(15,23,42,.08); --block-shadow-hover:0 22px 44px rgba(15,23,42,.16); --block-ring-hover:0 0 0 1px rgba(37,99,235,.14); }
|
|
*{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{display:none !important}
|
|
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)}
|
|
.canvas-inner{width:100%;max-width:none;margin:0 auto}
|
|
.block{background:var(--site-block-bg,var(--site-card,#fff));border-radius:var(--site-block-radius,var(--radius-md));padding:var(--space-3);margin-bottom:var(--space-4);border:1px solid var(--site-block-border,transparent);box-shadow:var(--site-block-shadow,var(--block-shadow-rest));transition:transform var(--motion-base) var(--ease-standard),box-shadow var(--motion-base) var(--ease-standard),border-color var(--motion-fast) var(--ease-standard);animation:fadeUp var(--motion-base) var(--ease-standard);color:var(--site-text,#0b0c10);position:relative;cursor:grab;touch-action:pan-y;overflow:hidden;isolation:isolate}
|
|
.block::before{content:"";position:absolute;inset:0;z-index:0;pointer-events:none;background:linear-gradient(165deg,rgba(255,255,255,.28),rgba(255,255,255,0) 40%,rgba(15,23,42,.04));opacity:var(--site-block-sheen-opacity,.34);transition:opacity var(--motion-base) var(--ease-standard)}
|
|
.block::after{content:"";position:absolute;inset:-40% -20% auto auto;width:260px;height:260px;z-index:0;pointer-events:none;background:radial-gradient(circle,rgba(37,99,235,.16),rgba(37,99,235,0) 64%);opacity:var(--site-block-accent-opacity,0);transition:opacity var(--motion-slow) var(--ease-emphasis),transform var(--motion-slow) var(--ease-emphasis);transform:translate3d(0,8px,0)}
|
|
.block.dragging,.block.resizing{transition:none;cursor:grabbing}
|
|
body.dragging{user-select:none}
|
|
.block:hover{transform:translate3d(0,var(--block-hover-lift),0);box-shadow:var(--site-block-shadow-hover,var(--block-shadow-hover)),var(--site-block-ring-hover,var(--block-ring-hover))}
|
|
.block:hover::before{opacity:var(--site-block-sheen-opacity-hover,.48)}
|
|
.block:hover::after{opacity:var(--site-block-accent-opacity-hover,.22);transform:translate3d(0,0,0)}
|
|
.block.selected{border-color:#7aa7ff}
|
|
.block.anim-in-none{animation:none}
|
|
.block.anim-in-slide{animation:slideIn .28s ease}
|
|
.block.anim-in-zoom{animation:zoomIn .26s ease}
|
|
.block.hover-none:hover{transform:none;box-shadow:var(--site-block-shadow,var(--block-shadow-rest))}
|
|
.block.hover-none:hover::after{opacity:var(--site-block-accent-opacity,0)}
|
|
.block.hover-none:hover::before{opacity:var(--site-block-sheen-opacity,.34)}
|
|
.block.style-clean-landing{--site-block-bg:#ffffff;--site-block-border:#dbe3ee;--site-block-shadow:0 10px 26px rgba(15,23,42,.09);--site-block-shadow-hover:0 24px 44px rgba(15,23,42,.15);--site-block-ring-hover:0 0 0 1px rgba(15,23,42,.08);--site-block-sheen-opacity:.4;--site-block-sheen-opacity-hover:.52;--site-block-accent-opacity:0;--site-block-accent-opacity-hover:.14}
|
|
.block.style-dark-glow{--site-block-bg:linear-gradient(165deg,rgba(7,12,22,.92),rgba(3,8,16,.88));--site-block-border:#1f3657;--site-block-shadow:0 16px 36px rgba(2,8,20,.46);--site-block-shadow-hover:0 24px 56px rgba(8,24,56,.58);--site-block-ring-hover:0 0 0 1px rgba(37,99,235,.42);--site-block-sheen-opacity:.08;--site-block-sheen-opacity-hover:.16;--site-block-accent-opacity:.24;--site-block-accent-opacity-hover:.38;color:#e6edf8}
|
|
.block.style-glass{--site-block-bg:linear-gradient(155deg,rgba(255,255,255,.52),rgba(255,255,255,.28));--site-block-border:rgba(255,255,255,.48);--site-block-shadow:0 16px 34px rgba(15,23,42,.13);--site-block-shadow-hover:0 24px 44px rgba(15,23,42,.2);--site-block-sheen-opacity:.46;--site-block-sheen-opacity-hover:.58;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}
|
|
.block.style-soft-gradient{--site-block-bg:linear-gradient(150deg,#f8fafc 0%,#eef2ff 55%,#e2e8f0 100%);--site-block-border:#d5deef;--site-block-shadow:0 12px 28px rgba(15,23,42,.1);--site-block-sheen-opacity:.36;--site-block-sheen-opacity-hover:.48;--site-block-accent-opacity:.12;--site-block-accent-opacity-hover:.22}
|
|
.block.bg-motion-flow::before{background:linear-gradient(120deg,rgba(255,255,255,.3) 0%,rgba(255,255,255,.06) 34%,rgba(59,130,246,.2) 52%,rgba(255,255,255,.06) 68%,rgba(255,255,255,.25) 100%);background-size:220% 220%;animation:bgFlow var(--block-motion-duration,18s) linear infinite}
|
|
.block.bg-motion-aurora::after{opacity:var(--site-block-accent-opacity-hover,.26);animation:bgAurora var(--block-motion-duration,22s) var(--ease-emphasis) infinite}
|
|
.block.bg-motion-parallax::before{animation:bgParallax var(--block-motion-duration,20s) var(--ease-standard) infinite}
|
|
.block.bg-motion-parallax::after{opacity:var(--site-block-accent-opacity-hover,.2);animation:bgAurora calc(var(--block-motion-duration,20s) * 1.15) var(--ease-emphasis) infinite reverse}
|
|
.block.hover-glow:hover{transform:translateY(-2px);box-shadow:0 16px 34px rgba(37,99,235,.24),0 0 0 1px rgba(37,99,235,.28)}
|
|
.block.hover-tilt:hover{transform:translateY(-2px) rotate(-.3deg)}
|
|
.block.has-bg-media{background-size:cover;background-position:center;background-repeat:no-repeat}
|
|
.block.has-bg-media .editable,.block.has-bg-media h1,.block.has-bg-media h2,.block.has-bg-media h3,.block.has-bg-media p,.block.has-bg-media li,.block.has-bg-media a,.block.has-bg-media label,.block.has-bg-media strong,.block.has-bg-media span{position:relative;z-index:1}
|
|
.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}
|
|
.drag-handle{cursor:grab;touch-action:none}
|
|
.block-drag-handle{
|
|
position:absolute;left:8px;top:8px;z-index:3;
|
|
width:24px;height:24px;border-radius:8px;border:1px solid #e5e7eb;
|
|
background:#ffffff;color:#334155;display:flex;align-items:center;justify-content:center;
|
|
box-shadow:0 8px 18px rgba(15,23,42,.12);cursor:grab
|
|
}
|
|
.preview-mode .block-drag-handle{display:none !important}
|
|
@keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
|
|
@keyframes slideIn{from{opacity:0;transform:translateY(10px) translateX(-8px)}to{opacity:1;transform:translateY(0) translateX(0)}}
|
|
@keyframes zoomIn{from{opacity:0;transform:scale(.98)}to{opacity:1;transform:scale(1)}}
|
|
@keyframes bgFlow{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
|
|
@keyframes bgAurora{0%{transform:translate3d(0,12px,0) scale(.96)}50%{transform:translate3d(-8px,-8px,0) scale(1.04)}100%{transform:translate3d(0,12px,0) scale(.96)}}
|
|
@keyframes bgParallax{0%{transform:translate3d(0,0,0)}50%{transform:translate3d(0,-8px,0)}100%{transform:translate3d(0,0,0)}}
|
|
@media (prefers-reduced-motion: reduce){
|
|
.block,.block::before,.block::after{animation:none !important;transition:none !important}
|
|
}
|
|
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}
|
|
.social-surface-pro{position:relative;overflow:hidden}
|
|
.social-surface-pro::before{content:"";position:absolute;inset:-30% -10% auto auto;width:340px;height:340px;background:radial-gradient(circle,rgba(37,99,235,.16),transparent 62%);pointer-events:none}
|
|
.social-grid-pro{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:14px}
|
|
.social-card-pro{display:flex;align-items:center;gap:12px;min-height:84px;padding:16px;border-radius:16px;text-decoration:none;transition:transform .2s ease,box-shadow .2s ease,border-color .2s ease}
|
|
.social-card-pro:hover{transform:translateY(-3px);box-shadow:0 14px 28px rgba(15,23,42,.2)}
|
|
.social-card-meta{display:flex;flex-direction:column;min-width:0}
|
|
.social-card-title{font-size:17px;line-height:1.1;font-weight:750}
|
|
.social-card-value{font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
.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-layout.reverse .hero-copy{order:2}
|
|
.hero-layout.reverse .hero-media{order:1}
|
|
.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 var(--site-nav-border,#dde4ef);border-radius:var(--site-nav-radius,var(--radius-md));background:var(--site-nav-bg,#ffffff)}
|
|
.site-brand{display:flex;align-items:center;gap:10px;font-weight:800;letter-spacing:.2px;min-width:120px;max-width:320px;flex:0 1 auto}
|
|
.site-brand img{height:28px;width:auto;border-radius:8px;border:1px solid #dbe3ee}
|
|
.site-brand-badge{height:28px;min-width:28px;padding:0 8px;border-radius:8px;background:var(--site-primary);display:inline-flex;align-items:center;justify-content:center;color:#0b0f16;font-weight:800;font-size:12px}
|
|
.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:flex-end;flex:1 1 auto}
|
|
.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-drawer-toggle{display:none;align-items:center;justify-content:center;width:42px;height:42px;border-radius:10px;border:1px solid #2b2b2b;background:#202020;color:#f5f5f5;cursor:pointer}
|
|
.menu-drawer-toggle:hover{border-color:#4b5563}
|
|
.menu-drawer-overlay{position:fixed;inset:0;background:rgba(8,11,16,.5);z-index:1500;opacity:0;visibility:hidden;pointer-events:none;transition:opacity .2s ease,visibility .2s ease}
|
|
.menu-drawer{position:fixed;top:0;right:0;height:100vh;width:min(92vw,360px);background:#1f1f1f;color:#f5f5f5;border-left:1px solid #2d2d2d;z-index:1510;box-shadow:-14px 0 30px rgba(0,0,0,.25);transform:translateX(100%);opacity:0;visibility:hidden;pointer-events:none;transition:transform .24s ease,opacity .2s ease,visibility .2s ease}
|
|
.menu-drawer.open,.menu-drawer-overlay.open{opacity:1;visibility:visible;pointer-events:auto}
|
|
.menu-drawer.open{transform:translateX(0)}
|
|
.menu-drawer-head{height:74px;padding:0 16px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #2d2d2d}
|
|
.menu-drawer-brand{display:flex;align-items:center;gap:10px;font-weight:800;font-size:24px}
|
|
.menu-drawer-close{width:42px;height:42px;border-radius:10px;border:2px solid #fbbf24;background:transparent;color:#f8fafc;font-size:22px;line-height:1;cursor:pointer}
|
|
.menu-drawer-links{display:grid;gap:16px;padding:34px 28px}
|
|
.menu-drawer-links .site-nav-link{height:auto;padding:0;border:0;border-radius:0;color:#f8fafc;font-size:clamp(22px,4vw,34px);line-height:1.15}
|
|
.menu-drawer-links .site-nav-link:hover{background:transparent;border:0;transform:none;color:#cbd5e1}
|
|
.menu-accordion{display:none;border:1px solid #e5e7eb;border-radius:var(--radius-md);padding:8px 10px;background:var(--site-card);width:auto;min-width:220px;max-width:420px;margin-left:auto;flex:0 1 auto}
|
|
.menu-accordion summary{list-style:none;cursor:pointer;font-weight:600;touch-action:manipulation}
|
|
.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)}
|
|
.edu-nav{display:grid;grid-template-columns:auto 1fr auto;gap:16px;align-items:center;padding:14px 16px;border:1px solid #d8e2f2;border-radius:16px;background:#ffffff}
|
|
.edu-nav-links{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}
|
|
.edu-nav-link{padding:9px 12px;border-radius:10px;text-decoration:none;color:#274168;font-weight:600;font-size:13px}
|
|
.edu-nav-link:hover{background:#eef4ff;color:#0a4dcf}
|
|
.edu-nav-cta{padding:10px 14px;border-radius:10px;background:#0a4dcf;color:#fff;text-decoration:none;font-weight:700;font-size:13px}
|
|
.edu-hero{display:grid;grid-template-columns:minmax(320px,1.15fr) minmax(280px,.85fr);gap:18px;align-items:stretch}
|
|
.edu-hero-copy{padding:6px 4px}
|
|
.edu-kicker{display:inline-flex;align-items:center;gap:8px;padding:6px 10px;border-radius:999px;background:#e9f0ff;color:#0a4dcf;font-size:11px;font-weight:700;letter-spacing:.8px;text-transform:uppercase;margin-bottom:10px}
|
|
.edu-hero h2.editable{font-size:clamp(36px,4.8vw,62px);line-height:1.03;letter-spacing:-.5px;margin:0 0 10px}
|
|
.edu-hero p.editable{font-size:20px;line-height:1.45;color:#3e5378;max-width:58ch}
|
|
.edu-hero-panel{background:#ffffff;border:1px solid #d8e2f2;border-radius:16px;padding:16px;display:grid;gap:12px}
|
|
.edu-stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
|
.edu-stat{border:1px solid #e3ebf8;border-radius:12px;padding:12px;background:#f8fbff}
|
|
.edu-stat strong{display:block;font-size:22px;color:#0a4dcf}
|
|
.edu-stat span{font-size:12px;color:#4f6286}
|
|
.edu-step-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}
|
|
.edu-step{background:#fff;border:1px solid #d9e4f5;border-radius:14px;padding:14px}
|
|
.edu-step-num{width:30px;height:30px;border-radius:999px;background:#0a4dcf;color:#fff;display:inline-flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;margin-bottom:8px}
|
|
.edu-cards-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px}
|
|
.edu-card{background:#fff;border:1px solid #d9e4f5;border-radius:14px;padding:16px}
|
|
.edu-card .card-pro-title{font-size:22px;line-height:1.15;color:#0b1733}
|
|
.edu-card .card-pro-desc{font-size:14px;line-height:1.6;color:#41557a}
|
|
.education-site{padding:26px;background:linear-gradient(180deg,#f3f7ff 0%,#eef3fb 100%);border-radius:18px}
|
|
.education-site .block{background:transparent;border:0;box-shadow:none;padding:0}
|
|
.education-site .block[data-block-type="menu"]{margin-bottom:6px}
|
|
.education-site .block[data-block-type="hero"]{background:linear-gradient(140deg,#ffffff 0%,#f6f9ff 100%);border:1px solid #d8e2f2;border-radius:18px;padding:22px}
|
|
.education-site .block[data-block-type="cards"]{background:#ffffff;border:1px solid #d8e2f2;border-radius:18px;padding:20px}
|
|
.education-site .block[data-block-type="iconlist"]{background:#ffffff;border:1px solid #d8e2f2;border-radius:18px;padding:20px}
|
|
.education-site .block[data-block-type="features"]{background:#ffffff;border:1px solid #d8e2f2;border-radius:18px;padding:20px}
|
|
.education-site .block[data-block-type="gallery"]{background:#ffffff;border:1px solid #d8e2f2;border-radius:18px;padding:20px}
|
|
.education-site .block[data-block-type="review"]{background:linear-gradient(140deg,#0a4dcf 0%,#123c8a 100%);color:#fff;border-radius:18px;padding:20px}
|
|
.education-site .block[data-block-type="review"] .editable{color:#fff}
|
|
.education-site .block[data-block-type="review"] i{color:#ffd86b !important}
|
|
.education-site .block[data-block-type="contact"]{background:#ffffff;border:1px solid #d8e2f2;border-radius:18px;padding:20px}
|
|
.education-site .block[data-block-type="map"]{background:#ffffff;border:1px solid #d8e2f2;border-radius:18px;padding:20px}
|
|
.education-site .block h3{font-size:32px;line-height:1.1;margin-bottom:14px !important;letter-spacing:-.3px}
|
|
.education-site .edu-nav-link{border:1px solid transparent}
|
|
.education-site .edu-nav-link:hover{border-color:#cddcf6;background:#f3f7ff}
|
|
.education-site .edu-hero{position:relative}
|
|
.education-site .edu-card:nth-child(1){border-top:4px solid #0a4dcf}
|
|
.education-site .edu-card:nth-child(2){border-top:4px solid #1f6feb}
|
|
.education-site .edu-card:nth-child(3){border-top:4px solid #58a6ff}
|
|
.education-site .edu-step{position:relative;overflow:hidden}
|
|
.education-site .edu-step:after{content:"";position:absolute;right:-26px;top:-26px;width:90px;height:90px;border-radius:999px;background:rgba(10,77,207,.07)}
|
|
.edu-apply{border:1px solid #d8e2f2;background:#fff;border-radius:14px;padding:12px}
|
|
.edu-apply h4{margin:0 0 8px;font-size:14px;color:#17356e}
|
|
.edu-apply ul{margin:0;padding-left:18px;color:#38527f;font-size:13px;line-height:1.5}
|
|
.edu-apply .deadline{display:inline-flex;margin-top:8px;padding:4px 8px;border-radius:999px;background:#e9f0ff;color:#0a4dcf;font-size:11px;font-weight:700}
|
|
@media (max-width:980px){.edu-hero{grid-template-columns:1fr}}
|
|
.site-global-footer{margin-top:26px;padding:16px 18px;border-radius:12px;border:1px solid #dbe3ee;background:var(--site-footer-bg,#ffffff);color:var(--site-muted);font-size:12px;line-height:1.4;text-align:center;width:100%;grid-column:1 / -1;justify-self:stretch}
|
|
body.menu-drawer-open{overflow:hidden;touch-action:none}
|
|
.canvas-bg-overlay{position:absolute;inset:0;z-index:0;pointer-events:none}
|
|
.restaurant-site .canvas-bg-overlay{background:linear-gradient(180deg,rgba(10,12,16,.12) 0%,rgba(10,12,16,.06) 40%,rgba(10,12,16,0) 100%);backdrop-filter:blur(1px)}
|
|
.restaurant-site{padding:28px;background:linear-gradient(180deg,var(--restaurant-bg-1,#f8fafc) 0%,var(--restaurant-bg-2,#f1f5f9) 100%)}
|
|
.restaurant-site .block{transform:translate3d(0,0,0)}
|
|
.restaurant-site .block:hover{transform:translate3d(0,var(--block-hover-lift),0);box-shadow:var(--site-block-shadow-hover,var(--block-shadow-hover)),var(--site-block-ring-hover,var(--block-ring-hover))}
|
|
.restaurant-site .block.bg-motion-flow::before{opacity:.5}
|
|
.restaurant-site .block[data-block-type="menu"]{background:var(--restaurant-surface,#fff);border:1px solid var(--restaurant-border,#dbe3ee);border-radius:16px;box-shadow:0 8px 24px rgba(15,23,42,.06)}
|
|
.restaurant-site .site-nav{border-color:var(--restaurant-border,#dbe3ee);background:var(--restaurant-surface,#fff)}
|
|
.restaurant-site .site-brand{font-size:clamp(17px,1.9vw,24px);font-family:var(--site-font-heading,Playfair Display),serif}
|
|
.restaurant-site .site-nav-link{font-size:14px}
|
|
.restaurant-site .block[data-block-type="hero"]{background:linear-gradient(160deg,var(--restaurant-surface,#fff) 0%,var(--restaurant-surface-soft,#f8fafc) 100%);border:1px solid var(--restaurant-border,#dbe3ee);border-radius:20px;padding:26px;box-shadow:0 12px 28px rgba(15,23,42,.07)}
|
|
.restaurant-site .hero-kicker{letter-spacing:2.2px}
|
|
.restaurant-site .hero-pro h2.editable{font-size:clamp(40px,5.3vw,72px);line-height:1.02;max-width:13ch}
|
|
.restaurant-site .hero-pro p.editable{font-size:clamp(16px,1.75vw,21px);line-height:1.58;max-width:54ch}
|
|
.restaurant-site .hero-media{border-color:var(--restaurant-border,#dbe3ee);border-radius:18px;min-height:360px}
|
|
.restaurant-site .block[data-block-type="gallery"]{background:var(--restaurant-surface,#fff);border:1px dashed var(--restaurant-border,#dbe3ee);border-radius:20px;padding:22px;box-shadow:0 8px 20px rgba(15,23,42,.06)}
|
|
.restaurant-site .block[data-block-type="gallery"] .gallery-slot{border:1px solid var(--restaurant-border,#dbe3ee)}
|
|
.restaurant-site .block[data-block-type="cards"]{background:var(--restaurant-surface,#fff);border:1px solid var(--restaurant-border,#dbe3ee);border-radius:20px;padding:22px;box-shadow:0 8px 20px rgba(15,23,42,.06)}
|
|
.restaurant-site .block[data-block-type="cards"] .card-pro{background:var(--restaurant-surface-soft,#f8fafc);border:1px solid var(--restaurant-border,#dbe3ee);box-shadow:none}
|
|
.restaurant-site .block[data-block-type="review"]{background:var(--restaurant-surface,#fff);border:1px solid var(--restaurant-border,#dbe3ee);border-radius:20px;padding:20px;box-shadow:0 8px 20px rgba(15,23,42,.06)}
|
|
.restaurant-site .block[data-block-type="review"] h3{color:var(--site-text)}
|
|
.restaurant-site .block[data-block-type="contact"]{background:var(--restaurant-surface,#fff);border:1px solid var(--restaurant-border,#dbe3ee);border-radius:20px;padding:22px;box-shadow:0 8px 20px rgba(15,23,42,.06)}
|
|
.restaurant-site .block[data-block-type="map"]{background:var(--restaurant-surface,#fff);border:1px solid var(--restaurant-border,#dbe3ee);border-radius:20px;padding:20px;box-shadow:0 8px 20px rgba(15,23,42,.06)}
|
|
/* Ensure visual presets override restaurante hardcoded surfaces */
|
|
.restaurant-site .block.style-clean-landing,
|
|
.restaurant-site .block.style-dark-glow,
|
|
.restaurant-site .block.style-glass,
|
|
.restaurant-site .block.style-soft-gradient{
|
|
background:var(--site-block-bg,var(--restaurant-surface,#fff));
|
|
border-color:var(--site-block-border,var(--restaurant-border,#dbe3ee));
|
|
box-shadow:var(--site-block-shadow,0 8px 20px rgba(15,23,42,.06));
|
|
color:var(--site-text,#0b0c10);
|
|
}
|
|
.restaurant-site .site-global-footer{background:var(--restaurant-surface,#fff);border-color:var(--restaurant-border,#dbe3ee)}
|
|
.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-drawer-toggle{display:inline-flex}
|
|
.menu-accordion{display:block;width:100%;min-width:0;max-width:none;margin-left:0}
|
|
}
|
|
.preview-shell.size-phone .menu-inline,
|
|
.preview-shell.size-tablet .menu-inline{display:none}
|
|
.preview-shell.size-phone .menu-drawer-toggle,
|
|
.preview-shell.size-tablet .menu-drawer-toggle{display:inline-flex}
|
|
.preview-shell.size-phone .menu-accordion,
|
|
.preview-shell.size-tablet .menu-accordion{display:block;width:100%;min-width:0;max-width:none;margin-left:0}
|
|
.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{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}
|
|
/* Keep preview-final visually aligned with editor canvas:
|
|
only hide editor chrome, do not rewrite block/canvas geometry. */
|
|
body.preview-mode .main{padding:10px !important}
|
|
.preview-back{position:fixed;top:14px;left:14px;z-index:1200;border:1px solid #cbd5e1;background:#fff;color:#0f172a;padding:9px 14px;border-radius:999px;font-weight:700;cursor:pointer;box-shadow:0 10px 24px rgba(15,23,42,.15)}
|
|
.preview-back:hover{transform:translateY(-1px)}
|
|
.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 '' }} {{ 'preview-mode' if preview_only 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 'Page Builder' }}</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">Seccion 2</option>
|
|
<option value="galeria">Galeria</option>
|
|
<option value="contacto">Contacto</option>
|
|
</select>
|
|
{% endif %}
|
|
<select id="templateSelect" class="top-select">
|
|
<option value="">Plantillas por rubro</option>
|
|
<option value="restaurante">Restaurante</option>
|
|
<option value="danza">Danza</option>
|
|
<option value="cosmeticos">Cosméticos</option>
|
|
<option value="despachos">Despachos</option>
|
|
<option value="gimnasios">Gimnasios</option>
|
|
<option value="educacion">Educación</option>
|
|
<option value="base_otro">Base (Otro)</option>
|
|
</select>
|
|
<button class="btn secondary" id="btnBack">Atras</button>
|
|
<button class="btn secondary" id="btnPreview">Vista previa</button>
|
|
<button class="btn secondary" id="btnReset">Reset</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" 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>
|
|
<option value="Source Serif 4">Source Serif 4</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>
|
|
<option value="Oswald">Oswald</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>Preset global bloques</label><select id="globalBlockPresetSelect"><option value="clean-landing">Clean Landing</option><option value="dark-glow">Dark Glow</option><option value="glass">Glass</option><option value="soft-gradient">Soft Gradient</option></select></div>
|
|
<div class="row"><label>Movimiento fondo global</label><select id="globalBlockMotionSelect"><option value="none">Sin movimiento</option><option value="flow">Gradient Flow</option><option value="aurora">Aurora</option><option value="parallax">Parallax Soft</option></select></div>
|
|
<div class="row"><label>Velocidad movimiento global</label><select id="globalBlockMotionSpeedSelect"><option value="slow">Lenta</option><option value="normal">Normal</option><option value="fast">Rapida</option></select></div>
|
|
<div class="row"><label>Animaciones</label><input id="animToggle" type="checkbox"></div>
|
|
<div class="row"><label>Alto minimo pagina (px)</label><input id="canvasMinHeightInput" type="number" min="700" max="5000" step="50"></div>
|
|
<div class="row"><label>Espacio inferior (px)</label><input id="canvasBottomSpaceInput" type="number" min="0" max="1200" step="10"></div>
|
|
<div class="row"><label>Marca registrada (footer)</label><input id="registeredBrandInput" type="text" placeholder="Tu marca registrada"></div>
|
|
<div class="row"><label>Empresa desarrolladora (footer)</label><input id="developerBrandInput" type="text" placeholder="GKACHELE™"></div>
|
|
<div class="row"><label>Creado por</label><input id="siteAuthorInput" type="text" placeholder="Tu nombre o marca"></div>
|
|
</div>
|
|
</details>
|
|
</aside>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js"></script>
|
|
<script>
|
|
const SITE_ID = {{ site_id }};
|
|
const SITE_SLUG = "{{ slug }}";
|
|
const SERVER_CONTENT = {{ content|tojson }};
|
|
const BUILDER_MODE = "{{ builder_mode or 'default' }}";
|
|
const PREVIEW_ONLY = {{ 'true' if preview_only else 'false' }};
|
|
const SERVER_RUBRO = "{{ rubro|default('restaurante') }}";
|
|
const BUILDER_BASE_PATH = BUILDER_MODE === "ub24" ? `/ub24/${SITE_ID}` : `/elementor/${SITE_ID}`;
|
|
const OFFICIAL_RUBROS = [
|
|
{ value: "restaurante", label: "Restaurante" },
|
|
{ value: "danza", label: "Danza" },
|
|
{ value: "cosmeticos", label: "Cosméticos" },
|
|
{ value: "despachos", label: "Despachos" },
|
|
{ value: "gimnasios", label: "Gimnasios" },
|
|
{ value: "educacion", label: "Educación" },
|
|
{ value: "base_otro", label: "Base (Otro)" }
|
|
];
|
|
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: "",
|
|
canvas_min_height: 1200,
|
|
canvas_bottom_space: 180,
|
|
site_author: "",
|
|
registered_brand: "",
|
|
developer_brand: "GKACHELE™",
|
|
business_rubro: "restaurante",
|
|
global_block_preset: "clean-landing",
|
|
global_block_motion: "flow",
|
|
global_block_motion_speed: "normal"
|
|
};
|
|
const templates = {
|
|
base_otro: {
|
|
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: 'Solucion profesional para tu negocio', subtitle: 'Crecemos contigo con ejecucion clara 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: 'Propuesta', 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') }
|
|
]
|
|
},
|
|
gimnasios: {
|
|
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: 'Entrena mejor con acompañamiento profesional', subtitle: 'Planes personalizados, equipos modernos y seguimiento real.', button_text: 'Prueba gratis', button_url: '#contacto', image_url: '' } },
|
|
{ id: makeId(), type: 'iconlist', data: { title: 'Diferenciales', items: ['Entrenadores|Acompañamiento experto','Programas|Objetivos medibles','Comunidad|Motivación constante'] } },
|
|
{ id: makeId(), type: 'features', data: { title: 'Áreas', items: ['Fuerza','Cardio','Funcional'] } },
|
|
{ id: makeId(), type: 'gallery', data: { title: 'Instalaciones', images: ['','',''], captions: ['','',''], fit: 'cover' } },
|
|
{ id: makeId(), type: 'contact', data: { title: 'Inscripción', email: '', phone: '', address: '' } }
|
|
]
|
|
},
|
|
restaurante: {
|
|
settings: {
|
|
primary_color: '#9b2f16',
|
|
bg_color: '#e9e3d5',
|
|
bg_color2: '#f4efe3',
|
|
text_color: '#1f1b17',
|
|
muted_color: '#5f564b',
|
|
font_body: 'Source Serif 4',
|
|
font_heading: 'Oswald',
|
|
bg_gradient: true,
|
|
global_block_preset: 'soft-gradient',
|
|
global_block_motion: 'flow',
|
|
global_block_motion_speed: 'slow'
|
|
},
|
|
blocks: [
|
|
{ id: makeId(), type: 'menu', data: { ...defaultData('menu'), width: 100 } },
|
|
{ id: makeId(), type: 'hero', data: { title: 'Hectarea en Obera + Chacra en Jardin America', subtitle: 'Presentacion comercial clara con contacto directo, mapas embebidos y cierre por WhatsApp.', kicker: 'Venta Directa', button_text: 'Hablar por WhatsApp', button_url: '#contacto', button_secondary_text: 'Ver propiedades', button_secondary_url: '#menu', image_url: '', align: 'left', width: 60 } },
|
|
{ id: makeId(), type: 'cards', data: { title: 'Propiedades destacadas', items: ['Obera|1 hectarea con excelente proyeccion.','Jardin America|25 hectareas con potencial productivo.','Documentacion|Informacion lista para visita y cierre.'], columns: 3, width: 60 } },
|
|
{ id: makeId(), type: 'gallery', data: { title: 'Galeria referencial', images: ['','',''], captions: ['Zona 1','Zona 2','Zona 3'], fit: 'cover', width: 60 } },
|
|
{ id: makeId(), type: 'review', data: { title: 'Resenas', name: 'Cliente verificado', text: 'Presentacion impecable y proceso de contacto muy claro.', rating: 5, style: 'card', width: 60 } },
|
|
{ id: makeId(), type: 'contact', data: { title: 'Contacto directo', email: '', phone: '', address: '', cta_text: 'Escribir ahora', cta_url: '#', width: 60 } },
|
|
{ id: makeId(), type: 'social', data: { instagram: '', facebook: '', whatsapp: '', tiktok: '', youtube: '', icon_size: 18, icon_color: '#1f1b17', show_text: true, icon_style: 'pill', social_preset: 'pro', width: 60 } },
|
|
{ id: makeId(), type: 'map', data: { title: 'Ubicacion', address: '', embed_url: '', height: 320, width: 60 } }
|
|
]
|
|
},
|
|
cosmeticos: {
|
|
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: '' } }
|
|
]
|
|
},
|
|
danza: {
|
|
settings: { primary_color: '#7c3aed', bg_color: '#f8f5ff', text_color: '#160f29', muted_color: '#6d5c8f', font_body: 'Outfit', font_heading: 'Playfair Display', bg_gradient: true, bg_color2: '#efe7ff' },
|
|
blocks: [
|
|
{ id: makeId(), type: 'menu', data: defaultData('menu') },
|
|
{ id: makeId(), type: 'hero', data: { title: 'Escuela de danza con enfoque escénico', subtitle: 'Formación técnica, expresión artística y montaje profesional.', button_text: 'Agendar clase', button_url: '#contacto', image_url: '' } },
|
|
{ id: makeId(), type: 'video', data: { url: '', description: 'Video de presentacion' } },
|
|
{ id: makeId(), type: 'features', data: { title: 'Programas', items: ['Iniciación','Intermedio','Avanzado'] } },
|
|
{ id: makeId(), type: 'calendar', data: { title: 'Horarios', note: 'Consulta disponibilidad semanal.', embed_url: '' } },
|
|
{ id: makeId(), type: 'contact', data: { title: 'Inscripciones', email: '', phone: '', address: '' } }
|
|
]
|
|
},
|
|
despachos: {
|
|
settings: { primary_color: '#0f766e', bg_color: '#f7faf9', text_color: '#0b0c10', muted_color: '#54656a', font_body: 'IBM Plex Sans', font_heading: 'Merriweather', bg_gradient: false },
|
|
blocks: [
|
|
{ id: makeId(), type: 'menu', data: defaultData('menu') },
|
|
{ id: makeId(), type: 'hero', data: { title: 'Asesoría estratégica para decisiones seguras', subtitle: 'Despacho especializado en gestión legal y administrativa.', button_text: 'Solicitar consulta', button_url: '#contacto', image_url: '' } },
|
|
{ id: makeId(), type: 'iconlist', data: { title: 'Áreas', items: ['Corporativo|Acompañamiento empresarial','Tributario|Planificación y cumplimiento','Laboral|Prevención y defensa'] } },
|
|
{ id: makeId(), type: 'review', data: { title: 'Casos', name: 'Cliente empresarial', text: 'Resolución eficiente y comunicación clara en cada etapa.', rating: 5, style: 'quote' } },
|
|
{ id: makeId(), type: 'contact', data: { title: 'Contacto', email: '', phone: '', address: '' } }
|
|
]
|
|
},
|
|
educacion: {
|
|
settings: { primary_color: '#0a4dcf', bg_color: '#f4f7ff', text_color: '#0b1733', muted_color: '#4f6286', font_body: 'IBM Plex Sans', font_heading: 'Space Grotesk', bg_gradient: true, bg_color2: '#dbe8ff' },
|
|
blocks: [
|
|
{ id: makeId(), type: 'menu', data: { title: 'Academia', items: ['Inicio','Programas','Admisiones','Campus','Contacto'], menu_mode: 'both', menu_mobile_style: 'accordion', width: 100 } },
|
|
{ id: makeId(), type: 'hero', data: { title: 'Formacion academica con enfoque profesional', subtitle: 'Desarrolla competencias reales con docentes expertos, laboratorios modernos y acompanamiento continuo.', button_text: 'Iniciar admision', button_url: '#contacto', image_url: '' } },
|
|
{ id: makeId(), type: 'cards', data: { title: 'Programas destacados', items: ['Pregrado|Carreras con plan curricular actualizado y enfoque en empleabilidad.','Diplomados|Especializacion intensiva para perfiles tecnicos y profesionales.','Educacion continua|Trayectos cortos para actualizar habilidades de alto impacto.'] } },
|
|
{ id: makeId(), type: 'iconlist', data: { title: 'Ruta de admision', items: ['Postula online|Completa tu solicitud y adjunta documentos en minutos.','Entrevista academica|Recibe orientacion personalizada segun tu perfil.','Matricula guiada|Formaliza tu ingreso con soporte del equipo academico.'] } },
|
|
{ id: makeId(), type: 'features', data: { title: 'Por que elegirnos', items: ['Docentes activos en industria','Modelo hibrido flexible','Convenios empresariales','Tutoria academica permanente'] } },
|
|
{ id: makeId(), type: 'gallery', data: { title: 'Vida universitaria', images: ['','',''], captions: ['Laboratorios y practica aplicada','Clases colaborativas','Eventos academicos y comunidad'], fit: 'cover' } },
|
|
{ id: makeId(), type: 'calendar', data: { title: 'Fechas clave', note: 'Convocatoria abierta. Inicio de clases y cronograma de admision disponible para edicion.', embed_url: '' } },
|
|
{ id: makeId(), type: 'review', data: { title: 'Historias de estudiantes', name: 'Estudiante de Ingenieria', text: 'La metodologia es practica y exigente; pude aplicar lo aprendido desde el primer ciclo.', rating: 5, style: 'quote' } },
|
|
{ id: makeId(), type: 'button', data: { text: 'Descargar brochure academico', url: '#contacto', style: 'primary', size: 'lg' } },
|
|
{ id: makeId(), type: 'contact', data: { title: 'Oficina de admisiones', email: 'admisiones@tuinstitucion.edu', phone: '+54 9 11 0000 0000', address: 'Campus Central - Av. Principal 123' } },
|
|
{ id: makeId(), type: 'map', data: { title: 'Visita el campus', address: 'Campus Central - Av. Principal 123' } },
|
|
{ id: makeId(), type: 'social', data: { instagram: '', facebook: '', whatsapp: '', tiktok: '', youtube: '', icon_size: 18, icon_color: '#0b1733', show_text: true, icon_style: 'pill', width: 100 } }
|
|
]
|
|
}
|
|
};
|
|
|
|
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 isSaving = false;
|
|
let draggingBlockId = null;
|
|
let manualDrag = { active: false, id: null, index: null };
|
|
let pointerDrag = { active: false, id: null, index: null };
|
|
let canvasSortable = null;
|
|
const menuDrawerState = { activeDrawerId: null, lastFocused: null };
|
|
let drawerGlobalEventsBound = false;
|
|
|
|
function hasSortable(){ return typeof Sortable !== "undefined"; }
|
|
function isBlockVisibleInCanvas(block){
|
|
return (BUILDER_MODE === "ub24") ? ((block.page || "home") === currentPage) : true;
|
|
}
|
|
function widthToSpan(widthPct){
|
|
const pct = Math.max(30, Math.min(100, Number(widthPct || 50)));
|
|
return Math.max(4, Math.min(12, Math.round((pct / 100) * 12)));
|
|
}
|
|
|
|
function makeId(){ return "block_" + Date.now() + "_" + Math.floor(Math.random()*1000); }
|
|
function snapBlockWidth(type, raw){
|
|
if (type === "menu") return 100;
|
|
return Math.max(30, Math.min(100, Number(raw || 100)));
|
|
}
|
|
function hexToRgba(hex, alpha){
|
|
const raw = String(hex || "#0f172a").replace("#", "").trim();
|
|
const full = raw.length === 3 ? raw.split("").map((c)=>c+c).join("") : raw;
|
|
const num = parseInt(full || "0f172a", 16);
|
|
const r = (num >> 16) & 255;
|
|
const g = (num >> 8) & 255;
|
|
const b = num & 255;
|
|
const a = Math.max(0, Math.min(1, Number(alpha || 0)));
|
|
return `rgba(${r},${g},${b},${a})`;
|
|
}
|
|
function normalizeBlockPreset(value){
|
|
const raw = String(value || "").trim().toLowerCase();
|
|
return ["clean-landing","dark-glow","glass","soft-gradient","inherit"].includes(raw) ? raw : "inherit";
|
|
}
|
|
function normalizeBlockMotion(value){
|
|
const raw = String(value || "").trim().toLowerCase();
|
|
return ["none","flow","aurora","parallax","inherit"].includes(raw) ? raw : "inherit";
|
|
}
|
|
function normalizeMotionSpeed(value){
|
|
const raw = String(value || "").trim().toLowerCase();
|
|
return ["slow","normal","fast","inherit"].includes(raw) ? raw : "inherit";
|
|
}
|
|
function getMotionDuration(speed){
|
|
const key = normalizeMotionSpeed(speed);
|
|
if (key === "slow") return "24s";
|
|
if (key === "fast") return "12s";
|
|
return "18s";
|
|
}
|
|
function getDefaultPos(seedIndex){
|
|
const base = 20;
|
|
const gap = 120;
|
|
const raw = Number(seedIndex);
|
|
const i = Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 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:["Inicio","Productos","Blog","La Empresa","Contacto"], menu_mode:"both", menu_mobile_style:"accordion", width:100 };
|
|
case "hero": return { title:"Tu propuesta de valor", subtitle:"Explica en una frase por que elegirte.", kicker:"", button_text:"Contactar", button_url:"#contacto", button_secondary_text:"Ver mas", button_secondary_url:"#", image_url:"", align:"left", layout:"media-right", width:60, visual_preset:"inherit", bg_motion_style:"inherit", bg_motion_speed:"inherit" };
|
|
case "text": return { text:"Describe tu negocio aqui.", width:60, visual_preset:"inherit", bg_motion_style:"inherit", bg_motion_speed:"inherit" };
|
|
case "image": return { url:"", alt:"", caption:"", fit:"cover", overlay_text:"", width:60, visual_preset:"inherit", bg_motion_style:"inherit", bg_motion_speed:"inherit" };
|
|
case "features": return { title:"Beneficios", items:["Rapido","Profesional","Confiable"], width:60, visual_preset:"inherit", bg_motion_style:"inherit", bg_motion_speed:"inherit" };
|
|
case "gallery": return { title:"Proyectos", images:["","",""], captions:["","",""], fit:"cover", width:60, visual_preset:"inherit", bg_motion_style:"inherit", bg_motion_speed:"inherit" };
|
|
case "cards": return { title:"Propuesta", items:["Titulo 1|Texto breve","Titulo 2|Texto breve","Titulo 3|Texto breve"], columns:3, width:60, visual_preset:"inherit", bg_motion_style:"inherit", bg_motion_speed:"inherit" };
|
|
case "iconlist": return { title:"Diferenciales", items:["Rapido|Ahorra tiempo","Seguro|Datos protegidos","Soporte|Respuesta rapida"], width:60, visual_preset:"inherit", bg_motion_style:"inherit", bg_motion_speed:"inherit" };
|
|
case "contact": return { title:"Hablemos", email:"", phone:"", address:"", cta_text:"Reservar ahora", cta_url:"#", width:60, visual_preset:"inherit", bg_motion_style:"inherit", bg_motion_speed:"inherit" };
|
|
case "map": return { title:"Ubicacion", address:"", embed_url:"", height:320, width:60, visual_preset:"inherit", bg_motion_style:"inherit", bg_motion_speed:"inherit" };
|
|
case "button": return { text:"Accion", url:"#", style:"primary", size:"md", width:60, visual_preset:"inherit", bg_motion_style:"inherit", bg_motion_speed:"inherit"};
|
|
case "social": return { instagram:"", facebook:"", whatsapp:"", tiktok:"", youtube:"", icon_size:18, icon_color:"#0b0c10", show_text:true, icon_style:"pill", width:60, visual_preset:"inherit", bg_motion_style:"inherit", bg_motion_speed:"inherit" };
|
|
case "video": return { url:"", description:"", width:60, visual_preset:"inherit", bg_motion_style:"inherit", bg_motion_speed:"inherit" };
|
|
case "review": return { title:"Reseña destacada", name:"Cliente feliz", text:"Excelente servicio y resultados profesionales.", rating:5, style:"card", width:60, visual_preset:"inherit", bg_motion_style:"inherit", bg_motion_speed:"inherit" };
|
|
case "calendar": return { title:"Agenda una cita", note:"Disponible en plan premium. Proximamente.", embed_url:"", width:60, visual_preset:"inherit", bg_motion_style:"inherit", bg_motion_speed:"inherit" };
|
|
default: return {};
|
|
}
|
|
}
|
|
function normalizeMenuMode(value){
|
|
const raw = String(value || "both").trim().toLowerCase();
|
|
if (raw === "acordeon" || raw === "accordion") return "accordion";
|
|
if (raw === "horizontal" || raw === "inline") return "inline";
|
|
if (raw === "ambos" || raw === "both") return "both";
|
|
return "both";
|
|
}
|
|
function normalizeKey(value){
|
|
return String(value || "")
|
|
.normalize("NFD")
|
|
.replace(/[\u0300-\u036f]/g, "")
|
|
.toLowerCase()
|
|
.trim();
|
|
}
|
|
function findFirstBlockIdByTypes(types){
|
|
const set = new Set(types || []);
|
|
const found = state.blocks.find((b)=>b && b.type !== "menu" && set.has(b.type));
|
|
return found ? found.id : "";
|
|
}
|
|
function resolveEducationMenuTarget(label){
|
|
const key = normalizeKey(label);
|
|
if (key.includes("inicio")) return findFirstBlockIdByTypes(["hero","cards","features"]);
|
|
if (key.includes("program")) return findFirstBlockIdByTypes(["cards","features"]);
|
|
if (key.includes("admi")) return findFirstBlockIdByTypes(["iconlist","calendar","review"]);
|
|
if (key.includes("campus") || key.includes("galer")) return findFirstBlockIdByTypes(["gallery","map"]);
|
|
if (key.includes("contact")) return findFirstBlockIdByTypes(["contact","map"]);
|
|
return findFirstBlockIdByTypes(["hero","cards","features","iconlist","gallery","contact","map"]);
|
|
}
|
|
function isEducationRubro(){
|
|
return normalizeRubro(state?.settings?.business_rubro || SERVER_RUBRO || "") === "educacion";
|
|
}
|
|
function normalizeRubro(value){
|
|
const raw = String(value || "").trim().toLowerCase();
|
|
const allowed = new Set(OFFICIAL_RUBROS.map((r)=>r.value));
|
|
return allowed.has(raw) ? raw : "restaurante";
|
|
}
|
|
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 isMobilePreviewContext(){
|
|
const shell = document.querySelector(".preview-shell");
|
|
const forced = !!(shell && (shell.classList.contains("size-phone") || shell.classList.contains("size-tablet")));
|
|
return forced || window.innerWidth <= 980;
|
|
}
|
|
function getDrawerNodes(drawerId){
|
|
if (!drawerId) return { panel: null, overlay: null, toggle: null };
|
|
return {
|
|
panel: document.getElementById(drawerId),
|
|
overlay: document.querySelector(`[data-drawer-overlay="${drawerId}"]`),
|
|
toggle: document.querySelector(`[data-drawer-toggle="${drawerId}"]`)
|
|
};
|
|
}
|
|
function closeMenuDrawer(drawerId, opts = {}){
|
|
const { restoreFocus = true } = opts;
|
|
if (!drawerId) return;
|
|
const { panel, overlay, toggle } = getDrawerNodes(drawerId);
|
|
if (panel){
|
|
panel.classList.remove("open");
|
|
panel.setAttribute("aria-hidden", "true");
|
|
}
|
|
if (overlay){
|
|
overlay.classList.remove("open");
|
|
overlay.setAttribute("aria-hidden", "true");
|
|
}
|
|
if (toggle){ toggle.setAttribute("aria-expanded", "false"); }
|
|
document.body.classList.remove("menu-drawer-open");
|
|
if (menuDrawerState.activeDrawerId === drawerId){
|
|
menuDrawerState.activeDrawerId = null;
|
|
const target = (restoreFocus && menuDrawerState.lastFocused && menuDrawerState.lastFocused.isConnected)
|
|
? menuDrawerState.lastFocused
|
|
: toggle;
|
|
if (restoreFocus && target && typeof target.focus === "function"){ target.focus(); }
|
|
}
|
|
}
|
|
function closeAllMenuDrawers(opts = {}){
|
|
document.querySelectorAll(".menu-drawer.open").forEach((panel)=>{
|
|
closeMenuDrawer(panel.id, opts);
|
|
});
|
|
document.querySelectorAll(".menu-drawer-overlay.open").forEach((overlay)=>{
|
|
overlay.classList.remove("open");
|
|
overlay.setAttribute("aria-hidden", "true");
|
|
});
|
|
document.body.classList.remove("menu-drawer-open");
|
|
menuDrawerState.activeDrawerId = null;
|
|
}
|
|
function openMenuDrawer(drawerId, trigger){
|
|
if (!drawerId || !isMobilePreviewContext()) return;
|
|
if (menuDrawerState.activeDrawerId && menuDrawerState.activeDrawerId !== drawerId){
|
|
closeMenuDrawer(menuDrawerState.activeDrawerId, { restoreFocus: false });
|
|
}
|
|
const { panel, overlay, toggle } = getDrawerNodes(drawerId);
|
|
if (!panel || !overlay) return;
|
|
menuDrawerState.lastFocused = trigger || document.activeElement;
|
|
panel.classList.add("open");
|
|
panel.setAttribute("aria-hidden", "false");
|
|
overlay.classList.add("open");
|
|
overlay.setAttribute("aria-hidden", "false");
|
|
if (toggle){ toggle.setAttribute("aria-expanded", "true"); }
|
|
menuDrawerState.activeDrawerId = drawerId;
|
|
document.body.classList.add("menu-drawer-open");
|
|
const focusTarget = panel.querySelector("[data-drawer-close], a, button");
|
|
if (focusTarget && typeof focusTarget.focus === "function"){ focusTarget.focus(); }
|
|
}
|
|
function syncActiveDrawerForViewport(){
|
|
if (!menuDrawerState.activeDrawerId) return;
|
|
if (!isMobilePreviewContext()){
|
|
closeMenuDrawer(menuDrawerState.activeDrawerId, { restoreFocus: false });
|
|
}
|
|
}
|
|
function bindDrawerGlobalEvents(){
|
|
if (drawerGlobalEventsBound) return;
|
|
drawerGlobalEventsBound = true;
|
|
document.addEventListener("keydown",(e)=>{
|
|
if (!menuDrawerState.activeDrawerId) return;
|
|
if (e.key === "Escape"){
|
|
e.preventDefault();
|
|
closeMenuDrawer(menuDrawerState.activeDrawerId);
|
|
return;
|
|
}
|
|
if (e.key === "Tab"){
|
|
const { panel } = getDrawerNodes(menuDrawerState.activeDrawerId);
|
|
if (!panel) return;
|
|
const focusables = [...panel.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])')]
|
|
.filter((node)=>node && node.offsetParent !== null);
|
|
if (!focusables.length) return;
|
|
const first = focusables[0];
|
|
const last = focusables[focusables.length - 1];
|
|
const active = document.activeElement;
|
|
if (e.shiftKey && active === first){
|
|
e.preventDefault();
|
|
last.focus();
|
|
} else if (!e.shiftKey && active === last){
|
|
e.preventDefault();
|
|
first.focus();
|
|
}
|
|
}
|
|
});
|
|
window.addEventListener("resize", syncActiveDrawerForViewport);
|
|
}
|
|
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"){
|
|
if (isEducationRubro()){
|
|
const items = Array.isArray(block.data?.items) && block.data.items.length ? block.data.items : ["Inicio","Programas","Admisiones","Campus","Contacto"];
|
|
const links = items.map((it, idx)=>{
|
|
const target = resolveEducationMenuTarget(it);
|
|
const href = target ? `#${target}` : "#";
|
|
return `<a class="edu-nav-link editable" data-field="items.${idx}" data-placeholder="Item menu" contenteditable="true" href="${href}">${escapeHtml(it)}</a>`;
|
|
}).join("");
|
|
const contactTarget = resolveEducationMenuTarget("contacto");
|
|
const contactHref = contactTarget ? `#${contactTarget}` : "#";
|
|
const brand = state.settings.site_name || "Academia";
|
|
return `<div class="edu-nav">
|
|
<div class="site-brand">${state.settings.logo_url ? `<img src="${escapeHtml(state.settings.logo_url)}" alt="Logo" />` : `<span class="site-brand-badge">${escapeHtml(String(brand).slice(0,1).toUpperCase())}</span>`}<span>${escapeHtml(brand)}</span></div>
|
|
<div class="edu-nav-links">${links}</div>
|
|
<a href="${contactHref}" class="edu-nav-cta">Postular ahora</a>
|
|
</div>`;
|
|
}
|
|
const manualItems = Array.isArray(block.data?.items) ? block.data.items.map((x)=>String(x || "").trim()).filter(Boolean) : [];
|
|
const contentBlocks = 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 norm = (v)=>String(v || "")
|
|
.toLowerCase()
|
|
.normalize("NFD")
|
|
.replace(/[\u0300-\u036f]/g, "")
|
|
.replace(/[^a-z0-9]+/g, " ")
|
|
.trim();
|
|
const items = manualItems.length
|
|
? (manualItems.length >= 3
|
|
? manualItems.map((label, i)=>{
|
|
const key = norm(label);
|
|
const exact = contentBlocks.find((b)=>norm(b.label) === key);
|
|
const byIndex = contentBlocks[i];
|
|
const target = exact || byIndex || null;
|
|
return { id: target ? target.id : "", label: String(label).slice(0, 24), index: i+1 };
|
|
})
|
|
: [])
|
|
: contentBlocks;
|
|
const safeItems = items.length ? items : [
|
|
{ id: "menu_item_1", label: "Inicio", index: 1 },
|
|
{ id: "menu_item_2", label: "Productos", index: 2 },
|
|
{ id: "menu_item_3", label: "Contacto", index: 3 }
|
|
];
|
|
const links = safeItems.map(it=>`<a class="site-nav-link" href="${it.id ? `#${it.id}` : "#"}">${escapeHtml(it.label)}</a>`).join("");
|
|
const mobileLinks = safeItems.map(it=>`<a class="site-nav-link" href="${it.id ? `#${it.id}` : "#"}" data-drawer-link="1">${escapeHtml(it.label)}</a>`).join("");
|
|
const siteName = String(state.settings.site_name || "GKACHELE");
|
|
const initial = escapeHtml(siteName.slice(0, 1).toUpperCase());
|
|
const logo = state.settings.logo_url
|
|
? `<img src="${escapeHtml(state.settings.logo_url)}" alt="Logo" />`
|
|
: `<span class="site-brand-badge">${initial}</span>`;
|
|
const mode = normalizeMenuMode(block.data?.menu_mode || "both");
|
|
const mobileStyle = (block.data?.menu_mobile_style || "accordion").toLowerCase() === "drawer" ? "drawer" : "accordion";
|
|
const showInline = mode === "both" || mode === "inline";
|
|
const showAccordion = mode === "both" || mode === "accordion";
|
|
const showDrawer = showAccordion && mobileStyle === "drawer";
|
|
const menuDrawerId = `drawer_${block.id}`;
|
|
const menuOverlayId = `drawer_overlay_${block.id}`;
|
|
return `<div class="site-nav">
|
|
<div class="site-brand">${logo}<span>${escapeHtml(siteName)}</span></div>
|
|
<div class="menu-inline site-nav-links" style="${showInline ? "" : "display:none"}">${links}</div>
|
|
<button class="menu-drawer-toggle" type="button" data-drawer-toggle="${menuDrawerId}" style="${showDrawer ? "" : "display:none"}"><i class="fa-solid fa-bars"></i></button>
|
|
<details class="menu-accordion" style="${showAccordion && !showDrawer ? "" : "display:none"}">
|
|
<summary>Menu</summary>
|
|
<div class="menu-links">${mobileLinks}</div>
|
|
</details>
|
|
<div class="menu-drawer-overlay menu-drawer-global" id="${menuOverlayId}" data-drawer-overlay="${menuDrawerId}"></div>
|
|
<aside class="menu-drawer menu-drawer-global" id="${menuDrawerId}">
|
|
<div class="menu-drawer-head">
|
|
<div class="menu-drawer-brand">${logo}<span>${escapeHtml(siteName)}</span></div>
|
|
<button class="menu-drawer-close" type="button" data-drawer-close="${menuDrawerId}"><i class="fa-solid fa-xmark"></i></button>
|
|
</div>
|
|
<nav class="menu-drawer-links">${mobileLinks}</nav>
|
|
</aside>
|
|
</div>`;
|
|
}
|
|
if (block.type==="hero"){
|
|
if (isEducationRubro()){
|
|
const admissionTarget = resolveEducationMenuTarget("admisiones");
|
|
const programsTarget = resolveEducationMenuTarget("programas");
|
|
const admissionHref = admissionTarget ? `#${admissionTarget}` : "#";
|
|
const programsHref = programsTarget ? `#${programsTarget}` : "#";
|
|
return `<div class="edu-hero">
|
|
<div class="edu-hero-copy">
|
|
<div class="edu-kicker">Admisiones 2026 abiertas</div>
|
|
${editable("h2","title",block.data.title,"Titulo",false,"")}
|
|
${editable("p","subtitle",block.data.subtitle,"Subtitulo",true,"")}
|
|
<div class="hero-actions">
|
|
<a href="${admissionHref}" class="editable hero-cta" data-field="button_text" data-placeholder="Boton" contenteditable="true">${escapeHtml(block.data.button_text || "Iniciar admision")}</a>
|
|
<a href="${programsHref}" class="hero-cta-secondary">Ver programas</a>
|
|
</div>
|
|
</div>
|
|
<div class="edu-hero-panel">
|
|
<div class="edu-stat-grid">
|
|
<div class="edu-stat"><strong>+4,200</strong><span>Estudiantes activos</span></div>
|
|
<div class="edu-stat"><strong>92%</strong><span>Insercion laboral</span></div>
|
|
<div class="edu-stat"><strong>+80</strong><span>Docentes especialistas</span></div>
|
|
<div class="edu-stat"><strong>35</strong><span>Convenios empresariales</span></div>
|
|
</div>
|
|
<div class="edu-apply">
|
|
<h4>Proceso de postulacion</h4>
|
|
<ul>
|
|
<li>Registro en linea y carga de documentos.</li>
|
|
<li>Entrevista de orientacion academica.</li>
|
|
<li>Evaluacion y confirmacion de vacante.</li>
|
|
</ul>
|
|
<span class="deadline">Cierre de convocatoria: 30 marzo</span>
|
|
</div>
|
|
<div class="hero-media" style="min-height:180px">${block.data.image_url ? `<img src="${escapeHtml(block.data.image_url)}" alt="Campus">` : `<div class="hero-media-empty">Agrega imagen institucional para completar la portada.</div>`}</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
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>`;
|
|
const align = (block.data.align || "left").toLowerCase() === "center" ? "center" : "left";
|
|
const kicker = block.data.kicker || state.settings.site_name || "GKACHELE";
|
|
const secondaryText = block.data.button_secondary_text || "Ver servicios";
|
|
const secondaryUrl = block.data.button_secondary_url || "#servicios";
|
|
const isMediaLeft = String(block.data.layout || "media-right").toLowerCase() === "media-left";
|
|
const copyHtml = `<div class="hero-copy" style="text-align:${align}">
|
|
<div class="hero-kicker">${escapeHtml(kicker)}</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="${escapeHtml(secondaryUrl)}" class="hero-cta-secondary">${escapeHtml(secondaryText)}</a>
|
|
</div>
|
|
</div>`;
|
|
const mediaHtml = `<div class="hero-media">${image}</div>`;
|
|
return `<div class="hero-pro hero-layout">${isMediaLeft ? `${mediaHtml}${copyHtml}` : `${copyHtml}${mediaHtml}`}</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:[];
|
|
if (isEducationRubro()){
|
|
return `${editable("h3","title",block.data.title,"Titulo",false,"margin:0 0 12px;font-size:28px")}<div class="edu-step-grid">${items.map((i,idx)=>`<div class="edu-step"><span class="edu-step-num">${idx+1}</span><div class="editable" data-field="items.${idx}" data-placeholder="Item" contenteditable="true" style="font-weight:600;color:#0b1733;line-height:1.45">${escapeHtml(i)}</div></div>`).join("")}</div>`;
|
|
}
|
|
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:[];
|
|
const cols = Math.max(2, Math.min(4, Number(block.data.columns || 3)));
|
|
if (isEducationRubro()){
|
|
return `${editable("h3","title",block.data.title,"Titulo",false,"font-size:30px;margin:0 0 14px")}<div class="edu-cards-grid">${items.map((raw,idx)=>{const parts=String(raw).split("|");const t=parts[0]||"";const d=parts[1]||"";return `<div class="edu-card"><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><a href="#contacto" style="display:inline-flex;margin-top:10px;color:#0a4dcf;font-weight:700;text-decoration:none">Ver plan de estudios</a></div>`;}).join("")}</div>`;
|
|
}
|
|
return `${editable("h3","title",block.data.title,"Titulo",false,"")}<div class="cards-grid" style="grid-template-columns:repeat(${cols}, minmax(0,1fr))">${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:[];
|
|
if (isEducationRubro()){
|
|
return `${editable("h3","title",block.data.title,"Titulo",false,"margin:0 0 12px;font-size:28px")}<div class="edu-step-grid">${items.map((raw,idx)=>{const parts=String(raw).split("|");const t=parts[0]||"";const d=parts[1]||"";return `<div class="edu-step"><span class="edu-step-num">${idx+1}</span><div class="editable" data-field="items.${idx}.title" data-placeholder="Titulo" contenteditable="true" style="font-weight:700;color:#0b1733">${escapeHtml(t)}</div><div class="editable" data-field="items.${idx}.desc" data-placeholder="Descripcion" contenteditable="true" style="color:#4f6286;margin-top:6px;line-height:1.45">${escapeHtml(d)}</div></div>`;}).join("")}</div>`;
|
|
}
|
|
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 ctaText = escapeHtml(block.data.cta_text || "Reservar ahora");
|
|
const ctaUrl = escapeHtml(block.data.cta_url || "#");
|
|
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>
|
|
<a href="${ctaUrl}" class="hero-cta" style="margin-top:6px">${ctaText}</a>
|
|
</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 || "#0f172a";
|
|
const showText = block.data?.show_text !== false;
|
|
const style = (block.data?.icon_style || "pill").toLowerCase();
|
|
const preset = String(block.data?.social_preset || "pro").toLowerCase();
|
|
const presets = {
|
|
pro: { surfaceBg: "#ffffff", surfaceBorder: "#e5e7eb", title: "#0f172a", subtitle: "#475569", cardBg: "#f8fafc", cardBorder: "#dbe3ee", label: "#0f172a", value: "#64748b", shadow: "0 18px 34px rgba(15,23,42,.12)" },
|
|
dark: { surfaceBg: "#0f172a", surfaceBorder: "#1e293b", title: "#f8fafc", subtitle: "#94a3b8", cardBg: "#111827", cardBorder: "#334155", label: "#e2e8f0", value: "#94a3b8", shadow: "0 18px 34px rgba(2,6,23,.45)" },
|
|
glass: { surfaceBg: "rgba(255,255,255,.70)", surfaceBorder: "rgba(255,255,255,.55)", title: "#0f172a", subtitle: "#475569", cardBg: "rgba(255,255,255,.55)", cardBorder: "rgba(148,163,184,.5)", label: "#0f172a", value: "#475569", shadow: "0 22px 42px rgba(15,23,42,.14)" }
|
|
};
|
|
const tone = presets[preset] || presets.pro;
|
|
const useBrandColors = block.data?.social_use_brand_colors !== false;
|
|
const surfaceBg = block.data?.social_surface_bg || tone.surfaceBg;
|
|
const surfaceBorder = block.data?.social_surface_border || tone.surfaceBorder;
|
|
const titleColor = block.data?.social_title_color || tone.title;
|
|
const subtitleColor = block.data?.social_subtitle_color || tone.subtitle;
|
|
const cardBg = block.data?.social_card_bg || tone.cardBg;
|
|
const cardBorder = block.data?.social_card_border || tone.cardBorder;
|
|
const labelColor = block.data?.social_label_color || tone.label;
|
|
const valueColor = block.data?.social_value_color || tone.value;
|
|
const brandStyles = {
|
|
whatsapp: { icon: "#25D366", bg: "#ecfdf3", border: "#bbf7d0" },
|
|
instagram: { icon: "#E1306C", bg: "#fff1f7", border: "#fecdd3" },
|
|
facebook: { icon: "#1877F2", bg: "#eff6ff", border: "#bfdbfe" },
|
|
tiktok: { icon: "#111111", bg: "#f8fafc", border: "#e2e8f0" },
|
|
youtube: { icon: "#FF0000", bg: "#fff1f2", border: "#fecdd3" }
|
|
};
|
|
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 `
|
|
<div class="social-surface-pro" style="width:100%;margin:0;background:${escapeHtml(surfaceBg)};border:1px solid ${escapeHtml(surfaceBorder)};border-radius:22px;padding:22px;box-shadow:${escapeHtml(tone.shadow)}">
|
|
<h3 style="margin:0;color:${escapeHtml(titleColor)};font-size:40px;line-height:1.02;letter-spacing:-.03em;font-weight:850">Redes Sociales</h3>
|
|
<p style="margin:10px 0 20px;color:${escapeHtml(subtitleColor)};font-size:16px">Conecta con nosotros en tus plataformas favoritas.</p>
|
|
<div class="social-grid-pro">
|
|
${items.map(([k,v])=>{
|
|
const brand = brandStyles[k] || { icon: iconColor, bg: cardBg, border: cardBorder };
|
|
const tileBg = useBrandColors ? brand.bg : cardBg;
|
|
const tileBorder = useBrandColors ? brand.border : cardBorder;
|
|
const tileIcon = useBrandColors ? brand.icon : iconColor;
|
|
const href = escapeHtml(linkFor(k,v) || "#");
|
|
const label = escapeHtml(k.charAt(0).toUpperCase() + k.slice(1));
|
|
return `<a class="social-btn social-style-${escapeHtml(style)} social-card-pro" href="${href}" target="_blank" rel="noreferrer" style="border:1px solid ${tileBorder};background:${tileBg}">
|
|
<i class="${icons[k]||'fa-solid fa-circle'}" style="font-size:${Math.max(28,size)}px;color:${tileIcon}"></i>
|
|
<div class="social-card-meta">
|
|
<strong class="social-card-title" style="color:${escapeHtml(labelColor)}">${label}</strong>
|
|
${showText ? `<span class="editable social-card-value" data-field="${escapeHtml(k)}" data-placeholder="${escapeHtml(k)}" contenteditable="true" style="color:${escapeHtml(valueColor)}">${escapeHtml(v)}</span>` : ""}
|
|
</div>
|
|
</a>`;
|
|
}).join("")}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
if (block.type==="map"){
|
|
const h = Math.max(220, Math.min(700, Number(block.data.height || 320)));
|
|
const embed = normalizeLink(block.data.embed_url || "");
|
|
const q = encodeURIComponent(block.data.address || "");
|
|
const src = embed || (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:${h}px;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 o URL embed</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(){
|
|
document.querySelectorAll("#blockList .block-item").forEach((item)=>{
|
|
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");
|
|
shell.style.setProperty("--restaurant-surface", s.theme === "dark" ? "#111827" : "#ffffff");
|
|
shell.style.setProperty("--restaurant-surface-soft", s.theme === "dark" ? "#0f172a" : "#f8fafc");
|
|
shell.style.setProperty("--restaurant-border", s.theme === "dark" ? "#263043" : "#dbe3ee");
|
|
shell.style.setProperty("--restaurant-bg-1", s.bg_color || "#f8fafc");
|
|
shell.style.setProperty("--restaurant-bg-2", s.bg_gradient ? (s.bg_color2 || "#eef2f7") : (s.bg_color || "#f8fafc"));
|
|
}
|
|
|
|
function renderPreview(){
|
|
const canvas = document.getElementById("previewCanvas");
|
|
closeAllMenuDrawers({ restoreFocus: false });
|
|
document.querySelectorAll(".menu-drawer-global").forEach((n)=>n.remove());
|
|
if (canvasSortable){
|
|
canvasSortable.destroy();
|
|
canvasSortable = null;
|
|
}
|
|
canvas.innerHTML = "";
|
|
canvas.classList.toggle("free-drag", !!state.settings.free_drag);
|
|
canvas.classList.toggle("education-site", isEducationRubro());
|
|
canvas.classList.toggle("restaurant-site", normalizeRubro(state.settings.business_rubro || SERVER_RUBRO || "") === "restaurante");
|
|
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.paddingBottom = `${Math.max(0, Number(state.settings.canvas_bottom_space || 0))}px`;
|
|
canvas.style.position = "relative";
|
|
canvas.style.overflow = "hidden";
|
|
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);
|
|
}
|
|
if (canvas.classList.contains("restaurant-site")){
|
|
const overlay = document.createElement("div");
|
|
overlay.className = "canvas-bg-overlay";
|
|
canvas.appendChild(overlay);
|
|
}
|
|
const inner = document.createElement("div");
|
|
inner.className = "canvas-inner";
|
|
inner.style.position = "relative";
|
|
inner.style.zIndex = "1";
|
|
if (!state.settings.free_drag){
|
|
inner.style.display = "grid";
|
|
inner.style.gridTemplateColumns = "repeat(12, minmax(0, 1fr))";
|
|
inner.style.alignItems = "stretch";
|
|
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>';
|
|
if (!state.settings.free_drag && hasSortable()){
|
|
initCanvasSortable(inner);
|
|
}
|
|
return;
|
|
}
|
|
const visibleBlocks = state.blocks.filter(isBlockVisibleInCanvas);
|
|
visibleBlocks.forEach((block, idx)=>{
|
|
const el = document.createElement("div");
|
|
el.className = "block";
|
|
if (!state.settings.free_drag){ el.removeAttribute("draggable"); }
|
|
el.dataset.blockId = block.id;
|
|
el.dataset.blockType = block.type;
|
|
el.id = block.id;
|
|
if (state.settings.free_drag){
|
|
const pos = block.pos || getDefaultPos(idx);
|
|
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 widthPct = snapBlockWidth(block.type, block.data?.width);
|
|
block.data = block.data || {};
|
|
block.data.width = widthPct;
|
|
const isFullWidth = block.type === "menu" || widthPct >= 99;
|
|
if (isFullWidth){
|
|
el.style.width = "100%";
|
|
el.style.gridColumn = "1 / -1";
|
|
} else {
|
|
const span = widthToSpan(widthPct);
|
|
el.style.width = "100%";
|
|
el.style.gridColumn = `span ${span}`;
|
|
el.style.height = "100%";
|
|
}
|
|
}
|
|
const blockData = block.data || {};
|
|
const globalPreset = normalizeBlockPreset(state.settings.global_block_preset || "clean-landing");
|
|
const globalMotion = normalizeBlockMotion(state.settings.global_block_motion || "none");
|
|
const globalMotionSpeed = normalizeMotionSpeed(state.settings.global_block_motion_speed || "normal");
|
|
const blockPreset = normalizeBlockPreset(blockData.visual_preset || "inherit");
|
|
const blockMotion = normalizeBlockMotion(blockData.bg_motion_style || "inherit");
|
|
const blockMotionSpeed = normalizeMotionSpeed(blockData.bg_motion_speed || "inherit");
|
|
const resolvedPreset = blockPreset === "inherit" ? (globalPreset === "inherit" ? "clean-landing" : globalPreset) : blockPreset;
|
|
const resolvedMotion = blockMotion === "inherit" ? (globalMotion === "inherit" ? "none" : globalMotion) : blockMotion;
|
|
const resolvedMotionSpeed = blockMotionSpeed === "inherit" ? (globalMotionSpeed === "inherit" ? "normal" : globalMotionSpeed) : blockMotionSpeed;
|
|
let animIn = ["fade","slide","zoom","none"].includes(String(blockData.anim_in || "").toLowerCase()) ? String(blockData.anim_in).toLowerCase() : "slide";
|
|
let animHover = ["lift","glow","tilt","none"].includes(String(blockData.anim_hover || "").toLowerCase()) ? String(blockData.anim_hover).toLowerCase() : "glow";
|
|
if (PREVIEW_ONLY && state.settings.animations !== false){
|
|
if (animIn === "none") animIn = "fade";
|
|
if (animHover === "none") animHover = "glow";
|
|
}
|
|
const animDuration = Math.max(120, Math.min(900, Number(blockData.anim_duration || 250)));
|
|
el.classList.add(`style-${resolvedPreset}`);
|
|
if (resolvedMotion !== "none"){ el.classList.add(`bg-motion-${resolvedMotion}`); }
|
|
el.classList.add(`anim-in-${animIn}`);
|
|
el.classList.add(`hover-${animHover}`);
|
|
el.style.transitionDuration = `${animDuration}ms`;
|
|
el.style.setProperty("--block-motion-duration", getMotionDuration(resolvedMotionSpeed));
|
|
const surfaceColor = String(blockData.surface_color || "").trim();
|
|
const borderColor = String(blockData.border_color || "").trim();
|
|
const textColor = String(blockData.text_color || "").trim();
|
|
if (surfaceColor){ el.style.setProperty("--site-block-bg", surfaceColor); } else { el.style.removeProperty("--site-block-bg"); }
|
|
if (borderColor){ el.style.setProperty("--site-block-border", borderColor); } else { el.style.removeProperty("--site-block-border"); }
|
|
if (textColor){ el.style.setProperty("--site-text", textColor); } else { el.style.removeProperty("--site-text"); }
|
|
const bgImageUrl = String(blockData.bg_image_url || "").trim();
|
|
if (bgImageUrl){
|
|
const overlayHex = String(blockData.bg_overlay_hex || "#0f172a");
|
|
const overlayOpacity = Math.max(0, Math.min(0.85, Number(blockData.bg_overlay_opacity || 0)));
|
|
const overlay = hexToRgba(overlayHex, overlayOpacity);
|
|
const safeBg = bgImageUrl.replace(/'/g, "%27");
|
|
el.classList.add("has-bg-media");
|
|
el.style.backgroundImage = `linear-gradient(${overlay}, ${overlay}), url('${safeBg}')`;
|
|
} else {
|
|
el.classList.remove("has-bg-media");
|
|
el.style.backgroundImage = "";
|
|
}
|
|
const blur = Math.max(0, Math.min(10, Number(blockData.bg_blur || 0)));
|
|
if (blur > 0){
|
|
el.style.backdropFilter = `blur(${blur}px)`;
|
|
el.style.webkitBackdropFilter = `blur(${blur}px)`;
|
|
} else {
|
|
el.style.backdropFilter = "";
|
|
el.style.webkitBackdropFilter = "";
|
|
}
|
|
el.innerHTML = renderBlockHtml(block);
|
|
if (block.type === "menu"){ wireMenuAccordionInteractions(el); }
|
|
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 drag = document.createElement("button");
|
|
drag.className = "drag-handle";
|
|
drag.innerHTML = '<i class="fa-solid fa-grip-vertical"></i>';
|
|
drag.title = "Mover bloque";
|
|
drag.addEventListener("click",(e)=>{ e.preventDefault(); e.stopPropagation(); });
|
|
if (!hasSortable()){
|
|
drag.addEventListener("mousedown",(e)=>{
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
startPointerDrag(block.id, e);
|
|
});
|
|
}
|
|
const up = document.createElement("button");
|
|
up.innerHTML = '<i class="fa-solid fa-arrow-up"></i>';
|
|
up.title = "Subir bloque";
|
|
up.addEventListener("click",(e)=>{
|
|
e.stopPropagation();
|
|
moveBlockByDelta(block.id, -1);
|
|
});
|
|
const down = document.createElement("button");
|
|
down.innerHTML = '<i class="fa-solid fa-arrow-down"></i>';
|
|
down.title = "Bajar bloque";
|
|
down.addEventListener("click",(e)=>{
|
|
e.stopPropagation();
|
|
moveBlockByDelta(block.id, 1);
|
|
});
|
|
const width = document.createElement("button");
|
|
const widthNow = snapBlockWidth(block.type, block.data?.width);
|
|
width.innerHTML = widthNow >= 95 ? '<i class="fa-solid fa-arrows-left-right-to-line"></i>' : '<i class="fa-solid fa-table-columns"></i>';
|
|
width.title = widthNow >= 95 ? "Pasar a 50%" : "Pasar a 100%";
|
|
width.addEventListener("click",(e)=>{
|
|
e.stopPropagation();
|
|
selectedBlockId = block.id;
|
|
toggleBlockWidth(block.id);
|
|
renderInspector();
|
|
});
|
|
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(drag);
|
|
actions.appendChild(up);
|
|
actions.appendChild(down);
|
|
actions.appendChild(width);
|
|
actions.appendChild(del);
|
|
el.appendChild(actions);
|
|
} else if (!state.settings.free_drag && hasSortable()){
|
|
const drag = document.createElement("button");
|
|
drag.className = "block-drag-handle drag-handle";
|
|
drag.innerHTML = '<i class="fa-solid fa-grip-vertical"></i>';
|
|
drag.title = "Mover bloque";
|
|
drag.addEventListener("click",(e)=>{ e.preventDefault(); e.stopPropagation(); });
|
|
el.appendChild(drag);
|
|
}
|
|
if (block.id === selectedBlockId) el.classList.add("selected");
|
|
if (!state.settings.free_drag && !hasSortable()){
|
|
el.style.touchAction = "none";
|
|
el.addEventListener("pointerdown",(e)=>{
|
|
if (e.button !== 0) return;
|
|
if (e.target && (e.target.closest(".resize-handle") || e.target.closest(".editable") || e.target.closest("input") || e.target.closest("textarea") || e.target.closest("select") || e.target.closest("a") || e.target.closest("button"))) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
startPointerDrag(block.id, e);
|
|
});
|
|
}
|
|
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("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 (!state.settings.free_drag && block.type !== "menu"){
|
|
const h = document.createElement("div");
|
|
h.className = "resize-handle edge e";
|
|
h.setAttribute("data-handle", "e");
|
|
h.title = "Redimensionar ancho";
|
|
h.addEventListener("mousedown",(e)=>startFreeResize(e, block));
|
|
h.addEventListener("touchstart",(e)=>startFreeResize(e, block), { passive: false });
|
|
el.appendChild(h);
|
|
} else if (BUILDER_MODE !== "ub24" && state.settings.free_drag){
|
|
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 && hasSortable()){
|
|
initCanvasSortable(inner);
|
|
}
|
|
let freeDragBottom = 0;
|
|
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);
|
|
});
|
|
freeDragBottom = maxBottom;
|
|
canvas.style.minHeight = maxBottom + "px";
|
|
} else {
|
|
const minH = Math.max(900, Number(state.settings.canvas_min_height || 1200));
|
|
canvas.style.minHeight = `${minH}px`;
|
|
}
|
|
if (state.settings.free_drag){
|
|
const spacer = document.createElement("div");
|
|
spacer.style.height = `${Math.max(0, freeDragBottom)}px`;
|
|
spacer.style.width = "100%";
|
|
spacer.style.pointerEvents = "none";
|
|
inner.appendChild(spacer);
|
|
}
|
|
const footer = document.createElement("footer");
|
|
footer.className = "site-global-footer";
|
|
const year = new Date().getFullYear();
|
|
const registered = String(state.settings.registered_brand || state.settings.site_name || "Tu marca").trim();
|
|
const devBrand = String(state.settings.developer_brand || "GKACHELE™").trim();
|
|
const author = String(state.settings.site_author || "").trim();
|
|
footer.innerHTML = `© ${year} ${escapeHtml(registered)}. Todos los derechos reservados.<br>Desarrollado por ${escapeHtml(devBrand)}${author ? ` · Creado por ${escapeHtml(author)}` : ""}`;
|
|
inner.appendChild(footer);
|
|
const scrollBtn = document.createElement("button");
|
|
scrollBtn.className = "scroll-btn";
|
|
scrollBtn.innerHTML = '<i class="fa-solid fa-arrow-up"></i>';
|
|
scrollBtn.addEventListener("click",()=>{
|
|
const main = document.querySelector(".main");
|
|
if (main && typeof main.scrollTo === "function"){
|
|
main.scrollTo({ top: 0, behavior: "smooth" });
|
|
}
|
|
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 getCanvasContainer(canvas){
|
|
if (!canvas) return null;
|
|
return canvas.querySelector(".canvas-inner") || canvas;
|
|
}
|
|
function getDropIndex(container,y,x){
|
|
const blocks=[...container.querySelectorAll(".block")]
|
|
.filter((n)=>n.dataset.blockId !== draggingBlockId);
|
|
for (let i=0;i<blocks.length;i++){
|
|
const r=blocks[i].getBoundingClientRect();
|
|
const halfY = r.top + r.height/2;
|
|
if (y >= r.top && y <= r.bottom){
|
|
if (typeof x === "number"){
|
|
const halfX = r.left + r.width/2;
|
|
return x < halfX ? i : i + 1;
|
|
}
|
|
return i;
|
|
}
|
|
if (y < halfY) 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 getSplitTarget(container, y, x){
|
|
if (typeof x !== "number") return null;
|
|
const blocks=[...container.querySelectorAll(".block")]
|
|
.filter((n)=>n.dataset.blockId !== draggingBlockId);
|
|
for (const n of blocks){
|
|
const r = n.getBoundingClientRect();
|
|
if (y < r.top || y > r.bottom) continue;
|
|
if (x < r.left || x > r.right) continue;
|
|
const block = state.blocks.find(b=>b.id===n.dataset.blockId);
|
|
const widthPct = Math.max(30, Math.min(100, Number(block?.data?.width || 100)));
|
|
if (n.dataset.blockId && widthPct >= 95){
|
|
const halfX = r.left + r.width / 2;
|
|
return { id: n.dataset.blockId, before: x < halfX };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function startPointerDrag(blockId, e){
|
|
if (!blockId || state.settings.free_drag) return;
|
|
const canvas = document.getElementById("previewCanvas");
|
|
const container = getCanvasContainer(canvas);
|
|
if (!container) return;
|
|
pointerDrag = { active: true, id: blockId, index: null, split: null };
|
|
draggingBlockId = blockId;
|
|
const node = document.getElementById(blockId);
|
|
if (node) node.classList.add("dragging");
|
|
const updateAt = (clientY, clientX)=>{
|
|
const idx = getDropIndex(container, clientY, clientX);
|
|
pointerDrag.index = idx;
|
|
pointerDrag.split = getSplitTarget(container, clientY, clientX);
|
|
showDrop(container, idx);
|
|
};
|
|
updateAt(e.clientY, e.clientX);
|
|
const onMove = (ev)=>{
|
|
if (!pointerDrag.active) return;
|
|
updateAt(ev.clientY, ev.clientX);
|
|
};
|
|
const onEnd = ()=>{
|
|
const current = pointerDrag;
|
|
pointerDrag = { active: false, id: null, index: null };
|
|
const n = document.getElementById(blockId);
|
|
if (n) n.classList.remove("dragging");
|
|
removeDrop();
|
|
window.removeEventListener("pointermove", onMove);
|
|
window.removeEventListener("pointerup", onEnd);
|
|
window.removeEventListener("pointercancel", onEnd);
|
|
draggingBlockId = null;
|
|
if (current.id && current.split && current.split.id){
|
|
const moving = state.blocks.find(b=>b.id===current.id);
|
|
const target = state.blocks.find(b=>b.id===current.split.id);
|
|
if (moving){
|
|
moving.data = moving.data || {};
|
|
moving.data.width = 50;
|
|
}
|
|
if (target){
|
|
target.data = target.data || {};
|
|
target.data.width = 50;
|
|
}
|
|
const targetIdx = state.blocks.findIndex(b=>b.id===current.split.id);
|
|
if (targetIdx >= 0){
|
|
moveBlock(current.id, current.split.before ? targetIdx : targetIdx + 1);
|
|
return;
|
|
}
|
|
}
|
|
if (current.id && typeof current.index === "number"){
|
|
moveBlock(current.id, current.index);
|
|
}
|
|
};
|
|
window.addEventListener("pointermove", onMove);
|
|
window.addEventListener("pointerup", onEnd);
|
|
window.addEventListener("pointercancel", onEnd);
|
|
}
|
|
function startManualDrag(blockId, e){
|
|
if (!blockId || state.settings.free_drag) return;
|
|
const canvas = document.getElementById("previewCanvas");
|
|
const container = getCanvasContainer(canvas);
|
|
if (!container) return;
|
|
manualDrag = { active: true, id: blockId, index: null };
|
|
draggingBlockId = blockId;
|
|
const node = document.getElementById(blockId);
|
|
if (node) node.classList.add("dragging");
|
|
const initialIdx = getDropIndex(container, e.clientY, e.clientX);
|
|
manualDrag.index = initialIdx;
|
|
showDrop(container, initialIdx);
|
|
const onMove = (ev)=>{
|
|
if (!manualDrag.active) return;
|
|
const idx = getDropIndex(container, ev.clientY, ev.clientX);
|
|
manualDrag.index = idx;
|
|
showDrop(container, idx);
|
|
};
|
|
const onUp = ()=>{
|
|
const current = manualDrag;
|
|
manualDrag = { active: false, id: null, index: null };
|
|
const n = document.getElementById(blockId);
|
|
if (n) n.classList.remove("dragging");
|
|
removeDrop();
|
|
document.removeEventListener("mousemove", onMove);
|
|
document.removeEventListener("mouseup", onUp);
|
|
draggingBlockId = null;
|
|
if (current.id && typeof current.index === "number"){
|
|
moveBlock(current.id, current.index);
|
|
}
|
|
};
|
|
document.addEventListener("mousemove", onMove);
|
|
document.addEventListener("mouseup", onUp);
|
|
}
|
|
function addBlock(type,index=state.blocks.length){
|
|
const b={ id: makeId(), type, data: defaultData(type) };
|
|
if (typeof b.data.width === "undefined") b.data.width = snapBlockWidth(type, 100);
|
|
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){
|
|
const from=state.blocks.findIndex(b=>b.id===id);
|
|
if (from<0) return;
|
|
const numericTo = Number(toIndex);
|
|
if (!Number.isFinite(numericTo)) return;
|
|
const clamped = Math.max(0, Math.min(state.blocks.length - 1, numericTo));
|
|
const [b]=state.blocks.splice(from,1);
|
|
const target = Math.max(0, Math.min(state.blocks.length, clamped));
|
|
state.blocks.splice(target,0,b);
|
|
selectedBlockId = b.id;
|
|
renderPreview();
|
|
}
|
|
function moveBlockByDelta(id, delta){
|
|
const from = state.blocks.findIndex(b=>b.id===id);
|
|
if (from < 0) return;
|
|
moveBlock(id, from + delta);
|
|
}
|
|
function toggleBlockWidth(id){
|
|
const block = state.blocks.find(b=>b.id===id);
|
|
if (!block || block.type === "menu") return;
|
|
block.data = block.data || {};
|
|
const current = Math.max(30, Math.min(100, Number(block.data.width || 100)));
|
|
block.data.width = current >= 95 ? 50 : 100;
|
|
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>`;
|
|
html += `<div class="row" style="display:flex;gap:8px"><button class="btn secondary" id="moveBlockUpBtn" type="button" style="flex:1">Mover arriba</button><button class="btn secondary" id="moveBlockDownBtn" type="button" style="flex:1">Mover abajo</button></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>`;
|
|
if (block.type==="menu"){
|
|
html+=input("Titulo","menuTitle",data.title);
|
|
const mm = escapeHtml(normalizeMenuMode(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>`;
|
|
const mms = escapeHtml((data.menu_mobile_style || "accordion").toLowerCase() === "drawer" ? "drawer" : "accordion");
|
|
html+=`<div class="row"><label>Movil</label><select id="menuMobileStyle"><option value="accordion" ${mms==="accordion"?"selected":""}>Acordeon</option><option value="drawer" ${mms==="drawer"?"selected":""}>Drawer Pro</option></select></div>`;
|
|
html+=`<div class="row"><label>Items menu (una linea)</label><textarea id="menuItems">${escapeHtml((data.items||[]).join("\n"))}</textarea></div>`;
|
|
} else if (block.type==="hero"){
|
|
html+=input("Kicker","heroKicker",data.kicker);
|
|
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("Texto boton secundario","heroBtn2Text",data.button_secondary_text);
|
|
html+=input("URL boton secundario","heroBtn2Url",data.button_secondary_url);
|
|
const alignVal = escapeHtml((data.align || "left").toLowerCase());
|
|
html+=`<div class="row"><label>Alineacion</label><select id="heroAlign"><option value="left" ${alignVal==="left"?"selected":""}>Izquierda</option><option value="center" ${alignVal==="center"?"selected":""}>Centro</option></select></div>`;
|
|
const heroLayoutVal = escapeHtml((data.layout || "media-right").toLowerCase());
|
|
html+=`<div class="row"><label>Posicion contenido</label><select id="heroLayout"><option value="media-right" ${heroLayoutVal==="media-right"?"selected":""}>Texto izquierda / Imagen derecha</option><option value="media-left" ${heroLayoutVal==="media-left"?"selected":""}>Imagen izquierda / Texto derecha</option></select></div>`;
|
|
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>`;
|
|
html+=`<div class="row"><label>Columnas</label><input id="cardsColumns" type="number" min="2" max="4" step="1" value="${Math.max(2, Math.min(4, Number(data.columns || 3)))}"></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);
|
|
html+=input("CTA texto","contactCtaText",data.cta_text);
|
|
html+=input("CTA URL","contactCtaUrl",data.cta_url);
|
|
} 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>`;
|
|
const presetVal = escapeHtml(String(data.social_preset || "pro").toLowerCase());
|
|
html+=`<div class="row"><label>Preset visual</label><select id="socialPreset"><option value="pro" ${presetVal==="pro"?"selected":""}>Pro</option><option value="dark" ${presetVal==="dark"?"selected":""}>Dark</option><option value="glass" ${presetVal==="glass"?"selected":""}>Glass</option></select></div>`;
|
|
html+=`<div class="row"><label>Usar colores de marca</label><input id="socialUseBrandColors" type="checkbox" ${data.social_use_brand_colors !== false ? "checked" : ""}></div>`;
|
|
html+=`<div class="row"><label>Fondo panel</label><input id="socialSurfaceBg" type="color" value="${escapeHtml(data.social_surface_bg || "#ffffff")}"></div>`;
|
|
html+=`<div class="row"><label>Borde panel</label><input id="socialSurfaceBorder" type="color" value="${escapeHtml(data.social_surface_border || "#e5e7eb")}"></div>`;
|
|
html+=`<div class="row"><label>Color titulo</label><input id="socialTitleColor" type="color" value="${escapeHtml(data.social_title_color || "#0f172a")}"></div>`;
|
|
html+=`<div class="row"><label>Color subtitulo</label><input id="socialSubtitleColor" type="color" value="${escapeHtml(data.social_subtitle_color || "#475569")}"></div>`;
|
|
html+=`<div class="row"><label>Fondo tarjetas</label><input id="socialCardBg" type="color" value="${escapeHtml(data.social_card_bg || "#f8fafc")}"></div>`;
|
|
html+=`<div class="row"><label>Borde tarjetas</label><input id="socialCardBorder" type="color" value="${escapeHtml(data.social_card_border || "#dbe3ee")}"></div>`;
|
|
html+=`<div class="row"><label>Color etiquetas</label><input id="socialLabelColor" type="color" value="${escapeHtml(data.social_label_color || "#0f172a")}"></div>`;
|
|
html+=`<div class="row"><label>Color texto enlace</label><input id="socialValueColor" type="color" value="${escapeHtml(data.social_value_color || "#64748b")}"></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);
|
|
html+=input("URL embed (opcional)","mapEmbedUrl",data.embed_url);
|
|
html+=`<div class="row"><label>Alto mapa (px)</label><input id="mapHeight" type="number" min="220" max="700" step="10" value="${Math.max(220, Math.min(700, Number(data.height || 320)))}"></div>`;
|
|
} 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>`;
|
|
}
|
|
if (block.type !== "menu"){
|
|
const w = snapBlockWidth(block.type, data.width);
|
|
html+=`<div class="row"><label>Ancho bloque (%)</label><input id="blockWidth" type="range" min="30" max="100" step="1" value="${w}"></div>`;
|
|
html+=`<div class="row"><input id="blockWidthNumber" type="number" min="30" max="100" step="1" value="${w}"></div>`;
|
|
const visualPreset = escapeHtml(normalizeBlockPreset(data.visual_preset || "inherit"));
|
|
html+=`<div class="row"><label>Preset visual bloque</label><select id="blockVisualPreset"><option value="inherit" ${visualPreset==="inherit"?"selected":""}>Heredar global</option><option value="clean-landing" ${visualPreset==="clean-landing"?"selected":""}>Clean Landing</option><option value="dark-glow" ${visualPreset==="dark-glow"?"selected":""}>Dark Glow</option><option value="glass" ${visualPreset==="glass"?"selected":""}>Glass</option><option value="soft-gradient" ${visualPreset==="soft-gradient"?"selected":""}>Soft Gradient</option></select></div>`;
|
|
const blockMotion = escapeHtml(normalizeBlockMotion(data.bg_motion_style || "inherit"));
|
|
html+=`<div class="row"><label>Movimiento fondo bloque</label><select id="blockMotionStyle"><option value="inherit" ${blockMotion==="inherit"?"selected":""}>Heredar global</option><option value="none" ${blockMotion==="none"?"selected":""}>Sin movimiento</option><option value="flow" ${blockMotion==="flow"?"selected":""}>Gradient Flow</option><option value="aurora" ${blockMotion==="aurora"?"selected":""}>Aurora</option><option value="parallax" ${blockMotion==="parallax"?"selected":""}>Parallax Soft</option></select></div>`;
|
|
const blockMotionSpeed = escapeHtml(normalizeMotionSpeed(data.bg_motion_speed || "inherit"));
|
|
html+=`<div class="row"><label>Velocidad movimiento</label><select id="blockMotionSpeed"><option value="inherit" ${blockMotionSpeed==="inherit"?"selected":""}>Heredar global</option><option value="slow" ${blockMotionSpeed==="slow"?"selected":""}>Lenta</option><option value="normal" ${blockMotionSpeed==="normal"?"selected":""}>Normal</option><option value="fast" ${blockMotionSpeed==="fast"?"selected":""}>Rapida</option></select></div>`;
|
|
html+=`<div class="row"><label>Color superficie (opcional)</label><input id="blockSurfaceColor" type="color" value="${escapeHtml(data.surface_color || "#ffffff")}"></div>`;
|
|
html+=`<div class="row"><label>Color borde (opcional)</label><input id="blockBorderColor" type="color" value="${escapeHtml(data.border_color || "#dbe3ee")}"></div>`;
|
|
html+=`<div class="row"><label>Color texto (opcional)</label><input id="blockTextColor" type="color" value="${escapeHtml(data.text_color || "#0b0c10")}"></div>`;
|
|
html+=`<div class="row"><label>Fondo imagen (URL)</label><input id="blockBgImage" type="text" value="${escapeHtml(data.bg_image_url || "")}" placeholder="https://..."></div>`;
|
|
html+=`<div class="row"><label>Overlay color</label><input id="blockBgOverlayHex" type="color" value="${escapeHtml(data.bg_overlay_hex || "#0f172a")}"></div>`;
|
|
html+=`<div class="row"><label>Overlay opacidad</label><input id="blockBgOverlayOpacity" type="range" min="0" max="0.85" step="0.05" value="${Math.max(0, Math.min(0.85, Number(data.bg_overlay_opacity || 0)))}"></div>`;
|
|
html+=`<div class="row"><label>Blur bloque</label><input id="blockBgBlur" type="range" min="0" max="10" step="1" value="${Math.max(0, Math.min(10, Number(data.bg_blur || 0)))}"></div>`;
|
|
const animIn = escapeHtml(String(data.anim_in || "fade").toLowerCase());
|
|
html+=`<div class="row"><label>Animacion entrada</label><select id="blockAnimIn"><option value="fade" ${animIn==="fade"?"selected":""}>Fade</option><option value="slide" ${animIn==="slide"?"selected":""}>Slide</option><option value="zoom" ${animIn==="zoom"?"selected":""}>Zoom</option><option value="none" ${animIn==="none"?"selected":""}>None</option></select></div>`;
|
|
const hoverAnim = escapeHtml(String(data.anim_hover || "lift").toLowerCase());
|
|
html+=`<div class="row"><label>Animacion hover</label><select id="blockAnimHover"><option value="lift" ${hoverAnim==="lift"?"selected":""}>Lift</option><option value="glow" ${hoverAnim==="glow"?"selected":""}>Glow</option><option value="tilt" ${hoverAnim==="tilt"?"selected":""}>Tilt</option><option value="none" ${hoverAnim==="none"?"selected":""}>None</option></select></div>`;
|
|
html+=`<div class="row"><label>Duracion transicion (ms)</label><input id="blockAnimDuration" type="number" min="120" max="900" step="10" value="${Math.max(120, Math.min(900, Number(data.anim_duration || 250)))}"></div>`;
|
|
}
|
|
html+=`<button class="danger" id="deleteBlockBtn">Eliminar bloque</button>`;
|
|
panel.innerHTML=html;
|
|
const moveUpBtn = document.getElementById("moveBlockUpBtn");
|
|
const moveDownBtn = document.getElementById("moveBlockDownBtn");
|
|
if (moveUpBtn){ moveUpBtn.addEventListener("click",()=>moveBlockByDelta(block.id, -1)); }
|
|
if (moveDownBtn){ moveDownBtn.addEventListener("click",()=>moveBlockByDelta(block.id, 1)); }
|
|
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");
|
|
});
|
|
}
|
|
const widthRange = document.getElementById("blockWidth");
|
|
const widthNum = document.getElementById("blockWidthNumber");
|
|
if (widthRange && widthNum){
|
|
widthRange.addEventListener("input",()=>{ widthNum.value = widthRange.value; });
|
|
widthNum.addEventListener("input",()=>{ widthRange.value = widthNum.value; });
|
|
}
|
|
panel.querySelectorAll("input,textarea,select").forEach((el)=>{
|
|
el.addEventListener("input",()=>applyInspector(block));
|
|
el.addEventListener("change",()=>applyInspector(block));
|
|
});
|
|
document.getElementById("deleteBlockBtn").addEventListener("click",()=>{ state.blocks=state.blocks.filter(b=>b.id!==block.id); selectedBlockId=null; renderInspector(); renderPreview(); });
|
|
}
|
|
|
|
function applyInspector(block){
|
|
if (block.type==="menu"){
|
|
block.data.title=document.getElementById("menuTitle").value;
|
|
const mm = document.getElementById("menuMode");
|
|
if (mm){ block.data.menu_mode = normalizeMenuMode(mm.value || "both"); }
|
|
const mms = document.getElementById("menuMobileStyle");
|
|
if (mms){ block.data.menu_mobile_style = (mms.value || "accordion") === "drawer" ? "drawer" : "accordion"; }
|
|
const mi = document.getElementById("menuItems");
|
|
if (mi){ block.data.items = mi.value.split("\n").map((x)=>x.trim()).filter(Boolean); }
|
|
}
|
|
else if (block.type==="hero"){
|
|
const hk = document.getElementById("heroKicker");
|
|
if (hk){ block.data.kicker = hk.value; }
|
|
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;
|
|
const hb2 = document.getElementById("heroBtn2Text");
|
|
if (hb2){ block.data.button_secondary_text = hb2.value; }
|
|
const hu2 = document.getElementById("heroBtn2Url");
|
|
if (hu2){ block.data.button_secondary_url = hu2.value; }
|
|
const ha = document.getElementById("heroAlign");
|
|
if (ha){ block.data.align = ha.value || "left"; }
|
|
const hl = document.getElementById("heroLayout");
|
|
if (hl){ block.data.layout = hl.value === "media-left" ? "media-left" : "media-right"; }
|
|
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); const cc = document.getElementById("cardsColumns"); if (cc){ block.data.columns = Math.max(2, Math.min(4, Number(cc.value || 3))); } }
|
|
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; const ctt = document.getElementById("contactCtaText"); if (ctt){ block.data.cta_text = ctt.value; } const ctu = document.getElementById("contactCtaUrl"); if (ctu){ block.data.cta_url = ctu.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 presetEl = document.getElementById("socialPreset");
|
|
if (presetEl){ block.data.social_preset = presetEl.value || "pro"; }
|
|
const brandEl = document.getElementById("socialUseBrandColors");
|
|
if (brandEl){ block.data.social_use_brand_colors = !!brandEl.checked; }
|
|
const surfaceBgEl = document.getElementById("socialSurfaceBg");
|
|
if (surfaceBgEl){ block.data.social_surface_bg = surfaceBgEl.value || "#ffffff"; }
|
|
const surfaceBorderEl = document.getElementById("socialSurfaceBorder");
|
|
if (surfaceBorderEl){ block.data.social_surface_border = surfaceBorderEl.value || "#e5e7eb"; }
|
|
const titleColorEl = document.getElementById("socialTitleColor");
|
|
if (titleColorEl){ block.data.social_title_color = titleColorEl.value || "#0f172a"; }
|
|
const subtitleColorEl = document.getElementById("socialSubtitleColor");
|
|
if (subtitleColorEl){ block.data.social_subtitle_color = subtitleColorEl.value || "#475569"; }
|
|
const cardBgEl = document.getElementById("socialCardBg");
|
|
if (cardBgEl){ block.data.social_card_bg = cardBgEl.value || "#f8fafc"; }
|
|
const cardBorderEl = document.getElementById("socialCardBorder");
|
|
if (cardBorderEl){ block.data.social_card_border = cardBorderEl.value || "#dbe3ee"; }
|
|
const labelColorEl = document.getElementById("socialLabelColor");
|
|
if (labelColorEl){ block.data.social_label_color = labelColorEl.value || "#0f172a"; }
|
|
const valueColorEl = document.getElementById("socialValueColor");
|
|
if (valueColorEl){ block.data.social_value_color = valueColorEl.value || "#64748b"; }
|
|
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;
|
|
const me = document.getElementById("mapEmbedUrl");
|
|
if (me){ block.data.embed_url = me.value; }
|
|
const mh = document.getElementById("mapHeight");
|
|
if (mh){ block.data.height = Math.max(220, Math.min(700, Number(mh.value || 320))); }
|
|
} 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; }
|
|
}
|
|
const widthEl = document.getElementById("blockWidth");
|
|
if (widthEl && block.type !== "menu"){
|
|
block.data = block.data || {};
|
|
block.data.width = snapBlockWidth(block.type, Number(widthEl.value || 50));
|
|
const visualPresetEl = document.getElementById("blockVisualPreset");
|
|
if (visualPresetEl){ block.data.visual_preset = normalizeBlockPreset(visualPresetEl.value || "inherit"); }
|
|
const motionStyleEl = document.getElementById("blockMotionStyle");
|
|
if (motionStyleEl){ block.data.bg_motion_style = normalizeBlockMotion(motionStyleEl.value || "inherit"); }
|
|
const motionSpeedEl = document.getElementById("blockMotionSpeed");
|
|
if (motionSpeedEl){ block.data.bg_motion_speed = normalizeMotionSpeed(motionSpeedEl.value || "inherit"); }
|
|
const surfaceColorEl = document.getElementById("blockSurfaceColor");
|
|
if (surfaceColorEl){ block.data.surface_color = surfaceColorEl.value || ""; }
|
|
const borderColorEl = document.getElementById("blockBorderColor");
|
|
if (borderColorEl){ block.data.border_color = borderColorEl.value || ""; }
|
|
const textColorEl = document.getElementById("blockTextColor");
|
|
if (textColorEl){ block.data.text_color = textColorEl.value || ""; }
|
|
const bgImgEl = document.getElementById("blockBgImage");
|
|
if (bgImgEl){ block.data.bg_image_url = bgImgEl.value || ""; }
|
|
const bgOverlayHexEl = document.getElementById("blockBgOverlayHex");
|
|
if (bgOverlayHexEl){ block.data.bg_overlay_hex = bgOverlayHexEl.value || "#0f172a"; }
|
|
const bgOverlayOpacityEl = document.getElementById("blockBgOverlayOpacity");
|
|
if (bgOverlayOpacityEl){ block.data.bg_overlay_opacity = Math.max(0, Math.min(0.85, Number(bgOverlayOpacityEl.value || 0))); }
|
|
const bgBlurEl = document.getElementById("blockBgBlur");
|
|
if (bgBlurEl){ block.data.bg_blur = Math.max(0, Math.min(10, Number(bgBlurEl.value || 0))); }
|
|
const animInEl = document.getElementById("blockAnimIn");
|
|
if (animInEl){ block.data.anim_in = animInEl.value || "fade"; }
|
|
const animHoverEl = document.getElementById("blockAnimHover");
|
|
if (animHoverEl){ block.data.anim_hover = animHoverEl.value || "lift"; }
|
|
const animDurationEl = document.getElementById("blockAnimDuration");
|
|
if (animDurationEl){ block.data.anim_duration = Math.max(120, Math.min(900, Number(animDurationEl.value || 250))); }
|
|
}
|
|
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 getVisibleStateIndices(){
|
|
const indices = [];
|
|
state.blocks.forEach((b, i)=>{
|
|
if (isBlockVisibleInCanvas(b)) indices.push(i);
|
|
});
|
|
return indices;
|
|
}
|
|
function insertBlockAtVisibleIndex(type, visibleIndex){
|
|
if (!type) return;
|
|
const b = { id: makeId(), type, data: defaultData(type) };
|
|
if (typeof b.data.width === "undefined") b.data.width = snapBlockWidth(type, 100);
|
|
if (BUILDER_MODE === "ub24"){ b.page = currentPage; }
|
|
const visible = getVisibleStateIndices();
|
|
let insertAt = state.blocks.length;
|
|
if (visible.length){
|
|
if (visibleIndex <= 0) insertAt = visible[0];
|
|
else if (visibleIndex >= visible.length) insertAt = visible[visible.length - 1] + 1;
|
|
else insertAt = visible[visibleIndex];
|
|
}
|
|
state.blocks.splice(insertAt, 0, b);
|
|
selectedBlockId = b.id;
|
|
renderInspector();
|
|
renderPreview();
|
|
}
|
|
function reorderVisibleBlocks(orderedIds){
|
|
if (!Array.isArray(orderedIds) || !orderedIds.length) return;
|
|
const idSet = new Set(orderedIds);
|
|
const byId = new Map(state.blocks.map((b)=>[b.id, b]));
|
|
const reordered = orderedIds.map((id)=>byId.get(id)).filter(Boolean);
|
|
if (!reordered.length) return;
|
|
let cursor = 0;
|
|
state.blocks = state.blocks.map((b)=>{
|
|
if (!idSet.has(b.id)) return b;
|
|
const next = reordered[cursor];
|
|
cursor += 1;
|
|
return next || b;
|
|
});
|
|
}
|
|
function wireSortableDnD(){ return; }
|
|
function wireMenuAccordionInteractions(blockEl){
|
|
if (!blockEl) return;
|
|
bindDrawerGlobalEvents();
|
|
blockEl.querySelectorAll(".menu-accordion, .menu-accordion summary, .menu-accordion a").forEach((node)=>{
|
|
node.addEventListener("pointerdown",(e)=>{ e.stopPropagation(); });
|
|
node.addEventListener("click",(e)=>{ e.stopPropagation(); });
|
|
});
|
|
blockEl.querySelectorAll(".menu-drawer-global").forEach((node)=>{
|
|
if (node.parentElement !== document.body){
|
|
document.body.appendChild(node);
|
|
}
|
|
});
|
|
blockEl.querySelectorAll(".menu-drawer").forEach((panel)=>{
|
|
panel.setAttribute("role", "dialog");
|
|
panel.setAttribute("aria-modal", "true");
|
|
panel.setAttribute("aria-hidden", panel.classList.contains("open") ? "false" : "true");
|
|
panel.setAttribute("tabindex", "-1");
|
|
});
|
|
blockEl.querySelectorAll(".menu-drawer-overlay").forEach((overlay)=>{
|
|
overlay.setAttribute("aria-hidden", overlay.classList.contains("open") ? "false" : "true");
|
|
});
|
|
blockEl.querySelectorAll("[data-drawer-toggle]").forEach((btn)=>{
|
|
btn.setAttribute("aria-haspopup", "dialog");
|
|
btn.setAttribute("aria-expanded", "false");
|
|
btn.addEventListener("click",(e)=>{
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
openMenuDrawer(btn.getAttribute("data-drawer-toggle"), btn);
|
|
});
|
|
});
|
|
blockEl.querySelectorAll("[data-drawer-close]").forEach((btn)=>{
|
|
btn.addEventListener("click",(e)=>{
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
closeMenuDrawer(btn.getAttribute("data-drawer-close"));
|
|
});
|
|
});
|
|
blockEl.querySelectorAll("[data-drawer-overlay]").forEach((ov)=>{
|
|
ov.addEventListener("click",(e)=>{
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
closeMenuDrawer(ov.getAttribute("data-drawer-overlay"));
|
|
});
|
|
});
|
|
blockEl.querySelectorAll("[data-drawer-link]").forEach((a)=>{
|
|
a.addEventListener("click",()=>{
|
|
const parent = a.closest(".menu-drawer");
|
|
if (parent && parent.id) closeMenuDrawer(parent.id, { restoreFocus: false });
|
|
});
|
|
});
|
|
}
|
|
function initCanvasSortable(container){
|
|
if (!hasSortable() || !container) return;
|
|
canvasSortable = Sortable.create(container, {
|
|
group: { name: "builderBlocks", pull: false, put: false },
|
|
draggable: ".block",
|
|
handle: ".drag-handle",
|
|
filter: "input,textarea,select,a,summary,details,.editable,.menu-links,.menu-accordion",
|
|
preventOnFilter: false,
|
|
animation: 150,
|
|
forceFallback: true,
|
|
fallbackOnBody: true,
|
|
fallbackTolerance: 8,
|
|
delayOnTouchOnly: true,
|
|
delay: 120,
|
|
touchStartThreshold: 6,
|
|
bubbleScroll: true,
|
|
onEnd: (evt)=>{
|
|
if (!evt || evt.from !== evt.to) return;
|
|
const orderedIds = [...container.querySelectorAll(".block")].map((n)=>n.dataset.blockId).filter(Boolean);
|
|
reorderVisibleBlocks(orderedIds);
|
|
if (evt.item && evt.item.dataset.blockId){ selectedBlockId = evt.item.dataset.blockId; }
|
|
renderPreview();
|
|
}
|
|
});
|
|
}
|
|
function wirePreviewDrop(){
|
|
const canvas=document.getElementById("previewCanvas");
|
|
const getDropPosition = (block, clientX, clientY)=>{
|
|
const rect = canvas.getBoundingClientRect();
|
|
const widthPct = Math.max(20, Math.min(100, Number(block?.data?.width || 60)));
|
|
const estWidth = Math.max(120, (rect.width * widthPct) / 100);
|
|
const estHeight = Math.max(80, Number(block?.data?.height || 160));
|
|
const left = snap(clientX - rect.left - (estWidth / 2));
|
|
const top = snap(clientY - rect.top - (estHeight / 2));
|
|
const maxX = Math.max(0, rect.width - estWidth);
|
|
const maxY = Math.max(0, rect.height - estHeight);
|
|
return {
|
|
x: Math.max(0, Math.min(maxX, left)),
|
|
y: Math.max(0, Math.min(maxY, top))
|
|
};
|
|
};
|
|
canvas.addEventListener("dragover",(e)=>{
|
|
e.preventDefault();
|
|
if (!state.settings.free_drag){
|
|
const container=getCanvasContainer(canvas);
|
|
if (!container) return;
|
|
const index=getDropIndex(container,e.clientY,e.clientX);
|
|
showDrop(container,index);
|
|
}
|
|
});
|
|
canvas.addEventListener("drop",(e)=>{
|
|
e.preventDefault();
|
|
const id=(e.dataTransfer && e.dataTransfer.getData("text/block-id")) || "";
|
|
const type=e.dataTransfer.getData("text/block-type");
|
|
if (type){
|
|
const b={ id: makeId(), type, data: defaultData(type) };
|
|
if (typeof b.data.width === "undefined") b.data.width = snapBlockWidth(type, 100);
|
|
if (state.settings.free_drag){
|
|
b.pos = getDropPosition(b, e.clientX, e.clientY);
|
|
} else {
|
|
const container=getCanvasContainer(canvas);
|
|
if (!container) return;
|
|
const index=getDropIndex(container,e.clientY,e.clientX);
|
|
state.blocks.splice(index,0,b);
|
|
selectedBlockId=b.id;
|
|
renderInspector(); renderPreview();
|
|
removeDrop();
|
|
return;
|
|
}
|
|
state.blocks.push(b);
|
|
selectedBlockId=b.id;
|
|
renderInspector(); renderPreview();
|
|
}
|
|
removeDrop();
|
|
});
|
|
canvas.addEventListener("dragstart",(e)=>{ e.preventDefault(); });
|
|
}
|
|
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, freeMode){
|
|
pendingResize = { id, x, y, widthPct, heightPx, freeMode };
|
|
if (resizeRaf) return;
|
|
resizeRaf = requestAnimationFrame(()=>{
|
|
resizeRaf = 0;
|
|
const job = pendingResize;
|
|
if (!job) return;
|
|
const el = document.getElementById(job.id);
|
|
if (el){
|
|
if (job.freeMode){
|
|
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";
|
|
} else {
|
|
el.style.transform = "";
|
|
if (job.widthPct >= 99){
|
|
el.style.width = "100%";
|
|
el.style.gridColumn = "1 / -1";
|
|
} else {
|
|
el.style.width = "100%";
|
|
el.style.gridColumn = `span ${widthToSpan(job.widthPct)}`;
|
|
}
|
|
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, widthPctStart: Math.max(30, Math.min(100, Number(block.data?.width || 100))) };
|
|
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);
|
|
if (!resizeStart.free){
|
|
const deltaPct = (dx / Math.max(120, resizeStart.width)) * 100;
|
|
const widthPct = Math.max(30, Math.min(100, Math.round(resizeStart.widthPctStart + deltaPct)));
|
|
block.data.width = widthPct;
|
|
scheduleBlockResize(block.id, 0, 0, widthPct, 0, false);
|
|
return;
|
|
}
|
|
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, true);
|
|
}
|
|
function endFreeResize(){
|
|
const wasGridResize = !resizeStart.free;
|
|
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);
|
|
if (wasGridResize){
|
|
renderPreview();
|
|
}
|
|
}
|
|
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");
|
|
const globalBlockPreset=document.getElementById("globalBlockPresetSelect");
|
|
const globalBlockMotion=document.getElementById("globalBlockMotionSelect");
|
|
const globalBlockMotionSpeed=document.getElementById("globalBlockMotionSpeedSelect");
|
|
const canvasMinHeight=document.getElementById("canvasMinHeightInput");
|
|
const canvasBottomSpace=document.getElementById("canvasBottomSpaceInput");
|
|
const registeredBrand=document.getElementById("registeredBrandInput");
|
|
const developerBrand=document.getElementById("developerBrandInput");
|
|
const siteAuthor=document.getElementById("siteAuthorInput");
|
|
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;
|
|
if (globalBlockPreset){ globalBlockPreset.value = normalizeBlockPreset(s.global_block_preset || "clean-landing"); }
|
|
if (globalBlockMotion){ globalBlockMotion.value = normalizeBlockMotion(s.global_block_motion || "none"); }
|
|
if (globalBlockMotionSpeed){ globalBlockMotionSpeed.value = normalizeMotionSpeed(s.global_block_motion_speed || "normal"); }
|
|
canvasMinHeight.value = Math.max(700, Number(s.canvas_min_height || 1200));
|
|
canvasBottomSpace.value = Math.max(0, Number(s.canvas_bottom_space || 180));
|
|
registeredBrand.value = s.registered_brand || s.site_name || "";
|
|
developerBrand.value = s.developer_brand || "GKACHELE™";
|
|
siteAuthor.value = s.site_author || "";
|
|
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(); });
|
|
if (globalBlockPreset){ globalBlockPreset.addEventListener("change",()=>{ s.global_block_preset = normalizeBlockPreset(globalBlockPreset.value || "clean-landing"); renderPreview(); }); }
|
|
if (globalBlockMotion){ globalBlockMotion.addEventListener("change",()=>{ s.global_block_motion = normalizeBlockMotion(globalBlockMotion.value || "none"); renderPreview(); }); }
|
|
if (globalBlockMotionSpeed){ globalBlockMotionSpeed.addEventListener("change",()=>{ s.global_block_motion_speed = normalizeMotionSpeed(globalBlockMotionSpeed.value || "normal"); renderPreview(); }); }
|
|
canvasMinHeight.addEventListener("input",()=>{ s.canvas_min_height = Math.max(700, Number(canvasMinHeight.value || 1200)); renderPreview(); });
|
|
canvasBottomSpace.addEventListener("input",()=>{ s.canvas_bottom_space = Math.max(0, Number(canvasBottomSpace.value || 0)); renderPreview(); });
|
|
registeredBrand.addEventListener("input",()=>{ s.registered_brand = registeredBrand.value; renderPreview(); });
|
|
developerBrand.addEventListener("input",()=>{ s.developer_brand = developerBrand.value; renderPreview(); });
|
|
siteAuthor.addEventListener("input",()=>{ s.site_author = siteAuthor.value; renderPreview(); });
|
|
|
|
bindDrop(logoDrop, logoFile, (data)=>{ s.logo_url = data; }, "Logo cargado");
|
|
bindDrop(bgDrop, bgFile, (data)=>{ s.bg_image_url = data; }, "Fondo cargado");
|
|
}
|
|
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)=>{
|
|
shell.style.maxWidth = w;
|
|
setActive(btnId);
|
|
setShellClass(btnId==="btnSizePhone" ? "size-phone" : btnId==="btnSizeTablet" ? "size-tablet" : "size-desktop");
|
|
syncActiveDrawerForViewport();
|
|
};
|
|
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 wirePreviewToggle(){
|
|
const btnPreview = document.getElementById("btnPreview");
|
|
if (btnPreview){
|
|
btnPreview.addEventListener("click", async ()=>{
|
|
btnPreview.disabled = true;
|
|
const old = btnPreview.textContent;
|
|
btnPreview.textContent = "Abriendo...";
|
|
try{
|
|
await saveDraftSilently();
|
|
const url = new URL(`${BUILDER_BASE_PATH}/preview-final`, window.location.origin);
|
|
window.open(url.toString(), "_blank", "noopener,noreferrer");
|
|
} catch(_e){
|
|
window.alert("No se pudo abrir la vista previa. Reintenta.");
|
|
} finally {
|
|
btnPreview.disabled = false;
|
|
btnPreview.textContent = old;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
function ensurePreviewBackButton(){
|
|
let back = document.getElementById("previewBackBtn");
|
|
if (!back){
|
|
back = document.createElement("button");
|
|
back.id = "previewBackBtn";
|
|
back.className = "preview-back";
|
|
back.textContent = "Atras";
|
|
back.addEventListener("click", ()=>{
|
|
if (PREVIEW_ONLY){
|
|
window.location.href = BUILDER_BASE_PATH;
|
|
return;
|
|
}
|
|
if (window.history.length > 1){
|
|
window.history.back();
|
|
return;
|
|
}
|
|
document.body.classList.remove("preview-mode");
|
|
const clean = new URL(window.location.href);
|
|
clean.hash = "";
|
|
window.history.replaceState({}, "", clean.toString());
|
|
});
|
|
document.body.appendChild(back);
|
|
}
|
|
return back;
|
|
}
|
|
function initStandalonePreviewMode(){
|
|
const apply = ()=>{
|
|
const isPreview = PREVIEW_ONLY;
|
|
document.body.classList.toggle("preview-mode", isPreview);
|
|
if (isPreview){
|
|
ensurePreviewBackButton();
|
|
} else {
|
|
const back = document.getElementById("previewBackBtn");
|
|
if (back) back.remove();
|
|
}
|
|
};
|
|
apply();
|
|
}
|
|
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 setSaveStatus(msg, kind=""){
|
|
const status = document.getElementById("saveStatus");
|
|
if (!status) return;
|
|
status.textContent = msg;
|
|
status.className = `save-status${kind ? ` ${kind}` : ""}`;
|
|
}
|
|
async function saveDraftSilently(){
|
|
const payload = {
|
|
site_id: SITE_ID,
|
|
publish: false,
|
|
content: { ...SERVER_CONTENT, settings: state.settings, blocks: state.blocks }
|
|
};
|
|
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 draft failed");
|
|
return true;
|
|
}
|
|
function normalizeLoadedBlocks(blocks){
|
|
if (!Array.isArray(blocks)) return [];
|
|
return blocks
|
|
.filter((b)=>b && typeof b === "object")
|
|
.map((b)=>{
|
|
const data = (b.data && typeof b.data === "object") ? { ...b.data } : {};
|
|
if (b.type === "menu"){
|
|
data.menu_mode = normalizeMenuMode(data.menu_mode || "both");
|
|
data.menu_mobile_style = (data.menu_mobile_style || "accordion") === "drawer" ? "drawer" : "accordion";
|
|
if (!Array.isArray(data.items) || data.items.length < 3){
|
|
data.items = ["Inicio","Productos","Blog","La Empresa","Contacto"];
|
|
} else {
|
|
data.items = data.items.map((x)=>String(x || "").trim()).filter(Boolean);
|
|
}
|
|
}
|
|
const fallback = b.type === "menu" ? 100 : 60;
|
|
data.width = snapBlockWidth(b.type, (typeof data.width === "number" ? data.width : fallback));
|
|
delete data.full_width;
|
|
return {
|
|
...b,
|
|
id: b.id || makeId(),
|
|
data
|
|
};
|
|
});
|
|
}
|
|
function migrateLegacyFullWidth(blocks){
|
|
if (!Array.isArray(blocks) || !blocks.length) return false;
|
|
const nonMenu = blocks.filter((b)=>b && b.type !== "menu");
|
|
if (!nonMenu.length) return false;
|
|
const allFull = nonMenu.every((b)=>{
|
|
const w = Number(b?.data?.width || 0);
|
|
return w >= 99;
|
|
});
|
|
if (!allFull) return false;
|
|
nonMenu.forEach((b)=>{
|
|
b.data = b.data || {};
|
|
b.data.width = 60;
|
|
});
|
|
return true;
|
|
}
|
|
function hasMeaningfulFreeDragPositions(blocks){
|
|
const nonMenu = (Array.isArray(blocks) ? blocks : []).filter((b)=>b && b.type !== "menu");
|
|
if (!nonMenu.length) return false;
|
|
const positioned = nonMenu.filter((b)=>{
|
|
const p = b && b.pos;
|
|
return p && Number.isFinite(Number(p.x)) && Number.isFinite(Number(p.y));
|
|
});
|
|
return positioned.length >= Math.max(2, Math.ceil(nonMenu.length * 0.5));
|
|
}
|
|
function normalizeDuplicatedFreeDragPositions(blocks){
|
|
const list = Array.isArray(blocks) ? blocks : [];
|
|
const positioned = list.filter((b)=>b && b.pos && Number.isFinite(Number(b.pos.x)) && Number.isFinite(Number(b.pos.y)));
|
|
if (positioned.length < 3) return false;
|
|
const unique = new Set(positioned.map((b)=>`${Math.round(Number(b.pos.x))}:${Math.round(Number(b.pos.y))}`));
|
|
if (unique.size >= Math.max(3, Math.ceil(positioned.length * 0.6))) return false;
|
|
let seq = 0;
|
|
list.forEach((b)=>{
|
|
if (!b || b.type === "menu") return;
|
|
b.pos = getDefaultPos(seq);
|
|
seq += 1;
|
|
});
|
|
return true;
|
|
}
|
|
function estimateFreeDragBlockHeight(block){
|
|
const type = String(block?.type || "").toLowerCase();
|
|
if (type === "menu") return 92;
|
|
if (type === "hero") return 520;
|
|
if (type === "gallery") return 330;
|
|
if (type === "map") return 360;
|
|
if (type === "contact") return 340;
|
|
if (type === "cards") return 280;
|
|
if (type === "review") return 250;
|
|
if (type === "social") return 280;
|
|
return 240;
|
|
}
|
|
function hasSevereFreeDragOverlap(blocks){
|
|
const list = (Array.isArray(blocks) ? blocks : [])
|
|
.filter((b)=>b && b.type !== "menu" && b.pos && Number.isFinite(Number(b.pos.x)) && Number.isFinite(Number(b.pos.y)))
|
|
.map((b)=>({
|
|
id: b.id,
|
|
x: Number(b.pos.x),
|
|
y: Number(b.pos.y),
|
|
h: estimateFreeDragBlockHeight(b)
|
|
}));
|
|
if (list.length < 2) return false;
|
|
const ordered = list.slice().sort((a, b)=>a.y - b.y);
|
|
let overlaps = 0;
|
|
for (let i = 0; i < ordered.length - 1; i++){
|
|
const curr = ordered[i];
|
|
const next = ordered[i + 1];
|
|
const sameLane = Math.abs(curr.x - next.x) <= 180;
|
|
const currBottom = curr.y + curr.h;
|
|
const verticalOverlap = next.y < (currBottom - 24);
|
|
if (sameLane && verticalOverlap){
|
|
overlaps += 1;
|
|
if (overlaps >= 1) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function applyStackedFreeDragLayout(blocks){
|
|
const list = Array.isArray(blocks) ? blocks : [];
|
|
let y = 20;
|
|
list.forEach((b)=>{
|
|
if (!b) return;
|
|
b.data = (b.data && typeof b.data === "object") ? b.data : {};
|
|
if (b.type === "menu"){
|
|
b.data.width = 100;
|
|
b.pos = { x: 20, y };
|
|
y += estimateFreeDragBlockHeight(b) + 18;
|
|
return;
|
|
}
|
|
b.data.width = snapBlockWidth(b.type, Number(b.data.width || 92));
|
|
b.pos = { x: 20, y };
|
|
y += estimateFreeDragBlockHeight(b) + 22;
|
|
});
|
|
}
|
|
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";
|
|
}
|
|
}
|
|
}
|
|
async function resetBlocks(){
|
|
if (isSaving) return;
|
|
const ok = window.confirm("Esto borrara todos los bloques actuales. Deseas continuar?");
|
|
if (!ok) return;
|
|
const keepRubro = normalizeRubro(state.settings.business_rubro || SERVER_RUBRO || "restaurante");
|
|
state.blocks = [];
|
|
state.settings = { ...defaultSettings, business_rubro: keepRubro };
|
|
state.settings.free_drag = false;
|
|
selectedBlockId = null;
|
|
renderInspector();
|
|
renderPreview();
|
|
wireSettings();
|
|
setSaveStatus("Reseteando...", "busy");
|
|
try{
|
|
await saveDraftSilently();
|
|
setSaveStatus("Reset aplicado", "ok");
|
|
} catch(_e){
|
|
setSaveStatus("Error al resetear", "error");
|
|
}
|
|
}
|
|
function init(){
|
|
const initialRubro = normalizeRubro(state.settings.business_rubro || SERVER_RUBRO || "restaurante");
|
|
state.settings.business_rubro = initialRubro;
|
|
state.blocks = normalizeLoadedBlocks(state.blocks);
|
|
if (!state.settings.legacy_width_migrated_v1){
|
|
const migrated = migrateLegacyFullWidth(state.blocks);
|
|
state.settings.legacy_width_migrated_v1 = true;
|
|
if (migrated){
|
|
console.log("Legacy widths migrated from 100 to 60 for non-menu blocks");
|
|
}
|
|
}
|
|
if (BUILDER_MODE === "ub24"){
|
|
state.blocks.forEach(b=>{ if (!b.page) b.page = "home"; });
|
|
state.settings.free_drag = false;
|
|
} else {
|
|
// Mantener layout tipo template estable por defecto en builder web.
|
|
// Evita superposiciones y bloqueos al agregar nuevos bloques.
|
|
state.settings.free_drag = false;
|
|
}
|
|
selectedBlockId = null;
|
|
wireSidebar();
|
|
wirePreviewDrop();
|
|
if (hasSortable()){ wireSortableDnD(); }
|
|
wireInlineEditing(); wireSettings(); wireJumpSelect(); wirePreviewSize(); wirePreviewToggle(); wireThemeToggle();
|
|
initStandalonePreviewMode();
|
|
const backBtn = document.getElementById("btnBack");
|
|
if (backBtn){ backBtn.addEventListener("click",()=>{ window.history.back(); }); }
|
|
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.settings.business_rubro = normalizeRubro(key);
|
|
state.settings.free_drag = false;
|
|
state.blocks = t.blocks.map(b=>({ ...b, id: makeId(), page: (BUILDER_MODE==="ub24" ? "home" : b.page) }));
|
|
selectedBlockId = null;
|
|
renderInspector(); renderPreview();
|
|
wireSettings();
|
|
};
|
|
if (templateSelect){
|
|
templateSelect.value = state.settings.business_rubro || "";
|
|
templateSelect.addEventListener("change", ()=>{
|
|
applyTemplate(templateSelect.value);
|
|
});
|
|
}
|
|
const hasSavedBlocks = Array.isArray(SERVER_CONTENT && SERVER_CONTENT.blocks) && SERVER_CONTENT.blocks.length > 0;
|
|
const hasSavedSettings = !!(SERVER_CONTENT && SERVER_CONTENT.settings && Object.keys(SERVER_CONTENT.settings).length);
|
|
const shouldAutoloadTemplate = !hasSavedBlocks && !hasSavedSettings;
|
|
if (!state.blocks.length && templates[initialRubro] && shouldAutoloadTemplate){
|
|
applyTemplate(initialRubro);
|
|
if (templateSelect){ templateSelect.value = initialRubro; }
|
|
}
|
|
document.getElementById("previewCanvas").addEventListener("click",()=>{ selectedBlockId=null; renderInspector(); renderPreview(); });
|
|
document.getElementById("btnSave").addEventListener("click",saveContent);
|
|
const resetBtn = document.getElementById("btnReset");
|
|
if (resetBtn){ resetBtn.addEventListener("click", resetBlocks); }
|
|
renderInspector(); renderPreview();
|
|
}
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|