Files
acai-scaffold/docs/css-js-conventions.md
Dmielgo f2021361ec Add production rules discovered during real projects
- Correct hooks-and-api.md: file hooks vs module hooks param injection,
  FK naming not always _num, CmsApi.hook Promise pattern
- Add 9 new rules to hooks-and-api.md: reserved `tipo` var, ghost modules,
  cms_uploads schema, name vs title by menuType, menuOrder, localCache
  gotcha, slug generation, uploads from hooks, CocoEmail
- Add 5 rules to modular-system.md: minified/ dir, Docker workflow,
  debug tools (?compiletwig/?pruebas), general sections deploy, controlador
- Add 2 rules to css-js-conventions.md: Vue inline conflict, Vue mount delay
- Add testing section to quick-reference.md
- Create docs/deploy-and-sync.md for production deploy and sync rules
- Promote 3 critical rules to CLAUDE.md (rules 11-13)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 16:17:00 +00:00

6.7 KiB

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

Usar TailwindCSS como método principal. Solo CSS custom cuando Tailwind no cubra el estilo o se necesiten estados complejos/transiciones específicas.

<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 se necesite CSS personalizado, siempre scopeado bajo la clase raíz con BEM:

.hero-section { }
.hero-section__title { }
.hero-section__image { }
.hero-section--dark { }

Nunca usar clases globales sin prefijo de módulo.

CSS Variables del tema

var(--main-color)        /* Color de marca primario */
var(--main-color-light)  /* Variante clara */
var(--main-color-dark)   /* Variante oscura */

Estilos inline con fallbacks

Patrón para colores configurables por el usuario:

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

JavaScript scopeado al módulo usando section_id:

const section = document.getElementById('{{ section_id }}');
if (section) {
  const buttons = section.querySelectorAll('.btn');
  // ...
}

CmsApi (Client-Side)

// Callback
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, function(response) {
  console.log(response);
});

// Promise
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }).then(function(res) {
  console.log(res);
}).catch(function(err) {
  console.error(err);
});

Cuándo usar Vue 3

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

<div id="app-{{ section_id }}">
  <p>${ message }</p>
  <button @click="increment">${ count }</button>
</div>

<script>
const { createApp, ref } = Vue;
createApp({
  delimiters: ['${', '}'],  // Evitar 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>

Siempre usar '${' y '}' como delimitadores Vue para evitar conflicto con Twig.

IMPORTANTE: JS Vue SIEMPRE en script.js, nunca inline. Twig también interpreta ${ }. Si el JS con Vue está inline en <script> dentro de index-base.tpl, Twig lo corrompe al renderizar. Todo el código Vue debe ir en el archivo script.js del módulo.


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:

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

Componentes Nativos

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

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

AOS (Animate On Scroll)

<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

<!-- 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="">

Testing con Vue

Vue necesita 3-5 segundos para montar después de navegar a una página. El display:none inicial se quita cuando checkAuth() o el mounted() completan. Al testear con Playwright, esperar antes de verificar contenido Vue.


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