- 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>
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
Carousel (c-tns-wrapper)
<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