15 Commits

Author SHA1 Message Date
komkida91
cdebdf9ddf fix(builder): disable cache and normalize restaurante default template 2026-03-03 10:48:28 +01:00
komkida91
9cccbab1c8 fix(builder): calcular alto free-drag con posicion real y evitar desorden visual 2026-03-01 14:48:23 +01:00
komkida91
864846da0f fix(builder): ordenar template restaurante en carga sin tocar drag libre 2026-03-01 14:44:39 +01:00
komkida91
02e32c9673 fix(builder): restaurante libre y ordenado; otros rubros sin cambios 2026-03-01 14:40:33 +01:00
komkida91
40738cc65b fix(builder): forzar layout template estable y desactivar free-drag auto 2026-03-01 14:35:58 +01:00
komkida91
d18a92d017 fix(builder): forzar auto-orden restaurante al detectar solape vertical 2026-03-01 14:32:45 +01:00
komkida91
10d8fb8cae fix(builder): detectar solape severo en free-drag y reordenar restaurante 2026-03-01 14:30:05 +01:00
komkida91
14eca53c91 fix(builder): ordenar layout default restaurante y corregir alto real en free-drag 2026-03-01 14:25:47 +01:00
komkida91
0787c7bc46 fix(builder): evitar solape en free-drag y auto-reparar posiciones duplicadas 2026-03-01 14:17:19 +01:00
komkida91
f7a1c2dffc fix(builder): evitar caos de layout cuando free-drag no tiene posiciones validas 2026-03-01 14:13:35 +01:00
komkida91
2cc845eb4d fix(builder): activar free drag persistente para template restaurante 2026-03-01 14:11:25 +01:00
komkida91
6c1ccc0a48 fix(builder): alinear preview-final con geometria visual del editor 2026-03-01 13:55:37 +01:00
komkida91
846ad5c7c6 docs(builder): integra STATE y baseline premium en memoria codex 2026-03-01 13:47:36 +01:00
komkida91
48742a2736 feat(builder): baseline premium campos misiones en elementor 2026-03-01 13:47:14 +01:00
komkida91
208dca9f05 feat(builder): add social design controls and per-block background/transitions 2026-02-24 16:48:49 +01:00
3 changed files with 603 additions and 102 deletions

View File

@@ -1,44 +1,80 @@
# Memoria Codex - GKACHELE
**Fecha de corte:** 22 Febrero 2026
**Fecha de corte:** 24 Febrero 2026
## Fuente de verdad (estado IA)
## Fuente de verdad
- `codex/VERSIONADO_IA.md`
- `codex/BUILDER_ELEMENTOR_VERSION.md`
- `codex/HISTORIAL_CAMBIOS.md`
- `codex/MEMORIA_CODEX.md`
- `STATE.md` (raiz del workspace, memoria operativa Docker/SaaS desde 2026-02-28)
## Punto exacto
## Estado acordado (retomar desde aqui)
- Rama activa: `ai/ub24-builder-v1`
- Commit de referencia funcional: `e83e915`
- Version base builder: `gkachele-elementor-templates-20260210-v21`
- Estado de sync local/remoto al corte: `3/0`
- Checkpoint actual: `208dca9`
- Tag local de checkpoint: `builder-social-blockfx-20260224-v1`
- Base funcional estable previa: `e83e915` (tag local: `builder-stable-e83e915`)
## Estado actual funcional
- Builder visual operativo en `/elementor/<site_id>`.
- Drag/drop y resize fluido en canvas.
- Inspector para estilos y contenido.
- Modo libre con snapping.
- Menu mobile con modo `Drawer Pro` estabilizado (apertura/cierre, overlay, escape, foco, lock scroll).
- Reset corrige bloques + settings y evita recarga de plantilla por autoload no deseado.
- Tema restaurante mejorado, pero aun con ajustes pendientes de calidad fina.
## Estado funcional verificado
- Builder operativo en `/elementor/<site_id>`.
- Customizer unificado en una sola ruta:
- `/customizer/<id>` activo
- `/customizer2/<id>` y `/customizer3/<id>` fuera (`404`)
- Mejoras ya hechas en builder:
- menu manual con mejor mapeo a bloques reales
- drop en modo libre mas preciso
- bloque `Redes` con mas controles visuales
- controles de fondo/transicion por bloque (version inicial)
## Pendientes inmediatos
- Revisar cada icono del menu en preview final (flujo completo, uno por uno).
- Validar responsive del menu (desktop/tablet/mobile) con focus y accesibilidad.
- Ajuste fino visual del tema restaurante (estado "mejorable").
- Consolidar puente Builder -> SaaS (`/customizer/<id>`).
## Feedback del usuario memorizado (obligatorio)
- Hay cosas buenas en lo hecho.
- El resultado visual actual todavia se percibe "cutre".
- Las animaciones actuales no alcanzan nivel pro.
- Los fondos (sobre todo imagen de fondo) no se ven profesionales.
- Lo de `Redes` debe escalarse y aplicarse mejor a cada bloque.
- No perder este contexto ni volver a "no saber en que quedamos".
- Estandar visual confirmado por usuario: nivel premium como landing `campos-misiones`
- Hero con fondo potente y composicion pro.
- Bloques de contacto/redes con look comercial real.
- Mapas embebidos y galerias con acabado profesional.
- Botones con micro-movimiento y sombra consistente.
- Este nivel debe replicarse en plantillas y en el customizer de `word`.
## Flujo operativo memorizado
1. Construir y validar local en `http://127.0.0.1:5001`.
2. Guardar cambios en commits atomicos.
3. Push a `origin/ai/ub24-builder-v1` en cada lote validado.
4. Actualizar hash y estado en `codex/VERSIONADO_IA.md`.
## Siguiente lote acordado
1. Subir calidad visual global (no solo `Redes`) con sistema consistente por bloque.
2. Rehacer fondos animados a nivel pro (capas, movimiento elegante, control fino).
3. Rehacer transiciones/hover con calidad premium y coherencia global.
4. Mantener flujo: `1 archivo + 1 cambio + 1 prueba` por iteracion.
## Metodo acordado (memorizado)
## Metodo operativo
- Misma rama + control por hash/revert.
- Fixes acotados (un archivo por lote cuando aplique).
- Probar inmediatamente cada cambio antes de avanzar.
- Sin commit/push hasta orden explicita del usuario.
- Sin push/tag sin orden explicita del usuario.
- Validacion inmediata por URL canonica:
- `http://127.0.0.1:5001/elementor/1`
- Arranque:
- `python -m demo.app` desde `c:\word`
## Comando de arranque
- `python -m demo.app` desde `c:\word`
## Memoria integrada desde STATE.md (raiz)
- Objetivo paralelo vigente: automatizacion Docker estable en `C:\word` con flujo reproducible.
- Regla activa: un solo flujo oficial de automatizacion, cambios pequenos y verificables, registro de fallo+fix.
- Flujo Docker objetivo:
1. `docker compose build`
2. `docker compose up -d`
3. `docker compose ps`
4. `docker compose logs --tail=200`
5. `docker compose restart <service>` cuando aplique
- Proximo paso operativo en esa linea: definir compose canonico unico y retirar variantes no usadas.
- Estandar visual SaaS validado (2026-02-28): referencia `Campos Misiones` como base premium replicable.
## Pendientes criticos memorizados (24 Febrero 2026)
1. Paridad real entre editor y preview final:
- lo que se ve mientras se edita debe verse igual en `/elementor/<id>/preview-final`.
2. Movimiento libre real de objetos:
- drag/reordenamiento debe funcionar estable y predecible.
3. Cache:
- revisar y corregir cache agresiva que oculta cambios recientes.
4. Ancho de bloques:
- no forzar 100% por defecto.
- permitir control real de ancho (mas angosto/mas ancho) sin romper layout.
5. Hero izquierda/derecha:
- swap de contenido debe reflejarse claramente en editor y preview final.
6. Modularizacion tecnica:
- dividir `elementor_builder.html` en archivos mas pequenos (CSS/JS por responsabilidades) para evitar regressiones y acelerar fixes.

View File

@@ -1,4 +1,4 @@
from flask import Blueprint, render_template, session, request, jsonify
from flask import Blueprint, render_template, session, request, jsonify, make_response
import json
from db import get_db
from utils.theme_engine import get_theme_config
@@ -13,7 +13,7 @@ elementor_bp = Blueprint(
)
def _render_builder(site_id, builder_mode='default', **_kwargs):
def _render_builder(site_id, builder_mode='default', preview_only=False, **_kwargs):
conn = get_db()
c = conn.cursor()
c.execute('SELECT user_id, slug, theme, content_json FROM sites WHERE id = ?', (site_id,))
@@ -40,7 +40,7 @@ def _render_builder(site_id, builder_mode='default', **_kwargs):
user_plan = user_data[0] if user_data else 'base'
user_rubro = user_data[1] if user_data else 'restaurante'
return render_template(
html = render_template(
'elementor_builder.html',
site_id=site_id,
slug=site[1],
@@ -49,8 +49,15 @@ def _render_builder(site_id, builder_mode='default', **_kwargs):
theme_config=theme_config,
user_plan=user_plan,
rubro=user_rubro,
builder_mode=builder_mode
builder_mode=builder_mode,
preview_only=bool(preview_only)
)
response = make_response(html)
# Dynamic builder output should never be cached.
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
return response
@elementor_bp.route('/elementor/<int:site_id>')
@@ -58,11 +65,21 @@ def elementor_view(site_id):
return _render_builder(site_id, builder_mode='default')
@elementor_bp.route('/elementor/<int:site_id>/preview-final')
def elementor_preview_final(site_id):
return _render_builder(site_id, builder_mode='default', preview_only=True)
@elementor_bp.route('/ub24/<int:site_id>')
def ub24_view(site_id):
return _render_builder(site_id, builder_mode='ub24')
@elementor_bp.route('/ub24/<int:site_id>/preview-final')
def ub24_preview_final(site_id):
return _render_builder(site_id, builder_mode='ub24', preview_only=True)
@elementor_bp.route('/api/elementor/save', methods=['POST'])
def save_elementor():
data = request.get_json(silent=True) or {}
@@ -103,4 +120,9 @@ def save_elementor():
conn.commit()
conn.close()
return jsonify({'success': True, 'published': publish})
response = jsonify({'success': True, 'published': publish})
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
return response

View File

@@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GKACHELE Builder</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700&family=DM+Sans:wght@400;500;700&family=Space+Grotesk:wght@400;600;700&family=Outfit:wght@400;500;700&family=Sora:wght@400;600;700&family=Poppins:wght@400;500;700&family=Playfair+Display:wght@400;600;700&family=IBM+Plex+Sans:wght@400;500;700&family=Merriweather:wght@400;700&display=swap" rel="stylesheet">
<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); }
: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}
@@ -50,12 +50,34 @@
.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:min(100%, var(--canvas-max, 1280px));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,0 12px 30px rgba(15,23,42,.08));transition:transform .25s ease,box-shadow .25s ease,border-color .25s ease;animation:fadeUp .25s ease;color:var(--site-text,#0b0c10);position:relative;cursor:grab;touch-action:pan-y}
.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:translateY(-2px);box-shadow:0 20px 46px rgba(15,23,42,.14)}
.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}
@@ -67,6 +89,14 @@
}
.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}
@@ -86,6 +116,14 @@
.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}
@@ -103,6 +141,8 @@
.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}
@@ -217,8 +257,9 @@
.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:none}
.restaurant-site .block:hover{transform:none}
.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}
@@ -236,6 +277,16 @@
.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)}
@@ -265,24 +316,9 @@
body.preview-mode .sidebar,
body.preview-mode .inspector,
body.preview-mode .topbar{display:none !important}
body.preview-mode .main{padding:0 !important}
body.preview-mode .preview-shell{
max-width:100% !important;
border:0 !important;
border-radius:0 !important;
padding:0 !important;
margin:0 !important;
background:transparent !important;
}
body.preview-mode .apple{
border:0 !important;
border-radius:0 !important;
min-height:100vh;
}
body.preview-mode .canvas{
min-height:100vh !important;
padding:0 !important;
}
/* 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}
@@ -441,6 +477,7 @@
<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>
@@ -453,6 +490,7 @@
<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>
@@ -478,6 +516,9 @@
<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>
@@ -530,7 +571,10 @@
site_author: "",
registered_brand: "",
developer_brand: "GKACHELE™",
business_rubro: "restaurante"
business_rubro: "restaurante",
global_block_preset: "clean-landing",
global_block_motion: "flow",
global_block_motion_speed: "normal"
};
const templates = {
base_otro: {
@@ -557,15 +601,28 @@
]
},
restaurante: {
settings: { primary_color: '#ef4444', bg_color: '#fff7ed', text_color: '#0b0c10', muted_color: '#6b7280', font_body: 'IBM Plex Sans', font_heading: 'Playfair Display', bg_gradient: false },
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: 'Sabores que enamoran', subtitle: 'Cocina artesanal, ambiente unico y atencion cercana.', kicker: 'Restaurante', button_text: 'Reservar', button_url: '#contacto', button_secondary_text: 'Ver menu', button_secondary_url: '#menu', image_url: '', align: 'left', width: 100 } },
{ id: makeId(), type: 'gallery', data: { title: 'Platos destacados', images: ['','',''], captions: ['','',''], fit: 'cover', width: 100 } },
{ id: makeId(), type: 'cards', data: { title: 'Especialidades', items: ['Entradas|Frescas y ligeras','Platos fuertes|Hechos al momento','Postres|Dulce final'], columns: 3, width: 100 } },
{ id: makeId(), type: 'review', data: { title: 'Rese?as', name: 'Cliente feliz', text: 'Excelente sabor y servicio impecable.', rating: 5, style: 'card', width: 100 } },
{ id: makeId(), type: 'contact', data: { title: 'Reservas', email: '', phone: '', address: '', cta_text: 'Reservar por WhatsApp', cta_url: '#', width: 100 } },
{ id: makeId(), type: 'map', data: { title: 'Ubicacion', address: '', embed_url: '', height: 320, width: 100 } }
{ id: makeId(), type: 'hero', data: { title: 'Cocina de autor con sabor local', subtitle: 'Menu de temporada, ingredientes frescos y una experiencia pensada para que quieras volver.', kicker: 'Restaurante', button_text: 'Reservar mesa', button_url: '#contacto', button_secondary_text: 'Ver menu', button_secondary_url: '#menu', image_url: '', align: 'left', width: 100 } },
{ id: makeId(), type: 'cards', data: { title: 'Especialidades de la casa', items: ['Entradas|Opciones frescas y para compartir.','Platos principales|Recetas de autor con producto local.','Postres|Cierre dulce hecho en casa.'], columns: 3, width: 100 } },
{ id: makeId(), type: 'gallery', data: { title: 'Nuestro espacio', images: ['','',''], captions: ['Salon principal','Plato destacado','Barra de tragos'], fit: 'cover', width: 100 } },
{ id: makeId(), type: 'review', data: { title: 'Resenas', name: 'Cliente frecuente', text: 'Excelente servicio, ambiente calido y platos que superan expectativas.', rating: 5, style: 'card', width: 100 } },
{ id: makeId(), type: 'contact', data: { title: 'Reserva y contacto', email: '', phone: '', address: '', cta_text: 'Reservar ahora', cta_url: '#', width: 100 } },
{ 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: 100 } },
{ id: makeId(), type: 'map', data: { title: 'Como llegar', address: '', embed_url: '', height: 320, width: 100 } }
]
},
cosmeticos: {
@@ -657,29 +714,58 @@ const state = {
if (type === "menu") return 100;
return Math.max(30, Math.min(100, Number(raw || 100)));
}
function getDefaultPos(){
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 i = state.blocks.length;
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", width:100 };
case "text": return { text:"Describe tu negocio aqui.", width:100 };
case "image": return { url:"", alt:"", caption:"", fit:"cover", overlay_text:"", width:100 };
case "features": return { title:"Beneficios", items:["Rapido","Profesional","Confiable"], width:100 };
case "gallery": return { title:"Proyectos", images:["","",""], captions:["","",""], fit:"cover", width:100 };
case "cards": return { title:"Propuesta", items:["Titulo 1|Texto breve","Titulo 2|Texto breve","Titulo 3|Texto breve"], columns:3, width:100 };
case "iconlist": return { title:"Diferenciales", items:["Rapido|Ahorra tiempo","Seguro|Datos protegidos","Soporte|Respuesta rapida"], width:100 };
case "contact": return { title:"Hablemos", email:"", phone:"", address:"", cta_text:"Reservar ahora", cta_url:"#", width:100 };
case "map": return { title:"Ubicacion", address:"", embed_url:"", height:320, width:100 };
case "button": return { text:"Accion", url:"#", style:"primary", size:"md", width:100};
case "social": return { instagram:"", facebook:"", whatsapp:"", tiktok:"", youtube:"", icon_size:18, icon_color:"#0b0c10", show_text:true, icon_style:"pill", width:100 };
case "video": return { url:"", description:"", width:100 };
case "review": return { title:"Reseña destacada", name:"Cliente feliz", text:"Excelente servicio y resultados profesionales.", rating:5, style:"card", width:100 };
case "calendar": return { title:"Agenda una cita", note:"Disponible en plan premium. Proximamente.", embed_url:"", 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 {};
}
}
@@ -1131,8 +1217,8 @@ const state = {
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";
return `<div class="hero-pro hero-layout">
<div class="hero-copy" style="text-align:${align}">
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,"")}
@@ -1140,9 +1226,9 @@ const state = {
<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>
<div class="hero-media">${image}</div>
</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"){
@@ -1259,9 +1345,32 @@ const state = {
threads:"fa-brands fa-threads"
};
const size = Math.max(14, Math.min(36, Number(block.data?.icon_size || 18)));
const iconColor = block.data?.icon_color || "var(--site-text)";
const 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)=>{
@@ -1272,7 +1381,29 @@ const state = {
}
return normalizeLink(v);
};
return `<h3 style="margin:0 0 10px">Redes</h3><div class="social-icons social-style-${escapeHtml(style)}">${items.map(([k,v],idx)=>`<a class="social-btn" href="${escapeHtml(linkFor(k,v)||"#")}" target="_blank" rel="noreferrer" style="font-size:${size}px"><i class="${icons[k]||'fa-solid fa-circle'}" style="font-size:${size}px;color:${escapeHtml(iconColor)}"></i>${showText ? `<span class="editable" data-field="${escapeHtml(k)}" data-placeholder="${escapeHtml(k)}" contenteditable="true" style="font-size:12px">${escapeHtml(v)}</span>` : ""}</a>`).join("")}</div>`;
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)));
@@ -1418,7 +1549,7 @@ const state = {
return;
}
const visibleBlocks = state.blocks.filter(isBlockVisibleInCanvas);
visibleBlocks.forEach((block)=>{
visibleBlocks.forEach((block, idx)=>{
const el = document.createElement("div");
el.className = "block";
if (!state.settings.free_drag){ el.removeAttribute("draggable"); }
@@ -1426,7 +1557,7 @@ const state = {
el.dataset.blockType = block.type;
el.id = block.id;
if (state.settings.free_drag){
const pos = block.pos || getDefaultPos();
const pos = block.pos || getDefaultPos(idx);
block.pos = pos;
el.style.position = "absolute";
el.style.left = "0px";
@@ -1454,6 +1585,55 @@ const state = {
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); }
@@ -1575,17 +1755,29 @@ const state = {
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);
const blockId = n.dataset.blockId;
const source = state.blocks.find((b)=>b && b.id === blockId);
const y = Number(source?.pos?.y || 0);
maxBottom = Math.max(maxBottom, y + n.offsetHeight + 140);
});
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();
@@ -1796,6 +1988,7 @@ const state = {
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"){
@@ -1815,6 +2008,8 @@ const state = {
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>`;
@@ -1868,6 +2063,17 @@ const state = {
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);
@@ -1894,9 +2100,31 @@ const state = {
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");
@@ -1957,6 +2185,8 @@ const state = {
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"){
@@ -1997,6 +2227,26 @@ const state = {
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; }
}
@@ -2029,6 +2279,32 @@ const state = {
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();
}
@@ -2445,6 +2721,9 @@ const state = {
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");
@@ -2460,6 +2739,9 @@ const state = {
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 || "";
@@ -2478,6 +2760,9 @@ const state = {
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(); });
@@ -2643,7 +2928,7 @@ const state = {
data.items = data.items.map((x)=>String(x || "").trim()).filter(Boolean);
}
}
const fallback = 100;
const fallback = b.type === "menu" ? 100 : 60;
data.width = snapBlockWidth(b.type, (typeof data.width === "number" ? data.width : fallback));
delete data.full_width;
return {
@@ -2653,9 +2938,136 @@ const state = {
};
});
}
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;
});
}
function applyRestaurantTemplateOrder(blocks){
const list = Array.isArray(blocks) ? blocks : [];
const rank = {
menu: 0,
hero: 1,
cards: 2,
gallery: 3,
review: 4,
contact: 5,
social: 6,
map: 7
};
const indexed = list.map((b, i)=>({ b, i }));
indexed.sort((a, z)=>{
const ar = Object.prototype.hasOwnProperty.call(rank, a.b?.type) ? rank[a.b.type] : 99;
const zr = Object.prototype.hasOwnProperty.call(rank, z.b?.type) ? rank[z.b.type] : 99;
if (ar !== zr) return ar - zr;
return a.i - z.i;
});
let y = 20;
indexed.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;
}
const currentWidth = Number(b?.data?.width || 92);
b.data.width = snapBlockWidth(b.type, currentWidth);
b.pos = { x: 20, y };
y += estimateFreeDragBlockHeight(b) + 22;
});
return indexed.map((x)=>x.b);
}
async function saveContent(){
if (isSaving) return;
state.settings.free_drag = false;
isSaving = true;
const btn = document.getElementById("btnSave");
if (btn){
@@ -2695,6 +3107,11 @@ const state = {
const keepRubro = normalizeRubro(state.settings.business_rubro || SERVER_RUBRO || "restaurante");
state.blocks = [];
state.settings = { ...defaultSettings, business_rubro: keepRubro };
if (BUILDER_MODE === "ub24"){
state.settings.free_drag = false;
} else {
state.settings.free_drag = keepRubro === "restaurante";
}
selectedBlockId = null;
renderInspector();
renderPreview();
@@ -2708,16 +3125,33 @@ const state = {
}
}
function init(){
// By default we keep section flow layout for stable full-page composition.
state.settings.free_drag = false;
const initialRubro = normalizeRubro(state.settings.business_rubro || SERVER_RUBRO || "restaurante");
state.settings.business_rubro = initialRubro;
state.blocks = normalizeLoadedBlocks(state.blocks);
if (BUILDER_MODE === "ub24"){
state.blocks.forEach(b=>{ if (!b.page) b.page = "home"; });
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 {
const isRestaurante = initialRubro === "restaurante";
// Solo restaurante mantiene modo libre por requerimiento del usuario.
state.settings.free_drag = isRestaurante;
if (isRestaurante){
const needsTemplateRepair = !state.settings.restaurant_layout_repaired_v1;
if (needsTemplateRepair){
state.blocks = applyRestaurantTemplateOrder(state.blocks);
state.settings.restaurant_layout_repaired_v1 = true;
} else if (!hasMeaningfulFreeDragPositions(state.blocks) || hasSevereFreeDragOverlap(state.blocks)){
applyStackedFreeDragLayout(state.blocks);
normalizeDuplicatedFreeDragPositions(state.blocks);
}
}
}
selectedBlockId = null;
wireSidebar();
@@ -2740,7 +3174,16 @@ const state = {
const t = templates[key];
state.settings = { ...state.settings, ...t.settings };
state.settings.business_rubro = normalizeRubro(key);
if (BUILDER_MODE === "ub24"){
state.settings.free_drag = false;
} else {
state.settings.free_drag = state.settings.business_rubro === "restaurante";
}
state.blocks = t.blocks.map(b=>({ ...b, id: makeId(), page: (BUILDER_MODE==="ub24" ? "home" : b.page) }));
if (state.settings.free_drag){
state.blocks = applyRestaurantTemplateOrder(state.blocks);
state.settings.restaurant_layout_repaired_v1 = true;
}
selectedBlockId = null;
renderInspector(); renderPreview();
wireSettings();