Ajustes del scaffold
This commit is contained in:
10
CLAUDE.md
10
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.)
|
||||
|
||||
@@ -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` | `<p>` | String |
|
||||
| `headfield` | `<h1>`-`<h6>` | String + variable `_tag` con la etiqueta elegida |
|
||||
| `textbox` | `<div>` | String multi-línea |
|
||||
| `wysiwyg` | `<div class="wysiwyg">` | HTML string |
|
||||
| `link` | `<a>` | URL string (ya incluye barras) |
|
||||
| `upload` | `<img>` | **Array** de `{urlPath, info1, info2, info3, info4}` |
|
||||
| `uploadMulti` | `<li>` | Itera sobre archivos subidos |
|
||||
| `list` (fijo) | `<div data-list-options="...">` | Valor seleccionado |
|
||||
| `list` (tabla) | `<div data-list-table="...">` | `num` del registro |
|
||||
| `multiv2` | `<li>` 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
|
||||
<p data-field-type="textfield" data-field-label="Título">
|
||||
Elemento editable
|
||||
</p>
|
||||
```
|
||||
|
||||
### 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
|
||||
</{{ title_tag | default('h2') }}>
|
||||
```
|
||||
|
||||
### upload Example
|
||||
### textbox
|
||||
|
||||
```html
|
||||
<!-- Single image -->
|
||||
<img data-field-type="upload"
|
||||
src="{{ image[0].urlPath }}"
|
||||
alt="{{ image[0].info1 }}"
|
||||
class="w-full rounded" />
|
||||
|
||||
<!-- Multiple images -->
|
||||
<div c-for="photo in gallery">
|
||||
<img src="{{ photo.urlPath }}" alt="{{ photo.info1 }}" />
|
||||
<div data-field-type="textbox" data-field-label="Descripción">
|
||||
Texto largo editable
|
||||
</div>
|
||||
```
|
||||
|
||||
### list Example
|
||||
### wysiwyg
|
||||
|
||||
```html
|
||||
<!-- Fixed options -->
|
||||
<select data-field-type="list" data-list-options="option1,option2,option3">
|
||||
<option>option1</option>
|
||||
</select>
|
||||
|
||||
<!-- From table -->
|
||||
<select data-field-type="list" data-list-table="categories" data-list-field="name">
|
||||
<option>Category</option>
|
||||
</select>
|
||||
<div class="wysiwyg" data-field-type="wysiwyg" data-field-label="Contenido Enriquecido">
|
||||
<p>Texto con <strong>estilos</strong> editables</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### link
|
||||
|
||||
```html
|
||||
<a data-field-type="link" data-field-label="Enlace Principal" href="#">
|
||||
Haz clic aquí
|
||||
</a>
|
||||
```
|
||||
|
||||
### upload
|
||||
|
||||
```html
|
||||
<div class="p-1/6 relative">
|
||||
<img
|
||||
class="absolute top-0 left-0 w-full h-full object-cover object-center lazyload"
|
||||
data-field-type="upload"
|
||||
data-field-label="Imagen Principal"
|
||||
data-lazy="true"
|
||||
data-field-info1="titulo"
|
||||
data-field-width="1400"
|
||||
alt=""
|
||||
>
|
||||
</div>
|
||||
```
|
||||
|
||||
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
|
||||
<li data-field-type="uploadMulti" data-field-label="Galería" data-field-info1="titulo">
|
||||
<div class="relative min-h-screen">
|
||||
<img class="absolute top-0 left-0 w-full h-full object-cover lazyload"
|
||||
data-src="{{ uploadMulti.urlPath | imagec(2100) }}"
|
||||
alt="{{ uploadMulti.info1 }}">
|
||||
</div>
|
||||
</li>
|
||||
```
|
||||
|
||||
### list (opciones fijas)
|
||||
|
||||
```html
|
||||
<div
|
||||
data-field-type="list"
|
||||
data-field-label="Color Producto"
|
||||
data-list-options="Rojo,Azul,|Verde,3|Amarillo"
|
||||
>
|
||||
</div>
|
||||
```
|
||||
|
||||
Formato de opciones: `opcion1,opcion2,|opcion3,valor3|opcion4`
|
||||
|
||||
### list (tabla)
|
||||
|
||||
```html
|
||||
<div
|
||||
data-field-type="list"
|
||||
data-field-label="Noticia Destacada"
|
||||
data-list-table="noticias"
|
||||
data-list-value="num"
|
||||
data-list-label="titulo"
|
||||
>
|
||||
{{ record.titulo }}
|
||||
</div>
|
||||
```
|
||||
|
||||
- `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
|
||||
<ul>
|
||||
<li data-field-type="multiv2" data-field-label="Productos">
|
||||
<div data-field-type="textfield" data-field-label="Nombre">
|
||||
Nombre del producto
|
||||
</div>
|
||||
<div data-field-type="textbox" data-field-label="Descripción">
|
||||
Descripción del producto
|
||||
</div>
|
||||
<div class="p-1/6 relative">
|
||||
<img
|
||||
class="absolute top-0 left-0 w-full h-full object-cover lazyload"
|
||||
data-field-type="upload"
|
||||
data-field-label="Imagen"
|
||||
data-lazy="true"
|
||||
data-field-width="800"
|
||||
alt=""
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Uso en Twig — las variables son propiedades del objeto iterado:
|
||||
|
||||
```twig
|
||||
{% for record in productos %}
|
||||
<div class="producto">
|
||||
<h3>{{ record.nombre }}</h3>
|
||||
<p>{{ record.descripcion }}</p>
|
||||
<img src="{{ record.imagen[0].urlPath }}" alt="">
|
||||
</div>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Acai Attributes
|
||||
|
||||
### `c-if` — Conditional Rendering
|
||||
### `c-if` — Renderizado condicional
|
||||
|
||||
```html
|
||||
<!-- Boolean check -->
|
||||
<div c-if="showBanner">Banner content</div>
|
||||
|
||||
<!-- Equality check (uses = not ==) -->
|
||||
<div c-if="layout = 'grid'">Grid layout</div>
|
||||
|
||||
<!-- Variable exists / is not empty -->
|
||||
<!-- Verificar existencia de variable -->
|
||||
<div c-if="subtitle">{{ subtitle }}</div>
|
||||
|
||||
<!-- Comparación de valores (usa = no ==) -->
|
||||
<div c-if="layout = 'grid'">Grid layout</div>
|
||||
```
|
||||
|
||||
### `c-else`
|
||||
|
||||
Must immediately follow the `c-if` element:
|
||||
Debe ir inmediatamente después del elemento `c-if`:
|
||||
|
||||
```html
|
||||
<div c-if="image">
|
||||
@@ -82,91 +201,173 @@ Must immediately follow the `c-if` element:
|
||||
</div>
|
||||
```
|
||||
|
||||
### `c-for` — Iteration
|
||||
### `c-for` — Iteración sobre array
|
||||
|
||||
```html
|
||||
<!-- Over array/multiv2 -->
|
||||
<div c-for="item in record.features">
|
||||
<h3>{{ item.title }}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Over database table -->
|
||||
<div c-for="product in products" c-where="active = 1" c-order="orden ASC" c-limit="6">
|
||||
<h3>{{ product.nombre }}</h3>
|
||||
</div>
|
||||
```
|
||||
|
||||
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
|
||||
<div c-class="{ 'bg-blue-500': isActive, 'text-white': isActive, 'hidden': !showElement }">
|
||||
Content
|
||||
</div>
|
||||
<ul>
|
||||
<li c-for="producto in productos" c-where="'visible=1'" c-order="'num desc'" c-limit="10">
|
||||
{{ producto.title }}
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
### `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) %}
|
||||
<li>{{ producto.title }}</li>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
Dentro del loop: `loop.index` (1-based), `loop.index is odd`, `loop.index is even`
|
||||
|
||||
### `c-class` — Clases CSS condicionales
|
||||
|
||||
```html
|
||||
<!-- Simple -->
|
||||
<div c-class="{ 'text-center': alineacion == '1', 'text-right': alineacion == '2' }">
|
||||
|
||||
<!-- Múltiples condiciones -->
|
||||
<div c-class="{
|
||||
'flex-row-reverse': orden == '1',
|
||||
'cursor-pointer click-a-child': record.enlace_anchor,
|
||||
'rounded-xl': radioborde == '4'
|
||||
}">
|
||||
|
||||
<!-- Con expresiones Twig (loop) -->
|
||||
<div c-class="{
|
||||
'md:order-1': loop.index is odd,
|
||||
'md:pl-6': loop.index is even
|
||||
}">
|
||||
|
||||
<!-- Combinado con clases estáticas -->
|
||||
<div class="flex items-center" c-class="{ 'justify-center': centrado }">
|
||||
```
|
||||
|
||||
### `c-hidden` — Elementos ocultos
|
||||
|
||||
Elemento que no se renderiza pero puede declarar variables builder:
|
||||
|
||||
```html
|
||||
<div c-hidden="true">
|
||||
<input data-field-type="textfield" value="default config value" />
|
||||
<input data-field-type="textfield" data-field-label="Config" value="default" />
|
||||
</div>
|
||||
```
|
||||
|
||||
### `c-required` — Conditional Required Fields
|
||||
### `c-required` — Campos requeridos condicionales
|
||||
|
||||
```html
|
||||
<input type="text" name="company" c-required="userType = 'business'" />
|
||||
<input type="text" name="telefono" c-required="'2' not in camposquitar" placeholder="Teléfono">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Forms (`c-form`)
|
||||
|
||||
Complete form handling with automatic validation, storage, and email sending.
|
||||
## Definiendo variables con `<set>`
|
||||
|
||||
```html
|
||||
<form c-form
|
||||
tableName="'contacto'"
|
||||
<!-- Obtener configuración de la BD -->
|
||||
<set :tienda="'configuracion_tienda' | get('num != 0')[0]"></set>
|
||||
|
||||
<!-- Construir URLs dinámicas -->
|
||||
<set :logo="tienda.logo.0.urlPath ? 'https://' ~ server.HTTP_HOST ~ tienda.logo.0.urlPath : 'https://' ~ server.HTTP_HOST ~ '/template/estandar/images/logo.png'"></set>
|
||||
|
||||
<!-- Twig set para expresiones complejas -->
|
||||
{% 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
|
||||
<module_id :param1="value1" :param2="value2"></module_id>
|
||||
```
|
||||
|
||||
Ejemplo:
|
||||
```html
|
||||
<header_menu :showLogo="true" :menuItems="items"></header_menu>
|
||||
<product_card :product="selectedProduct" :showPrice="true"></product_card>
|
||||
```
|
||||
|
||||
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
|
||||
class="max-w-2xl mx-auto p-6 bg-white rounded-lg shadow"
|
||||
tableName="'solicitudes'"
|
||||
mailRecord="['correos', 'CONTACTO']"
|
||||
sendTo="'admin@domain.com'"
|
||||
sendTo="'contacto@empresa.com'"
|
||||
sendToClient="'email'"
|
||||
captcha="true"
|
||||
honeypot="true"
|
||||
messageOK="'Mensaje enviado correctamente'"
|
||||
messageKO="'Error al enviar el mensaje'"
|
||||
redirect="'/gracias/'"
|
||||
messageOK="'¡Gracias! Te contactaremos pronto'"
|
||||
messageKO="'Por favor, completa todos los campos'"
|
||||
redirect="'/gracias'"
|
||||
attachFiles="true"
|
||||
showImages="true"
|
||||
>
|
||||
<input type="text" name="nombre" required placeholder="Nombre" />
|
||||
<input type="email" name="email" required placeholder="Email" />
|
||||
<textarea name="mensaje" required placeholder="Mensaje"></textarea>
|
||||
<div class="mb-4">
|
||||
<label class="block mb-2">Nombre</label>
|
||||
<input name="nombre" type="text" class="w-full p-2 border rounded" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block mb-2">Email</label>
|
||||
<input name="email" type="text" class="w-full p-2 border rounded" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block mb-2">Mensaje</label>
|
||||
<textarea name="mensaje" class="w-full p-2 border rounded" rows="5" required></textarea>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center">
|
||||
<input name="acepto_politica" type="checkbox" class="mr-2" required>
|
||||
<span>Acepto la política de privacidad</span>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="bg-teal-500 text-white px-6 py-2 rounded hover:bg-teal-600">Enviar</button>
|
||||
<captcha/>
|
||||
<button type="submit">Enviar</button>
|
||||
</form>
|
||||
</c-form>
|
||||
```
|
||||
|
||||
### 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="'<div>...</div>'"` | HTML cabecera del email |
|
||||
| `footer="'<div>...</div>'"` | 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
|
||||
<img class="lazyload" data-src="{{ image[0].urlPath }}" />
|
||||
<!-- or -->
|
||||
<!-- o -->
|
||||
<img data-lazy="true" src="{{ image[0].urlPath }}" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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`
|
||||
|
||||
@@ -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
|
||||
<!-- Good: Tailwind classes -->
|
||||
<div class="flex items-center gap-4 p-6 bg-white rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold text-gray-900">Title</h2>
|
||||
</div>
|
||||
|
||||
<!-- Custom CSS only when needed (animations, complex selectors, etc.) -->
|
||||
```
|
||||
|
||||
### 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
|
||||
<div style="background-color: {{ colordefondo ? colordefondo : 'transparent' }}">
|
||||
<p style="color: {{ colordeltexto ? colordeltexto : '#111827' }}">
|
||||
```
|
||||
|
||||
### Clases utilitarias de Acai
|
||||
|
||||
| Clase | Descripción |
|
||||
|-------|-------------|
|
||||
| `transition3s` | 0.3s smooth transition |
|
||||
| `click-a-child` | Makes parent clickable via child `<a>` 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 `<a>` 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
|
||||
<div id="app-{{ section_id }}">
|
||||
@@ -87,7 +110,7 @@ For complex interactivity, use Vue 3 via CDN with Composition API:
|
||||
<script>
|
||||
const { createApp, ref } = Vue;
|
||||
createApp({
|
||||
delimiters: ['${', '}'], // Avoid conflict with Twig {{ }}
|
||||
delimiters: ['${', '}'], // Evitar conflicto con Twig {{ }}
|
||||
setup() {
|
||||
const message = ref('Hello');
|
||||
const count = ref(0);
|
||||
@@ -98,4 +121,105 @@ createApp({
|
||||
</script>
|
||||
```
|
||||
|
||||
**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 | `<div id="{{section_id}}">` |
|
||||
| `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
|
||||
<div id="{{section_id}}"></div>
|
||||
<section id="id_{{ section_id }}" class="relative">
|
||||
<!-- contenido del módulo -->
|
||||
</section>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Componentes Nativos
|
||||
|
||||
### Carousel (`c-tns-wrapper`)
|
||||
|
||||
```html
|
||||
<div class="c-tns-wrapper" data-responsive="sm:2, md:3, lg:4" data-speed="1000" data-nav="true">
|
||||
<ul class="c-tns-container">
|
||||
<li data-field-type="multiv2" data-field-label="Slides" class="px-2">
|
||||
<!-- contenido del slide -->
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
| 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
|
||||
<div class="c-tns-nav-container absolute bottom-4 left-0 w-full flex justify-center items-end z-20">
|
||||
<div c-for="item in records"
|
||||
class="pointer-events-auto cursor-pointer rounded-full border-2 border-white w-4 h-4 mx-1 bg-black bg-opacity-50">
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Lightbox
|
||||
|
||||
```html
|
||||
<a href="{{ image[0].urlPath }}" class="glightbox" data-gallery="gallery1">
|
||||
<img src="{{ image[0].urlPath | imagec(400) }}" />
|
||||
</a>
|
||||
```
|
||||
|
||||
### Breadcrumb
|
||||
|
||||
```html
|
||||
<breadCrumb class="bg-gray-200 p-3 rounded" c-prevlinks="null"></breadCrumb>
|
||||
```
|
||||
|
||||
### AOS (Animate On Scroll)
|
||||
|
||||
```html
|
||||
<div data-aos="fade-up" data-aos-duration="800">Contenido</div>
|
||||
```
|
||||
|
||||
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
|
||||
<!-- Builder var con lazy loading -->
|
||||
<img data-field-type="upload" data-field-label="Imagen" data-lazy="true" data-field-width="800" alt="">
|
||||
|
||||
<!-- Manual en templates -->
|
||||
<img class="lazyload" data-src="{{ record.imagen[0].urlPath | imagec(800) }}" alt="">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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/<module-id>/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/<module-id>/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
|
||||
<hook result="myVar" endpoint="/hooks/module_id/" :param1="value1" :param2="'string'"></hook>
|
||||
<p>{{ myVar }}</p>
|
||||
```
|
||||
|
||||
**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
|
||||
<?php
|
||||
// Called with: 'hooks/my_hook/' | hook({category: 'electronics', limit: 10})
|
||||
// Available as:
|
||||
$category; // 'electronics'
|
||||
$limit; // 10
|
||||
// Los parámetros se reciben como variables directamente
|
||||
// Ejemplo: Si llamas hook con {param1: 100}, tendrás $param1 = 100
|
||||
|
||||
$resultado = $param1 * 2;
|
||||
|
||||
// Retornar un array (se convierte a JSON)
|
||||
return [
|
||||
"success" => true,
|
||||
"message" => "Valor procesado: " . $resultado,
|
||||
"value" => $resultado
|
||||
];
|
||||
?>
|
||||
```
|
||||
|
||||
### Hook Return Values
|
||||
### Testing Hooks
|
||||
|
||||
Hooks can `echo` or `return` values. When called from Twig or `<hook>` 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
|
||||
<hook result="myVar" endpoint="/hooks/module_id/" :param1="value1" :param2="'string'"></hook>
|
||||
<p>{{ myVar.message }}</p>
|
||||
```
|
||||
|
||||
**Desde Twig:**
|
||||
```twig
|
||||
{% set resultado = 'hooks/mimodulo/' | hook({param1: 100, param2: 'texto'}) %}
|
||||
<p>{{ resultado.message }}</p>
|
||||
```
|
||||
|
||||
**Desde JavaScript:**
|
||||
```js
|
||||
CmsApi.hook('/hooks/mimodulo/', {param1: 100, param2: 'texto'}, (data) => {
|
||||
console.log(data.message);
|
||||
});
|
||||
```
|
||||
|
||||
**Desde otro Hook PHP:**
|
||||
```php
|
||||
<?php
|
||||
$result = hook("/hooks/mimodulo/", ["param1" => 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 | `"<p class=\"font-bold\">Texto</p>"` |
|
||||
| **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
|
||||
<?php
|
||||
// hook.php del módulo "calcular_precio"
|
||||
$precioUnitario = 50;
|
||||
|
||||
if ($tipo === 'mayoreo' && $cantidad > 10) {
|
||||
$precioUnitario *= 0.85; // 15% descuento
|
||||
}
|
||||
|
||||
return [
|
||||
"success" => true,
|
||||
"precioUnitario" => round($precioUnitario, 2),
|
||||
"total" => round($precioUnitario * $cantidad, 2),
|
||||
"descuento" => $tipo === 'mayoreo' ? 15 : 0
|
||||
];
|
||||
?>
|
||||
```
|
||||
|
||||
```html
|
||||
<hook result="precio" endpoint="/hooks/calcular_precio/" :cantidad="10" :tipo="'mayoreo'"></hook>
|
||||
<p>Total: ${{ precio.total }}</p>
|
||||
```
|
||||
|
||||
### Hook con Operaciones de BD
|
||||
|
||||
```php
|
||||
<?php
|
||||
// hook.php del módulo "procesar_compra"
|
||||
$producto = CmsApi::get('productos', "num=$producto_id");
|
||||
|
||||
if (empty($producto)) {
|
||||
return ["success" => 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];
|
||||
?>
|
||||
```
|
||||
|
||||
262
docs/production-patterns.md
Normal file
262
docs/production-patterns.md
Normal file
@@ -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
|
||||
<div c-hidden="true">
|
||||
<div data-field-type="textfield" data-field-label="Color de fondo"></div>
|
||||
<div data-field-type="textfield" data-field-label="Color del pretitulo"></div>
|
||||
<div data-field-type="textfield" data-field-label="Color del titulo"></div>
|
||||
<div data-field-type="list" data-field-label="Color titulo resaltado"
|
||||
data-list-options="|Main color,1|Main color light,2|Main color dark,3|Blanco"></div>
|
||||
<div data-field-type="textfield" data-field-label="Color del subtitulo"></div>
|
||||
</div>
|
||||
|
||||
<div id="{{section_id}}"></div>
|
||||
<section id="id_{{ section_id }}" class="relative"
|
||||
style="background-color: {{ colordefondo ? colordefondo : 'transparent' }}">
|
||||
<div class="container mx-auto max-w-7xl px-6 2xl:px-0 py-10 lg:py-20">
|
||||
<div c-if="pretitulo or titulo or subtitulo" class="mb-10 lg:mb-16">
|
||||
<div c-if="pretitulo" data-field-type="textfield" data-field-label="Pretitulo"
|
||||
class="w-fit mx-auto text-xl md:text-2xl lg:text-3xl text-center px-4 py-2 mb-2"
|
||||
style="color: {{ colordelpretitulo ? colordelpretitulo : '#111827' }}"
|
||||
data-aos="fade-down" data-aos-duration="500"></div>
|
||||
|
||||
<div c-class="{
|
||||
'titulo-main-color': colortituloresaltado is empty,
|
||||
'titulo-main-color-light': colortituloresaltado == '1',
|
||||
'titulo-main-color-dark': colortituloresaltado == '2',
|
||||
'titulo-white': colortituloresaltado == '3'
|
||||
}" style="color: {{ colordeltitulo ? colordeltitulo : '#111827' }}"
|
||||
data-aos="zoom-in" data-aos-duration="500">
|
||||
{% if titulo %}
|
||||
<div data-field-type="headfield" data-field-label="Titulo"
|
||||
class="titulo-kd text-3xl md:text-4xl lg:text-5xl font-semibold text-center"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div c-if="subtitulo" data-field-type="textfield" data-field-label="Subtitulo"
|
||||
class="text-xl md:text-2xl lg:text-3xl text-center mt-2"
|
||||
style="color: {{ colordelsubtitulo ? colordelsubtitulo : '#111827' }}"
|
||||
data-aos="fade-up" data-aos-duration="500"></div>
|
||||
</div>
|
||||
|
||||
<!-- Contenido del módulo aquí -->
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patrón 2: Layout Zigzag/Ajedrez (Imagen + Texto alternado)
|
||||
|
||||
Usa `loop.index is odd/even` para alternar:
|
||||
|
||||
```html
|
||||
<li c-for="record in records" class="w-full py-6">
|
||||
<div class="flex flex-wrap items-center"
|
||||
c-class="{ 'flex-row-reverse': loop.index is even }">
|
||||
<!-- Lado imagen -->
|
||||
<div class="w-full md:w-2/5">
|
||||
<img src="{{ record.imagen[0].urlPath | imagec(800) }}" alt=""
|
||||
class="w-full h-full object-cover rounded-xl">
|
||||
</div>
|
||||
<!-- Lado texto -->
|
||||
<div class="w-full md:w-3/5 px-6">
|
||||
<h3 class="text-2xl font-semibold">{{ record.titulobloque | raw }}</h3>
|
||||
<div class="wysiwyg mt-4">{{ record.textobloque | raw }}</div>
|
||||
<a c-if="record.enlacebloque_anchor" href="{{ record.enlacebloque }}"
|
||||
class="inline-block bg-main-color text-white rounded-xl px-6 py-3 mt-6 transition3s">
|
||||
{{ record.enlacebloque_anchor }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patrón 3: Acordeón FAQ
|
||||
|
||||
```html
|
||||
<li data-field-type="multiv2" data-field-label="Records" data-aos="fade-up" data-aos-duration="800">
|
||||
<div c-if="record.pregunta" class="border-t border-black">
|
||||
<div class="flex py-6">
|
||||
<div class="text-main-color-dark text-2xl font-bold mr-4">{{ loop.index }}.</div>
|
||||
<div class="faq-wrapper w-full">
|
||||
<div class="faq-page select-none flex justify-between items-center cursor-pointer text-2xl font-medium">
|
||||
<div data-field-type="textfield" data-field-label="Pregunta" class="flex-1 pr-[10%]"></div>
|
||||
<span class="faq-icon w-12 h-12 ml-4"></span>
|
||||
</div>
|
||||
<div class="faq-body hidden py-4" data-field-type="wysiwyg" data-field-label="Respuesta"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
```
|
||||
|
||||
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 :tienda="'configuracion_tienda' | get('num != 0')[0]"></set>
|
||||
<set :logo="tienda.logo.0.urlPath ? 'https://' ~ server.HTTP_HOST ~ tienda.logo.0.urlPath : 'https://' ~ server.HTTP_HOST ~ '/template/estandar/images/logo.png'"></set>
|
||||
{% set imagen = '<img src="' ~ logo ~ '" style="max-height:150px; display: block; margin: 0 auto;">' %}
|
||||
{% set gracias = 'apartados' | get('num = 20').0 %}
|
||||
|
||||
<c-form method="post"
|
||||
mailRecord="['correos', 'CONTACTO']"
|
||||
honeypot="true"
|
||||
header="imagen"
|
||||
sendToClient="'email'"
|
||||
tableName="'solicitudes'"
|
||||
redirect="gracias.enlace"
|
||||
class="text-black lg:text-lg max-w-2xl mx-auto">
|
||||
|
||||
<input type="text" name="nombre" required
|
||||
placeholder="{{ 'Nombre' | translate }}"
|
||||
class="w-full bg-white border border-neutral-400 rounded-xl px-6 py-2 my-1" />
|
||||
<input type="email" name="email" required
|
||||
placeholder="{{ 'Email' | translate }}"
|
||||
class="w-full bg-white border border-neutral-400 rounded-xl px-6 py-2 my-1" />
|
||||
<input type="text" name="telefono" required
|
||||
placeholder="{{ 'Teléfono' | translate }}"
|
||||
class="w-full bg-white border border-neutral-400 rounded-xl px-6 py-2 my-1" />
|
||||
<textarea name="comentario" cols="30" rows="5"
|
||||
placeholder="{{ 'Escribe aquí tu comentario' | translate }}..."
|
||||
class="w-full bg-white border border-neutral-400 rounded-xl resize-none px-6 py-2 my-1"></textarea>
|
||||
|
||||
<label class="w-full flex items-start mt-4">
|
||||
<input required type="checkbox" class="mt-1" />
|
||||
<span class="text-xs sm:text-sm ml-3">{{ 'Acepto las condiciones legales' | translate | raw }}</span>
|
||||
</label>
|
||||
|
||||
<captcha class="mt-8"></captcha>
|
||||
|
||||
<div class="flex justify-center mt-10">
|
||||
<button type="submit"
|
||||
class="bg-main-color hover:bg-white text-white hover:text-main-color-dark font-semibold rounded-xl border-2 border-main-color-dark transition3s px-6 py-3">
|
||||
{{ 'Enviar' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</c-form>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patrón 5: Compartir en Redes Sociales
|
||||
|
||||
```html
|
||||
<ul class="flex flex-wrap -mx-1 mt-4">
|
||||
<li class="w-1/2 sm:w-1/3 lg:w-1/5 p-1">
|
||||
<a href="https://www.facebook.com/sharer.php?u=https://{{ server.HTTP_HOST }}{{ thisrecord.enlace }}"
|
||||
target="_blank" rel="noopener">
|
||||
<div class="flex items-center bg-[#306199] text-white rounded-md px-4 py-2.5">Facebook</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="w-1/2 sm:w-1/3 lg:w-1/5 p-1">
|
||||
<a href="https://wa.me/?text={{ thisrecord.name }} - https://{{ server.HTTP_HOST }}{{ thisrecord.enlace }}"
|
||||
target="_blank" rel="noopener">
|
||||
<div class="flex items-center bg-[#03c100] text-white rounded-md px-4 py-2.5">WhatsApp</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patrón 6: Sección General — Detalle de Producto
|
||||
|
||||
```html
|
||||
<set :tienda="'configuracion_tienda' | get('num !=0')[0]"></set>
|
||||
|
||||
<section class="detalle-producto">
|
||||
<div class="container mx-auto max-w-7xl px-6 2xl:px-0 mt-20 mb-10">
|
||||
<!-- Imagen con lightbox -->
|
||||
<div class="relative p-1/5 rounded-xl overflow-hidden" data-aos="zoom-in" data-aos-duration="800">
|
||||
<a href="{{ thisrecord.foto.0.urlPath }}" class="glightbox">
|
||||
<img src="{{ thisrecord.foto.0.urlPath | imagec(800) }}" alt="{{ thisrecord.name }}"
|
||||
class="absolute top-0 left-0 w-full h-full object-cover">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h1 class="text-2xl font-semibold mt-6">{{ thisrecord.name }}</h1>
|
||||
<span class="text-lg text-gray-600">{{ thisrecord.categoria_bd.0.name }}</span>
|
||||
|
||||
<!-- Precio con descuento -->
|
||||
<div c-if="thisrecord.precio_descuento" class="flex items-center mt-4">
|
||||
<div class="text-red-500 text-xl line-through">{{ thisrecord.precio }} €</div>
|
||||
<div class="text-2xl font-semibold ml-4">{{ thisrecord.precio_descuento }} €</div>
|
||||
</div>
|
||||
<div c-else class="text-2xl font-semibold mt-4">{{ thisrecord.precio }} €</div>
|
||||
|
||||
<div c-if="thisrecord.descripcion" class="wysiwyg mt-6">{{ thisrecord.descripcion | raw }}</div>
|
||||
|
||||
<!-- Galería secundaria -->
|
||||
<div c-if="thisrecord.otras_fotos" class="flex flex-wrap -mx-1 mt-8">
|
||||
<div c-for="foto in thisrecord.otras_fotos" class="w-1/3 md:w-1/4 p-1">
|
||||
<a href="{{ foto.urlPath }}" class="glightbox">
|
||||
<img src="{{ foto.urlPath | imagec(400) }}" alt="{{ thisrecord.name }}" class="w-full h-40 object-cover rounded">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Productos relacionados -->
|
||||
{% set productosRelacionados = 'productos' | get('categoria = ' ~ thisrecord.categoria ~ ' and num!=' ~ thisrecord.num, 'globalOrder ASC', '3') %}
|
||||
<section c-if="productosRelacionados" class="py-20 bg-gray-100">
|
||||
<div class="container mx-auto max-w-7xl px-6 2xl:px-0">
|
||||
<h2 class="text-3xl text-center mb-10">{{ 'Productos relacionados' | translate }}</h2>
|
||||
<ul class="flex flex-wrap -mx-4">
|
||||
<li c-for="producto in productosRelacionados" class="w-full sm:w-1/2 lg:w-1/3 px-4">
|
||||
<bloqueproducto_i7aunn :producto="producto"></bloqueproducto_i7aunn>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patrón 7: Galería con Carousel (modo Gallery)
|
||||
|
||||
```html
|
||||
<div class="c-tns-wrapper" data-autoplay-timeout="8000" data-mode="gallery" data-speed="400" data-nav="true">
|
||||
<ul class="c-tns-container">
|
||||
<li data-field-type="uploadMulti" data-field-label="Imagenes" data-field-info1="titulo">
|
||||
<div class="relative min-h-screen">
|
||||
<img class="absolute top-0 left-0 w-full h-full object-cover lazyload"
|
||||
data-src="{{ uploadMulti.urlPath | imagec(2100) }}"
|
||||
alt="{{ uploadMulti.info1 }}">
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="pointer-events-none c-tns-nav-container absolute bottom-10 left-0 w-full h-full flex justify-center items-end z-20">
|
||||
<div c-for="imagen in imagenes"
|
||||
class="select-none pointer-events-auto transition3s flex items-center bg-white bg-opacity-50 cursor-pointer rounded-full w-4 lg:w-5 h-4 lg:h-5 mx-1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
85
docs/quick-reference.md
Normal file
85
docs/quick-reference.md
Normal file
@@ -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` | `<p>` | String |
|
||||
| `headfield` | `<h1>`-`<h6>` | String + var `_tag` |
|
||||
| `textbox` | `<div>` | String multilínea |
|
||||
| `wysiwyg` | `<div class="wysiwyg">` | HTML string |
|
||||
| `link` | `<a>` | URL string |
|
||||
| `upload` | `<img>` | Array de `{urlPath, info1}` |
|
||||
| `uploadMulti` | `<li>` | Itera archivos subidos |
|
||||
| `list` (fijo) | `<div data-list-options="...">` | Valor seleccionado |
|
||||
| `list` (tabla) | `<div data-list-table="...">` | `num` del registro |
|
||||
| `multiv2` | `<li>` wrapper | Array de objetos |
|
||||
|
||||
## Acai HTML Attributes
|
||||
|
||||
| Atributo | Uso | Ejemplo |
|
||||
|----------|-----|---------|
|
||||
| `c-if` | Condicional | `<p c-if="activo = 1">` |
|
||||
| `c-else` | Rama else | `<p c-else>` |
|
||||
| `c-for` | Loop array | `<li c-for="item in items">` |
|
||||
| `c-for` | Loop tabla | `<li c-for="p in productos" c-where="'activo=1'" c-limit="10">` |
|
||||
| `c-hidden` | Variable oculta | `<p c-hidden="true" data-field-type="textfield">` |
|
||||
| `c-class` | Clase condicional | `<div c-class="{ 'bg-red': color == '1' }">` |
|
||||
| `c-form` | Formulario | `<c-form tableName="'contacto'" captcha="true">` |
|
||||
|
||||
## 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 | `"<p class=\"font-bold\">Texto</p>"` |
|
||||
| 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) |
|
||||
@@ -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) %}
|
||||
<h3>{{ producto.titulo }}</h3>
|
||||
{% 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'}) %}
|
||||
<p>Total: ${{ result.total }}</p>
|
||||
```
|
||||
|
||||
### `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 #}
|
||||
<img src="{{ record.image[0].urlPath | imagec(400) }}" />
|
||||
|
||||
{# En srcset #}
|
||||
<img src="{{ record.image[0].urlPath | imagec(800) }}"
|
||||
srcset="{{ record.image[0].urlPath | imagec(400) }} 400w,
|
||||
{{ record.image[0].urlPath | imagec(800) }} 800w" />
|
||||
```
|
||||
|
||||
## `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 #}
|
||||
<img src="{{ record.image[0].urlPath | imagec(400) }}" />
|
||||
---
|
||||
|
||||
{# In srcset #}
|
||||
<img src="{{ record.image[0].urlPath | imagec(800) }}"
|
||||
srcset="{{ record.image[0].urlPath | imagec(400) }} 400w,
|
||||
{{ record.image[0].urlPath | imagec(800) }} 800w" />
|
||||
```
|
||||
## 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) %}
|
||||
<div class="producto-card">
|
||||
<img src="{{ producto.imagen[0].urlPath | imagec(400) }}" alt="{{ producto.titulo }}">
|
||||
<h3>{{ producto.titulo }}</h3>
|
||||
<p>{{ producto.descripcion | truncate(100) }}</p>
|
||||
|
||||
{% set stock = 'stocks' | get('producto_num=' ~ producto.num) %}
|
||||
<span>Stock: {{ stock[0].cantidad }}</span>
|
||||
</div>
|
||||
{% 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'}) %}
|
||||
|
||||
<h1>{{ stats.titulo | translate }}</h1>
|
||||
|
||||
<nav>
|
||||
{% for cat in categorias %}
|
||||
<a href="">{{ cat.nombre }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
|
||||
{% for prod in productos %}
|
||||
<div>
|
||||
<img src="{{ prod.imagen[0].urlPath | imagec(300) }}" alt="">
|
||||
<h3>{{ prod.titulo }}</h3>
|
||||
</div>
|
||||
{% 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)`
|
||||
|
||||
695
docs/vue-builder-examples.md
Normal file
695
docs/vue-builder-examples.md
Normal file
@@ -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: '<svg>...</svg>' },
|
||||
{ id: "imagenes", label: "Imágenes", color: "#10b981", icon: '<svg>...</svg>' },
|
||||
{ id: "textos", label: "Textos", color: "#3b82f6", icon: '<svg>...</svg>' },
|
||||
{ id: "enlaces", label: "Enlaces", color: "#ef4444", icon: '<svg>...</svg>' },
|
||||
{ id: "colores", label: "Colores", color: "#8b5cf6", icon: '<svg>...</svg>' }
|
||||
]
|
||||
```
|
||||
|
||||
### 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: {
|
||||
'': '<svg>...(icon-tabler-x)</svg>',
|
||||
'1': '<svg>...(icon-tabler-check)</svg>'
|
||||
},
|
||||
iconosTipoImagen: {
|
||||
'': '<svg>...(icon-tabler-photo)</svg>',
|
||||
'1': '<svg>...(icon-tabler-video)</svg>'
|
||||
},
|
||||
iconosOverlay: {
|
||||
'': '<svg>...(icon-tabler-square)</svg>',
|
||||
'1': '<svg>...(icon-tabler-gradient)</svg>'
|
||||
}
|
||||
```
|
||||
|
||||
### Código completo
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div v-if="data">
|
||||
<acai-vue-tabs v-if="data" :tabs="tabsConfig" :storage-key="'banner-slideshow-tabs-' + (section_id || 'default')" :apply-theme-styles="true">
|
||||
|
||||
<!-- TAB: CONFIGURACIÓN -->
|
||||
<template #configuracion="{ color }">
|
||||
<div class="w-full mb-6">
|
||||
<p class="text-xl font-semibold text-gray-800">Ajustes generales del banner</p>
|
||||
</div>
|
||||
|
||||
<!-- Lado texto -->
|
||||
<div class="flex w-full items-center">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 flex-shrink-0 mr-4 stroke-current" :style="{ color: color }" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M3 3m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M9 15h-2" /><path d="M13 12h-6" /><path d="M11 9h-4" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Posición del texto :</b> Define la alineación del contenido dentro del banner.</p>
|
||||
</div>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-selectv2 :builder="builder" :data="data" :field="'ladotexto'" @save-data="saveData"></acai-vue-selectv2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ver sombra -->
|
||||
<div class="w-full items-center mt-6">
|
||||
<div class="w-full flex items-center">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-shadow"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M13 12h5" /><path d="M13 15h4" /><path d="M13 18h1" /><path d="M13 9h4" /><path d="M13 6h1" /></svg>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<acai-vue-selectv2 :builder="builder" :data="data" :field="'versombra'" :toggle-icons="iconosSombra" @save-data="saveData"></acai-vue-selectv2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Sombra en textos :</b> Aplica un efecto de sombra a los textos del banner.</p>
|
||||
</div>
|
||||
|
||||
<!-- Container -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-arrow-autofit-width"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 12v-6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v6" /><path d="M10 18h-7" /><path d="M21 18h-7" /><path d="M6 15l-3 3l3 3" /><path d="M18 15l3 3l-3 3" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Ancho del contenedor :</b> Limita el ancho máximo del contenido textual.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> por defecto ocupa todo el ancho disponible (Full container).</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-selectv2 :builder="builder" :data="data" :field="'container'" @save-data="saveData"></acai-vue-selectv2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Altura banner -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-arrow-autofit-height"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 20h-6a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h6" /><path d="M18 14v7" /><path d="M18 3v7" /><path d="M15 18l3 3l3 -3" /><path d="M15 6l3 -3l3 3" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Altura del banner :</b> Altura visible de la sección del banner.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> por defecto es pantalla completa (100vh).</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-selectv2 :builder="builder" :data="data" :field="'alturadelbanner'" @save-data="saveData"></acai-vue-selectv2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- TAB: IMÁGENES -->
|
||||
<template #imagenes="{ color }">
|
||||
<div class="w-full mb-6">
|
||||
<p class="text-xl font-semibold text-gray-800">Fondo y elementos visuales del banner</p>
|
||||
</div>
|
||||
|
||||
<!-- Tipo de imagen (selector imagen/video) -->
|
||||
<div class="w-full items-center mt-6">
|
||||
<div class="w-full flex items-center">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 stroke-current icon icon-tabler icons-tabler-outline icon-tabler-photo-video"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 15h-3a3 3 0 0 1 -3 -3v-6a3 3 0 0 1 3 -3h6a3 3 0 0 1 3 3v3" /><path d="M9 12a3 3 0 0 1 3 -3h6a3 3 0 0 1 3 3v6a3 3 0 0 1 -3 3h-6a3 3 0 0 1 -3 -3l0 -6" /><path d="M3 12l2.296 -2.296a2.41 2.41 0 0 1 3.408 0l.296 .296" /><path d="M14 13.5v3l2.5 -1.5l-2.5 -1.5" /><path d="M7 6v.01" /></svg>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<acai-vue-selectv2 :builder="builder" :data="data" :field="'tipodeimagen'" :toggle-icons="iconosTipoImagen" @save-data="saveData"></acai-vue-selectv2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Tipo de fondo :</b> Selecciona si el fondo del banner será una imagen o un vídeo.</p>
|
||||
</div>
|
||||
|
||||
<!-- Imágenes (visible cuando es imagen o vacío) -->
|
||||
<div class="flex w-full items-center mt-6" v-show="data.tipodeimagen && data.tipodeimagen.newValues.builder_custom.value == ''">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" :style="{ color: color }" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 stroke-current"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M15 6l.01 0" /><path d="M3 6a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3l0 -8" /><path d="M3 13l4 -4a3 5 0 0 1 3 0l4 4" /><path d="M13 12l2 -2a3 5 0 0 1 3 0l3 3" /><path d="M8 21l.01 0" /><path d="M12 21l.01 0" /><path d="M16 21l.01 0" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Imágenes :</b> Añade las imágenes que rotarán en el slideshow del banner.</p>
|
||||
</div>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-upload ref="upload_imagenes" :reference="'upload_imagenes'" :tablename="'builder_custom'" :fieldname="builder.vars.imagenes.relations.builder_custom" :recordnum="data.imagenes.recordNum" :field="data.imagenes" :builder_field="builder.vars.imagenes" :presavetempid="data.imagenes.preSaveTempId" :add_button="true" @add_button_click="$parent.openCute('imagenes',data,false,'upload_imagenes')" class="border-2 px-3 py-2 border-gray-600 rounded-lg shadow bg-gray-200"></acai-vue-upload>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Video (visible cuando es video) -->
|
||||
<div class="flex w-full items-center mt-6" v-show="data.tipodeimagen && data.tipodeimagen.newValues.builder_custom.value == '1'">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-video"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 10l4.553 -2.276a1 1 0 0 1 1.447 .894v6.764a1 1 0 0 1 -1.447 .894l-4.553 -2.276v-4z" /><path d="M3 6m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Vídeo :</b> Sube el vídeo de fondo del banner.</p>
|
||||
</div>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-upload ref="upload_video" :reference="'upload_video'" :tablename="'builder_custom'" :fieldname="builder.vars.video.relations.builder_custom" :recordnum="data.video.recordNum" :field="data.video" :builder_field="builder.vars.video" :presavetempid="data.video.preSaveTempId" :add_button="true" @add_button_click="$parent.openCute('video',data,false,'upload_video')" class="border-2 px-3 py-2 border-gray-600 rounded-lg shadow bg-gray-200"></acai-vue-upload>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logo -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-icons"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M6.5 6.5m-3.5 0a3.5 3.5 0 1 0 7 0a3.5 3.5 0 1 0 -7 0" /><path d="M2.5 21h8l-4 -7z" /><path d="M14 3l7 7" /><path d="M14 10l7 -7" /><path d="M14 14h7v7h-7z" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Logo :</b> Imagen del logotipo que aparecerá sobre el banner.</p>
|
||||
</div>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-upload ref="upload_logo" :reference="'upload_logo'" :tablename="'builder_custom'" :fieldname="builder.vars.logo.relations.builder_custom" :recordnum="data.logo.recordNum" :field="data.logo" :builder_field="builder.vars.logo" :presavetempid="data.logo.preSaveTempId" :add_button="true" @add_button_click="$parent.openCute('logo',data,false,'upload_logo')" class="border-2 px-3 py-2 border-gray-600 rounded-lg shadow bg-gray-200"></acai-vue-upload>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tipo de overlay -->
|
||||
<div class="w-full items-center mt-6">
|
||||
<div class="w-full flex items-center">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-background"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 8l4 -4" /><path d="M14 4l-10 10" /><path d="M4 20l16 -16" /><path d="M20 10l-10 10" /><path d="M20 16l-4 4" /></svg>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<acai-vue-selectv2 :builder="builder" :data="data" :field="'tipodeoverlay'" :toggle-icons="iconosOverlay" @save-data="saveData"></acai-vue-selectv2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Tipo de overlay :</b> Elige si la capa de color se aplica de forma uniforme o con degradado.</p>
|
||||
</div>
|
||||
|
||||
<!-- Color del overlay -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-test-pipe"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M20 8.04l-12.122 12.124a2.857 2.857 0 1 1 -4.041 -4.04l12.122 -12.124" /><path d="M7 13h8" /><path d="M19 15l1.5 1.6a2 2 0 1 1 -3 0l1.5 -1.6" /><path d="M15 3l6 6" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Color del overlay :</b> Color y opacidad de la capa que se superpone sobre la imagen o vídeo.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> por defecto el overlay es transparente.</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-colorpicker :builder="builder" :data="data" :field="'colordeloverlay'" :label="'Color overlay'" :color="'transparent'" @save-data="saveData"></acai-vue-colorpicker>
|
||||
</div>
|
||||
<div style="display: none">
|
||||
<acai-vue-textfield :builder="builder" :data="data" :field="'colordeloverlay'" @save-data="saveData"></acai-vue-textfield>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<!-- TAB: TEXTOS -->
|
||||
<template #textos="{ color }">
|
||||
<div class="w-full mb-6">
|
||||
<p class="text-xl font-semibold text-gray-800">Contenido textual del banner</p>
|
||||
</div>
|
||||
|
||||
<!-- Pretítulo -->
|
||||
<div class="flex w-full items-center">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-text-size"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 7v-2h13v2" /><path d="M10 5v14" /><path d="M12 19h-4" /><path d="M15 13v-1h6v1" /><path d="M18 12v7" /><path d="M17 19h2" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Pretítulo :</b> Texto que aparece encima del título principal.</p>
|
||||
</div>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-textfield :builder="builder" :data="data" :field="'pretitulo'" :placeholder="'Ej: Bienvenidos'" @save-data="saveData"></acai-vue-textfield>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Título -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 flex-shrink-0 mr-4 stroke-current" :style="{ color: color }" viewBox="0 0 24 24" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M7 12h10" /><path d="M7 4v16" /><path d="M17 4v16" /><path d="M15 20h4" /><path d="M15 4h4" /><path d="M5 20h4" /><path d="M5 4h4" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Título :</b> Encabezado principal del banner.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-600 font-light mt-2"><b class="text-black">Recuerda :</b> utiliza las etiquetas <span class="text-black font-semibold"><span> </span></span> para resaltar las palabras clave.</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-title :builder="builder" :data="data" :field="'titulo'" placeholder="Título del banner" @save-data="saveData"></acai-vue-title>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subtítulo -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-text-size"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 7v-2h13v2" /><path d="M10 5v14" /><path d="M12 19h-4" /><path d="M15 13v-1h6v1" /><path d="M18 12v7" /><path d="M17 19h2" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Subtítulo :</b> Texto que aparece debajo del título principal.</p>
|
||||
</div>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-textfield :builder="builder" :data="data" :field="'subtitulo'" :placeholder="'Ej: Tu solución ideal'" @save-data="saveData"></acai-vue-textfield>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- TAB: ENLACES -->
|
||||
<template #enlaces="{ color }">
|
||||
<div class="w-full mb-6">
|
||||
<p class="text-xl font-semibold text-gray-800">Botón de llamada a la acción</p>
|
||||
</div>
|
||||
|
||||
<!-- Enlace -->
|
||||
<div class="flex w-full items-center">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 flex-shrink-0 mr-4 stroke-current" :style="{ color: color }" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentcolor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M9 15l6 -6" /><path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" /><path d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Enlace :</b> Configura el botón con su texto y destino.</p>
|
||||
</div>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-linkv2 :builder="builder" :data="data" :field="'enlace'" @save-data="saveData" :show_text="true"></acai-vue-linkv2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Radio borde enlace -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 stroke-current icon icon-tabler icons-tabler-outline icon-tabler-border-radius"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 12v-4a4 4 0 0 1 4 -4h4" /><path d="M16 4l0 .01" /><path d="M20 4l0 .01" /><path d="M20 8l0 .01" /><path d="M20 12l0 .01" /><path d="M4 16l0 .01" /><path d="M20 16l0 .01" /><path d="M4 20l0 .01" /><path d="M8 20l0 .01" /><path d="M12 20l0 .01" /><path d="M16 20l0 .01" /><path d="M20 20l0 .01" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Radio del borde :</b> Redondeo de las esquinas del botón de enlace.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> por defecto es 'sm' (ligeramente redondeado).</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-selectv2 :builder="builder" :data="data" :field="'radiobordeenlace'" @save-data="saveData"></acai-vue-selectv2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- TAB: COLORES -->
|
||||
<template #colores="{ color }">
|
||||
<div class="w-full mb-6">
|
||||
<p class="text-xl font-semibold text-gray-800">Personalización de colores</p>
|
||||
</div>
|
||||
|
||||
<!-- Color del texto -->
|
||||
<div class="flex w-full items-center">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 flex-shrink-0 mr-4 stroke-current" :style="{ color: color }" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 21a9 9 0 0 1 0 -18c4.97 0 9 3.582 9 8c0 1.06 -.474 2.078 -1.318 2.828c-.844 .75 -1.989 1.172 -3.182 1.172h-2.5a2 2 0 0 0 -1 3.75a1.3 1.3 0 0 1 -1 2.25" /><path d="M8.5 10.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M12.5 7.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M16.5 10.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Color del texto :</b> Color general de todos los textos del banner.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> por defecto es blanco (#ffffff).</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-colorpicker :builder="builder" :data="data" :field="'colordeltexto'" :label="'Color del texto'" :color="'#ffffff'" @save-data="saveData"></acai-vue-colorpicker>
|
||||
</div>
|
||||
<div style="display: none">
|
||||
<acai-vue-textfield :builder="builder" :data="data" :field="'colordeltexto'" @save-data="saveData"></acai-vue-textfield>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</acai-vue-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
props: ["active", "section_id"],
|
||||
data() {
|
||||
return {
|
||||
data: null,
|
||||
builder: null,
|
||||
idiomas: IDIOMAS,
|
||||
tabsConfig: [
|
||||
{ id: "configuracion", label: "Configuración", color: "#f59e0b", icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"/><path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0"/></svg>' },
|
||||
{ id: "imagenes", label: "Imágenes", color: "#10b981", icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 8h.01" /><path d="M11 20h-4a3 3 0 0 1 -3 -3v-10a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v4" /><path d="M4 15l4 -4c.928 -.893 2.072 -.893 3 0l3 3" /><path d="M14 14l1 -1c.31 -.298 .644 -.497 .987 -.596" /><path d="M18.42 15.61a2.1 2.1 0 0 1 2.97 2.97l-3.39 3.42h-3v-3l3.42 -3.39" /></svg>' },
|
||||
{ id: "textos", label: "Textos", color: "#3b82f6", icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19 10h-14" /><path d="M5 6h14" /><path d="M14 14h-9" /><path d="M5 18h6" /><path d="M18 15v6" /><path d="M15 18h6" /></svg>' },
|
||||
{ id: "enlaces", label: "Enlaces", color: "#ef4444", icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 15l6 -6"/><path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464"/><path d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463"/></svg>' },
|
||||
{ id: "colores", label: "Colores", color: "#8b5cf6", icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19 3h-4a2 2 0 0 0 -2 2v12a4 4 0 0 0 8 0v-12a2 2 0 0 0 -2 -2" /><path d="M13 7.35l-2 -2a2 2 0 0 0 -2.828 0l-2.828 2.828a2 2 0 0 0 0 2.828l9 9" /><path d="M7.3 13h-2.3a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h12" /><path d="M17 17l0 .01" /></svg>' },
|
||||
],
|
||||
iconosSombra: {
|
||||
'': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-shadow-off"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5.634 5.638a9 9 0 0 0 12.728 12.727m1.68 -2.32a9 9 0 0 0 -12.086 -12.088" /><path d="M16 12h2" /><path d="M13 15h2" /><path d="M13 18h1" /><path d="M13 9h4" /><path d="M13 6h1" /><path d="M3 3l18 18" /></svg>',
|
||||
'1': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-shadow"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M13 12h5" /><path d="M13 15h4" /><path d="M13 18h1" /><path d="M13 9h4" /><path d="M13 6h1" /></svg>'
|
||||
},
|
||||
iconosTipoImagen: {
|
||||
'': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-photo"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 8h.01" /><path d="M3 6a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3v-12z" /><path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l5 5" /><path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0l3 3" /></svg>',
|
||||
'1': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-video"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 10l4.553 -2.276a1 1 0 0 1 1.447 .894v6.764a1 1 0 0 1 -1.447 .894l-4.553 -2.276v-4z" /><path d="M3 6m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z" /></svg>'
|
||||
},
|
||||
iconosOverlay: {
|
||||
'': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-square"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 3m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /></svg>',
|
||||
'1': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-gradient"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 3m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M7 3v18" /><path d="M3 14h4" /><path d="M3 10h4" /><path d="M3 6h4" /><path d="M3 18h4" /></svg>'
|
||||
}
|
||||
};
|
||||
},
|
||||
components: {
|
||||
'acai-vue-tabs': httpVueLoader('https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivuetabs.vue?timestamp=' + new Date().getTime()),
|
||||
"acai-vue-selectv2": httpVueLoader("https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivueselect.vue?timestamp=" + new Date().getTime()),
|
||||
"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-title": httpVueLoader("https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivuetitle.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-textfield": httpVueLoader("https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivuetextfield.vue?timestamp=" + new Date().getTime()),
|
||||
},
|
||||
mounted() { this.$emit("child-mounted"); },
|
||||
methods: { saveData() { this.$emit("save-data"); } },
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ejemplo 2: [Módulo de texto genérico]
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<acai-vue-tabs v-if="data" :tabs="tabsConfig" :storage-key="'texto-cabecera-tabs-' + (section_id || 'default')" :apply-theme-styles="true">
|
||||
|
||||
<!-- Tab Configuración -->
|
||||
<template #config="{ color }">
|
||||
<div class="w-full mb-6">
|
||||
<p class="text-xl font-semibold text-gray-800">Configuración general del módulo</p>
|
||||
</div>
|
||||
|
||||
<!-- Formato (2 opciones = toggle) -->
|
||||
<div class="w-full items-center mt-6">
|
||||
<div class="w-full flex items-center">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-layout"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v1a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z" /><path d="M4 13m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v3a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z" /><path d="M14 4m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z" /></svg>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<acai-vue-selectv2 :builder="builder" :data="data" :field="'formato'" :toggle-icons="iconosFormato" @save-data="saveData"></acai-vue-selectv2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Formato :</b> Vertical (todo en una columna) u Horizontal (cabecera y texto en 2 columnas).</p>
|
||||
</div>
|
||||
|
||||
<!-- Alineación texto -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-align-justified"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 6l16 0" /><path d="M4 12l16 0" /><path d="M4 18l12 0" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Alineación texto :</b> Alineación del contenido.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> Por defecto es izquierda.</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-selectv2 :builder="builder" :data="data" :field="'alineaciontexto'" @save-data="saveData"></acai-vue-selectv2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Container texto -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-arrow-autofit-width"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 12v-6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v6" /><path d="M10 18h-7" /><path d="M21 18h-7" /><path d="M6 15l-3 3l3 3" /><path d="M18 15l3 3l-3 3" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Container texto :</b> Ancho máximo del contenido.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> Por defecto ocupa el ancho completo.</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-selectv2 :builder="builder" :data="data" :field="'containertexto'" @save-data="saveData"></acai-vue-selectv2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estilo enlace -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-click"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 12l3 0" /><path d="M12 3l0 3" /><path d="M7.8 7.8l-2.2 -2.2" /><path d="M16.2 7.8l2.2 -2.2" /><path d="M7.8 16.2l-2.2 2.2" /><path d="M12 12l9 3l-4 2l-2 4l-3 -9" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Estilo enlace :</b> Estilo visual del botón.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> Por defecto usa el color principal (Main color).</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-selectv2 :builder="builder" :data="data" :field="'estiloenlace'" @save-data="saveData"></acai-vue-selectv2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Radio borde enlace -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 stroke-current icon icon-tabler icons-tabler-outline icon-tabler-border-radius"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 12v-4a4 4 0 0 1 4 -4h4" /><path d="M16 4l0 .01" /><path d="M20 4l0 .01" /><path d="M20 8l0 .01" /><path d="M20 12l0 .01" /><path d="M4 16l0 .01" /><path d="M20 16l0 .01" /><path d="M4 20l0 .01" /><path d="M8 20l0 .01" /><path d="M12 20l0 .01" /><path d="M16 20l0 .01" /><path d="M20 20l0 .01" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Radio borde enlace :</b> Redondeo del botón.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> Por defecto es "sm".</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-selectv2 :builder="builder" :data="data" :field="'radiobordeenlace'" @save-data="saveData"></acai-vue-selectv2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<!-- Tab Textos -->
|
||||
<template #textos="{ color }">
|
||||
<div class="w-full mb-6">
|
||||
<p class="text-xl font-semibold text-gray-800">Contenido textual del módulo</p>
|
||||
</div>
|
||||
|
||||
<!-- Pretítulo -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-text-size"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 7v-2h13v2" /><path d="M10 5v14" /><path d="M12 19h-4" /><path d="M15 13v-1h6v1" /><path d="M18 12v7" /><path d="M17 19h2" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Pretítulo :</b> Texto que aparece encima del título principal.</p>
|
||||
</div>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-textfield :builder="builder" :data="data" :field="'pretitulo'" :placeholder="'Ej: Descubre más'" @save-data="saveData"></acai-vue-textfield>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Título -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 stroke-current icon icon-tabler icons-tabler-outline icon-tabler-heading"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 12h10" /><path d="M7 5v14" /><path d="M17 5v14" /><path d="M15 19h4" /><path d="M15 5h4" /><path d="M5 19h4" /><path d="M5 5h4" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Título :</b> Encabezado principal del módulo.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-600 font-light mt-2"><b class="text-black">Recuerda :</b> utiliza las etiquetas <span class="text-black font-semibold"><span> </span></span> para resaltar las palabras clave.</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-title :builder="builder" :data="data" :field="'titulo'" placeholder="Título del módulo" @save-data="saveData"></acai-vue-title>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subtítulo -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-text-size"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 7v-2h13v2" /><path d="M10 5v14" /><path d="M12 19h-4" /><path d="M15 13v-1h6v1" /><path d="M18 12v7" /><path d="M17 19h2" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Subtítulo :</b> Texto que aparece debajo del título principal.</p>
|
||||
</div>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-textfield :builder="builder" :data="data" :field="'subtitulo'" :placeholder="'Ej: Conoce nuestros servicios'" @save-data="saveData"></acai-vue-textfield>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Texto -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-file-text"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14 3v4a1 1 0 0 0 1 1h4" /><path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2" /><path d="M9 9l1 0" /><path d="M9 13l6 0" /><path d="M9 17l6 0" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Texto :</b> Contenido descriptivo principal.</p>
|
||||
</div>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-wysiwyg :builder="builder" :data="data" :field="'texto'" @save-data="saveData"></acai-vue-wysiwyg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<!-- Tab Enlaces -->
|
||||
<template #enlaces="{ color }">
|
||||
<div class="w-full mb-6">
|
||||
<p class="text-xl font-semibold text-gray-800">Enlaces del módulo</p>
|
||||
</div>
|
||||
|
||||
<!-- Enlace -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-link"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 15l6 -6" /><path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" /><path d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Enlace :</b> Botón de acción principal.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> Si no se configura, el botón no se mostrará.</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-linkv2 :builder="builder" :data="data" :field="'enlace'" :show_text="true" @save-data="saveData"></acai-vue-linkv2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<!-- Tab Colores -->
|
||||
<template #colores="{ color }">
|
||||
<div class="w-full mb-6">
|
||||
<p class="text-xl font-semibold text-gray-800">Personalización de colores</p>
|
||||
</div>
|
||||
|
||||
<!-- Color de fondo -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-paint"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z" /><path d="M19 6h1a2 2 0 0 1 2 2a5 5 0 0 1 -5 5l-5 0v2" /><path d="M10 15m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Color de fondo :</b> Color de fondo de toda la sección.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> Por defecto es transparente.</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-colorpicker :builder="builder" :data="data" :field="'colordefondo'" :label="'Color de fondo'" :color="'transparent'" @save-data="saveData"></acai-vue-colorpicker>
|
||||
</div>
|
||||
<div style="display: none">
|
||||
<acai-vue-textfield :builder="builder" :data="data" :field="'colordefondo'" @save-data="saveData"></acai-vue-textfield>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color del pretítulo -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-palette"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 21a9 9 0 0 1 0 -18c4.97 0 9 3.582 9 8c0 1.06 -.474 2.078 -1.318 2.828c-.844 .75 -1.989 1.172 -3.182 1.172h-2.5a2 2 0 0 0 -1 3.75a1.3 1.3 0 0 1 -1 2.25" /><path d="M7.5 10.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M11.5 7.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M15.5 10.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Color del pretítulo :</b> Color del texto del pretítulo.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> Por defecto es #111827.</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-colorpicker :builder="builder" :data="data" :field="'colordelpretitulo'" :label="'Color pretítulo'" :color="'#111827'" @save-data="saveData"></acai-vue-colorpicker>
|
||||
</div>
|
||||
<div style="display: none">
|
||||
<acai-vue-textfield :builder="builder" :data="data" :field="'colordelpretitulo'" @save-data="saveData"></acai-vue-textfield>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color del título -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-palette"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 21a9 9 0 0 1 0 -18c4.97 0 9 3.582 9 8c0 1.06 -.474 2.078 -1.318 2.828c-.844 .75 -1.989 1.172 -3.182 1.172h-2.5a2 2 0 0 0 -1 3.75a1.3 1.3 0 0 1 -1 2.25" /><path d="M7.5 10.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M11.5 7.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M15.5 10.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Color del título :</b> Color del texto del título principal.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> Por defecto es #111827.</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-colorpicker :builder="builder" :data="data" :field="'colordeltitulo'" :label="'Color título'" :color="'#111827'" @save-data="saveData"></acai-vue-colorpicker>
|
||||
</div>
|
||||
<div style="display: none">
|
||||
<acai-vue-textfield :builder="builder" :data="data" :field="'colordeltitulo'" @save-data="saveData"></acai-vue-textfield>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color título resaltado -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-color-swatch"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19 3h-4a2 2 0 0 0 -2 2v12a4 4 0 0 0 8 0v-12a2 2 0 0 0 -2 -2" /><path d="M13 7.35l-2 -2a2 2 0 0 0 -2.828 0l-2.828 2.828a2 2 0 0 0 0 2.828l9 9" /><path d="M7.3 13h-2.3a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h12" /><path d="M17 17l0 .01" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Color título resaltado :</b> Color de las palabras resaltadas con <span>.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> Por defecto usa el color principal (Main color).</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-selectv2 :builder="builder" :data="data" :field="'colortituloresaltado'" @save-data="saveData"></acai-vue-selectv2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color del subtítulo -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-palette"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 21a9 9 0 0 1 0 -18c4.97 0 9 3.582 9 8c0 1.06 -.474 2.078 -1.318 2.828c-.844 .75 -1.989 1.172 -3.182 1.172h-2.5a2 2 0 0 0 -1 3.75a1.3 1.3 0 0 1 -1 2.25" /><path d="M7.5 10.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M11.5 7.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M15.5 10.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Color del subtítulo :</b> Color del texto del subtítulo.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> Por defecto es #111827.</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-colorpicker :builder="builder" :data="data" :field="'colordelsubtitulo'" :label="'Color subtítulo'" :color="'#111827'" @save-data="saveData"></acai-vue-colorpicker>
|
||||
</div>
|
||||
<div style="display: none">
|
||||
<acai-vue-textfield :builder="builder" :data="data" :field="'colordelsubtitulo'" @save-data="saveData"></acai-vue-textfield>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color texto -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-typography"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 20l3 0" /><path d="M14 20l7 0" /><path d="M6.9 15l6.9 0" /><path d="M10.2 6.3l5.8 13.7" /><path d="M5 20l6 -16l2 0l7 16" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Color texto :</b> Color del contenido descriptivo.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> Por defecto es #374151.</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-colorpicker :builder="builder" :data="data" :field="'colortexto'" :label="'Color texto'" :color="'#374151'" @save-data="saveData"></acai-vue-colorpicker>
|
||||
</div>
|
||||
<div style="display: none">
|
||||
<acai-vue-textfield :builder="builder" :data="data" :field="'colortexto'" @save-data="saveData"></acai-vue-textfield>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
</acai-vue-tabs>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
props: ["active", "section_id"],
|
||||
data() {
|
||||
return {
|
||||
data: null,
|
||||
builder: null,
|
||||
idiomas: IDIOMAS,
|
||||
tabsConfig: [
|
||||
{ id: "config", label: "Configuración", color: "#f59e0b", icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"/><path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0"/></svg>' },
|
||||
{ id: "textos", label: "Textos", color: "#3b82f6", icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19 10h-14" /><path d="M5 6h14" /><path d="M14 14h-9" /><path d="M5 18h6" /><path d="M18 15v6" /><path d="M15 18h6" /></svg>' },
|
||||
{ id: "enlaces", label: "Enlaces", color: "#ef4444", icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 15l6 -6" /><path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" /><path d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" /></svg>' },
|
||||
{ id: "colores", label: "Colores", color: "#8b5cf6", icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19 3h-4a2 2 0 0 0 -2 2v12a4 4 0 0 0 8 0v-12a2 2 0 0 0 -2 -2" /><path d="M13 7.35l-2 -2a2 2 0 0 0 -2.828 0l-2.828 2.828a2 2 0 0 0 0 2.828l9 9" /><path d="M7.3 13h-2.3a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h12" /><path d="M17 17l0 .01" /></svg>' }
|
||||
],
|
||||
iconosFormato: {
|
||||
'': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-layout-rows"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z" /><path d="M4 12l16 0" /></svg>',
|
||||
'1': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-layout-columns"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z" /><path d="M12 4l0 16" /></svg>'
|
||||
}
|
||||
};
|
||||
},
|
||||
components: {
|
||||
'acai-vue-tabs': httpVueLoader('https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivuetabs.vue?timestamp=' + new Date().getTime()),
|
||||
'acai-vue-selectv2': httpVueLoader('https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivueselect.vue?timestamp=' + new Date().getTime()),
|
||||
'acai-vue-colorpicker': httpVueLoader('https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivuecolorpicker.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-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-textfield': httpVueLoader('https://cms.cocosolution.com/lib/plugins/builder_saas/tpl/componentes/builder-acaivuetextfield.vue?timestamp=' + new Date().getTime()),
|
||||
},
|
||||
mounted() { this.$emit("child-mounted"); },
|
||||
methods: { saveData() { this.$emit("save-data"); } },
|
||||
};
|
||||
</script>
|
||||
```
|
||||
484
docs/vue-builder-rules.md
Normal file
484
docs/vue-builder-rules.md
Normal file
@@ -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 `<acai-vue-tabs>` para evitar renderizar antes de que los datos estén listos.
|
||||
|
||||
### Template de cada tab:
|
||||
```html
|
||||
<template #idtab="{ color }">
|
||||
<div class="w-full mb-6">
|
||||
<p class="text-xl font-semibold text-gray-800">Título descriptivo del tab</p>
|
||||
</div>
|
||||
<!-- campos -->
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-10 h-10 flex-shrink-0 mr-4 stroke-current" :style="{ color: color }" ...>...</svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Nombre :</b> Descripción.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> valor por defecto.</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<acai-vue-colorpicker :builder="builder" :data="data" :field="'campo'" :label="'Etiqueta'" :color="'#hex'" @save-data="saveData"></acai-vue-colorpicker>
|
||||
</div>
|
||||
<div style="display: none">
|
||||
<acai-vue-textfield :builder="builder" :data="data" :field="'campo'" @save-data="saveData"></acai-vue-textfield>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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 `<acai-vue-records>` (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
|
||||
<p class="text-xs leading-snug text-gray-500 mt-4 ml-14"><b class="text-gray-700">Nota :</b> color por defecto (#hex).</p>
|
||||
<div class="relative mt-1 ml-14">
|
||||
<acai-vue-colorpicker :builder="builder.vars.records" :data="record" :field="'campo'" :label="'Etiqueta'" :color="'#hex'" @save-data="saveData"></acai-vue-colorpicker>
|
||||
</div>
|
||||
<div style="display: none">
|
||||
<acai-vue-textfield :builder="builder.vars.records" :data="record" :field="'campo'" @save-data="saveData"></acai-vue-textfield>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 2.4 Campos tipo `list` para colores
|
||||
Se usan como `<acai-vue-selectv2>` 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
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-10 h-10 flex-shrink-0 mr-4 stroke-current" :style="{ color: color }" ...>...</svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Nombre :</b> Descripción.</p>
|
||||
</div>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> info adicional.</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<!-- componente -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Dentro de records:
|
||||
```html
|
||||
<div class="w-full mt-6">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-10 h-10 flex-shrink-0 mr-4 stroke-current" :style="{ color: color }" ...>...</svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Nombre :</b> Descripción.</p>
|
||||
</div>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<!-- componente con builder.vars.records y record -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
<acai-vue-upload ref="upload_campo" :reference="'upload_campo'" :tablename="'builder_custom'" :fieldname="builder.vars.campo.relations.builder_custom" :recordnum="data.campo.recordNum" :field="data.campo" :builder_field="builder.vars.campo" :presavetempid="data.campo.preSaveTempId" :add_button="true" @add_button_click="$parent.openCute('campo',data,false,'upload_campo')" class="border-2 px-3 py-2 border-gray-600 rounded-lg shadow bg-gray-200"></acai-vue-upload>
|
||||
```
|
||||
|
||||
### En records:
|
||||
```html
|
||||
<acai-vue-upload :ref="'upload_campo_' + builder.vars.records.vars.campo.relations.builder_custom + '_' + record.campo.recordNum" :reference="'upload_campo_' + builder.vars.records.vars.campo.relations.builder_custom + '_' + record.campo.recordNum" :tablename="'builder_custom'" :fieldname="builder.vars.records.vars.campo.relations.builder_custom" :recordnum="record.campo.recordNum" :field="record.campo" :builder_field="builder.vars.records.vars.campo" :presavetempid="record.campo.preSaveTempId" :add_button="true" @add_button_click="$parent.openCute('campo',record,true,'upload_campo_' + builder.vars.records.vars.campo.relations.builder_custom + '_' + record.campo.recordNum)" class="border-2 px-3 py-2 border-gray-600 rounded-lg shadow bg-gray-200"></acai-vue-upload>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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: { '': '<svg>...</svg>', '1': '<svg>...</svg>' }
|
||||
};
|
||||
},
|
||||
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
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 stroke-current icon icon-tabler icons-tabler-outline icon-tabler-photo-video"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 15h-3a3 3 0 0 1 -3 -3v-6a3 3 0 0 1 3 -3h6a3 3 0 0 1 3 3v3" /><path d="M9 12a3 3 0 0 1 3 -3h6a3 3 0 0 1 3 3v6a3 3 0 0 1 -3 3h-6a3 3 0 0 1 -3 -3l0 -6" /><path d="M3 12l2.296 -2.296a2.41 2.41 0 0 1 3.408 0l.296 .296" /><path d="M14 13.5v3l2.5 -1.5l-2.5 -1.5" /><path d="M7 6v.01" /></svg>
|
||||
```
|
||||
- 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: {
|
||||
'': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-photo"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 8h.01" /><path d="M3 6a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3v-12z" /><path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l5 5" /><path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0l3 3" /></svg>',
|
||||
'1': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-video"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 10l4.553 -2.276a1 1 0 0 1 1.447 .894v6.764a1 1 0 0 1 -1.447 .894l-4.553 -2.276v-4z" /><path d="M3 6m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z" /></svg>'
|
||||
}
|
||||
```
|
||||
|
||||
### 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: {
|
||||
'': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-square"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 3m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /></svg>',
|
||||
'1': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-gradient"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 3m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M7 3v18" /><path d="M3 14h4" /><path d="M3 10h4" /><path d="M3 6h4" /><path d="M3 18h4" /></svg>'
|
||||
}
|
||||
```
|
||||
|
||||
### 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: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"/><path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0"/></svg>' },
|
||||
|
||||
{ id: "imagenes", label: "Imágenes", color: "#10b981", icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 8h.01" /><path d="M11 20h-4a3 3 0 0 1 -3 -3v-10a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v4" /><path d="M4 15l4 -4c.928 -.893 2.072 -.893 3 0l3 3" /><path d="M14 14l1 -1c.31 -.298 .644 -.497 .987 -.596" /><path d="M18.42 15.61a2.1 2.1 0 0 1 2.97 2.97l-3.39 3.42h-3v-3l3.42 -3.39" /></svg>' },
|
||||
|
||||
{ id: "textos", label: "Textos", color: "#3b82f6", icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19 10h-14" /><path d="M5 6h14" /><path d="M14 14h-9" /><path d="M5 18h6" /><path d="M18 15v6" /><path d="M15 18h6" /></svg>' },
|
||||
|
||||
{ id: "bloques", label: "Bloques", color: "#ec4899", icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 4l-8 4l8 4l8 -4l-8 -4" /><path d="M4 12l8 4l8 -4" /><path d="M4 16l8 4l8 -4" /></svg>' },
|
||||
|
||||
{ id: "enlaces", label: "Enlaces", color: "#ef4444", icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 15l6 -6"/><path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464"/><path d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463"/></svg>' },
|
||||
|
||||
{ id: "colores", label: "Colores", color: "#8b5cf6", icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19 3h-4a2 2 0 0 0 -2 2v12a4 4 0 0 0 8 0v-12a2 2 0 0 0 -2 -2" /><path d="M13 7.35l-2 -2a2 2 0 0 0 -2.828 0l-2.828 2.828a2 2 0 0 0 0 2.828l9 9" /><path d="M7.3 13h-2.3a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h12" /><path d="M17 17l0 .01" /></svg>' },
|
||||
```
|
||||
|
||||
### 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 `<acai-vue-records>`. 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 `<acai-vue-records>`:
|
||||
```html
|
||||
<!-- Multi -->
|
||||
<div class="flex w-full items-center mt-6">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" :style="{ color: color }" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-stack-2"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 4l-8 4l8 4l8 -4l-8 -4" /><path d="M4 12l8 4l8 -4" /><path d="M4 16l8 4l8 -4" /></svg>
|
||||
<p class="leading-snug text-gray-600"><b class="text-black">Bloques del multi :</b> Personaliza los bloques del multi.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 10.7 Slot de acai-vue-records: NO desestructurar `color`
|
||||
El slot de `<acai-vue-records>` NUNCA debe desestructurar `color`. Siempre usar:
|
||||
```html
|
||||
<template v-slot="{ record, index }">
|
||||
```
|
||||
**NUNCA** usar:
|
||||
```html
|
||||
<template v-slot="{ record, color, index }">
|
||||
```
|
||||
De esta forma, `color` dentro del multi resuelve al `color` del tab padre (`<template #bloques="{ color }">`), y los iconos SVG con `:style="{ color: color }"` siempre mostrarán el color correcto del tab.
|
||||
|
||||
### 10.8 Estructura del componente acai-vue-records
|
||||
El componente `<acai-vue-records>` siempre debe incluir todas estas props y atributos:
|
||||
```html
|
||||
<acai-vue-records :data="data" :builder="builder" :active="active" :section_id="section_id" :root_builder_vue="$parent" ref="recordsNode">
|
||||
<template v-slot="{ record, index }">
|
||||
<!-- campos del record -->
|
||||
</template>
|
||||
</acai-vue-records>
|
||||
```
|
||||
**NUNCA** usar una versión simplificada sin `:active`, `:section_id`, `:root_builder_vue` o `ref`.
|
||||
|
||||
### 10.9 Orden del campo "Color título resaltado" en tab Colores
|
||||
Cuando el módulo tenga un campo de **Color título resaltado** (tipo `list` con opciones Main color / Main color light / Main color dark), este campo debe colocarse **inmediatamente debajo** del campo **Color del título** en el tab **Colores**. Nunca en el tab Textos ni en otra posición del tab Colores.
|
||||
|
||||
---
|
||||
|
||||
## 11. Consistencia de iconos y textos descriptivos entre VUEs
|
||||
|
||||
### 11.1 Iconos
|
||||
Los campos que ya tienen un icono SVG asignado en VUEs anteriores deben usar SIEMPRE ese mismo icono en todos los VUEs futuros. Solo se crean o personalizan iconos nuevos para campos que no se hayan visto antes en ningún VUE previo.
|
||||
|
||||
### 11.2 Textos descriptivos
|
||||
Los textos descriptivos (título en negrita + descripción + nota) de campos recurrentes (pretítulo, título, subtítulo, texto largo, enlace, color de fondo, color del texto, etc.) deben ser idénticos en todos los VUEs. Solo se modifican si el HTML del módulo revela un comportamiento diferente para ese campo concreto.
|
||||
|
||||
### 11.3 Registro de referencia
|
||||
Usar como referencia los iconos y textos del primer VUE en que apareció cada tipo de campo. Ante cualquier duda, mantener consistencia con lo ya establecido.
|
||||
|
||||
---
|
||||
|
||||
## 12. Componente acai-vue-selectv2 (reemplazo de acai-vue-list)
|
||||
|
||||
### 12.1 Descripción general
|
||||
`acai-vue-selectv2` reemplaza completamente a `acai-vue-list`. Es un componente inteligente que detecta automáticamente cómo renderizar según el número y tipo de opciones:
|
||||
- **2 opciones** → modo **toggle** (pill deslizante con animación)
|
||||
- **2+ opciones con nombres de color** → modo **color selector** (dropdown con swatches)
|
||||
- **3+ opciones normales** → modo **select** (dropdown estándar con vue-select)
|
||||
|
||||
### 12.2 Props
|
||||
```html
|
||||
<acai-vue-selectv2
|
||||
:builder="builder"
|
||||
:data="data"
|
||||
:field="'nombrecampo'"
|
||||
:toggle-icons="iconosObjeto" <!-- opcional, solo para toggles con iconos -->
|
||||
@save-data="saveData">
|
||||
</acai-vue-selectv2>
|
||||
```
|
||||
|
||||
### 12.3 Toggle con iconos (`:toggle-icons`)
|
||||
Para campos de 2 opciones donde se quieran iconos visuales en el toggle, se pasa un objeto con las claves correspondientes a los valores de las opciones:
|
||||
```javascript
|
||||
iconosNombreCampo: {
|
||||
'': '<svg>...</svg>', // icono para la primera opción (valor vacío)
|
||||
'1': '<svg>...</svg>' // icono para la segunda opción
|
||||
}
|
||||
```
|
||||
|
||||
Iconos de toggle establecidos:
|
||||
- **Lado texto (2 opciones: Izquierda/Derecha):** `icon-tabler-align-box-left-middle` / `icon-tabler-align-box-right-middle`
|
||||
- **Ver sombra (No/Si):** `icon-tabler-x` / `icon-tabler-check`
|
||||
- **Tipo imagen (Imagen/Video):** `icon-tabler-photo` / `icon-tabler-video`
|
||||
- **Tipo overlay (Sin degradado/Con degradado):** `icon-tabler-square` / `icon-tabler-gradient`
|
||||
|
||||
### 12.4 Modo color automático
|
||||
El componente detecta automáticamente si las opciones son colores cuando al menos la mitad de las labels coinciden con:
|
||||
- Nombres del mapa interno: main color, blanco, negro, gris, gris claro, gris oscuro, gris calido, rojo, azul, verde, etc. (español e inglés)
|
||||
- Códigos hex (#fff, #ff0000)
|
||||
- Valores rgb/rgba
|
||||
- Valores hsl/hsla
|
||||
|
||||
Los colores main color, main color light y main color dark se resuelven consultando la configuración del CMS en tiempo real.
|
||||
|
||||
### 12.5 Campos de 3+ opciones sin iconos
|
||||
No necesitan `:toggle-icons`. Se renderizan como dropdown estándar:
|
||||
```html
|
||||
<acai-vue-selectv2 :builder="builder" :data="data" :field="'container'" @save-data="saveData"></acai-vue-selectv2>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Componente acai-vue-datepicker (campos de fecha)
|
||||
|
||||
### 13.1 Uso
|
||||
Cuando un campo `textfield` en el HTML se usa para fechas (se identifica por el label "Fecha" o similar), se usa `acai-vue-datepicker` junto con un `acai-vue-textfield` oculto:
|
||||
```html
|
||||
<div class="relative mt-2">
|
||||
<acai-vue-datepicker :builder="builder" :data="data" :field="'fecha'" :label="'Fecha'" @save-data="saveData"></acai-vue-datepicker>
|
||||
</div>
|
||||
<div style="display: none">
|
||||
<acai-vue-textfield :builder="builder" :data="data" :field="'fecha'" @save-data="saveData"></acai-vue-textfield>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 13.2 Notas estándar para datepicker
|
||||
```html
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> puedes elegir el formato de la fecha en el selector.</p>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-1 ml-14"><b class="text-gray-700">Recuerda :</b> también puedes mostrar la hora activando el botón del reloj.</p>
|
||||
```
|
||||
|
||||
### 13.3 Icono estándar para fecha
|
||||
```html
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" :style="{ color: color }" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-10 h-10 flex-shrink-0 mr-4 icon icon-tabler icons-tabler-outline icon-tabler-calendar-week"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12" /><path d="M16 3v4" /><path d="M8 3v4" /><path d="M4 11h16" /><path d="M7 14h.013" /><path d="M10.01 14h.005" /><path d="M13.01 14h.005" /><path d="M16.015 14h.005" /><path d="M13.015 17h.005" /><path d="M7.01 17h.005" /><path d="M10.01 17h.005" /></svg>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Reglas de spacing (separación entre elementos)
|
||||
|
||||
### 14.1 Separación entre nota/recuerda y componente
|
||||
- El componente siempre lleva `mt-2` respecto a la nota o recuerda que lo precede.
|
||||
- Nunca `mt-1` entre nota/recuerda y componente.
|
||||
|
||||
### 14.2 Nota y Recuerda juntos
|
||||
Cuando un campo tiene **Nota** y **Recuerda**:
|
||||
- **Nota** siempre lleva `mt-2` respecto al bloque de icono+texto anterior.
|
||||
- **Recuerda** lleva `mt-1` respecto a la Nota (va justo debajo).
|
||||
- El componente lleva `mt-2` respecto al Recuerda.
|
||||
|
||||
Ejemplo:
|
||||
```html
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> texto de la nota.</p>
|
||||
<p class="text-xs leading-snug text-gray-500 mt-1 ml-14"><b class="text-gray-700">Recuerda :</b> texto del recuerda.</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<!-- componente -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### 14.3 Solo Nota (sin Recuerda)
|
||||
```html
|
||||
<p class="text-xs leading-snug text-gray-500 mt-2 ml-14"><b class="text-gray-700">Nota :</b> texto.</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<!-- componente -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### 14.4 Solo Recuerda (sin Nota)
|
||||
El Recuerda usa el estilo especial (sin ml-14, con font-light):
|
||||
```html
|
||||
<p class="text-xs leading-snug text-gray-600 font-light mt-2"><b class="text-black">Recuerda :</b> texto del recuerda.</p>
|
||||
<div class="relative mt-2 ml-14">
|
||||
<!-- componente -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### 14.5 Sin Nota ni Recuerda
|
||||
El componente lleva `mt-2` directamente:
|
||||
```html
|
||||
<div class="relative mt-2 ml-14">
|
||||
<!-- componente -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### 14.6 Separación entre campos
|
||||
Siempre `mt-6` entre bloques de campo:
|
||||
```html
|
||||
<div class="flex w-full items-center mt-6">
|
||||
```
|
||||
El primer campo de cada tab NO lleva `mt-6` (no hay campo previo).
|
||||
Reference in New Issue
Block a user