284 lines
10 KiB
Markdown
284 lines
10 KiB
Markdown
# CSS y JavaScript — Convenciones del Módulo
|
|
|
|
Este documento define cómo escribir CSS, JavaScript y, cuando hace falta, Vue 3 dentro de un módulo Acai. Cubre la regla "Tailwind first" + BEM para CSS custom, las clases utilitarias propias de Acai (`transition3s`, `click-a-child`, `line-clamp2`, `lazyload`, `bg-main-color`, etc.), las CSS variables del tema (`--main-color`), el patrón obligatorio de **scoping** vía la clase raíz del módulo, la regla dura de que `script.js` y `style.css` son **archivos estáticos** (sin Twig dentro), cómo pasar valores dinámicos desde `index-base.tpl` a JS vía `data-*`, cuándo usar Vue 3 y cómo integrarlo evitando conflicto de delimiters con Twig, y los componentes nativos del builder (Carousel `c-tns-wrapper`, Lightbox, Breadcrumb, AOS, Lazy loading). Léelo antes de escribir cualquier `style.css` o `script.js`.
|
|
|
|
## Estructura del módulo
|
|
|
|
- Cada módulo genera HTML + CSS + JS (y opcionalmente Vue 3).
|
|
- Define una **clase raíz en kebab-case** específica del módulo: `product-card`, `hero-section`, `buscador-apartados`.
|
|
- **Todo el CSS y JS deben quedar scopeados bajo esa clase raíz.**
|
|
|
|
## CSS
|
|
|
|
### Tailwind first
|
|
|
|
Usa TailwindCSS como método principal. Reserva CSS custom solo cuando Tailwind no cubra el caso (estados complejos, transiciones específicas, animaciones).
|
|
|
|
```html
|
|
<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>
|
|
```
|
|
|
|
### BEM para CSS custom
|
|
|
|
Cuando necesites CSS propio, scopealo bajo la clase raíz con BEM:
|
|
|
|
```css
|
|
.hero-section { }
|
|
.hero-section__title { }
|
|
.hero-section__image { }
|
|
.hero-section--dark { }
|
|
```
|
|
|
|
Nunca uses clases globales sin prefijo de módulo.
|
|
|
|
### CSS variables del tema
|
|
|
|
| Variable | Descripción |
|
|
|----------|-------------|
|
|
| `var(--main-color)` | Color de marca principal |
|
|
| `var(--main-color-light)` | Variante clara |
|
|
| `var(--main-color-dark)` | Variante oscura |
|
|
|
|
### Estilos inline con fallbacks
|
|
|
|
Patrón estándar para colores configurables por el usuario (variables del builder):
|
|
|
|
```html
|
|
<div style="background-color: {{ colordefondo ? colordefondo : 'transparent' }}">
|
|
<p style="color: {{ colordeltexto ? colordeltexto : '#111827' }}">
|
|
```
|
|
|
|
### Clases utilitarias de Acai
|
|
|
|
| Clase | Descripción |
|
|
|-------|-------------|
|
|
| `transition3s` | Transición suave 0.3s |
|
|
| `click-a-child` | Hace al padre clickeable vía el primer `<a>` hijo |
|
|
| `line-clamp2` / `line-clamp3` / `line-clamp5` | Truncar texto a N líneas |
|
|
| `filter-white` | Filtro CSS para teñir imágenes/iconos en blanco |
|
|
| `lazyload` | Activa lazy loading (usar con `data-src`) |
|
|
| `text-shadow` | Sombra de texto para legibilidad sobre imágenes |
|
|
| `wysiwyg` | Wrapper para contenido rico (estilos coherentes) |
|
|
| `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 |
|
|
| `titulo-main-color` / `titulo-white` | Estilos de título resaltado |
|
|
| `c-tns-wrapper` / `c-tns-container` / `c-tns-nav-container` | Carousel built-in |
|
|
| `glightbox` | Activa lightbox |
|
|
|
|
### Reglas para `style.css`
|
|
|
|
`style.css` es **estático**. Reglas duras:
|
|
- **NO** uses sintaxis Twig (`{{ ... }}`, `{% ... %}`) ni atributos builder (`c-if`, `c-for`).
|
|
- **NO** escribas selectores que dependan de `{{ section_id }}` — scopea con la clase raíz del módulo.
|
|
|
|
```css
|
|
/* CORRECTO — scopeado con clase raíz */
|
|
.product-card { }
|
|
.product-card__title { color: var(--main-color); }
|
|
|
|
/* INCORRECTO — Twig inside */
|
|
#{{ section_id }} h3 { }
|
|
```
|
|
|
|
## JavaScript
|
|
|
|
### `script.js` es estático
|
|
|
|
Igual que `style.css`: **NO** uses sintaxis Twig dentro. Si necesitas valores dinámicos, pásalos desde `index-base.tpl` vía atributos `data-*`.
|
|
|
|
#### Patrón correcto
|
|
|
|
`index-base.tpl`:
|
|
```html
|
|
<section class="buscador-apartados"
|
|
id="{{ section_id }}"
|
|
data-section-id="{{ section_id }}"
|
|
data-hook-endpoint="/hooks/buscadorapartados_hjd8s/"
|
|
data-limit="{{ limite | default('10') }}">
|
|
<input class="buscador-apartados__input" type="text">
|
|
<ul class="buscador-apartados__results"></ul>
|
|
</section>
|
|
```
|
|
|
|
`script.js`:
|
|
```js
|
|
document.querySelectorAll('.buscador-apartados').forEach((section) => {
|
|
const sectionId = section.dataset.sectionId;
|
|
const hookEndpoint = section.dataset.hookEndpoint;
|
|
const limit = parseInt(section.dataset.limit, 10);
|
|
const input = section.querySelector('.buscador-apartados__input');
|
|
const results = section.querySelector('.buscador-apartados__results');
|
|
|
|
input.addEventListener('input', (e) => {
|
|
CmsApi.hook(hookEndpoint, { termino: e.target.value, limite: limit }, (data) => {
|
|
results.innerHTML = data.items.map((it) => `<li>${it.titulo}</li>`).join('');
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
NUNCA hagas `CmsApi.hook('/hooks/{{ moduleId }}/', ...)` dentro de `script.js`. Si necesitas el endpoint, pásalo por `data-hook-endpoint`.
|
|
|
|
### CmsApi (cliente)
|
|
|
|
```js
|
|
// Llamar hook
|
|
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, (response) => {
|
|
console.log(response);
|
|
});
|
|
|
|
// Si llamas un hook propio del módulo, usa el module-id real:
|
|
CmsApi.hook('/hooks/buscadorapartados_hjd8s/', { termino: 'vela' }, (response) => {
|
|
console.log(response);
|
|
});
|
|
```
|
|
|
|
### Embeber `<script>` directamente — NO
|
|
|
|
No embebas `<script>` con lógica del módulo dentro de `index-base.tpl`. La lógica vive en `script.js`. La excepción: scripts mínimos para inicializar Vue 3 (ver más abajo) que sí necesitan los delimitadores `{{ section_id }}` para mountear sobre IDs únicos.
|
|
|
|
## Cuándo usar Vue 3
|
|
|
|
Usa Vue 3 (vía CDN) cuando la lógica requiera:
|
|
- Doble binding / reactividad
|
|
- Solicitudes asíncronas complejas con UI reactiva
|
|
- Componentes reutilizables dentro del módulo
|
|
- Gestión de estado local
|
|
- Ciclos de vida (mounted, unmounted, etc.)
|
|
|
|
Para lógica simple (listeners, fetch único, toggle de clases), usa JavaScript vanilla.
|
|
|
|
### Integración Vue 3
|
|
|
|
```html
|
|
<div id="app-{{ section_id }}">
|
|
<p>${ message }</p>
|
|
<button @click="increment">${ count }</button>
|
|
</div>
|
|
|
|
<script>
|
|
const { createApp, ref } = Vue;
|
|
createApp({
|
|
delimiters: ['${', '}'], // OBLIGATORIO — evita conflicto con Twig {{ }}
|
|
setup() {
|
|
const message = ref('Hello');
|
|
const count = ref(0);
|
|
const increment = () => count.value++;
|
|
return { message, count, increment };
|
|
}
|
|
}).mount('#app-{{ section_id }}');
|
|
</script>
|
|
```
|
|
|
|
Reglas:
|
|
- **Siempre** redefine `delimiters` a `['${', '}']` para no chocar con Twig.
|
|
- Mountea sobre un id único usando `section_id`: `#app-{{ section_id }}`.
|
|
- Vue se carga como librería global vía `add_global_library` o ya incluida en el proyecto (ver `08-layout-and-libraries.md`).
|
|
|
|
## Variables globales disponibles en JS / Twig
|
|
|
|
| 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 1-based en `c-for`/`{% for %}` | `{{ loop.index }}` |
|
|
| `loop.index is odd` / `is even` | Para layouts alternados | zigzag |
|
|
| `interno` | `true` dentro del editor CMS | `c-class="{'editor-mode': interno}"` |
|
|
|
|
### Patrón `section_id`
|
|
|
|
Cada instancia de un módulo recibe un `section_id` único. Úsalo para anchors, IDs HTML y selector raíz de JS:
|
|
|
|
```html
|
|
<div id="{{section_id}}"></div>
|
|
<section id="id_{{ section_id }}" class="relative">
|
|
<!-- contenido -->
|
|
</section>
|
|
```
|
|
|
|
## Componentes nativos
|
|
|
|
### Carousel — `c-tns-wrapper`
|
|
|
|
```html
|
|
<div class="c-tns-wrapper"
|
|
data-responsive='{"0":1,"768":2,"1024":3}'
|
|
data-autoplay-timeout="5000"
|
|
data-mode="carousel"
|
|
data-speed="400"
|
|
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 |
|
|
|----------|-------------|
|
|
| `data-responsive` | Items por breakpoint. JSON `{"0":1,"768":2,"1024":3}` o sintaxis corta `"sm:2, md:3, lg:4"` |
|
|
| `data-autoplay-timeout` | Intervalo autoplay (ms) |
|
|
| `data-mode` | `"gallery"` o `"carousel"` |
|
|
| `data-speed` | Velocidad de transición (ms) |
|
|
| `data-nav` | `"true"` para mostrar dots |
|
|
|
|
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>
|
|
```
|
|
|
|
### Animate On Scroll (AOS)
|
|
|
|
```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`.
|
|
|
|
Tras cambios dinámicos en JS:
|
|
```js
|
|
AOS.refresh();
|
|
```
|
|
|
|
### Lazy loading
|
|
|
|
```html
|
|
<!-- Builder var con lazy automático -->
|
|
<img data-field-type="upload" data-field-label="Imagen" data-lazy="true" data-field-width="800" alt="">
|
|
|
|
<!-- Manual en plantillas -->
|
|
<img class="lazyload" data-src="{{ record.imagen[0].urlPath | imagec(800) }}" alt="">
|
|
```
|
|
|
|
## Reglas críticas
|
|
|
|
1. `script.js` y `style.css` son **estáticos** — sin sintaxis Twig dentro.
|
|
2. Para valores dinámicos en JS, pásalos desde `index-base.tpl` vía atributos `data-*`.
|
|
3. Define una **clase raíz en kebab-case** por módulo. Scopea TODO el CSS/JS bajo ella.
|
|
4. Tailwind first; CSS custom solo donde Tailwind no llegue, siempre con BEM.
|
|
5. Vue 3: redefine `delimiters: ['${', '}']` para evitar conflicto con Twig.
|
|
6. Mountea Vue sobre `#app-{{ section_id }}`.
|
|
7. Usa las clases utilitarias de Acai (`transition3s`, `lazyload`, `bg-main-color`, etc.) antes de inventar utilidades.
|
|
8. NO embebas lógica `<script>` dentro de `index-base.tpl` (Vue init es la única excepción común).
|