Files
agenticSystem/docs/07-css-js-conventions.md
Jordan Diaz 6881d64a08 ajustes
2026-04-25 10:27:51 +00:00

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).