ajustes
This commit is contained in:
283
docs/07-css-js-conventions.md
Normal file
283
docs/07-css-js-conventions.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user