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

10 KiB

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

<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:

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

<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.
/* 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:

<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:

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)

// 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

<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:

<div id="{{section_id}}"></div>
<section id="id_{{ section_id }}" class="relative">
  <!-- contenido -->
</section>

Componentes nativos

<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:

<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

<a href="{{ image[0].urlPath }}" class="glightbox" data-gallery="gallery1">
  <img src="{{ image[0].urlPath | imagec(400) }}">
</a>

Breadcrumb

<breadCrumb class="bg-gray-200 p-3 rounded" c-prevlinks="null"></breadCrumb>

Animate On Scroll (AOS)

<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:

AOS.refresh();

Lazy loading

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