Compare commits
18 Commits
cierre-202
...
685659c0f1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
685659c0f1 | ||
|
|
f363eefdca | ||
|
|
400a8230b5 | ||
|
|
f9f7d23b8d | ||
|
|
f2fbc6eedd | ||
|
|
a6089ee341 | ||
|
|
f8935e7c00 | ||
|
|
e5df6de8fc | ||
|
|
df641372fa | ||
|
|
e20f0867fe | ||
|
|
22fcd505f4 | ||
|
|
c2ee81d202 | ||
|
|
b6fb4dadff | ||
|
|
7dddbc4764 | ||
|
|
f6d8ab13c0 | ||
|
|
1a5778be2e | ||
|
|
53aa755c39 | ||
|
|
075dad6f1a |
40
codex/ARRANQUE_RAPIDO_UB24.md
Normal file
40
codex/ARRANQUE_RAPIDO_UB24.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Arranque Rapido UB24 (Local Windows)
|
||||||
|
|
||||||
|
## URL canonica
|
||||||
|
- `http://127.0.0.1:5001/elementor/1`
|
||||||
|
|
||||||
|
## Inicio en 1 comando
|
||||||
|
Desde `c:\word`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m demo.app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verificar que quedo arriba
|
||||||
|
En otra terminal:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Invoke-WebRequest http://127.0.0.1:5001/elementor/1 -UseBasicParsing
|
||||||
|
```
|
||||||
|
|
||||||
|
Si responde `StatusCode: 200`, esta listo.
|
||||||
|
|
||||||
|
## Tiempo de arranque esperado
|
||||||
|
- Primer arranque: puede tardar ~40-50 segundos por inicializacion de DB.
|
||||||
|
- Arranques siguientes: normalmente mas rapido.
|
||||||
|
|
||||||
|
## Levantar en background
|
||||||
|
Desde `c:\word`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Start-Process -FilePath python -ArgumentList @('-m','demo.app') -WorkingDirectory 'c:\word'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parar servidor
|
||||||
|
```powershell
|
||||||
|
Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'python.exe' -and $_.CommandLine -match 'demo.app' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
- `c:\word\logs_demo_app.txt`
|
||||||
|
- `c:\word\logs_demo_app.err`
|
||||||
261
codex/HISTORIAL_CAMBIOS.md
Normal file
261
codex/HISTORIAL_CAMBIOS.md
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
# 📝 Historial de Cambios - GKACHELE™
|
||||||
|
|
||||||
|
**© 2025 GKACHELE™. Todos los derechos reservados.**
|
||||||
|
|
||||||
|
## 🔄 Historial de Versiones
|
||||||
|
|
||||||
|
### Hash: `gkachele-elementor-apple-20260209-v14`
|
||||||
|
**Fecha:** 09 Febrero 2026
|
||||||
|
**Estado:** OK Fase 1
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- OK Modo libre unico activo
|
||||||
|
- OK Drag/resize con handles 8 puntos + Shift
|
||||||
|
- OK Guías de snap
|
||||||
|
- OK Touch (capacitivos)
|
||||||
|
- OK Auto-height del canvas
|
||||||
|
- OK Menu superior movible y redimensionable
|
||||||
|
- OK Boton Alinear
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Hash: `gkachele-elementor-apple-20260209-v13`
|
||||||
|
**Fecha:** 09 Febrero 2026
|
||||||
|
**Estado:** OK UX Builder
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- OK Sidebar: solo drag (sin agregar por click)
|
||||||
|
- OK Menu: acordeon responsive por tamanos de preview
|
||||||
|
- OK Media: captions + fit (cover/contain) en imagen/galeria
|
||||||
|
- OK Video: drag & drop (dataURL) o URL
|
||||||
|
- OK Delete inline en bloques (icono en preview)
|
||||||
|
- OK Free drag: links no bloquean movimiento
|
||||||
|
- OK Boton atras (history.back)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Hash: `gkachele-elementor-apple-20260209-v12`
|
||||||
|
**Fecha:** 09 Febrero 2026
|
||||||
|
**Estado:** OK Responsive + fondos
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- OK Menu responsive con acordeon en movil
|
||||||
|
- OK Fondo: animacion de gradiente + GIF de fondo
|
||||||
|
- OK Boton: estilos (primario/outline/ghost) y tamanos
|
||||||
|
- OK Boton flotante WhatsApp (si hay numero)
|
||||||
|
- OK Transiciones premium (botones/iconos/hover)
|
||||||
|
- OK Modo libre: sin etiqueta ON, estado visual
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Hash: `gkachele-elementor-apple-20260209-v11`
|
||||||
|
**Fecha:** 09 Febrero 2026
|
||||||
|
**Estado:** OK Mejoras UX/UI
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- OK Mas tipografias en el inspector
|
||||||
|
- OK Redes: estilos premium (pill/circulo/outline/minimal/solid) + iconos extra
|
||||||
|
- OK Drag & drop de imagenes desde el preview (imagen y galeria)
|
||||||
|
- OK Mapa: embed corregido (google.com/maps)
|
||||||
|
- OK Contacto: mailto/tel
|
||||||
|
- OK Calendario: vista tipo calendario (si no hay embed)
|
||||||
|
- OK Resenas: estilos (card/quote/compacto)
|
||||||
|
- OK Resize activo en ambos modos
|
||||||
|
- OK Modo libre con tamanos mas controlados
|
||||||
|
- OK Transiciones y hover premium en iconos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Hash: `gkachele-elementor-apple-20260209-v10`
|
||||||
|
**Fecha:** 09 Febrero 2026
|
||||||
|
**Estado:** OK Pendientes anotados
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- OK Memoria actualizada para adaptacion a SaaS
|
||||||
|
- OK Pendientes: color de texto, transiciones, colores de iconos
|
||||||
|
- OK Pendientes: resize en modo libre, mapa direccion
|
||||||
|
- OK Pendientes: tarjetas con 3 opciones y menu movil acordeon
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
### Hash: `gkachele-elementor-apple-20260209-v9`
|
||||||
|
**Fecha:** 09 Febrero 2026
|
||||||
|
**Estado:** OK Ajustado
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- OK Iconos de preview (movil/tablet/web)
|
||||||
|
- OK Redes con iconos y tamano ajustable
|
||||||
|
- OK Mapa sin API y boton scroll
|
||||||
|
- OK Carga de logo/fondo por arrastre
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
### Hash: `gkachele-elementor-apple-20260209-v8`
|
||||||
|
**Fecha:** 09 Febrero 2026
|
||||||
|
**Estado:** OK Ajustado
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- OK Menu superior detecta secciones
|
||||||
|
- OK Layout 2 columnas basico
|
||||||
|
- OK Contador ilimitado en topbar
|
||||||
|
- OK Botones de tamano preview
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
### Hash: `gkachele-elementor-apple-20260209-v7`
|
||||||
|
**Fecha:** 09 Febrero 2026
|
||||||
|
**Estado:** OK Ajustado
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- OK Fondos con gradiente y controles visibles
|
||||||
|
- OK Tama?o de bloques por porcentaje
|
||||||
|
- OK Topbar con contador y salto a bloques
|
||||||
|
- OK Animaciones configurables
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
### Hash: `gkachele-elementor-apple-20260209-v6`
|
||||||
|
**Fecha:** 09 Febrero 2026
|
||||||
|
**Estado:** OK Ajustado
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- OK Modo libre (drag X/Y) con snapping
|
||||||
|
- OK Boton de activacion en topbar
|
||||||
|
- OK Posiciones guardadas por bloque
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
### Hash: `gkachele-elementor-apple-20260209-v5`
|
||||||
|
**Fecha:** 09 Febrero 2026
|
||||||
|
**Estado:** OK Ajustado
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- OK Componentes nuevos: tarjetas e iconos
|
||||||
|
- OK Sidebar e inspector con configuracion de items
|
||||||
|
- OK Render en preview y tema apple-pro
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
### Hash: `gkachele-elementor-apple-20260209-v4`
|
||||||
|
**Fecha:** 09 Febrero 2026
|
||||||
|
**Estado:** OK Ajustado
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- OK Editor de tipografias (texto y titulos) en el inspector
|
||||||
|
- OK Paleta de colores (fondo, texto, secundario)
|
||||||
|
- OK Preview con variables de estilo aplicadas
|
||||||
|
- OK Animaciones sutiles en bloques (hover + entrada)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
### Hash: `gkachele-builder-apple-20260208-v2`
|
||||||
|
**Fecha:** 08 Febrero 2026
|
||||||
|
**Estado:** ✅ Ajustado
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- ✅ Restaurado template del builder en `elementor/templates/elementor_builder.html`
|
||||||
|
- ✅ Drag & drop funcional (agregar y reordenar en preview)
|
||||||
|
- ✅ Inspector estable para editar bloques y ajustes del sitio
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Hash: `gkachele-elementor-apple-20260208-v3`
|
||||||
|
**Fecha:** 08 Febrero 2026
|
||||||
|
**Estado:** ✅ Base de trabajo (congelada)
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- ✅ Version base del builder para continuar mañana
|
||||||
|
- ✅ Drag & drop estable en preview
|
||||||
|
- ✅ Inspector con edicion de bloques
|
||||||
|
- ✅ Fondo y video por URL en preview
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Hash: `gkachele-builder-apple-20260208-v1`
|
||||||
|
**Fecha:** 08 Febrero 2026
|
||||||
|
**Estado:** ✅ En progreso
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- ✅ Builder tipo drag & drop en ruta `/elementor/{site_id}`
|
||||||
|
- ✅ Vista lienzo con bloques sobre la pagina (no lista)
|
||||||
|
- ✅ Preview en vivo con marco estilo navegador
|
||||||
|
- ✅ Tema `apple-pro` para paginas profesionales
|
||||||
|
- ✅ Soporte de logo, fondo con imagen y video en ajustes
|
||||||
|
- ✅ Redes sociales ampliadas (TikTok, YouTube, LinkedIn, X, Telegram)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Hash: `gkachele-login-customizer-20250114-v3`
|
||||||
|
**Fecha:** 14 Enero 2025 - 11:34
|
||||||
|
**Estado:** ✅ Funcionando
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- ✅ **Login redirige correctamente a customizer**
|
||||||
|
- ✅ Añadido debugging completo (backend + frontend)
|
||||||
|
- ✅ Content-Type JSON explícito en respuesta
|
||||||
|
- ✅ Flush=True en todos los prints
|
||||||
|
- ✅ Console.log en frontend para debugging
|
||||||
|
- ✅ Verificación de sitios del usuario antes de redirigir
|
||||||
|
|
||||||
|
#### Funcionalidad:
|
||||||
|
- Login → Busca primer sitio del usuario
|
||||||
|
- Si tiene sitio → `/customizer/{site_id}`
|
||||||
|
- Si no tiene sitio → `/dashboard`
|
||||||
|
- Frontend recibe y procesa correctamente el redirect
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Hash: `gkachele-argentina-20250114-v2`
|
||||||
|
**Fecha:** 14 Enero 2025 - 11:15
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- Documentación objetivos Argentina
|
||||||
|
- Gitea y workflows documentados
|
||||||
|
- Subdominios y pagos documentados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Hash: `gkachele-template-system-20250114-v1`
|
||||||
|
**Fecha:** 14 Enero 2025 - 08:50
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- Sistema modular de templates
|
||||||
|
- Menús y widgets dinámicos
|
||||||
|
- Sistema de roles basado en DB
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ultima actualizacion:** 09 Febrero 2026
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**© 2025 GKACHELE™. Todos los derechos reservados.**
|
||||||
|
|
||||||
|
### Hash: `gkachele-builder-docs-20260208-v1`
|
||||||
|
**Fecha:** 08 Febrero 2026
|
||||||
|
**Estado:** ✅ Documentado
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- ✅ Guía rápida del builder en `memoria/BUILDER_ELEMENTOR.md`
|
||||||
|
- ✅ Entrada en índice de documentación
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Hash: `gkachele-docs-sync-20260214-v1`
|
||||||
|
**Fecha:** 14 Febrero 2026
|
||||||
|
**Estado:** OK Documentacion unificada
|
||||||
|
|
||||||
|
#### Cambios:
|
||||||
|
- Fuente vigente consolidada en `codex/*`.
|
||||||
|
- `codex/VERSIONADO_IA.md` actualizado con protocolo fijo de sync local/remoto.
|
||||||
|
- `codex/MEMORIA_CODEX.md` actualizado con punto exacto (rama, commit, version base, estado sync).
|
||||||
|
- `memoria/ESTADO_ACTUAL.md` marcado como historico para evitar decisiones con contexto 2025.
|
||||||
|
|
||||||
|
---
|
||||||
36
codex/MEMORIA_CODEX.md
Normal file
36
codex/MEMORIA_CODEX.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Memoria Codex - GKACHELE
|
||||||
|
|
||||||
|
**Fecha de corte:** 14 Febrero 2026
|
||||||
|
|
||||||
|
## Fuente de verdad (estado IA)
|
||||||
|
- `codex/VERSIONADO_IA.md`
|
||||||
|
- `codex/BUILDER_ELEMENTOR_VERSION.md`
|
||||||
|
- `codex/HISTORIAL_CAMBIOS.md`
|
||||||
|
|
||||||
|
## Punto exacto
|
||||||
|
- Rama activa: `ai/ub24-builder-v1`
|
||||||
|
- Commit HEAD: `6d696c4`
|
||||||
|
- Version base builder: `gkachele-elementor-templates-20260210-v21`
|
||||||
|
- Estado de sync local/remoto al corte: `0/0`
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
- Preview limpio y menu por modos (horizontal/acordeon/ambos).
|
||||||
|
|
||||||
|
## Pendientes inmediatos
|
||||||
|
- Cargar bloques reales por plantilla de rubro (no solo look-and-feel).
|
||||||
|
- Definir persistencia de resenas al publicar.
|
||||||
|
- Integrar calendario real (Calendly/Google).
|
||||||
|
- Consolidar puente Builder -> SaaS (`/customizer/<id>`).
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
|
||||||
|
## Comando de arranque
|
||||||
|
- `python -m demo.app` desde `c:\word`
|
||||||
@@ -3,20 +3,18 @@
|
|||||||
## Rama de trabajo
|
## Rama de trabajo
|
||||||
- `ai/ub24-builder-v1`
|
- `ai/ub24-builder-v1`
|
||||||
|
|
||||||
|
## Estado de sincronizacion (14 Febrero 2026)
|
||||||
|
- Local: `ai/ub24-builder-v1`
|
||||||
|
- Remoto: `origin/ai/ub24-builder-v1`
|
||||||
|
- Divergencia verificada: `0/0` (sin commits pendientes entre local y remoto)
|
||||||
|
|
||||||
## Regla de trabajo
|
## Regla de trabajo
|
||||||
1. Cada cambio funcional se guarda en un commit separado.
|
1. Cada cambio funcional se guarda en un commit separado.
|
||||||
2. Cada commit se registra con su hash.
|
2. Cada commit se registra con su hash.
|
||||||
3. Cada commit debe incluir comando de reversión rápida.
|
3. Cada commit debe incluir comando de reversion rapida.
|
||||||
|
4. La rama debe quedar sincronizada con remoto al cerrar bloque de trabajo.
|
||||||
|
|
||||||
## Registro de hashes
|
## Convencion de mensaje
|
||||||
### Baseline
|
|
||||||
- Commit: `cb99f26`
|
|
||||||
- Objetivo: crear rama y política de versionado para trabajo IA.
|
|
||||||
- Revert:
|
|
||||||
- `git revert <hash>`
|
|
||||||
- o volver a commit previo: `git reset --hard <hash_anterior>` (solo si se aprueba explícitamente)
|
|
||||||
|
|
||||||
## Convención de mensaje
|
|
||||||
- `feat(builder): ...`
|
- `feat(builder): ...`
|
||||||
- `fix(builder): ...`
|
- `fix(builder): ...`
|
||||||
- `refactor(builder): ...`
|
- `refactor(builder): ...`
|
||||||
@@ -27,6 +25,25 @@
|
|||||||
2. Push continuo a `origin/ai/ub24-builder-v1`.
|
2. Push continuo a `origin/ai/ub24-builder-v1`.
|
||||||
3. Merge cuando validemos en local y Raspberry.
|
3. Merge cuando validemos en local y Raspberry.
|
||||||
|
|
||||||
|
## Protocolo fijo de sincronizacion (siempre)
|
||||||
|
1. Verificar rama activa: `git branch --show-current`
|
||||||
|
2. Actualizar referencias remotas: `git fetch origin --prune`
|
||||||
|
3. Medir divergencia: `git rev-list --left-right --count ai/ub24-builder-v1...origin/ai/ub24-builder-v1`
|
||||||
|
4. Si el resultado no es `0 0`, sincronizar antes de continuar.
|
||||||
|
5. Despues de cada lote validado:
|
||||||
|
- `git add <archivos>`
|
||||||
|
- `git commit -m "tipo(scope): mensaje"`
|
||||||
|
- `git push origin ai/ub24-builder-v1`
|
||||||
|
6. Registrar hash y objetivo en este archivo y en `codex/HISTORIAL_CAMBIOS.md`.
|
||||||
|
|
||||||
|
## Registro de hashes
|
||||||
|
### Baseline
|
||||||
|
- Commit: `cb99f26`
|
||||||
|
- Objetivo: crear rama y politica de versionado para trabajo IA.
|
||||||
|
- Revert:
|
||||||
|
- `git revert <hash>`
|
||||||
|
- `git reset --hard <hash_anterior>` (solo con aprobacion explicita)
|
||||||
|
|
||||||
### Correccion historial
|
### Correccion historial
|
||||||
- Commit: `fe8657e`
|
- Commit: `fe8657e`
|
||||||
- Objetivo: revertir commit no deseado y mantener separacion de cambios.
|
- Objetivo: revertir commit no deseado y mantener separacion de cambios.
|
||||||
@@ -45,22 +62,6 @@
|
|||||||
- Revert:
|
- Revert:
|
||||||
- `git revert 1c04f04`
|
- `git revert 1c04f04`
|
||||||
|
|
||||||
## URL local canonica (unificada)
|
|
||||||
- Base local: `http://127.0.0.1:5001`
|
|
||||||
- Builder local: `http://127.0.0.1:5001/elementor/1`
|
|
||||||
- Regla: usar siempre `127.0.0.1` (no `localhost`) en scripts, pruebas y documentacion local.
|
|
||||||
|
|
||||||
## Control de rama (local/remoto)
|
|
||||||
- Rama local activa: `ai/ub24-builder-v1`
|
|
||||||
- Upstream remoto: `origin/ai/ub24-builder-v1`
|
|
||||||
- Estado al registrar: local `ahead 5`
|
|
||||||
- Politica: seguir commits atomicos y luego `git push origin ai/ub24-builder-v1` por lote validado.
|
|
||||||
|
|
||||||
## Fases memorizadas (builder)
|
|
||||||
1. Fase 1 (UI Pro base): navbar premium, hero premium, sistema de espaciado/grid, pulido visual consistente.
|
|
||||||
2. Fase 2 (estructura): separar renderers por bloque y reducir inline styles para automatizacion.
|
|
||||||
3. Fase 3 (presets): presets por rubro + reglas responsive + variantes exportables.
|
|
||||||
|
|
||||||
### Ajustes Builder (limpieza + preview + ancho)
|
### Ajustes Builder (limpieza + preview + ancho)
|
||||||
- Commit: `7c5f671`
|
- Commit: `7c5f671`
|
||||||
- Objetivo: quitar texto en barra Apple, limpiar menu vacio, preview local funcional sin salir de builder, ancho desktop al 100%, control de ancho por bloque y descripcion en bloque video.
|
- Objetivo: quitar texto en barra Apple, limpiar menu vacio, preview local funcional sin salir de builder, ancho desktop al 100%, control de ancho por bloque y descripcion en bloque video.
|
||||||
@@ -72,3 +73,87 @@
|
|||||||
- Objetivo: mejorar vista previa (forzar modo limpio y restaurar estado), eliminar precarga automatica de bloques, y agregar modo de menu (horizontal/acordeon/ambos).
|
- Objetivo: mejorar vista previa (forzar modo limpio y restaurar estado), eliminar precarga automatica de bloques, y agregar modo de menu (horizontal/acordeon/ambos).
|
||||||
- Revert:
|
- Revert:
|
||||||
- `git revert dd98e9d`
|
- `git revert dd98e9d`
|
||||||
|
|
||||||
|
### Runtime unificado (app + elementor)
|
||||||
|
- Commit: `1a5778b`
|
||||||
|
- Objetivo: unificar arranque con `python -m demo.app` y registrar blueprint de Elementor en runtime principal.
|
||||||
|
- Revert:
|
||||||
|
- `git revert 1a5778b`
|
||||||
|
|
||||||
|
### Fix SQLite wrapper (arranque sin error SQL)
|
||||||
|
- Commit: `f6d8ab1`
|
||||||
|
- Objetivo: evitar conversiones SQL invalidas en SQLite que rompian inicializacion y generaban reintentos.
|
||||||
|
- Revert:
|
||||||
|
- `git revert f6d8ab1`
|
||||||
|
|
||||||
|
### API Elementor save/publish
|
||||||
|
- Commit: `b6fb4da`
|
||||||
|
- Objetivo: agregar endpoint dedicado `/api/elementor/save` para guardar builder con opcion de publicar.
|
||||||
|
- Revert:
|
||||||
|
- `git revert b6fb4da`
|
||||||
|
|
||||||
|
### Builder persistencia y feedback de publicacion
|
||||||
|
- Commit: `c2ee81d`
|
||||||
|
- Objetivo: mantener bloques cargados al entrar, normalizar bloques sin id y mostrar estado de guardado/publicacion en topbar.
|
||||||
|
- Revert:
|
||||||
|
- `git revert c2ee81d`
|
||||||
|
|
||||||
|
### Preview full-page + layout estable
|
||||||
|
- Commit: `e20f086`
|
||||||
|
- Objetivo: hacer que vista previa ocupe pagina completa y forzar layout por secciones (sin modo libre por defecto) para alinear bloques.
|
||||||
|
- Revert:
|
||||||
|
- `git revert e20f086`
|
||||||
|
|
||||||
|
### Full width + dos columnas por bloque
|
||||||
|
- Commit: `e5df6de`
|
||||||
|
- Objetivo: expandir ancho util del canvas y habilitar 2 columnas reales con toggle "Ancho completo" por bloque.
|
||||||
|
- Revert:
|
||||||
|
- `git revert e5df6de`
|
||||||
|
|
||||||
|
### Drag inteligente columnas + preview completo
|
||||||
|
- Commit: `a6089ee`
|
||||||
|
- Objetivo: permitir decidir 1 o 2 columnas moviendo bloques al soltar (centro=ancho completo, lados=media columna) y agregar opcion de preview completo en nueva pestana.
|
||||||
|
- Revert:
|
||||||
|
- `git revert a6089ee`
|
||||||
|
|
||||||
|
### Quitar layout global + preview completo real
|
||||||
|
- Commit: `f9f7d23`
|
||||||
|
- Objetivo: eliminar toggle global de 2 columnas; mantener decision 1/2 columnas solo por movimiento de bloques; y forzar preview completo sin margenes.
|
||||||
|
- Revert:
|
||||||
|
- `git revert f9f7d23`
|
||||||
|
|
||||||
|
### Fix final dia: texto, modo libre y full preview
|
||||||
|
- Commit: `f363eef`
|
||||||
|
- Objetivo: reparar caracteres mojibake en UI, habilitar modo libre real para mover bloques completos y forzar modo completo con `?full=1`.
|
||||||
|
- Revert:
|
||||||
|
- `git revert f363eef`
|
||||||
|
|
||||||
|
## URL local canonica (unificada)
|
||||||
|
- Base local: `http://127.0.0.1:5001`
|
||||||
|
- Builder local: `http://127.0.0.1:5001/elementor/1`
|
||||||
|
- Regla: usar siempre `127.0.0.1` (no `localhost`) en scripts, pruebas y documentacion local.
|
||||||
|
|
||||||
|
## Arranque rapido local (Windows)
|
||||||
|
1. Desde `c:\word`, ejecutar:
|
||||||
|
- `python -m demo.app`
|
||||||
|
2. Abrir:
|
||||||
|
- `http://127.0.0.1:5001/elementor/1`
|
||||||
|
3. Verificacion rapida:
|
||||||
|
- `Invoke-WebRequest http://127.0.0.1:5001/elementor/1 -UseBasicParsing`
|
||||||
|
|
||||||
|
Notas:
|
||||||
|
- En el primer arranque puede tardar unos segundos adicionales por inicializacion de DB.
|
||||||
|
- Logs:
|
||||||
|
- `c:\word\logs_demo_app.txt`
|
||||||
|
- `c:\word\logs_demo_app.err`
|
||||||
|
|
||||||
|
## Control de rama (local/remoto)
|
||||||
|
- Rama local activa: `ai/ub24-builder-v1`
|
||||||
|
- Upstream remoto: `origin/ai/ub24-builder-v1`
|
||||||
|
- Estado al registrar: `en sync (0/0)` al 14 Febrero 2026
|
||||||
|
- Politica: commits atomicos + push por lote validado + verificacion de divergencia al inicio y al cierre.
|
||||||
|
|
||||||
|
## Fases memorizadas (builder)
|
||||||
|
1. Fase 1 (UI Pro base): navbar premium, hero premium, sistema de espaciado/grid, pulido visual consistente.
|
||||||
|
2. Fase 2 (estructura): separar renderers por bloque y reducir inline styles para automatizacion.
|
||||||
|
3. Fase 3 (presets): presets por rubro + reglas responsive + variantes exportables.
|
||||||
|
|||||||
48
demo/app.py
48
demo/app.py
@@ -1,17 +1,37 @@
|
|||||||
"""
|
"""
|
||||||
GKACHELE™ SaaS PageBuilder - Sistema Modular
|
GKACHELE SaaS PageBuilder - Sistema Modular
|
||||||
© 2025 GKACHELE™. Todos los derechos reservados.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from flask import Flask, jsonify, request
|
import sys
|
||||||
from config import SECRET_KEY, PORT
|
from flask import Flask, jsonify
|
||||||
from database import init_db
|
|
||||||
from routes.auth import auth_bp
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
from routes.dashboard import dashboard_bp
|
PROJECT_ROOT = os.path.dirname(BASE_DIR)
|
||||||
from routes.customizer import customizer_bp
|
if PROJECT_ROOT not in sys.path:
|
||||||
from routes.admin import admin_bp
|
sys.path.insert(0, PROJECT_ROOT)
|
||||||
from routes.public import public_bp
|
|
||||||
|
try:
|
||||||
|
from .config import SECRET_KEY, PORT
|
||||||
|
from .database import init_db
|
||||||
|
from .routes.auth import auth_bp
|
||||||
|
from .routes.dashboard import dashboard_bp
|
||||||
|
from .routes.customizer import customizer_bp
|
||||||
|
from .routes.admin import admin_bp
|
||||||
|
from .routes.public import public_bp
|
||||||
|
except ImportError:
|
||||||
|
from config import SECRET_KEY, PORT
|
||||||
|
from database import init_db
|
||||||
|
from routes.auth import auth_bp
|
||||||
|
from routes.dashboard import dashboard_bp
|
||||||
|
from routes.customizer import customizer_bp
|
||||||
|
from routes.admin import admin_bp
|
||||||
|
from routes.public import public_bp
|
||||||
|
|
||||||
|
try:
|
||||||
|
from elementor.routes import elementor_bp
|
||||||
|
except ImportError:
|
||||||
|
elementor_bp = None
|
||||||
|
|
||||||
app = Flask(__name__, template_folder='templates', static_folder='static')
|
app = Flask(__name__, template_folder='templates', static_folder='static')
|
||||||
app.secret_key = SECRET_KEY
|
app.secret_key = SECRET_KEY
|
||||||
@@ -26,6 +46,8 @@ app.register_blueprint(dashboard_bp)
|
|||||||
app.register_blueprint(customizer_bp)
|
app.register_blueprint(customizer_bp)
|
||||||
app.register_blueprint(admin_bp)
|
app.register_blueprint(admin_bp)
|
||||||
app.register_blueprint(public_bp)
|
app.register_blueprint(public_bp)
|
||||||
|
if elementor_bp is not None and 'elementor' not in app.blueprints:
|
||||||
|
app.register_blueprint(elementor_bp)
|
||||||
|
|
||||||
# Manejadores de Errores Globales
|
# Manejadores de Errores Globales
|
||||||
@app.errorhandler(500)
|
@app.errorhandler(500)
|
||||||
@@ -34,15 +56,18 @@ def handle_500(e):
|
|||||||
response.status_code = 500
|
response.status_code = 500
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def handle_404(e):
|
def handle_404(e):
|
||||||
return jsonify({'success': False, 'error': 'No encontrado'}), 404
|
return jsonify({'success': False, 'error': 'No encontrado'}), 404
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(Exception)
|
@app.errorhandler(Exception)
|
||||||
def handle_exception(e):
|
def handle_exception(e):
|
||||||
print(f"ERROR: EXCEPCION: {e}")
|
print(f"ERROR: EXCEPCION: {e}")
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
# Middleware
|
# Middleware
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def add_header(response):
|
def add_header(response):
|
||||||
@@ -50,6 +75,7 @@ def add_header(response):
|
|||||||
response.headers['Content-Security-Policy'] = "frame-ancestors *;"
|
response.headers['Content-Security-Policy'] = "frame-ancestors *;"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print(f"GKACHELE SaaS Modular iniciado en puerto {PORT}")
|
print(f"GKACHELE SaaS Modular iniciado en puerto {PORT}")
|
||||||
app.run(debug=True, host='0.0.0.0', port=PORT)
|
app.run(debug=True, host='0.0.0.0', port=PORT)
|
||||||
|
|||||||
104
demo/db.py
Normal file
104
demo/db.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import json
|
||||||
|
|
||||||
|
# DB Settings (Postgres)
|
||||||
|
DB_HOST = os.environ.get('DB_HOST', None)
|
||||||
|
DB_PORT = int(os.environ.get('DB_PORT', '5432'))
|
||||||
|
DB_NAME = os.environ.get('DB_NAME', 'gkachele')
|
||||||
|
DB_USER = os.environ.get('DB_USER', 'gkachele')
|
||||||
|
DB_PASSWORD = os.environ.get('DB_PASSWORD', 'gkachele_pass')
|
||||||
|
|
||||||
|
# SQLite Settings
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
SQLITE_PATH = os.path.join(BASE_DIR, 'database', 'main.db')
|
||||||
|
|
||||||
|
class CursorWrapper:
|
||||||
|
def __init__(self, cursor, is_sqlite=False):
|
||||||
|
self._cursor = cursor
|
||||||
|
self._is_sqlite = is_sqlite
|
||||||
|
|
||||||
|
def execute(self, query, params=None):
|
||||||
|
try:
|
||||||
|
if self._is_sqlite:
|
||||||
|
# Skip postgres-specific maintenance commands
|
||||||
|
if 'setval' in query or 'pg_get_serial_sequence' in query:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Adapt psycopg2-style placeholders (%s) to sqlite (?)
|
||||||
|
if isinstance(query, str) and '%s' in query:
|
||||||
|
query = query.replace('%s', '?')
|
||||||
|
|
||||||
|
# SQLite doesn't support IF NOT EXISTS in ALTER TABLE
|
||||||
|
if 'ALTER TABLE' in query and 'ADD COLUMN' in query and 'IF NOT EXISTS' in query:
|
||||||
|
query = query.replace('IF NOT EXISTS', '')
|
||||||
|
|
||||||
|
# Handle SERIAL PRIMARY KEY and other PG types
|
||||||
|
query = query.replace('SERIAL PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT')
|
||||||
|
if 'IF NOT EXISTS' not in query and 'CREATE TABLE' in query:
|
||||||
|
query = query.replace('CREATE TABLE', 'CREATE TABLE IF NOT EXISTS')
|
||||||
|
else:
|
||||||
|
# Adapt sqlite-style placeholders (?) to psycopg2 (%s)
|
||||||
|
if isinstance(query, str) and '?' in query:
|
||||||
|
query = query.replace('?', '%s')
|
||||||
|
|
||||||
|
if params is None:
|
||||||
|
return self._cursor.execute(query)
|
||||||
|
return self._cursor.execute(query, params)
|
||||||
|
except (sqlite3.OperationalError, sqlite3.IntegrityError) as e:
|
||||||
|
msg = str(e).lower()
|
||||||
|
if "duplicate column name" in msg or "already exists" in msg:
|
||||||
|
return None
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def fetchone(self):
|
||||||
|
return self._cursor.fetchone()
|
||||||
|
|
||||||
|
def fetchall(self):
|
||||||
|
return self._cursor.fetchall()
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self._cursor, name)
|
||||||
|
|
||||||
|
class ConnectionWrapper:
|
||||||
|
def __init__(self, conn, is_sqlite=False):
|
||||||
|
self._conn = conn
|
||||||
|
self._is_sqlite = is_sqlite
|
||||||
|
|
||||||
|
def cursor(self):
|
||||||
|
return CursorWrapper(self._conn.cursor(), is_sqlite=self._is_sqlite)
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
return self._conn.commit()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
return self._conn.close()
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self._conn, name)
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
if DB_HOST:
|
||||||
|
try:
|
||||||
|
import psycopg2
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
host=DB_HOST,
|
||||||
|
port=DB_PORT,
|
||||||
|
dbname=DB_NAME,
|
||||||
|
user=DB_USER,
|
||||||
|
password=DB_PASSWORD,
|
||||||
|
)
|
||||||
|
return ConnectionWrapper(conn, is_sqlite=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Postgres connection failed: {e}. Falling back to SQLite.")
|
||||||
|
|
||||||
|
# Fallback to SQLite
|
||||||
|
os.makedirs(os.path.dirname(SQLITE_PATH), exist_ok=True)
|
||||||
|
conn = sqlite3.connect(SQLITE_PATH)
|
||||||
|
return ConnectionWrapper(conn, is_sqlite=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import psycopg2
|
||||||
|
IntegrityError = psycopg2.IntegrityError
|
||||||
|
except ImportError:
|
||||||
|
IntegrityError = sqlite3.IntegrityError
|
||||||
106
elementor/routes.py
Normal file
106
elementor/routes.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
from flask import Blueprint, render_template, session, request, jsonify
|
||||||
|
import json
|
||||||
|
from db import get_db
|
||||||
|
from utils.theme_engine import get_theme_config
|
||||||
|
|
||||||
|
|
||||||
|
elementor_bp = Blueprint(
|
||||||
|
'elementor',
|
||||||
|
__name__,
|
||||||
|
template_folder='templates',
|
||||||
|
static_folder='static',
|
||||||
|
static_url_path='/elementor/static'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_builder(site_id, builder_mode='default', **_kwargs):
|
||||||
|
conn = get_db()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('SELECT user_id, slug, theme, content_json FROM sites WHERE id = ?', (site_id,))
|
||||||
|
site = c.fetchone()
|
||||||
|
|
||||||
|
if not site:
|
||||||
|
conn.close()
|
||||||
|
return 'Sitio no encontrado', 404
|
||||||
|
|
||||||
|
if 'user_id' in session and site[0] != session['user_id']:
|
||||||
|
conn.close()
|
||||||
|
return 'No autorizado', 403
|
||||||
|
|
||||||
|
content = json.loads(site[3]) if site[3] else {}
|
||||||
|
if not isinstance(content, dict):
|
||||||
|
content = {}
|
||||||
|
|
||||||
|
theme = site[2]
|
||||||
|
theme_config = get_theme_config(theme)
|
||||||
|
|
||||||
|
c.execute('SELECT plan, rubro FROM users WHERE id = ?', (site[0],))
|
||||||
|
user_data = c.fetchone()
|
||||||
|
conn.close()
|
||||||
|
user_plan = user_data[0] if user_data else 'base'
|
||||||
|
user_rubro = user_data[1] if user_data else 'restaurante'
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'elementor_builder.html',
|
||||||
|
site_id=site_id,
|
||||||
|
slug=site[1],
|
||||||
|
theme=theme,
|
||||||
|
content=content,
|
||||||
|
theme_config=theme_config,
|
||||||
|
user_plan=user_plan,
|
||||||
|
rubro=user_rubro,
|
||||||
|
builder_mode=builder_mode
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@elementor_bp.route('/elementor/<int:site_id>')
|
||||||
|
def elementor_view(site_id):
|
||||||
|
return _render_builder(site_id, builder_mode='default')
|
||||||
|
|
||||||
|
|
||||||
|
@elementor_bp.route('/ub24/<int:site_id>')
|
||||||
|
def ub24_view(site_id):
|
||||||
|
return _render_builder(site_id, builder_mode='ub24')
|
||||||
|
|
||||||
|
|
||||||
|
@elementor_bp.route('/api/elementor/save', methods=['POST'])
|
||||||
|
def save_elementor():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
site_id = data.get('site_id')
|
||||||
|
content = data.get('content')
|
||||||
|
publish = bool(data.get('publish'))
|
||||||
|
|
||||||
|
if not site_id or not isinstance(content, dict):
|
||||||
|
return jsonify({'success': False, 'error': 'Payload invalido'}), 400
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('SELECT user_id, content_json FROM sites WHERE id = ?', (site_id,))
|
||||||
|
row = c.fetchone()
|
||||||
|
if not row:
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'success': False, 'error': 'Sitio no encontrado'}), 404
|
||||||
|
|
||||||
|
owner_id = row[0]
|
||||||
|
if 'user_id' in session and session['user_id'] != owner_id:
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'success': False, 'error': 'No autorizado'}), 403
|
||||||
|
|
||||||
|
current_content = {}
|
||||||
|
try:
|
||||||
|
if row[1]:
|
||||||
|
current_content = json.loads(row[1]) or {}
|
||||||
|
except Exception:
|
||||||
|
current_content = {}
|
||||||
|
|
||||||
|
merged = dict(current_content)
|
||||||
|
merged.update(content)
|
||||||
|
|
||||||
|
if publish:
|
||||||
|
c.execute('UPDATE sites SET content_json = ?, status = ? WHERE id = ?', (json.dumps(merged), 'published', site_id))
|
||||||
|
else:
|
||||||
|
c.execute('UPDATE sites SET content_json = ? WHERE id = ?', (json.dumps(merged), site_id))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'success': True, 'published': publish})
|
||||||
@@ -17,14 +17,18 @@
|
|||||||
.dot{width:10px;height:10px;border-radius:50%;background:linear-gradient(135deg,#59d9c8,#7aa7ff)}
|
.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}
|
.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}
|
.block-item{background:var(--panel2);border:1px solid var(--border);padding:8px 10px;border-radius:10px;margin-bottom:8px;cursor:grab}
|
||||||
.main{padding:18px}
|
.main{padding:10px}
|
||||||
.topbar{display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap}
|
.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{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: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.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.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)}
|
.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:14px;width:100%;max-width:none;margin:0 auto}
|
.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 .preview-shell{background:#eef1f6;border-color:#e5e7eb}
|
||||||
body.ub24 .apple-bar{display:none}
|
body.ub24 .apple-bar{display:none}
|
||||||
body.ub24 .preview-shell{padding:0;border-radius:12px}
|
body.ub24 .preview-shell{padding:0;border-radius:12px}
|
||||||
@@ -45,7 +49,7 @@
|
|||||||
.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-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%}
|
.apple-dot{width:9px;height:9px;border-radius:50%}
|
||||||
.red{background:#f87171}.yellow{background:#fbbf24}.green{background:#4ade80}
|
.red{background:#f87171}.yellow{background:#fbbf24}.green{background:#4ade80}
|
||||||
.canvas{min-height:700px;padding:34px;background:#f6f7fb;transition:background .6s ease,color .4s ease; color:var(--site-text,#0b0c10)}
|
.canvas{min-height:700px;padding:18px;background:#f6f7fb;transition:background .6s ease,color .4s ease; color:var(--site-text,#0b0c10)}
|
||||||
.block{background:var(--site-card,#fff);border-radius:var(--radius-md);padding:var(--space-3);margin-bottom:var(--space-4);border:1px solid transparent;box-shadow:0 12px 30px rgba(15,23,42,.08);transition:transform .25s ease,box-shadow .25s ease,border-color .25s ease;animation:fadeUp .25s ease;color:var(--site-text,#0b0c10);position:relative;will-change:transform;cursor:grab}
|
.block{background:var(--site-card,#fff);border-radius:var(--radius-md);padding:var(--space-3);margin-bottom:var(--space-4);border:1px solid transparent;box-shadow:0 12px 30px rgba(15,23,42,.08);transition:transform .25s ease,box-shadow .25s ease,border-color .25s ease;animation:fadeUp .25s ease;color:var(--site-text,#0b0c10);position:relative;will-change:transform;cursor:grab}
|
||||||
.block.dragging,.block.resizing{transition:none;cursor:grabbing}
|
.block.dragging,.block.resizing{transition:none;cursor:grabbing}
|
||||||
body.dragging{user-select:none}
|
body.dragging{user-select:none}
|
||||||
@@ -159,6 +163,28 @@
|
|||||||
.preview-mode .block-actions,
|
.preview-mode .block-actions,
|
||||||
.preview-mode .resize-handle,
|
.preview-mode .resize-handle,
|
||||||
.preview-mode .scroll-btn{display:none !important}
|
.preview-mode .scroll-btn{display:none !important}
|
||||||
|
body.preview-mode .app{grid-template-columns:1fr !important}
|
||||||
|
body.preview-mode .sidebar,
|
||||||
|
body.preview-mode .inspector,
|
||||||
|
body.preview-mode .topbar{display:none !important}
|
||||||
|
body.preview-mode .main{padding:0 !important}
|
||||||
|
body.preview-mode .preview-shell{
|
||||||
|
max-width:100% !important;
|
||||||
|
border:0 !important;
|
||||||
|
border-radius:0 !important;
|
||||||
|
padding:0 !important;
|
||||||
|
margin:0 !important;
|
||||||
|
background:transparent !important;
|
||||||
|
}
|
||||||
|
body.preview-mode .apple{
|
||||||
|
border:0 !important;
|
||||||
|
border-radius:0 !important;
|
||||||
|
min-height:100vh;
|
||||||
|
}
|
||||||
|
body.preview-mode .canvas{
|
||||||
|
min-height:100vh !important;
|
||||||
|
padding:0 !important;
|
||||||
|
}
|
||||||
.resize-handle{position:absolute;right:8px;bottom:8px;width:14px;height:14px;border-right:2px solid #94a3b8;border-bottom:2px solid #94a3b8;cursor:se-resize;opacity:.75}
|
.resize-handle{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.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.e{top:50%;right:-4px;transform:translateY(-50%);cursor:e-resize}
|
||||||
@@ -246,13 +272,15 @@
|
|||||||
</select>
|
</select>
|
||||||
<button class="btn secondary" id="btnBack">Atras</button>
|
<button class="btn secondary" id="btnBack">Atras</button>
|
||||||
<button class="btn secondary" id="btnPreview">Vista previa</button>
|
<button class="btn secondary" id="btnPreview">Vista previa</button>
|
||||||
|
<button class="btn secondary" id="btnFullPage">Completo</button>
|
||||||
<button class="btn secondary" id="btnTheme">Claro</button>
|
<button class="btn secondary" 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="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" id="btnSizeTablet" title="Tablet"><i class="fa-solid fa-tablet-screen-button"></i></button>
|
||||||
<button class="btn secondary icon active" id="btnSizeDesktop" title="Web"><i class="fa-solid fa-display"></i></button>
|
<button class="btn secondary icon active" id="btnSizeDesktop" title="Web"><i class="fa-solid fa-display"></i></button>
|
||||||
<button class="btn secondary" id="btnFreeDrag" style="display:none">Modo libre</button>
|
<button class="btn secondary" id="btnFreeDrag">Modo libre</button>
|
||||||
<button class="btn secondary" id="btnAlign">Alinear</button>
|
<button class="btn secondary" id="btnAlign">Alinear</button>
|
||||||
<button class="btn" id="btnSave">Publicar</button>
|
<button class="btn" id="btnSave">Publicar</button>
|
||||||
|
<div class="save-status" id="saveStatus">Listo</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-shell">
|
<div class="preview-shell">
|
||||||
@@ -349,7 +377,6 @@
|
|||||||
<details class="acc">
|
<details class="acc">
|
||||||
<summary>Layout</summary>
|
<summary>Layout</summary>
|
||||||
<div class="acc-body">
|
<div class="acc-body">
|
||||||
<div class="row"><label>Layout 2 columnas</label><input id="twoColToggle" type="checkbox"></div>
|
|
||||||
<div class="row"><label>Animaciones</label><input id="animToggle" type="checkbox"></div>
|
<div class="row"><label>Animaciones</label><input id="animToggle" type="checkbox"></div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@@ -361,6 +388,7 @@
|
|||||||
const SITE_SLUG = "{{ slug }}";
|
const SITE_SLUG = "{{ slug }}";
|
||||||
const SERVER_CONTENT = {{ content|tojson }};
|
const SERVER_CONTENT = {{ content|tojson }};
|
||||||
const BUILDER_MODE = "{{ builder_mode or 'default' }}";
|
const BUILDER_MODE = "{{ builder_mode or 'default' }}";
|
||||||
|
const FULL_PAGE_MODE = new URLSearchParams(window.location.search).get("full") === "1";
|
||||||
const defaultSettings = {
|
const defaultSettings = {
|
||||||
site_name: "{{ slug }}",
|
site_name: "{{ slug }}",
|
||||||
primary_color: "#59d9c8",
|
primary_color: "#59d9c8",
|
||||||
@@ -369,10 +397,9 @@
|
|||||||
muted_color: "#6b7280",
|
muted_color: "#6b7280",
|
||||||
font_body: "Manrope",
|
font_body: "Manrope",
|
||||||
font_heading: "Manrope",
|
font_heading: "Manrope",
|
||||||
free_drag: true,
|
free_drag: false,
|
||||||
bg_color2: "#e9eef5",
|
bg_color2: "#e9eef5",
|
||||||
bg_gradient: false,
|
bg_gradient: false,
|
||||||
two_col: true,
|
|
||||||
animations: true,
|
animations: true,
|
||||||
theme: "light",
|
theme: "light",
|
||||||
bg_motion: "none",
|
bg_motion: "none",
|
||||||
@@ -447,6 +474,7 @@ const state = {
|
|||||||
let pendingMove = null;
|
let pendingMove = null;
|
||||||
let pendingResize = null;
|
let pendingResize = null;
|
||||||
let previewStateBefore = null;
|
let previewStateBefore = null;
|
||||||
|
let isSaving = false;
|
||||||
|
|
||||||
function makeId(){ return "block_" + Date.now() + "_" + Math.floor(Math.random()*1000); }
|
function makeId(){ return "block_" + Date.now() + "_" + Math.floor(Math.random()*1000); }
|
||||||
function getDefaultPos(){
|
function getDefaultPos(){
|
||||||
@@ -457,7 +485,7 @@ const state = {
|
|||||||
}
|
}
|
||||||
function defaultData(type){
|
function defaultData(type){
|
||||||
switch(type){
|
switch(type){
|
||||||
case "menu": return { title:"Menu", items:[], menu_mode:"both" };
|
case "menu": return { title:"Menu", items:[], menu_mode:"both", full_width:true };
|
||||||
case "hero": return { title:"Tu propuesta de valor", subtitle:"Explica en una frase por que elegirte.", button_text:"Contactar", button_url:"#contacto", image_url:"" };
|
case "hero": return { title:"Tu propuesta de valor", subtitle:"Explica en una frase por que elegirte.", button_text:"Contactar", button_url:"#contacto", image_url:"" };
|
||||||
case "text": return { text:"Describe tu negocio aqui." };
|
case "text": return { text:"Describe tu negocio aqui." };
|
||||||
case "image": return { url:"", alt:"", caption:"", fit:"cover", overlay_text:"" };
|
case "image": return { url:"", alt:"", caption:"", fit:"cover", overlay_text:"" };
|
||||||
@@ -475,7 +503,15 @@ const state = {
|
|||||||
default: return {};
|
default: return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function escapeHtml(str){return String(str||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/\"/g,""").replace(/'/g,"'");}
|
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){
|
function editable(tag, field, text, placeholder, multiline, style){
|
||||||
const ph = placeholder ? ` data-placeholder="${escapeHtml(placeholder)}"` : ` data-placeholder=""`;
|
const ph = placeholder ? ` data-placeholder="${escapeHtml(placeholder)}"` : ` data-placeholder=""`;
|
||||||
const ml = multiline ? ` data-multiline="true"` : "";
|
const ml = multiline ? ` data-multiline="true"` : "";
|
||||||
@@ -922,10 +958,11 @@ const state = {
|
|||||||
const inner = document.createElement("div");
|
const inner = document.createElement("div");
|
||||||
inner.style.position = "relative";
|
inner.style.position = "relative";
|
||||||
inner.style.zIndex = "1";
|
inner.style.zIndex = "1";
|
||||||
if (!state.settings.free_drag && state.settings.two_col){
|
if (!state.settings.free_drag){
|
||||||
inner.style.display = "grid";
|
inner.style.display = "flex";
|
||||||
inner.style.gridTemplateColumns = "1fr 1fr";
|
inner.style.flexWrap = "wrap";
|
||||||
inner.style.gap = "16px";
|
inner.style.gap = "16px";
|
||||||
|
inner.style.width = "100%";
|
||||||
}
|
}
|
||||||
canvas.appendChild(inner);
|
canvas.appendChild(inner);
|
||||||
if (!state.blocks.length){ inner.innerHTML = '<div class="empty">Arrastra bloques para empezar.</div>'; return; }
|
if (!state.blocks.length){ inner.innerHTML = '<div class="empty">Arrastra bloques para empezar.</div>'; return; }
|
||||||
@@ -945,7 +982,7 @@ const state = {
|
|||||||
el.style.left = "0px";
|
el.style.left = "0px";
|
||||||
el.style.top = "0px";
|
el.style.top = "0px";
|
||||||
el.style.transform = `translate3d(${pos.x}px, ${pos.y}px, 0)`;
|
el.style.transform = `translate3d(${pos.x}px, ${pos.y}px, 0)`;
|
||||||
const defaultWidth = block.type === "menu" ? 90 : (state.settings.two_col ? 48 : 70);
|
const defaultWidth = block.type === "menu" ? 90 : 70;
|
||||||
const w = Math.max(30, Math.min(100, Number(block.data?.width || defaultWidth)));
|
const w = Math.max(30, Math.min(100, Number(block.data?.width || defaultWidth)));
|
||||||
el.style.width = `${w}%`;
|
el.style.width = `${w}%`;
|
||||||
const h = Number(block.data?.height || 0);
|
const h = Number(block.data?.height || 0);
|
||||||
@@ -953,15 +990,14 @@ const state = {
|
|||||||
el.style.cursor = "grab";
|
el.style.cursor = "grab";
|
||||||
} else {
|
} else {
|
||||||
el.style.transform = "";
|
el.style.transform = "";
|
||||||
if (state.settings.two_col){
|
const isFullWidth = block.type === "menu" || !!block.data?.full_width;
|
||||||
const fullTypes = ["menu","hero","gallery","contact","video","map","calendar"];
|
if (isFullWidth){
|
||||||
if (fullTypes.includes(block.type)){ el.style.gridColumn = "1 / -1"; }
|
|
||||||
const w = Math.max(30, Math.min(100, Number(block.data?.width || 100)));
|
const w = Math.max(30, Math.min(100, Number(block.data?.width || 100)));
|
||||||
el.style.width = `${w}%`;
|
el.style.width = `${w}%`;
|
||||||
el.style.justifySelf = "start";
|
el.style.flex = "0 0 100%";
|
||||||
} else {
|
} else {
|
||||||
const w = Math.max(30, Math.min(100, Number(block.data?.width || 100)));
|
el.style.width = "calc(50% - 8px)";
|
||||||
el.style.width = `${w}%`;
|
el.style.flex = "0 0 calc(50% - 8px)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
el.innerHTML = renderBlockHtml(block);
|
el.innerHTML = renderBlockHtml(block);
|
||||||
@@ -1053,6 +1089,13 @@ const state = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeDrop(){ if (dropIndicator && dropIndicator.parentNode) dropIndicator.parentNode.removeChild(dropIndicator); dropIndicator=null; }
|
function removeDrop(){ if (dropIndicator && dropIndicator.parentNode) dropIndicator.parentNode.removeChild(dropIndicator); dropIndicator=null; }
|
||||||
|
function resolveFullWidthByDropX(container, clientX){
|
||||||
|
if (state.settings.free_drag) return null;
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
if (!rect || !rect.width) return null;
|
||||||
|
const ratio = (clientX - rect.left) / rect.width;
|
||||||
|
return ratio > 0.33 && ratio < 0.67;
|
||||||
|
}
|
||||||
function getDropIndex(container,y){
|
function getDropIndex(container,y){
|
||||||
const blocks=[...container.querySelectorAll(".block")];
|
const blocks=[...container.querySelectorAll(".block")];
|
||||||
for (let i=0;i<blocks.length;i++){ const r=blocks[i].getBoundingClientRect(); if (y < r.top + r.height/2) return i; }
|
for (let i=0;i<blocks.length;i++){ const r=blocks[i].getBoundingClientRect(); if (y < r.top + r.height/2) return i; }
|
||||||
@@ -1067,15 +1110,22 @@ const state = {
|
|||||||
}
|
}
|
||||||
function addBlock(type,index=state.blocks.length){
|
function addBlock(type,index=state.blocks.length){
|
||||||
const b={ id: makeId(), type, data: defaultData(type) };
|
const b={ id: makeId(), type, data: defaultData(type) };
|
||||||
|
if (!state.settings.free_drag && type !== "menu"){
|
||||||
|
b.data.full_width = false;
|
||||||
|
}
|
||||||
if (BUILDER_MODE === "ub24"){ b.page = currentPage; }
|
if (BUILDER_MODE === "ub24"){ b.page = currentPage; }
|
||||||
if (state.settings.free_drag){ b.pos = getDefaultPos(); }
|
if (state.settings.free_drag){ b.pos = getDefaultPos(); }
|
||||||
state.blocks.splice(index,0,b); selectedBlockId=b.id; renderInspector(); renderPreview();
|
state.blocks.splice(index,0,b); selectedBlockId=b.id; renderInspector(); renderPreview();
|
||||||
}
|
}
|
||||||
function moveBlock(id,toIndex){
|
function moveBlock(id,toIndex,opts={}){
|
||||||
const from=state.blocks.findIndex(b=>b.id===id);
|
const from=state.blocks.findIndex(b=>b.id===id);
|
||||||
if (from<0) return;
|
if (from<0) return;
|
||||||
const [b]=state.blocks.splice(from,1);
|
const [b]=state.blocks.splice(from,1);
|
||||||
state.blocks.splice(toIndex,0,b);
|
state.blocks.splice(toIndex,0,b);
|
||||||
|
if (opts && typeof opts.fullWidth === "boolean" && b.type !== "menu"){
|
||||||
|
b.data = b.data || {};
|
||||||
|
b.data.full_width = opts.fullWidth;
|
||||||
|
}
|
||||||
renderPreview();
|
renderPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1088,6 +1138,9 @@ const state = {
|
|||||||
const input=(label,id,val)=>`<div class="row"><label>${label}</label><input id="${id}" type="text" value="${escapeHtml(val||"")}"></div>`;
|
const input=(label,id,val)=>`<div class="row"><label>${label}</label><input id="${id}" type="text" value="${escapeHtml(val||"")}"></div>`;
|
||||||
const widthVal = Math.max(30, Math.min(100, Number(data.width || 100)));
|
const widthVal = Math.max(30, Math.min(100, Number(data.width || 100)));
|
||||||
html += `<div class="row"><label>Ancho bloque (%)</label><input id="blockWidth" type="range" min="30" max="100" value="${widthVal}"><div id="blockWidthValue" style="margin-top:4px;color:var(--muted);font-size:12px">${widthVal}%</div></div>`;
|
html += `<div class="row"><label>Ancho bloque (%)</label><input id="blockWidth" type="range" min="30" max="100" value="${widthVal}"><div id="blockWidthValue" style="margin-top:4px;color:var(--muted);font-size:12px">${widthVal}%</div></div>`;
|
||||||
|
if (!state.settings.free_drag && block.type !== "menu"){
|
||||||
|
html += `<div class="row"><label>Ancho completo</label><input id="blockFullWidth" type="checkbox" ${data.full_width ? "checked" : ""}></div>`;
|
||||||
|
}
|
||||||
if (block.type==="menu"){
|
if (block.type==="menu"){
|
||||||
html+=input("Titulo","menuTitle",data.title);
|
html+=input("Titulo","menuTitle",data.title);
|
||||||
const mm = escapeHtml(data.menu_mode || "both");
|
const mm = escapeHtml(data.menu_mode || "both");
|
||||||
@@ -1212,6 +1265,10 @@ const state = {
|
|||||||
if (widthEl){
|
if (widthEl){
|
||||||
block.data.width = Math.max(30, Math.min(100, Number(widthEl.value || 100)));
|
block.data.width = Math.max(30, Math.min(100, Number(widthEl.value || 100)));
|
||||||
}
|
}
|
||||||
|
const fullWidthEl = document.getElementById("blockFullWidth");
|
||||||
|
if (fullWidthEl && block.type !== "menu"){
|
||||||
|
block.data.full_width = !!fullWidthEl.checked;
|
||||||
|
}
|
||||||
if (block.type==="menu"){
|
if (block.type==="menu"){
|
||||||
block.data.title=document.getElementById("menuTitle").value;
|
block.data.title=document.getElementById("menuTitle").value;
|
||||||
const mm = document.getElementById("menuMode");
|
const mm = document.getElementById("menuMode");
|
||||||
@@ -1311,12 +1368,19 @@ const state = {
|
|||||||
if (id && !state.settings.free_drag){
|
if (id && !state.settings.free_drag){
|
||||||
const container=canvas.querySelector("div")||canvas;
|
const container=canvas.querySelector("div")||canvas;
|
||||||
const index=getDropIndex(container,e.clientY);
|
const index=getDropIndex(container,e.clientY);
|
||||||
moveBlock(id,index);
|
const fullWidth = resolveFullWidthByDropX(container, e.clientX);
|
||||||
|
moveBlock(id,index,{ fullWidth });
|
||||||
} else if (type){
|
} else if (type){
|
||||||
const b={ id: makeId(), type, data: defaultData(type) };
|
const b={ id: makeId(), type, data: defaultData(type) };
|
||||||
if (state.settings.free_drag){
|
if (state.settings.free_drag){
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
b.pos = { x: Math.max(0, e.clientX - rect.left - 20), y: Math.max(0, e.clientY - rect.top - 20) };
|
b.pos = { x: Math.max(0, e.clientX - rect.left - 20), y: Math.max(0, e.clientY - rect.top - 20) };
|
||||||
|
} else {
|
||||||
|
const container=canvas.querySelector("div")||canvas;
|
||||||
|
const fullWidth = resolveFullWidthByDropX(container, e.clientX);
|
||||||
|
if (typeof fullWidth === "boolean" && type !== "menu"){
|
||||||
|
b.data.full_width = fullWidth;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
state.blocks.push(b);
|
state.blocks.push(b);
|
||||||
selectedBlockId=b.id;
|
selectedBlockId=b.id;
|
||||||
@@ -1530,7 +1594,6 @@ const state = {
|
|||||||
const bgColor=document.getElementById("bgColorInput");
|
const bgColor=document.getElementById("bgColorInput");
|
||||||
const bgColor2=document.getElementById("bgColor2Input");
|
const bgColor2=document.getElementById("bgColor2Input");
|
||||||
const bgGradient=document.getElementById("bgGradientToggle");
|
const bgGradient=document.getElementById("bgGradientToggle");
|
||||||
const twoCol=document.getElementById("twoColToggle");
|
|
||||||
const textColor=document.getElementById("textColorInput");
|
const textColor=document.getElementById("textColorInput");
|
||||||
const mutedColor=document.getElementById("mutedColorInput");
|
const mutedColor=document.getElementById("mutedColorInput");
|
||||||
const fontBody=document.getElementById("fontBodySelect");
|
const fontBody=document.getElementById("fontBodySelect");
|
||||||
@@ -1546,7 +1609,6 @@ const state = {
|
|||||||
siteName.value=s.site_name||""; primary.value=s.primary_color||"#59d9c8";
|
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;
|
bgColor.value=s.bg_color||"#f6f7fb"; bgColor2.value=s.bg_color2||"#e9eef5"; bgGradient.checked=!!s.bg_gradient;
|
||||||
bgMotion.value=s.bg_motion||"none";
|
bgMotion.value=s.bg_motion||"none";
|
||||||
twoCol.checked=!!s.two_col;
|
|
||||||
textColor.value=s.text_color||"#0b0c10"; mutedColor.value=s.muted_color||"#6b7280";
|
textColor.value=s.text_color||"#0b0c10"; mutedColor.value=s.muted_color||"#6b7280";
|
||||||
fontBody.value=s.font_body||"Manrope"; fontHeading.value=s.font_heading||"Manrope";
|
fontBody.value=s.font_body||"Manrope"; fontHeading.value=s.font_heading||"Manrope";
|
||||||
logoDrop.textContent = s.logo_url ? "Logo cargado" : "Suelta imagen o click";
|
logoDrop.textContent = s.logo_url ? "Logo cargado" : "Suelta imagen o click";
|
||||||
@@ -1560,7 +1622,6 @@ const state = {
|
|||||||
bgColor2.addEventListener("input",()=>{ s.bg_color2=bgColor2.value; renderPreview(); });
|
bgColor2.addEventListener("input",()=>{ s.bg_color2=bgColor2.value; renderPreview(); });
|
||||||
bgGradient.addEventListener("change",()=>{ s.bg_gradient=bgGradient.checked; renderPreview(); });
|
bgGradient.addEventListener("change",()=>{ s.bg_gradient=bgGradient.checked; renderPreview(); });
|
||||||
bgMotion.addEventListener("change",()=>{ s.bg_motion=bgMotion.value; renderPreview(); });
|
bgMotion.addEventListener("change",()=>{ s.bg_motion=bgMotion.value; renderPreview(); });
|
||||||
twoCol.addEventListener("change",()=>{ s.two_col=twoCol.checked; renderPreview(); });
|
|
||||||
textColor.addEventListener("input",()=>{ s.text_color=textColor.value; renderPreview(); });
|
textColor.addEventListener("input",()=>{ s.text_color=textColor.value; renderPreview(); });
|
||||||
mutedColor.addEventListener("input",()=>{ s.muted_color=mutedColor.value; renderPreview(); });
|
mutedColor.addEventListener("input",()=>{ s.muted_color=mutedColor.value; renderPreview(); });
|
||||||
fontBody.addEventListener("change",()=>{ s.font_body=fontBody.value; renderPreview(); });
|
fontBody.addEventListener("change",()=>{ s.font_body=fontBody.value; renderPreview(); });
|
||||||
@@ -1572,7 +1633,25 @@ const state = {
|
|||||||
bindDrop(logoDrop, logoFile, (data)=>{ s.logo_url = data; }, "Logo cargado");
|
bindDrop(logoDrop, logoFile, (data)=>{ s.logo_url = data; }, "Logo cargado");
|
||||||
bindDrop(bgDrop, bgFile, (data)=>{ s.bg_image_url = data; }, "Fondo cargado");
|
bindDrop(bgDrop, bgFile, (data)=>{ s.bg_image_url = data; }, "Fondo cargado");
|
||||||
}
|
}
|
||||||
function wireFreeDragToggle(){ return; }
|
function wireFreeDragToggle(){
|
||||||
|
const btn = document.getElementById("btnFreeDrag");
|
||||||
|
if (!btn) return;
|
||||||
|
const applyLabel = ()=>{
|
||||||
|
btn.textContent = state.settings.free_drag ? "Modo libre: ON" : "Modo libre";
|
||||||
|
btn.classList.toggle("active", !!state.settings.free_drag);
|
||||||
|
};
|
||||||
|
btn.addEventListener("click", ()=>{
|
||||||
|
state.settings.free_drag = !state.settings.free_drag;
|
||||||
|
if (state.settings.free_drag){
|
||||||
|
state.blocks.forEach((b)=>{
|
||||||
|
if (!b.pos) b.pos = getDefaultPos();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
applyLabel();
|
||||||
|
renderPreview();
|
||||||
|
});
|
||||||
|
applyLabel();
|
||||||
|
}
|
||||||
function wirePreviewSize(){
|
function wirePreviewSize(){
|
||||||
const shell=document.querySelector(".preview-shell");
|
const shell=document.querySelector(".preview-shell");
|
||||||
if (!shell) return;
|
if (!shell) return;
|
||||||
@@ -1657,19 +1736,66 @@ const state = {
|
|||||||
});
|
});
|
||||||
renderPreview();
|
renderPreview();
|
||||||
}
|
}
|
||||||
|
function setSaveStatus(msg, kind=""){
|
||||||
|
const status = document.getElementById("saveStatus");
|
||||||
|
if (!status) return;
|
||||||
|
status.textContent = msg;
|
||||||
|
status.className = `save-status${kind ? ` ${kind}` : ""}`;
|
||||||
|
}
|
||||||
|
function normalizeLoadedBlocks(blocks){
|
||||||
|
if (!Array.isArray(blocks)) return [];
|
||||||
|
return blocks
|
||||||
|
.filter((b)=>b && typeof b === "object")
|
||||||
|
.map((b)=>({
|
||||||
|
...b,
|
||||||
|
id: b.id || makeId(),
|
||||||
|
data: (b.data && typeof b.data === "object") ? b.data : {}
|
||||||
|
}));
|
||||||
|
}
|
||||||
async function saveContent(){
|
async function saveContent(){
|
||||||
const payload={ site_id: SITE_ID, content: { ...SERVER_CONTENT, settings: state.settings, blocks: state.blocks } };
|
if (isSaving) return;
|
||||||
try{ const res=await fetch("/api/customizer/save",{ method:"POST", headers:{ "Content-Type":"application/json" }, body: JSON.stringify(payload) }); const data=await res.json(); if (!data.success) throw new Error("save failed"); alert("Cambios guardados"); }
|
isSaving = true;
|
||||||
catch(err){ console.error(err); alert("No se pudo guardar"); }
|
const btn = document.getElementById("btnSave");
|
||||||
|
if (btn){
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = "Publicando...";
|
||||||
|
}
|
||||||
|
setSaveStatus("Guardando cambios...", "busy");
|
||||||
|
const payload = {
|
||||||
|
site_id: SITE_ID,
|
||||||
|
publish: true,
|
||||||
|
content: { ...SERVER_CONTENT, settings: state.settings, blocks: state.blocks }
|
||||||
|
};
|
||||||
|
try{
|
||||||
|
const res = await fetch("/api/elementor/save",{
|
||||||
|
method:"POST",
|
||||||
|
headers:{ "Content-Type":"application/json" },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok || !data.success) throw new Error(data.error || "save failed");
|
||||||
|
setSaveStatus("Publicado", "ok");
|
||||||
|
} catch(err){
|
||||||
|
console.error(err);
|
||||||
|
setSaveStatus("Error al publicar", "error");
|
||||||
|
} finally {
|
||||||
|
isSaving = false;
|
||||||
|
if (btn){
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = "Publicar";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function init(){
|
function init(){
|
||||||
|
// By default we keep section flow layout for stable full-page composition.
|
||||||
|
state.settings.free_drag = false;
|
||||||
|
state.blocks = normalizeLoadedBlocks(state.blocks);
|
||||||
if (BUILDER_MODE === "ub24"){
|
if (BUILDER_MODE === "ub24"){
|
||||||
state.blocks.forEach(b=>{ if (!b.page) b.page = "home"; });
|
state.blocks.forEach(b=>{ if (!b.page) b.page = "home"; });
|
||||||
}
|
}
|
||||||
if (BUILDER_MODE === "ub24"){
|
if (BUILDER_MODE === "ub24"){
|
||||||
state.settings.free_drag = false;
|
state.settings.free_drag = false;
|
||||||
}
|
}
|
||||||
state.blocks = [];
|
|
||||||
selectedBlockId = null;
|
selectedBlockId = null;
|
||||||
wireSidebar(); wirePreviewDrop(); wireInlineEditing(); wireSettings(); wireFreeDragToggle(); wireJumpSelect(); wirePreviewSize(); wireThemeToggle();
|
wireSidebar(); wirePreviewDrop(); wireInlineEditing(); wireSettings(); wireFreeDragToggle(); wireJumpSelect(); wirePreviewSize(); wireThemeToggle();
|
||||||
const backBtn = document.getElementById("btnBack");
|
const backBtn = document.getElementById("btnBack");
|
||||||
@@ -1694,11 +1820,11 @@ const state = {
|
|||||||
}
|
}
|
||||||
if (sidebar) sidebar.style.display = isPreview ? "none" : "";
|
if (sidebar) sidebar.style.display = isPreview ? "none" : "";
|
||||||
if (inspector) inspector.style.display = isPreview ? "none" : "";
|
if (inspector) inspector.style.display = isPreview ? "none" : "";
|
||||||
if (main) main.style.padding = isPreview ? "8px" : "";
|
if (main) main.style.padding = isPreview ? "0" : "";
|
||||||
if (shell){
|
if (shell){
|
||||||
if (isPreview){
|
if (isPreview){
|
||||||
shell.style.maxWidth = "100%";
|
shell.style.maxWidth = "100%";
|
||||||
shell.style.margin = "8px auto";
|
shell.style.margin = "0";
|
||||||
shell.classList.remove("size-phone","size-tablet","size-desktop");
|
shell.classList.remove("size-phone","size-tablet","size-desktop");
|
||||||
shell.classList.add("size-desktop");
|
shell.classList.add("size-desktop");
|
||||||
["btnSizePhone","btnSizeTablet","btnSizeDesktop"].forEach(id=>{
|
["btnSizePhone","btnSizeTablet","btnSizeDesktop"].forEach(id=>{
|
||||||
@@ -1721,6 +1847,25 @@ const state = {
|
|||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const fullPageBtn = document.getElementById("btnFullPage");
|
||||||
|
if (fullPageBtn){
|
||||||
|
fullPageBtn.addEventListener("click",()=>{
|
||||||
|
const url = `${window.location.pathname}?full=1`;
|
||||||
|
window.location.href = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (FULL_PAGE_MODE && !document.body.classList.contains("preview-mode")){
|
||||||
|
document.body.classList.add("preview-mode");
|
||||||
|
const shell = document.querySelector(".preview-shell");
|
||||||
|
const main = document.querySelector(".main");
|
||||||
|
if (main) main.style.padding = "0";
|
||||||
|
if (shell){
|
||||||
|
shell.style.maxWidth = "100%";
|
||||||
|
shell.style.margin = "0";
|
||||||
|
shell.classList.remove("size-phone","size-tablet","size-desktop");
|
||||||
|
shell.classList.add("size-desktop");
|
||||||
|
}
|
||||||
|
}
|
||||||
const pageSelect = document.getElementById("pageSelect");
|
const pageSelect = document.getElementById("pageSelect");
|
||||||
if (pageSelect){
|
if (pageSelect){
|
||||||
pageSelect.addEventListener("change",()=>{
|
pageSelect.addEventListener("change",()=>{
|
||||||
|
|||||||
@@ -1,133 +1,20 @@
|
|||||||
# 📊 Estado Actual del Proyecto - GKACHELE™
|
# Estado Actual del Proyecto - GKACHELE
|
||||||
|
|
||||||
**© 2025 GKACHELE™. Todos los derechos reservados.**
|
**Fecha de actualizacion:** 14 Febrero 2026
|
||||||
|
|
||||||
## 🎯 Hash Actual
|
## Aviso
|
||||||
|
Este archivo queda como referencia historica del ciclo 2025 (customizer inicial).
|
||||||
|
|
||||||
**Hash:** `gkachele-customizer-wordpress-adaptado-20250115-002`
|
## Fuente vigente de estado IA/UB24
|
||||||
**Fecha:** 15 Enero 2025
|
- `codex/VERSIONADO_IA.md`
|
||||||
**Estado:** ✅ **Demo WordPress completo creado - Listo para copiar y adaptar**
|
- `codex/MEMORIA_CODEX.md`
|
||||||
|
- `codex/BUILDER_ELEMENTOR_VERSION.md`
|
||||||
|
- `codex/HISTORIAL_CAMBIOS.md`
|
||||||
|
|
||||||
---
|
## Estado vigente resumido
|
||||||
|
- Rama activa: `ai/ub24-builder-v1`
|
||||||
|
- Builder UB24 en evolucion activa (ciclo 2026).
|
||||||
|
- Estado local/remoto verificado en sincronizacion al corte.
|
||||||
|
|
||||||
## ✅ FUNCIONANDO
|
## Nota operativa
|
||||||
|
No usar este archivo como fuente principal para decisiones de desarrollo actuales.
|
||||||
### Flujo Completo:
|
|
||||||
1. ✅ **Landing** - Funciona correctamente
|
|
||||||
2. ✅ **Registro** - Crea usuario + sitio automático
|
|
||||||
3. ✅ **Login** - Redirige DIRECTAMENTE a `/customizer/{site_id}`
|
|
||||||
4. ✅ **Customizer** - Personalización funcional
|
|
||||||
- Cambiar contenido
|
|
||||||
- Cambiar colores
|
|
||||||
- Cambiar tipografía
|
|
||||||
- **✅ Añadir bloques (video, imagen, texto, redes sociales, mapa)**
|
|
||||||
- **✅ Eliminar bloques**
|
|
||||||
- **✅ Ver lista de bloques añadidos**
|
|
||||||
- Guardar cambios (draft)
|
|
||||||
- Enviar solicitud (pending)
|
|
||||||
5. ✅ **Dashboard Cliente** - `/dashboard` - Ve sus sitios
|
|
||||||
6. ✅ **Dashboard Admin** - `/admin` - Gestión completa
|
|
||||||
- Ver usuarios
|
|
||||||
- Eliminar usuarios
|
|
||||||
- Ver solicitudes
|
|
||||||
- Aprobar/rechazar sitios
|
|
||||||
|
|
||||||
### Sistema de Base de Datos:
|
|
||||||
- ✅ Multi-tenant en `main.db`
|
|
||||||
- ✅ Tablas: users, sites, menus, widgets, content, settings, requests, media
|
|
||||||
- ✅ Sistema de roles basado en DB (administrator, editor, author, subscriber)
|
|
||||||
- ✅ Filtrado por `user_id` (cada cliente solo ve sus datos)
|
|
||||||
|
|
||||||
### Templates:
|
|
||||||
- ✅ Sistema modular (_gkachele/header.php, footer.php, sidebar.php)
|
|
||||||
- ✅ Menús dinámicos (header, footer, sidebar)
|
|
||||||
- ✅ Widgets dinámicos
|
|
||||||
- ✅ Temas: restaurante-moderno, restaurante-elegante, gimnasio-claro
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ PENDIENTE
|
|
||||||
|
|
||||||
### Funcionalidades Core:
|
|
||||||
- [x] **Añadir bloques (COMPLETADO)**
|
|
||||||
- [x] **Eliminar bloques (COMPLETADO)**
|
|
||||||
- [x] **Lista de bloques en sidebar (COMPLETADO)**
|
|
||||||
- [ ] Editar contenido de bloques existentes
|
|
||||||
- [ ] Mover bloques (drag & drop) en customizer
|
|
||||||
- [ ] Editar inline (contenteditable)
|
|
||||||
- [ ] UI completa para gestionar menús desde customizer
|
|
||||||
|
|
||||||
### Infraestructura:
|
|
||||||
- [ ] Subdominios automáticos
|
|
||||||
- [ ] Integración pagos Nominalia
|
|
||||||
- [ ] Gitea y workflows
|
|
||||||
- [ ] Control de versiones Git
|
|
||||||
|
|
||||||
### Mejoras:
|
|
||||||
- [ ] Landing optimizada para Argentina
|
|
||||||
- [ ] Limitaciones por plan
|
|
||||||
- [ ] Sistema de actividad/logs
|
|
||||||
- [ ] Mejorar UI/UX
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 PRÓXIMOS PASOS
|
|
||||||
|
|
||||||
1. **Probar con 1 rubro completo** (restaurante)
|
|
||||||
2. **Mejorar customizer** (mover bloques, editar inline)
|
|
||||||
3. **Gitea y workflows** (despliegues automáticos)
|
|
||||||
4. **Subdominios** (con tu dominio)
|
|
||||||
5. **Pagos Nominalia** (comodines)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📍 DEPLOYMENT
|
|
||||||
|
|
||||||
### Actual: Raspberry Pi 3
|
|
||||||
- ✅ Funcionando
|
|
||||||
- ✅ Login → Customizer funciona
|
|
||||||
- ✅ Todo operativo
|
|
||||||
|
|
||||||
### Futuro: VPS
|
|
||||||
- Si funciona 1 rubro bien → Migrar a VPS
|
|
||||||
- Mismo código
|
|
||||||
- Más recursos
|
|
||||||
- Dominio propio
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 ÚLTIMA ACTUALIZACIÓN
|
|
||||||
|
|
||||||
**Hash:** `gkachele-customizer-wordpress-adaptado-20250115-002`
|
|
||||||
**Fecha:** 15 Enero 2025
|
|
||||||
**Logro:** ✅ **Demo WordPress completo creado - Listo para copiar y adaptar**
|
|
||||||
|
|
||||||
### Cambios en este Hash:
|
|
||||||
- ✅ Creado demo completo WordPress: `customizer-wordpress-COMPLETO.html`
|
|
||||||
- ✅ Todas las funcionalidades de WordPress implementadas (paneles, toggles, sliders, repeaters, etc.)
|
|
||||||
- ✅ Documentación completa: `CUSTOMIZER_WORDPRESS_COMPLETO.md`
|
|
||||||
- ✅ Objetivo claro documentado: `OBJETIVO_CUSTOMIZER_ADAPTADO.md`
|
|
||||||
- ✅ Identificados problemas críticos: `PROBLEMAS_CRITICOS_CUSTOMIZER.md`
|
|
||||||
|
|
||||||
### Plan para Próxima Sesión:
|
|
||||||
1. **COPIAR WordPress** (demo completo)
|
|
||||||
2. **AUTOMATIZAR** procesos (guardado, preview, etc.)
|
|
||||||
3. **CAMBIAR branding** a GKACHELE™ (logo, nombres, colores)
|
|
||||||
4. **ADAPTAR** funcionalidades con lo que ya sabemos
|
|
||||||
5. **INTEGRAR** con backend Flask existente
|
|
||||||
|
|
||||||
### Archivos Creados:
|
|
||||||
- `customizer-wordpress-COMPLETO.html` - Demo completo independiente
|
|
||||||
- `CUSTOMIZER_WORDPRESS_COMPLETO.md` - Documentación completa
|
|
||||||
- `OBJETIVO_CUSTOMIZER_ADAPTADO.md` - Objetivo claro
|
|
||||||
- `PROBLEMAS_CRITICOS_CUSTOMIZER.md` - Problemas identificados
|
|
||||||
|
|
||||||
### Estado:
|
|
||||||
- ✅ Demo WordPress completo (funcional, independiente)
|
|
||||||
- ✅ Documentación completa
|
|
||||||
- ✅ Objetivo claro definido
|
|
||||||
- ⏳ Pendiente: Copiar y adaptar al customizer real
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**© 2025 GKACHELE™. Todos los derechos reservados.**
|
|
||||||
|
|||||||
Reference in New Issue
Block a user