From db90dfaca287ec0aad65bb560991155ecfb6967b Mon Sep 17 00:00:00 2001 From: Jordan Date: Mon, 23 Mar 2026 21:34:03 +0000 Subject: [PATCH] Ajustes del scaffold --- CLAUDE.md | 10 +- docs/builder-fields.md | 411 ++++++++++++++++----- docs/css-js-conventions.md | 180 +++++++-- docs/hooks-and-api.md | 417 +++++++++++++-------- docs/production-patterns.md | 262 +++++++++++++ docs/quick-reference.md | 85 +++++ docs/twig-filters.md | 175 ++++++--- docs/vue-builder-examples.md | 695 +++++++++++++++++++++++++++++++++++ docs/vue-builder-rules.md | 484 ++++++++++++++++++++++++ 9 files changed, 2400 insertions(+), 319 deletions(-) create mode 100644 docs/production-patterns.md create mode 100644 docs/quick-reference.md create mode 100644 docs/vue-builder-examples.md create mode 100644 docs/vue-builder-rules.md diff --git a/CLAUDE.md b/CLAUDE.md index 1af4e45..03ecfa9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,7 +105,11 @@ Do NOT modify web-base files — they are shared across all projects. ## Documentation - [docs/modular-system.md](docs/modular-system.md) — Modules, general sections, global variables -- [docs/builder-fields.md](docs/builder-fields.md) — Builder field types, c-form, c-if/c-for/c-class, data-field-type +- [docs/builder-fields.md](docs/builder-fields.md) — Builder field types, Acai attributes, c-form, components - [docs/twig-filters.md](docs/twig-filters.md) — Twig filters reference (get, hook, module, queryDB, etc.) -- [docs/hooks-and-api.md](docs/hooks-and-api.md) — PHP hooks, CmsApi, CocoDB, database operations -- [docs/css-js-conventions.md](docs/css-js-conventions.md) — CSS/JS patterns, Tailwind, BEM, Vue 3 integration +- [docs/hooks-and-api.md](docs/hooks-and-api.md) — PHP hooks, CmsApi, CocoDB, record creation +- [docs/css-js-conventions.md](docs/css-js-conventions.md) — CSS/JS/Vue 3, Tailwind, BEM, native components +- [docs/quick-reference.md](docs/quick-reference.md) — Cheat sheet: domain rules, field types, filters +- [docs/production-patterns.md](docs/production-patterns.md) — Real production patterns (header, zigzag, FAQ, forms) +- [docs/vue-builder-rules.md](docs/vue-builder-rules.md) — CMS-VUE rules (tabs, colorpicker, components) +- [docs/vue-builder-examples.md](docs/vue-builder-examples.md) — Vue builder examples (Banner Slideshow, etc.) diff --git a/docs/builder-fields.md b/docs/builder-fields.md index 1ebb724..bd65ed8 100644 --- a/docs/builder-fields.md +++ b/docs/builder-fields.md @@ -1,77 +1,196 @@ # Builder Fields & Acai Attributes +## Nombres de variables + +El atributo `data-field-label` se convierte a variable removiendo espacios y caracteres especiales (minúsculas). + +| Label | Variable | +|-------|----------| +| Categoría Noticia | `categoranoticia` | +| Color Principal | `colorprincipal` | +| Título Producto | `ttuloproducto` | + +--- + ## Field Types (`data-field-type`) -The builder uses `data-field-type` attributes on HTML elements to define editable areas. +| Type | Element | Returns | +|------|---------|---------| +| `textfield` | `

` | String | +| `headfield` | `

`-`

` | String + variable `_tag` con la etiqueta elegida | +| `textbox` | `
` | String multi-línea | +| `wysiwyg` | `
` | HTML string | +| `link` | `` | URL string (ya incluye barras) | +| `upload` | `` | **Array** de `{urlPath, info1, info2, info3, info4}` | +| `uploadMulti` | `
  • ` | Itera sobre archivos subidos | +| `list` (fijo) | `
    ` | Valor seleccionado | +| `list` (tabla) | `
    ` | `num` del registro | +| `multiv2` | `
  • ` wrapper | Array de objetos | -| Type | Description | Returns | -|------|-------------|---------| -| `textfield` | Single line text | String | -| `headfield` | Heading text (generates `_tag` variable for semantic tag: h1-h6) | String | -| `textbox` | Multi-line text | String | -| `wysiwyg` | Rich text editor (HTML output) | HTML string | -| `link` | URL field (already includes slashes) | String | -| `upload` | Single image/file | Array: `[0].urlPath`, `[0].info1` (alt), `[0].info2-4` | -| `uploadMulti` | Multiple images | Iterable: `item.urlPath` | -| `list` | Dropdown (fixed options or from table) | String or foreign key num | -| `multiv2` | Repeatable group of fields | Array of objects | - -### headfield Example +### textfield ```html -<{{ title_tag | default('h2') }} data-field-type="headfield" class="text-3xl font-bold"> - Section Title +

    + Elemento editable +

    +``` + +### headfield + +Genera 2 variables: la estándar y otra con sufijo `_tag` con la etiqueta elegida por el usuario. + +```html +<{{ title_tag | default('h2') }} data-field-type="headfield" data-field-label="Título Sección" class="text-3xl font-bold"> + Título de la sección ``` -### upload Example +### textbox ```html - -{{ image[0].info1 }} - - -
    - {{ photo.info1 }} +
    + Texto largo editable
    ``` -### list Example +### wysiwyg ```html - - - - - +
    +

    Texto con estilos editables

    +
    ``` +### link + +```html +
    + Haz clic aquí + +``` + +### upload + +```html +
    + +
    +``` + +Atributos disponibles: +- `data-lazy="true"`: Carga perezosa +- `data-field-width="1400"`: Ancho máximo sugerido +- `data-field-info1="titulo"`: Campo de información adicional (usado como alt) + +Acceso en Twig: `{{ imagen[0].urlPath }}`, `{{ imagen[0].info1 }}` + +### uploadMulti + +Itera sobre todas las imágenes subidas: + +```html +
  • +
    + {{ uploadMulti.info1 }} +
    +
  • +``` + +### list (opciones fijas) + +```html +
    +
    +``` + +Formato de opciones: `opcion1,opcion2,|opcion3,valor3|opcion4` + +### list (tabla) + +```html +
    + {{ record.titulo }} +
    +``` + +- `data-list-table`: Nombre de tabla sin prefijo `cms_` +- `data-list-value`: Campo a usar como valor (generalmente `num`) +- `data-list-label`: Campo a mostrar como label + +### multiv2 — Campos repetibles + +```html +
      +
    • +
      + Nombre del producto +
      +
      + Descripción del producto +
      +
      + +
      +
    • +
    +``` + +Uso en Twig — las variables son propiedades del objeto iterado: + +```twig +{% for record in productos %} +
    +

    {{ record.nombre }}

    +

    {{ record.descripcion }}

    + +
    +{% endfor %} +``` + +--- ## Acai Attributes -### `c-if` — Conditional Rendering +### `c-if` — Renderizado condicional ```html - -
    Banner content
    - - -
    Grid layout
    - - +
    {{ subtitle }}
    + + +
    Grid layout
    ``` ### `c-else` -Must immediately follow the `c-if` element: +Debe ir inmediatamente después del elemento `c-if`: ```html
    @@ -82,91 +201,173 @@ Must immediately follow the `c-if` element:
    ``` -### `c-for` — Iteration +### `c-for` — Iteración sobre array ```html -

    {{ item.title }}

    - - -
    -

    {{ product.nombre }}

    -
    ``` -Available inside loop: `loop.index` (1-based), `loop.index is odd`, `loop.index is even` - -### `c-class` — Dynamic CSS Classes +### `c-for` — Iteración sobre tabla de BD ```html -
    - Content -
    +
      +
    • + {{ producto.title }} +
    • +
    ``` -### `c-hidden` — Hidden Elements +Parámetros opcionales: `c-where` (condición SQL), `c-order` (orden), `c-limit` (límite). -Element is not rendered but can declare builder variables: +Equivalente en Twig: +```twig +{% for producto in 'productos' | get('visible=1','num desc',10) %} +
  • {{ producto.title }}
  • +{% endfor %} +``` + +Dentro del loop: `loop.index` (1-based), `loop.index is odd`, `loop.index is even` + +### `c-class` — Clases CSS condicionales + +```html + +
    + + +
    + + +
    + + +
    +``` + +### `c-hidden` — Elementos ocultos + +Elemento que no se renderiza pero puede declarar variables builder: ```html
    - +
    ``` -### `c-required` — Conditional Required Fields +### `c-required` — Campos requeridos condicionales ```html - + ``` +--- -## Forms (`c-form`) - -Complete form handling with automatic validation, storage, and email sending. +## Definiendo variables con `` ```html -
    + + + + + + +{% set gracias = 'apartados' | get('num = 20').0 %} +``` + +--- + +## Incluyendo módulos + +Para incluir un módulo dentro de otro módulo o sección general, usa el ID del módulo como etiqueta HTML: + +```html + +``` + +Ejemplo: +```html + + +``` + +El módulo hijo recibe los parámetros como variables en su contexto. + +--- + +## Formularios (`c-form`) + +Manejo automático de validación, almacenamiento en BD y envío de emails. + +```html + - - - +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + - - +
    ``` -### c-form Attributes +### Atributos de c-form -| Attribute | Description | -|-----------|-------------| -| `tableName="'table'"` | Store submissions in database table | -| `mailRecord="['correos', 'ID']"` | Email template from `correos` table | -| `sendTo="'email@domain.com'"` | Recipient email(s), comma-separated | -| `sendToClient="'fieldname'"` | Field containing client's email for auto-reply | -| `captcha="true"` | Enable Google reCAPTCHA | -| `honeypot="true"` | Anti-spam hidden field | -| `messageOK="'text'"` | Success message | -| `messageKO="'text'"` | Error message | -| `redirect="'/path/'"` | Redirect after successful submit | -| `attachFiles="true"` | Attach uploaded files to email | -| `showImages="true"` | Show image thumbnails in email | +| Atributo | Descripción | +|----------|-------------| +| `tableName="'table'"` | Tabla donde almacenar registros | +| `mailRecord="['correos', 'ID']"` | Template de email de la tabla `correos` | +| `sendTo="'email@domain.com'"` | Destinatarios (separados por coma) | +| `sendToClient="'campo_email'"` | Campo con email del cliente para auto-reply | +| `captcha="true"` | Google reCAPTCHA | +| `honeypot="true"` | Campo oculto anti-spam | +| `messageOK="'texto'"` | Mensaje de éxito | +| `messageKO="'texto'"` | Mensaje de error | +| `redirect="'/path/'"` | Redirección tras envío exitoso | +| `attachFiles="true"` | Adjuntar archivos al email | +| `showImages="true"` | Mostrar thumbnails en email | +| `emailMode="'twig'"` | Email en formato Twig | +| `header="'
    ...
    '"` | HTML cabecera del email | +| `footer="'
    ...
    '"` | HTML footer del email | +| `styles="'body { ... }'"` | CSS para el email | +--- -## Built-in Components +## Componentes Built-in ### Carousel (`c-tns-wrapper`) @@ -208,6 +409,18 @@ Complete form handling with automatic validation, storage, and email sending. ```html - + ``` + +--- + +## Puntos importantes + +1. **Nombres de variables:** `data-field-label` → sin espacios ni caracteres especiales, minúsculas +2. **Variables en multiv2:** Son propiedades del objeto iterado (`record.nombre`) +3. **Campos upload:** Retornan arrays, no strings (`imagen[0].urlPath`, no `imagen`) +4. **c-if usa `=` no `==`:** `c-if="layout = 'grid'"` (un solo igual) +5. **c-for tabla:** El nombre de tabla va sin prefijo `cms_` +6. **Enlace:** Ya incluye barras, no añadir extras +7. **Checkbox:** Valores `1` o `0`, no `true`/`false` diff --git a/docs/css-js-conventions.md b/docs/css-js-conventions.md index f117c8a..8460082 100644 --- a/docs/css-js-conventions.md +++ b/docs/css-js-conventions.md @@ -1,63 +1,78 @@ # CSS & JavaScript Conventions +## Estructura del módulo + +- Genera HTML + CSS + JS (o Vue 3 si es necesario) +- Define una clase raíz en kebab-case: `product-card`, `hero-section`, etc. +- Todo el CSS y JS scopeado bajo esa clase raíz + +--- + ## CSS ### Tailwind First -Use Tailwind CSS as the primary styling method. Only use custom CSS when Tailwind is insufficient. +Usar TailwindCSS como método principal. Solo CSS custom cuando Tailwind no cubra el estilo o se necesiten estados complejos/transiciones específicas. ```html -

    Title

    - - ``` -### BEM for Custom CSS +### BEM para CSS Custom -When custom CSS is needed, scope everything under a root class using BEM naming: +Cuando se necesite CSS personalizado, siempre scopeado bajo la clase raíz con BEM: ```css -/* Root class in kebab-case */ .hero-section { } .hero-section__title { } .hero-section__image { } .hero-section--dark { } ``` -Never use global classes without a module prefix. +Nunca usar clases globales sin prefijo de módulo. -### CSS Variables - -Acai provides theme variables: +### CSS Variables del tema ```css -var(--main-color) /* Primary brand color */ -var(--main-color-light) /* Lighter variant */ -var(--main-color-dark) /* Darker variant */ +var(--main-color) /* Color de marca primario */ +var(--main-color-light) /* Variante clara */ +var(--main-color-dark) /* Variante oscura */ ``` -### Utility Classes (Built-in) +### Estilos inline con fallbacks -| Class | Description | +Patrón para colores configurables por el usuario: + +```html +
    +

    +``` + +### Clases utilitarias de Acai + +| Clase | Descripción | |-------|-------------| -| `transition3s` | 0.3s smooth transition | -| `click-a-child` | Makes parent clickable via child `` tag | -| `line-clamp2` / `line-clamp3` / `line-clamp5` | Text truncation with ellipsis | -| `filter-white` | CSS filter to make images/icons white | -| `lazyload` | Lazy loading (use with `data-src`) | +| `transition3s` | Transición suave 0.3s | +| `click-a-child` | Hace el padre clickeable via primer `` hijo | +| `line-clamp2` / `line-clamp3` / `line-clamp5` | Truncar texto a N líneas | +| `filter-white` | Filtro CSS para hacer imágenes/iconos blancos | +| `lazyload` | Lazy loading (usar con `data-src`) | +| `text-shadow` | Sombra de texto para legibilidad sobre imágenes | +| `wysiwyg` | Wrapper para contenido de texto enriquecido | +| `bg-main-color` / `bg-main-color-light` / `bg-main-color-dark` | Fondos con color primario | +| `text-main-color` / `text-main-color-light` / `text-main-color-dark` | Texto con color primario | +--- ## JavaScript ### Module Scripts (`script.js`) -Keep JavaScript scoped to the module. Use `section_id` for targeting: +JavaScript scopeado al módulo usando `section_id`: ```js -// Scope to this module instance const section = document.getElementById('{{ section_id }}'); if (section) { const buttons = section.querySelectorAll('.btn'); @@ -68,15 +83,23 @@ if (section) { ### CmsApi (Client-Side) ```js -// Call a hook CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, function(response) { console.log(response); }); ``` -### Vue 3 Integration +### Cuándo usar Vue 3 -For complex interactivity, use Vue 3 via CDN with Composition API: +Usar Vue 3 CDN cuando la lógica requiera: +- Doble binding / reactividad +- Solicitudes asíncronas complejas +- Componentes reutilizables +- Gestión de estado local +- Ciclos de vida + +Para lógica simple, usar JavaScript vanilla. + +### Vue 3 Integration ```html

    @@ -87,7 +110,7 @@ For complex interactivity, use Vue 3 via CDN with Composition API: ``` -**Important:** Use `'${'` and `'}'` as Vue delimiters to avoid conflicts with Twig's `{{ }}` syntax. +Siempre usar `'${'` y `'}'` como delimitadores Vue para evitar conflicto con Twig. + +--- + +## Variables Globales Disponibles + +| Variable | Descripción | Ejemplo | +|----------|-------------|---------| +| `section_id` | ID único por instancia del módulo | `
    ` | +| `server.HTTP_HOST` | Dominio actual | `https://{{ server.HTTP_HOST }}/path` | +| `loop.index` | Índice de iteración (1-based) en c-for/for | `{{ loop.index }}` | +| `loop.index is odd` | True en iteraciones impares | Layouts alternados | +| `loop.index is even` | True en iteraciones pares | Patrones zigzag | +| `interno` | True dentro del editor CMS | `c-class="{'editor-mode': interno}"` | + +### Patrón section_id + +Cada instancia de módulo recibe un `section_id` único. Usar para navigation anchor e IDs: + +```html +
    +
    + +
    +``` + +--- + +## Componentes Nativos + +### Carousel (`c-tns-wrapper`) + +```html +
    +
      +
    • + +
    • +
    +
    +``` + +| Atributo | Descripción | Ejemplo | +|----------|-------------|---------| +| `data-responsive` | Items por breakpoint | `"sm:2, md:3, lg:4"` | +| `data-autoplay-timeout` | Intervalo autoplay (ms) | `"5000"` | +| `data-mode` | Modo de transición | `"gallery"` o `"carousel"` | +| `data-speed` | Velocidad de transición (ms) | `"400"` | +| `data-nav` | Puntos de navegación | `"true"` | + +Dots de navegación custom: +```html +
    +
    +
    +
    +``` + +### Lightbox + +```html +
    + + +``` + +### Breadcrumb + +```html + +``` + +### AOS (Animate On Scroll) + +```html +
    Contenido
    +``` + +Valores comunes: `fade-up`, `fade-down`, `fade-left`, `fade-right`, `zoom-in`, `zoom-in-up`, `fade-up-right`, `fade-up-left` + +Después de cambios dinámicos: `AOS.refresh()` en JavaScript. + +### Lazy Loading + +```html + + + + + +``` + +--- + +## Buenas prácticas + +- HTML/Twig semántico +- Código limpio y organizado +- Evitar dependencias externas innecesarias +- Evitar estilos inline salvo casos justificados (colores dinámicos del usuario) +- No usar clases globales sin prefijo de módulo diff --git a/docs/hooks-and-api.md b/docs/hooks-and-api.md index 8e2b32c..21eec92 100644 --- a/docs/hooks-and-api.md +++ b/docs/hooks-and-api.md @@ -2,155 +2,210 @@ ## Hooks -Hooks are PHP files in the `hooks/` directory that execute server-side logic. They can also live inside a module at `template/estandar/modulos//hook.php`. +Hooks son archivos PHP en `hooks/` que ejecutan lógica server-side. También pueden estar dentro de un módulo en `template/estandar/modulos//hook.php`. -### Testing Hooks - -To test hooks, the site's Docker container must be running. Make a curl request to the Docker URL with the hook path. For example, if a hook is named `hooks.example_hook.php`: - -```bash -curl http://{DOCKER_URL_AND_PORT}/hooks/example_hook/ -``` - -Replace `{DOCKER_URL_AND_PORT}` with your local Docker address (e.g., `localhost:8080`) and parse hook name for url endpoint. - -Do not use X-Hooks-Token because its not needed on developer environment. - -### How to Call Hooks - -**From Twig:** -```twig -{{ 'hooks/module_id/' | hook({param1: 'value1', param2: variable}) }} -``` - -**From HTML (with result):** -```html - -

    {{ myVar }}

    -``` - -**From JavaScript:** -```js -CmsApi.hook('/hooks/module_id/', { param1: 'value1' }, function(response) { - console.log(response); -}); -``` - -**From c-form:** -Hooks are automatically triggered on form submission when configured. - -### Hook Parameters - -Parameters are received as PHP variables: +### Estructura de un Hook ```php true, + "message" => "Valor procesado: " . $resultado, + "value" => $resultado +]; +?> ``` -### Hook Return Values +### Testing Hooks -Hooks can `echo` or `return` values. When called from Twig or `` tag, the output is captured into the result variable. +El Docker debe estar corriendo. Hacer curl al endpoint del hook: +```bash +curl http://localhost:8080/hooks/example_hook/ +``` + +No usar X-Hooks-Token en desarrollo local. + +### Cómo Llamar Hooks + +**Desde HTML (recomendado para módulos):** +```html + +

    {{ myVar.message }}

    +``` + +**Desde Twig:** +```twig +{% set resultado = 'hooks/mimodulo/' | hook({param1: 100, param2: 'texto'}) %} +

    {{ resultado.message }}

    +``` + +**Desde JavaScript:** +```js +CmsApi.hook('/hooks/mimodulo/', {param1: 100, param2: 'texto'}, (data) => { + console.log(data.message); +}); +``` + +**Desde otro Hook PHP:** +```php + 100, "param2" => "texto"]); +$mensaje = $result["message"]; +?> +``` + +**Desde c-form:** Los hooks se ejecutan automáticamente al enviar el formulario si están configurados. + +--- ## CmsApi (PHP) -Server-side API for database operations. Available in all hooks. +API server-side para operaciones de base de datos. Disponible en todos los hooks. -### Read Records +### Read — `CmsApi::get()` ```php -// Get all records +// Todos los registros $products = CmsApi::get('productos'); -// With WHERE condition +// Con condición WHERE $active = CmsApi::get('productos', ['active' => 1]); -// With order and limit +// Con orden y límite $latest = CmsApi::get('noticias', [], 'fecha DESC', 5); -// With operators +// Con condición string +$activos = CmsApi::get('productos', 'activo=1'); + +// Condición compleja como array +$caros = CmsApi::get('productos', [ + ["column" => "precio", "operator" => ">", "value" => 100] +]); + +// Múltiples condiciones (AND) +$resultados = CmsApi::get('productos', [ + ["column" => "activo", "operator" => "=", "value" => 1], + ["column" => "stock", "operator" => ">", "value" => 0] +]); + +// Con operadores $expensive = CmsApi::get('productos', ['precio' => ['>=' => 100]]); $search = CmsApi::get('productos', ['nombre' => ['LIKE' => '%keyword%']]); $inList = CmsApi::get('productos', ['categoria_num' => ['IN' => [1, 2, 3]]]); -``` -### Insert Records - -```php -$newRecord = CmsApi::insert('contacto', [ - 'nombre' => 'John', - 'email' => 'john@example.com', - 'mensaje' => 'Hello', +// Con opciones +$datos = CmsApi::get('productos', '', '', '', [ + 'translates' => true, + 'uploads' => true, + 'relations' => true, + 'relationsDepth' => 2 ]); ``` -### Update Records +### Insert — `CmsApi::insert()` ```php -CmsApi::update('productos', - ['precio' => 29.99, 'activo' => 1], // fields to update - ['num' => 42] // where condition +// Un registro +CmsApi::insert('contacto', [ + ["nombre" => "John", "email" => "john@example.com", "mensaje" => "Hello"] +]); + +// Múltiples registros +CmsApi::insert('productos', [ + ["nombre" => "Producto A", "precio" => 100], + ["nombre" => "Producto B", "precio" => 200] +]); + +// Con retorno del último ID +CmsApi::insert('productos', + [["nombre" => "Nuevo", "precio" => 150]], + [], + ['return_last_id' => true] ); ``` -### Delete Records +### Update — `CmsApi::update()` ```php -CmsApi::delete('productos', ['num' => 42]); +// Con condición string +CmsApi::update('productos', ["precio" => 150], "num=1"); + +// Con condición array +CmsApi::update('productos', + ["activo" => 1], + [["column" => "num", "operator" => "=", "value" => 1]] +); + +// Múltiples registros +CmsApi::update('productos', ["activo" => 0], "precio < 50"); ``` -### Important Rules +### Delete — `CmsApi::delete()` -- Table names **without** `cms_` prefix -- Primary key is always `num`, never `id` -- Upload fields are handled separately (not via insert/update) -- Operators: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN` +```php +CmsApi::delete('productos', "num=5"); +CmsApi::delete('productos', + [["column" => "activo", "operator" => "=", "value" => 0]] +); +``` + +### Reglas importantes + +- Nombres de tabla **sin** prefijo `cms_` +- Primary key siempre es `num`, nunca `id` +- Foreign keys: `categoria_num`, no `categoria_id` +- Upload fields: no se manejan via insert/update +- Operadores: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN` + +--- ## CmsApi (JavaScript — Client-Side) ```js -// Hook call +// Llamar hook CmsApi.hook('/hooks/module_id/', { param: 'value' }, function(response) { - // response is the hook output + // response es la salida del hook }); -// Record operations (if exposed via hooks) +// Leer registros (si está expuesto via hooks) CmsApi.get('tableName', { where: conditions }, function(records) { // records array }); ``` +--- ## CocoDB -Low-level database abstraction layer used internally by CmsApi. Use directly from hooks when you need more control. +Capa de abstracción de BD de bajo nivel usada internamente por CmsApi. Usar directamente desde hooks cuando necesites más control. ### `CocoDB::get($table, $where, $order, $limit, $options)` -Reads records from a table. This is the same method CmsApi::get wraps. - ```php -// Basic query +// Básico $records = CocoDB::get('productos', ['activo' => 1], 'orden ASC', 10); -// With advanced where (array syntax for operators) +// Where con operadores avanzados $records = CocoDB::get('productos', [ ['column' => 'precio', 'value' => 100, 'operator' => '>='], ['column' => 'categoria_num', 'value' => [1, 2, 3], 'operator' => 'IN'], ]); -// OR conditions +// Condiciones OR $records = CocoDB::get('productos', [ ['column' => 'nombre', 'value' => '%keyword%', 'operator' => 'LIKE'], ['column' => 'descripcion', 'value' => '%keyword%', 'operator' => 'LIKE', 'or' => true], ]); -// NOT condition +// NOT $records = CocoDB::get('productos', [ ['column' => 'estado', 'value' => 'borrador', 'operator' => '=', 'not' => true], ]); @@ -160,63 +215,59 @@ $records = CocoDB::get('productos', [ ['column' => 'fecha_baja', 'value' => '', 'operator' => 'IS NULL'], ]); -// Limit with offset +// Limit con offset $records = CocoDB::get('productos', [], 'num DESC', ['limit' => 10, 'offset' => 20]); ``` -#### Options for `get()` +#### Opciones de `get()` | Option | Type | Default | Description | |--------|------|---------|-------------| -| `uploads` | bool | `true` | Include upload field data | -| `relations` | bool/array | `true` | Resolve foreign key relations. Pass array to limit: `['category']` | -| `relationsDepth` | int | 2 | Depth of nested relation resolution | -| `translates` | string | current lang | Language code for translations | +| `uploads` | bool | `true` | Incluir datos de upload fields | +| `relations` | bool/array | `true` | Resolver foreign keys. Array para limitar: `['category']` | +| `relationsDepth` | int | 2 | Profundidad de relaciones anidadas | +| `translates` | string | current lang | Código de idioma | | `groupBy` | string | null | GROUP BY clause | -| `aggregates` | array | `[]` | Aggregate functions | -| `onlyFields` | array | null | Select specific fields only | -| `debug` | bool | false | Output SQL query for debugging | -| `redis` | bool | null | Force Redis cache | -| `redis_expire` | int | 60 | Redis cache TTL in seconds | +| `aggregates` | array | `[]` | Funciones de agregación | +| `onlyFields` | array | null | Seleccionar solo campos específicos | +| `debug` | bool | false | Mostrar SQL query | +| `redis` | bool | null | Forzar cache Redis | +| `redis_expire` | int | 60 | TTL de cache Redis (segundos) | ### `CocoDB::insertRecords($table, $records, $functions, $options)` -Insert one or multiple records. - ```php -// Single record +// Un registro $count = CocoDB::insertRecords('contacto', [ 'nombre' => 'John', 'email' => 'john@example.com', ]); -// Returns number of inserted records. Use mysql_insert_id() to get the new num. +// Usar mysql_insert_id() para obtener el nuevo num -// Multiple records +// Múltiples $count = CocoDB::insertRecords('productos', [ ['nombre' => 'Product A', 'precio' => 10], ['nombre' => 'Product B', 'precio' => 20], ]); ``` -#### Options for insert/update +#### Opciones de insert/update | Option | Description | |--------|-------------| -| `forceNum` | Allow setting the `num` field manually | -| `ignoreSchema` | Skip schema validation | -| `ignoreFields` | Array of field names to skip | +| `forceNum` | Permite setear el campo `num` manualmente | +| `ignoreSchema` | Saltar validación de schema | +| `ignoreFields` | Array de campos a ignorar | ### `CocoDB::updateRecords($table, $records, $where, $functions, $options)` -Update records matching a where condition. - ```php CocoDB::updateRecords('productos', - ['precio' => 29.99, 'activo' => 1], // fields to update - ['num' => 42] // where condition + ['precio' => 29.99, 'activo' => 1], + ['num' => 42] ); -// With operator in where +// Con operador en where CocoDB::updateRecords('productos', ['activo' => 0], [['column' => 'stock', 'value' => 0, 'operator' => '<=']] @@ -225,68 +276,140 @@ CocoDB::updateRecords('productos', ### `CocoDB::deleteRecords($table, $where, $options)` -Delete records matching a where condition. - ```php CocoDB::deleteRecords('productos', ['num' => 42]); -// With operator CocoDB::deleteRecords('logs', [ ['column' => 'fecha', 'value' => '2024-01-01', 'operator' => '<'] ]); ``` -### Where Clause Syntax +### Parámetro `$functions` -The `$where` parameter supports two formats: - -**Simple (key-value):** -```php -['campo' => 'valor'] // campo = 'valor' -``` - -**Advanced (array of conditions):** -```php -[ - 'column' => 'field_name', // Required - 'value' => 'match_value', // Required - 'operator' => '=', // =, !=, <, >, <=, >=, LIKE, IN, IS NULL - 'or' => false, // Use OR instead of AND - 'not' => false, // Negate the condition - 'raw_key' => false, // Skip column existence check -] -``` - -### Functions Parameter - -The `$functions` parameter lets you apply MySQL functions to values during insert/update: +Permite aplicar funciones MySQL a valores durante insert/update: ```php CocoDB::insertRecords('logs', [ 'mensaje' => 'Login exitoso', 'fecha' => '', ], [ - 'fecha' => 'NOW()', // Will use MySQL NOW() instead of the value + 'fecha' => 'NOW()', ]); ``` +### Where Clause — Formatos + +**Simple (key-value):** +```php +['campo' => 'valor'] // campo = 'valor' +``` + +**Avanzado (array de condiciones):** +```php +[ + 'column' => 'field_name', + 'value' => 'match_value', + 'operator' => '=', // =, !=, <, >, <=, >=, LIKE, IN, IS NULL + 'or' => false, // OR en vez de AND + 'not' => false, // Negar la condición + 'raw_key' => false, // Saltar check de existencia de columna +] +``` + +--- + +## Creación y Actualización de Registros + +### Flujo correcto + +1. Consultar el esquema de la tabla (leer `cms/data/schema/{tabla}.ini.php`) +2. Revisar los tipos de campo +3. Rellenar según el tipo de dato +4. Enviar con la estructura correcta + +### Tipos de campo y formato + +| Tipo | Formato | Ejemplo | +|------|---------|---------| +| **Text field** | String | `"Texto"` | +| **Text box** | String multilínea | `"Línea 1\nLínea 2"` | +| **Date/time** | `YYYY-MM-DD HH:mm:ss` | `"2025-12-03 10:30:00"` | +| **Wysiwyg** | String HTML | `"

    Texto

    "` | +| **List** | String o número | `"activo"` o `"1"` (num si es foreign key) | +| **Checkbox** | Número 1/0 | `1` o `0` | +| **Multivalores** | String JSON | `"[{\"producto\":\"1\"}]"` | +| **Upload** | **NO enviar** — usar `upload_record_image` después de crear el registro | + +--- ## Table Schemas -Table schemas are stored as JSON in `cms/data/schema/`. Each file defines: -- Field names and types -- Validation rules -- Relationships (foreign keys) -- Display configuration +Los schemas están en `cms/data/schema/` como archivos `.ini.php`. Definen: +- Nombres y tipos de campo +- Reglas de validación +- Relaciones (foreign keys) +- Configuración de display -### Field Format Types +--- -| Type | PHP Format | Notes | -|------|-----------|-------| -| Text | String | Plain text | -| Date/time | `YYYY-MM-DD HH:mm:ss` | MySQL datetime format | -| Checkbox | `1` or `0` | Boolean as integer | -| WYSIWYG | HTML string | Rich text with Tailwind classes | -| List | String or num | Foreign key if linked to table | -| Multivalores | JSON string | Serialized array | -| Upload | — | Handled separately, never in insert/update | +## Ejemplos Prácticos + +### Hook de Cálculo de Precio + +```php + 10) { + $precioUnitario *= 0.85; // 15% descuento +} + +return [ + "success" => true, + "precioUnitario" => round($precioUnitario, 2), + "total" => round($precioUnitario * $cantidad, 2), + "descuento" => $tipo === 'mayoreo' ? 15 : 0 +]; +?> +``` + +```html + +

    Total: ${{ precio.total }}

    +``` + +### Hook con Operaciones de BD + +```php + false, "message" => "Producto no encontrado"]; +} + +$total = $producto[0]['precio'] * $cantidad; + +// Crear venta +CmsApi::insert('ventas', [[ + "usuario_num" => $usuario_id, + "producto_num" => $producto_id, + "cantidad" => $cantidad, + "total" => $total, + "fecha" => date('Y-m-d H:i:s') +]], [], ['return_last_id' => true]); + +// Actualizar stock +$stock = CmsApi::get('stocks', "producto_num=$producto_id"); +if (!empty($stock)) { + CmsApi::update('stocks', + ["cantidad" => $stock[0]['cantidad'] - $cantidad], + "producto_num=$producto_id" + ); +} + +return ["success" => true, "total" => $total]; +?> +``` diff --git a/docs/production-patterns.md b/docs/production-patterns.md new file mode 100644 index 0000000..1c294a6 --- /dev/null +++ b/docs/production-patterns.md @@ -0,0 +1,262 @@ +# Patrones de Producción + +Patrones reales usados en módulos y secciones generales de producción. Usar como referencia al crear nuevos módulos. + +--- + +## Patrón 1: Cabecera de Sección (Pretítulo + Título + Subtítulo) + +Bloque de cabecera con colores y alineación configurables. Casi todos los módulos lo usan: + +```html +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    + {% if titulo %} +
    + {% endif %} +
    + +
    +
    + + +
    +
    +``` + +--- + +## Patrón 2: Layout Zigzag/Ajedrez (Imagen + Texto alternado) + +Usa `loop.index is odd/even` para alternar: + +```html +
  • +
    + +
    + +
    + +
    +

    {{ record.titulobloque | raw }}

    +
    {{ record.textobloque | raw }}
    + + {{ record.enlacebloque_anchor }} + +
    +
    +
  • +``` + +--- + +## Patrón 3: Acordeón FAQ + +```html +
  • +
    +
    +
    {{ loop.index }}.
    +
    +
    +
    + +
    + +
    +
    +
    +
  • +``` + +JavaScript para toggle: +```javascript +document.addEventListener("DOMContentLoaded", function () { + document.querySelectorAll(".faq-page").forEach(faq => { + faq.addEventListener("click", function () { + const body = faq.nextElementSibling; + const isActive = faq.classList.toggle("active"); + body.classList.toggle("hidden", !isActive); + AOS.refresh(); + }); + }); +}); +``` + +--- + +## Patrón 4: Formulario de Contacto Completo + +```html + + +{% set imagen = '' %} +{% set gracias = 'apartados' | get('num = 20').0 %} + + + + + + + + + + + + +
    + +
    +
    +``` + +--- + +## Patrón 5: Compartir en Redes Sociales + +```html + +``` + +--- + +## Patrón 6: Sección General — Detalle de Producto + +```html + + +
    +
    + +
    + + {{ thisrecord.name }} + +
    + +

    {{ thisrecord.name }}

    + {{ thisrecord.categoria_bd.0.name }} + + +
    +
    {{ thisrecord.precio }} €
    +
    {{ thisrecord.precio_descuento }} €
    +
    +
    {{ thisrecord.precio }} €
    + +
    {{ thisrecord.descripcion | raw }}
    + + +
    +
    + + {{ thisrecord.name }} + +
    +
    +
    +
    + + +{% set productosRelacionados = 'productos' | get('categoria = ' ~ thisrecord.categoria ~ ' and num!=' ~ thisrecord.num, 'globalOrder ASC', '3') %} +
    +
    +

    {{ 'Productos relacionados' | translate }}

    +
      +
    • + +
    • +
    +
    +
    +``` + +--- + +## Patrón 7: Galería con Carousel (modo Gallery) + +```html +
    +
      +
    • +
      + {{ uploadMulti.info1 }} +
      +
    • +
    +
    +
    +
    +
    +
    +``` diff --git a/docs/quick-reference.md b/docs/quick-reference.md new file mode 100644 index 0000000..137e0e7 --- /dev/null +++ b/docs/quick-reference.md @@ -0,0 +1,85 @@ +# Quick Reference + +## Reglas Críticas + +| Regla | Correcto | Incorrecto | +|-------|----------|------------| +| Nombres de tabla | `'productos'` | `'cms_productos'` | +| Primary key | `record.num` | `record.id` | +| Foreign keys | `categoria_num` | `categoria_id` | +| Upload fields | `record.imagen[0].urlPath` | `record.imagen` | +| Optimizar imagen | `record.imagen[0].urlPath \| imagec(800)` | `record.imagen.url` | +| Filtros Twig | `{{ 'table' \| get() }}` | `{{ get('table') }}` | +| Campo enlace | `{{ producto.enlace }}` (ya tiene barras) | `"/{{ producto.enlace }}/"` | +| Nombres builder vars | `data-field-label` → sin espacios/especiales, minúsculas | Mantener casing original | +| Checkbox | `1` o `0` (número) | `true`/`false` | +| Formato fecha | `YYYY-MM-DD HH:mm:ss` | Cualquier otro formato | +| c-if igualdad | `c-if="x = 'valor'"` (un `=`) | `c-if="x == 'valor'"` | +| Twig if igualdad | `{% if x == 'valor' %}` (doble `==`) | `{% if x = 'valor' %}` | +| queryDB tablas | `SELECT * FROM cms_tabla` (con prefijo) | `SELECT * FROM tabla` | +| get tablas | `'tabla' \| get()` (sin prefijo) | `'cms_tabla' \| get()` | + +## Builder Variable Types + +| Type | Elemento | Retorna | +|------|----------|---------| +| `textfield` | `

    ` | String | +| `headfield` | `

    `-`

    ` | String + var `_tag` | +| `textbox` | `
    ` | String multilínea | +| `wysiwyg` | `
    ` | HTML string | +| `link` | `` | URL string | +| `upload` | `` | Array de `{urlPath, info1}` | +| `uploadMulti` | `
  • ` | Itera archivos subidos | +| `list` (fijo) | `
    ` | Valor seleccionado | +| `list` (tabla) | `
    ` | `num` del registro | +| `multiv2` | `
  • ` wrapper | Array de objetos | + +## Acai HTML Attributes + +| Atributo | Uso | Ejemplo | +|----------|-----|---------| +| `c-if` | Condicional | `

    ` | +| `c-else` | Rama else | `

    ` | +| `c-for` | Loop array | `

  • ` | +| `c-for` | Loop tabla | `
  • ` | +| `c-hidden` | Variable oculta | `

    ` | +| `c-class` | Clase condicional | `

    ` | +| `c-form` | Formulario | `` | + +## Twig Filters + +| Filtro | Uso | +|--------|-----| +| `get` | `'table' \| get(where, order, limit)` | +| `hook` | `'hooks/module_id/' \| hook({params})` | +| `module` | `'module_id' \| module({params})` | +| `queryDB` | `'SELECT ...' \| queryDB()` | +| `imagec` | `path \| imagec(width)` | +| `translate` | `'text' \| translate` | +| `json_decode` | `'json_string' \| json_decode` | +| `raw` | `variable \| raw` | +| `truncate` | `text \| truncate(100)` | + +## Formato de datos para registros + +| Tipo | Formato | Ejemplo | +|------|---------|---------| +| Text field | String | `"Texto"` | +| Text box | String multilínea | `"Línea 1\nLínea 2"` | +| Date/time | `YYYY-MM-DD HH:mm:ss` | `"2025-12-03 10:30:00"` | +| Wysiwyg | HTML string | `"

    Texto

    "` | +| List | String o número | `"activo"` o `"1"` | +| Checkbox | Número 1/0 | `1` o `0` | +| Multivalores | String JSON | `"[{\"producto\":\"1\"}]"` | +| Upload | NO enviar — subir imagen después de crear registro | + +## Variables globales + +| Variable | Descripción | +|----------|-------------| +| `section_id` | ID único por instancia del módulo | +| `server.HTTP_HOST` | Dominio actual | +| `loop.index` | Índice de iteración (1-based) | +| `loop.index is odd/even` | Para layouts alternados | +| `interno` | True dentro del editor CMS | +| `thisrecord` | Registro actual (en secciones generales) | diff --git a/docs/twig-filters.md b/docs/twig-filters.md index 54b2ce8..6382801 100644 --- a/docs/twig-filters.md +++ b/docs/twig-filters.md @@ -1,113 +1,153 @@ # Twig Filters Reference -Acai uses Twig **filters** (with `|` pipe syntax). Never use Twig functions — only filters are supported. +Acai usa filtros Twig con sintaxis `|`. No usar funciones Twig — solo filtros. -## Database Queries - -### `get` — Query Table +## `get` — Consultar tabla de BD ```twig -{# All records #} +{{ 'table_name' | get(where, order, limit) }} +``` + +- `table_name`: sin prefijo `cms_` +- `where`: string SQL o objeto (opcional) +- `order`: string de orden (opcional) +- `limit`: int (opcional) + +```twig +{# Todos los registros #} {% set products = 'productos' | get() %} -{# With WHERE #} +{# Con WHERE string #} +{% set active = 'productos' | get('activo=1') %} + +{# Con WHERE objeto #} {% set active = 'productos' | get({activo: 1}) %} -{# With WHERE + ORDER + LIMIT #} -{% set latest = 'noticias' | get({publicado: 1}, 'fecha DESC', 6) %} +{# Con WHERE + ORDER + LIMIT #} +{% set latest = 'noticias' | get('publicado=1', 'fecha DESC', 6) %} -{# Single record (first result) #} +{# Completo #} +{% set caros = 'productos' | get('precio > 100', 'precio DESC', 20) %} + +{# Single record (primer resultado) #} {% set product = 'productos' | get({num: 42}) %} {{ product[0].nombre }} ``` -### `queryDB` — Raw SQL Query +Iterar resultados: +```twig +{% for producto in 'productos' | get('activo=1', 'num DESC', 10) %} +

    {{ producto.titulo }}

    +{% endfor %} +``` + +## `queryDB` — SQL directo + +Usa nombre de tabla completo WITH prefijo `cms_`. ```twig {% set results = 'SELECT * FROM cms_productos WHERE precio > 100 ORDER BY precio ASC' | queryDB() %} + +{# JOIN complejo #} +{% set top = 'SELECT p.*, COUNT(v.num) as ventas + FROM cms_productos p + LEFT JOIN cms_ventas v ON v.producto_num = p.num + GROUP BY p.num + ORDER BY ventas DESC + LIMIT 5' | queryDB() %} ``` -Note: Raw SQL uses the full table name WITH `cms_` prefix. +Usar solo cuando `get` no sea suficiente. -## Module & Hook Execution - -### `hook` — Execute PHP Hook +## `hook` — Ejecutar PHP Hook ```twig -{# Call hook and output result #} +{# Llamar y mostrar resultado #} {{ 'hooks/module_id/' | hook({param1: 'value', param2: variable}) }} -{# Capture into variable #} -{% set result = 'hooks/module_id/' | hook({action: 'getData'}) %} +{# Capturar en variable #} +{% set result = 'hooks/calcular_precio/' | hook({cantidad: 5, tipo: 'mayoreo'}) %} +

    Total: ${{ result.total }}

    ``` -### `module` — Render Another Module +## `module` — Renderizar otro módulo ```twig {{ 'other_module_id' | module({param1: value1}) }} + +{# Capturar en variable #} +{% set carrito = 'carrito_compras' | module({usuario_id: 123}) %} ``` -## Text & Content - -### `translate` — Translation +## `imagec` — Optimizar/redimensionar imágenes ```twig -{{ 'Hello' | translate }} +{# Redimensionar a ancho #} + + +{# En srcset #} + +``` + +## `translate` — Traducción + +```twig +{{ 'Bienvenido' | translate }} {{ variable | translate }} ``` -### `raw` — Render HTML Without Escaping +## `raw` — Renderizar HTML sin escapar ```twig {{ record.description | raw }} ``` -### `truncate` — Text Truncation +## `truncate` — Truncar texto ```twig {{ record.description | truncate(150) }} ``` -### `json_decode` — Parse JSON String +## `json_decode` — Parsear JSON ```twig {% set data = jsonString | json_decode %} {{ data.key }} ``` -## Images +## `split`, `filter` — Filtros estándar Twig -### `imagec` — Image Optimization/Resize +Misma funcionalidad que Twig estándar. -```twig -{# Resize to width #} - +--- -{# In srcset #} - -``` +## Operadores y Sintaxis -## Operators & Syntax +### Concatenación -### Concatenation - -Twig uses `~` for string concatenation (not `.` or `+`): +Twig usa `~` (no `.` ni `+`): ```twig {{ 'Hello ' ~ name ~ '!' }} {% set url = '/products/' ~ product.slug ~ '/' %} ``` -### Ternary / Default +### Concatenar en filtros + +```twig +{% set stock = 'stocks' | get('producto_num=' ~ producto.num) %} +``` + +### Ternario / Default ```twig {{ title | default('Default Title') }} {{ isActive ? 'active' : 'inactive' }} ``` -### Comparisons in Twig +### Comparaciones ```twig {% if items | length > 0 %} @@ -115,4 +155,55 @@ Twig uses `~` for string concatenation (not `.` or `+`): {% if name is not empty %} ``` -Note: In `c-if` attributes, use `=` (single equals) for equality. In Twig `{% if %}` blocks, use `==` (double equals). +En `c-if` usar `=` (simple). En `{% if %}` usar `==` (doble). + +--- + +## Ejemplos complejos + +### Galería con productos y stock + +```twig +{% for producto in 'productos' | get('destacado=1', 'num DESC', 12) %} +
    + {{ producto.titulo }} +

    {{ producto.titulo }}

    +

    {{ producto.descripcion | truncate(100) }}

    + + {% set stock = 'stocks' | get('producto_num=' ~ producto.num) %} + Stock: {{ stock[0].cantidad }} +
    +{% endfor %} +``` + +### Múltiples filtros combinados + +```twig +{% set categorias = 'categorias' | get() %} +{% set productos = 'productos' | get('activo=1', 'titulo ASC', 20) %} +{% set stats = 'hooks/obtener_stats/' | hook({fecha_inicio: '2024-01-01'}) %} + +

    {{ stats.titulo | translate }}

    + +
    + +{% for prod in productos %} +
    + +

    {{ prod.titulo }}

    +
    +{% endfor %} +``` + +--- + +## Puntos importantes + +1. **Solo filtros, no funciones:** `'tabla' | get()` no `get('tabla')` +2. **Upload fields son arrays:** `record.imagen[0].urlPath`, no `record.imagen` +3. **Tablas sin prefijo `cms_`** en `get()`. Con prefijo en `queryDB()` +4. **Concatenar con `~`:** `'stocks' | get('producto_num=' ~ producto.num)` diff --git a/docs/vue-builder-examples.md b/docs/vue-builder-examples.md new file mode 100644 index 0000000..d32a2f1 --- /dev/null +++ b/docs/vue-builder-examples.md @@ -0,0 +1,695 @@ +# Ejemplos de Builder Vue - Producción + +Colección de ejemplos reales de archivos `builder.vue` implementados en producción. Cada ejemplo incluye el código completo y notas sobre decisiones de diseño importantes. + +--- + +## Ejemplo 1: Banner Slideshow + +### Descripción +Banner hero con slideshow de imágenes o video de fondo, overlay configurable, textos principales (pretítulo, título, subtítulo) y botón de llamada a la acción. + +### Características principales +- **5 tabs organizados**: Configuración, Imágenes, Textos, Enlaces, Colores +- **Selector imagen/video**: Toggle con iconos que alterna entre imagen y video con `v-show` +- **Overlay completo**: Tipo (sin degradado/con degradado), color y opacidad agrupados en tab Imágenes +- **Colorpickers**: Para overlay y color de texto general con textfield oculto +- **Toggles con iconos**: Sombra (X/check), tipo imagen (foto/video), tipo overlay (cuadrado/degradado) +- **Logo adicional**: Upload de logo que se superpone al banner +- **Configuraciones globales**: Posición texto, sombra, container, altura banner + +### Decisiones de diseño clave + +1. **Selector imagen/video como primer campo del tab Imágenes**: El toggle de tipo de fondo está al inicio del tab Imágenes, antes de los uploads, según la regla 10.1 +2. **v-show en uploads**: + - Upload de imágenes: `v-show="data.tipodeimagen && data.tipodeimagen.newValues.builder_custom.value == ''"` + - Upload de video: `v-show="data.tipodeimagen && data.tipodeimagen.newValues.builder_custom.value == '1'"` + - NUNCA quitar estos `v-show`, son esenciales +3. **Grupo overlay en tab Imágenes**: El grupo completo (tipo + color + opacidad) está en Imágenes, NO en Colores, porque afecta directamente al fondo visual (regla 10.2) +4. **Radio borde en tab Enlaces**: Campo que afecta al botón va en el tab del enlace, no en Configuración (regla 10.3) +5. **Recuerda con HTML escapado**: El campo título incluye un "Recuerda" con etiquetas HTML escapadas (`<span>`) para guiar al usuario +6. **Color del texto en tab Colores**: El color general del texto va en su propio tab, no mezclado con el overlay + +### Tabs configurados + +```javascript +tabsConfig: [ + { id: "configuracion", label: "Configuración", color: "#f59e0b", icon: '...' }, + { id: "imagenes", label: "Imágenes", color: "#10b981", icon: '...' }, + { id: "textos", label: "Textos", color: "#3b82f6", icon: '...' }, + { id: "enlaces", label: "Enlaces", color: "#ef4444", icon: '...' }, + { id: "colores", label: "Colores", color: "#8b5cf6", icon: '...' } +] +``` + +### Componentes utilizados +- `acai-vue-tabs` - Sistema de tabs con storage-key y apply-theme-styles +- `acai-vue-selectv2` - Selectores (algunos con `:toggle-icons`) +- `acai-vue-textfield` - Campos de texto simple (pretítulo, subtítulo) +- `acai-vue-title` - Encabezado principal con placeholder +- `acai-vue-linkv2` - Enlaces con `:show_text="true"` +- `acai-vue-upload` - Uploads de imagen/video/logo con todas las props necesarias +- `acai-vue-colorpicker` - Pickers de color con textfield oculto asociado + +### Iconos con toggle + +```javascript +iconosSombra: { + '': '...(icon-tabler-x)', + '1': '...(icon-tabler-check)' +}, +iconosTipoImagen: { + '': '...(icon-tabler-photo)', + '1': '...(icon-tabler-video)' +}, +iconosOverlay: { + '': '...(icon-tabler-square)', + '1': '...(icon-tabler-gradient)' +} +``` + +### Código completo + +```vue + + + + + +``` + +--- + +## Ejemplo 2: [Módulo de texto genérico] + +```vue + + + +``` \ No newline at end of file diff --git a/docs/vue-builder-rules.md b/docs/vue-builder-rules.md new file mode 100644 index 0000000..846e5cc --- /dev/null +++ b/docs/vue-builder-rules.md @@ -0,0 +1,484 @@ + +# Reglas CMS-VUE + +Aplica estas reglas ÚNICAMENTE cuando el usuario incluya "cms-vue" o "CMS-VUE" (en cualquier combinación de mayúsculas/minúsculas) en su mensaje. Ejemplos válidos: "dame el cms-vue", "cms-vue personalizado", "crea el CMS-VUE", "necesito el cms-vue de este módulo". Si el mensaje NO contiene "cms-vue", ignora completamente estas instrucciones. + +--- + +## 1. Estructura general: Tabs (`acai-vue-tabs`) + +- Analiza el HTML proporcionado para determinar cuántos tabs son necesarios y cómo nombrarlos. +- Tabs base comunes: **Configuración**, **Imágenes**, **Textos**, **Bloques** (records), **Enlaces**, **Colores**. +- Añade tabs adicionales si el módulo lo requiere (ej: "Formulario", "Video", "Overlay", "Slider", etc.). +- Si un tab solo tendría 1 campo, evalúa fusionarlo con otro tab relacionado. +- Cada tab tiene su propio `id`, `label`, `color` e `icon` (SVG inline). +- SVG dentro del template usan `:style="{ color: color }"` para heredar el color del tab. +- Textos descriptivos claros y orientados al usuario final del CMS. +- Usa `storage-key` único: `'nombre-modulo-tabs-' + (section_id || 'default')`. +- Siempre añade `:apply-theme-styles="true"`. +- **IMPORTANTE:** La prop para pasar los tabs es `:tabs` (NO `:tabs-config`). +- **IMPORTANTE:** Siempre añadir `v-if="data"` en el `` para evitar renderizar antes de que los datos estén listos. + +### Template de cada tab: +```html + +``` + +--- + +## 2. Colorpicker según contexto + +### 2.1 En tab "Colores" (campos generales de color de texto/fondo) +Siempre con SVG + título + descripción + colorpicker + textfield oculto: +```html +
    +
    +
    + ... +

    Nombre : Descripción.

    +
    +

    Nota : valor por defecto.

    +
    + +
    +
    + +
    +
    +
    +``` + +### 2.2 Colorpicker en otros tabs (ej: color del overlay en tab Imágenes) +Misma estructura con SVG + título + descripción + colorpicker + textfield oculto, pero usando el `color` del tab donde se encuentre. Se coloca junto a los campos relacionados (ver regla 10.2). + +### 2.3 Dentro de `` (sin icono ni descripción) +Se coloca debajo del campo al que corresponde: +- Nota con `mt-4` si campo anterior es `textfield` o `title`. +- Nota con `mt-3` si campo anterior es `textbox` o `wysiwyg`. +- Colorpicker siempre con `mt-1`. +```html +

    Nota : color por defecto (#hex).

    +
    + +
    +
    + +
    +``` + +### 2.4 Campos tipo `list` para colores +Se usan como `` con icono y nota. El componente detecta automáticamente si las opciones son colores y muestra el modo color selector con swatches. No llevan colorpicker ni textfield oculto. + +### 2.5 Extraer color por defecto +Del HTML: `style="color: {{ campo ? campo : '#HEX' }}"` → usar `#HEX`. Si no hay color, usar `#111827` (textos) o `transparent` (fondos). + +--- + +## 3. Campos: estructura en tabs + +### Fuera de records: +```html +
    +
    +
    + ... +

    Nombre : Descripción.

    +
    +

    Nota : info adicional.

    +
    + +
    +
    +
    +``` + +### Dentro de records: +```html +
    +
    + ... +

    Nombre : Descripción.

    +
    +
    + +
    +
    +``` + +--- + +## 4. Nombres de campos (`:field`) + +- Construir uniendo palabras del `data-field-label` en minúsculas sin espacios. +- Eliminar acentos: á→a, é→e, í→i, ó→o, ú→u, ñ→(eliminar). +- Ejemplos: `Color del título` → `colordeltitulo`, `Valoración` → `valoracion`, `Tamaño` → `tamao`. + +--- + +## 5. Upload de imágenes + +### General: +```html + +``` + +### En records: +```html + +``` + +--- + +## 6. Componentes y URLs + +Solo incluir los que se usen. Los componentes personalizados (tabs, selectv2) se cargan desde impulse; los estándar desde cocosolution: + +```javascript +// ── Componentes personalizados (impulse) ── +'acai-vue-tabs': httpVueLoader('https://impulse.webserver2.plandeweb.com/template/estandar/css/builder-acaivuetabsv2.vue?timestamp=' + new Date().getTime()), +'acai-vue-selectv2': httpVueLoader('https://impulse.webserver2.plandeweb.com/template/estandar/css/builder-acaivueselect-v2.vue?timestamp=' + new Date().getTime()), + +// ── Componentes estándar (cocosolution) ── +'acai-vue-colorpicker': httpVueLoader('https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivuecolorpicker.vue?timestamp=' + new Date().getTime()), +'acai-vue-upload': httpVueLoader('https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivueupload.vue?timestamp=' + new Date().getTime()), +'acai-vue-records': httpVueLoader('https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivuerecords.vue?timestamp=' + new Date().getTime()), +'acai-vue-title': httpVueLoader('https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivuetitle.vue?timestamp=' + new Date().getTime()), +'acai-vue-wysiwyg': httpVueLoader('https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivuewysiwyg.vue?timestamp=' + new Date().getTime()), +'acai-vue-linkv2': httpVueLoader('https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivuelinkv2.vue?timestamp=' + new Date().getTime()), +'acai-vue-textbox': httpVueLoader('https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivuetextbox.vue?timestamp=' + new Date().getTime()), +'acai-vue-textfield': httpVueLoader('https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivuetextfield.vue?timestamp=' + new Date().getTime()), +'acai-vue-datepicker': httpVueLoader('https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivuedatepicker.vue?timestamp=' + new Date().getTime()), +``` + +**IMPORTANTE:** `acai-vue-list` ha sido reemplazado por `acai-vue-selectv2` en todos los VUEs. NO usar `acai-vue-list` en nuevos VUEs. + +--- + +## 7. Mapeo HTML → Vue + +| `data-field-type` | Componente | +|---|---| +| `textfield` | `acai-vue-textfield` | +| `headfield` | `acai-vue-title` | +| `wysiwyg` | `acai-vue-wysiwyg` | +| `textbox` | `acai-vue-textbox` | +| `list` | `acai-vue-selectv2` | +| `upload` / `uploadMulti` | `acai-vue-upload` | +| `linkv2` | `acai-vue-linkv2` (siempre con `:show_text="true"`) | +| `multiv2` | `acai-vue-records` | +| `textfield` (usado como fecha) | `acai-vue-datepicker` + `acai-vue-textfield` oculto | + +--- + +## 8. Script base + +```javascript +module.exports = { + props: ["active", "section_id"], + data() { + return { + data: null, + builder: null, + idiomas: IDIOMAS, + tabsConfig: [ /* tabs */ ], + // iconos para toggles (solo si hay campos de 2 opciones con iconos) + // iconosNombreCampo: { '': '...', '1': '...' } + }; + }, + components: { /* solo los usados */ }, + mounted() { this.$emit("child-mounted"); }, + methods: { saveData() { this.$emit("save-data"); } }, +}; +``` + +--- + +## 9. Decisión de tabs según contenido HTML y contexto semántico + +### 9.1 Organización contextual (PRIORITARIA) + +**IMPORTANTE:** Primero analizar el **nombre del campo** para determinar su contexto semántico, independientemente del tipo. Un campo `list` llamado "tipo de imagen" debe ir en el tab **Imágenes**, no en Configuración. + +#### Keywords para tab Imágenes: +Campos que contengan: `imagen`, `photo`, `video`, `fondo`, `background`, `logo`, `icono`, `icon` + +**Ejemplos:** +- ✅ "tipo de imagen" (list) → **Imágenes** +- ✅ "video de fondo" (list) → **Imágenes** +- ✅ "logo principal" (upload) → **Imágenes** + +#### Keywords para tab Enlaces: +Campos que contengan: `enlace`, `link`, `boton`, `button`, `url`, `href` + +**Ejemplos:** +- ✅ "texto del botón" (textfield) → **Enlaces** +- ✅ "url externa" (textfield) → **Enlaces** +- ✅ "estilo del enlace" (list) → **Enlaces** + +#### Keywords para tab Textos: +Campos que contengan: `titulo`, `title`, `texto`, `text`, `descripcion`, `description`, `contenido`, `content`, `label`, `etiqueta` + +**Ejemplos:** +- ✅ "título principal" (headfield) → **Textos** +- ✅ "descripción corta" (textfield) → **Textos** + +### 9.2 Organización por tipo (fallback) + +Si el nombre del campo **no** coincide con ninguna keyword, usar el tipo: + +| Tipo | Tab | +|---|---| +| `headfield`, `textfield`, `textbox`, `wysiwyg` | Textos | +| `upload`, `image` | Imágenes | +| `linkv2` | Enlaces | +| `list`, `select` (sin contexto) | Configuración | +| `multiv2` (records) | Bloques | +| Otros campos de configuración | Configuración | + +--- + +## 10. Reglas especiales + +### 10.1 Selector imagen/video con v-show +Cuando el HTML tenga un campo `list` con opciones tipo `"|Imagen,1|Video"`: +- El selector "Tipo de fondo" va en el tab **Imágenes** como **primer campo** (encima de los uploads). +- Se renderiza como toggle con iconos (foto/vídeo) usando `acai-vue-selectv2` con `:toggle-icons`. +- Usa el icono `icon-tabler-photo-video`: +```html + +``` +- El upload de **imágenes** lleva: `v-show="data.tipodeimagen && data.tipodeimagen.newValues.builder_custom.value == ''"` (visible cuando es imagen o vacío). +- El upload de **video** lleva: `v-show="data.tipodeimagen && data.tipodeimagen.newValues.builder_custom.value == '1'"` (visible cuando es video). +- **NUNCA quitar estos `v-show`**, son esenciales para mostrar uno u otro según la selección. +- Iconos del toggle: +```javascript +iconosTipoImagen: { + '': '', + '1': '' +} +``` + +### 10.2 Grupo overlay (tipo + color + opacidad) +Cuando el HTML contenga campos de overlay (tipo de overlay, color del overlay, opacidad del overlay): +- Los tres campos van **juntos** en el tab **Imágenes**, **debajo** de la imagen/video sobre la que se aplica el overlay. +- El orden es: tipo de overlay → color del overlay (colorpicker) → opacidad del overlay. +- El **color del overlay NO va en el tab Colores**, va en Imágenes junto al resto del grupo overlay. +- El tipo de overlay (2 opciones: Sin degradado / Con degradado) se renderiza como toggle con iconos: +```javascript +iconosOverlay: { + '': '', + '1': '' +} +``` + +### 10.3 Campos que afectan al enlace +Los campos `list` que modifican propiedades del botón de enlace (radio borde, estilo, etc.) van en el tab **Enlaces**, debajo del campo `linkv2` al que afectan. NO van en Configuración ni en Imágenes. + +### 10.4 Tabs base: definición fija de id, label, color e icono +Los tabs base siempre usan la siguiente definición fija. Este es el orden por defecto; solo se incluyen los tabs que el módulo necesite. Tabs adicionales (ej: "Formulario", "Video") se crean con id, label, color e icono nuevos. + +```javascript +{ id: "configuracion", label: "Configuración", color: "#f59e0b", icon: '' }, + +{ id: "imagenes", label: "Imágenes", color: "#10b981", icon: '' }, + +{ id: "textos", label: "Textos", color: "#3b82f6", icon: '' }, + +{ id: "bloques", label: "Bloques", color: "#ec4899", icon: '' }, + +{ id: "enlaces", label: "Enlaces", color: "#ef4444", icon: '' }, + +{ id: "colores", label: "Colores", color: "#8b5cf6", icon: '' }, +``` + +### 10.5 Campos globales que afectan al multi van DENTRO del tab Bloques +Los campos `list` o `textfield` generales (no de records) que afectan visualmente a los elementos del multi (ej: radio de borde de los bloques, alineación del texto de los bloques, diseño del enlace de los bloques) deben colocarse **dentro del tab Bloques**, en la zona **superior**, ANTES del bloque descriptivo "Bloques del multi" y del ``. Estos campos NO van en Configuración ni en otros tabs, ya que pertenecen conceptualmente a los bloques. + +### 10.6 Bloque descriptivo "Bloques del multi" antes de acai-vue-records +Siempre añadir un bloque descriptivo con el icono `icon-tabler-stack-2` y el texto "Bloques del multi : Personaliza los bloques del multi." justo antes de ``: +```html + +
    +
    +
    + +

    Bloques del multi : Personaliza los bloques del multi.

    +
    +
    +
    +``` + +### 10.7 Slot de acai-vue-records: NO desestructurar `color` +El slot de `` NUNCA debe desestructurar `color`. Siempre usar: +```html +