El planner generaba 3+ steps para tareas simples causando que el coder repitiera acciones en cada step (creaba el módulo varias veces). Ahora el engine fusiona los steps en 1 coder con descripción combinada. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
262 lines
7.5 KiB
Markdown
262 lines
7.5 KiB
Markdown
# 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.
|
|
|
|
```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 se necesite CSS personalizado, siempre scopeado bajo la clase raíz con BEM:
|
|
|
|
```css
|
|
.hero-section { }
|
|
.hero-section__title { }
|
|
.hero-section__image { }
|
|
.hero-section--dark { }
|
|
```
|
|
|
|
Nunca usar clases globales sin prefijo de módulo.
|
|
|
|
### CSS Variables del tema
|
|
|
|
```css
|
|
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:
|
|
|
|
```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 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`:
|
|
|
|
This is the default and expected place for module JavaScript.
|
|
Do NOT embed `<script>` tags directly inside `index-base.tpl` for normal module behavior.
|
|
Keep `index-base.tpl` for HTML/Twig markup and put interactive logic in `script.js`.
|
|
`script.js` is a static file: do NOT use Twig syntax inside it.
|
|
Do NOT write `{{ section_id }}`, `{{ variable }}`, `{% if ... %}`, or builder attributes inside `script.js`.
|
|
|
|
If JavaScript needs dynamic values, pass them from `index-base.tpl` through `data-*` attributes:
|
|
|
|
```html
|
|
<section
|
|
class="buscador-apartados"
|
|
data-section-id="{{ section_id }}"
|
|
data-hook-endpoint="/hooks/buscadorapartados_hjd8s/">
|
|
</section>
|
|
```
|
|
|
|
```js
|
|
document.querySelectorAll('.buscador-apartados').forEach((section) => {
|
|
const sectionId = section.dataset.sectionId;
|
|
const hookEndpoint = section.dataset.hookEndpoint;
|
|
const buttons = section.querySelectorAll('.btn');
|
|
// ...
|
|
});
|
|
```
|
|
|
|
### CmsApi (Client-Side)
|
|
|
|
```js
|
|
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, function(response) {
|
|
console.log(response);
|
|
});
|
|
```
|
|
|
|
If you are calling a hook that belongs to the current module, the endpoint must use the real module id:
|
|
|
|
```js
|
|
// Module folder: template/estandar/modulos/buscadorapartados_hjd8s/
|
|
CmsApi.hook('/hooks/buscadorapartados_hjd8s/', { termino: 'vela' }, function(response) {
|
|
console.log(response);
|
|
});
|
|
```
|
|
|
|
Do not try to build this endpoint with Twig inside `script.js`. Put the final endpoint in a `data-hook-endpoint` attribute in `index-base.tpl` if needed.
|
|
|
|
### Module Styles (`style.css`)
|
|
|
|
`style.css` is also a static file.
|
|
Do NOT use Twig syntax or builder attributes inside it.
|
|
Do NOT write selectors or values that depend on `{{ section_id }}`.
|
|
|
|
Scope styles with the module root class instead of dynamic Twig ids.
|
|
|
|
### 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
|
|
|
|
```html
|
|
<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.
|
|
|
|
---
|
|
|
|
## 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:
|
|
|
|
```html
|
|
<div id="{{section_id}}"></div>
|
|
<section id="id_{{ section_id }}" class="relative">
|
|
<!-- contenido del módulo -->
|
|
</section>
|
|
```
|
|
|
|
---
|
|
|
|
## Componentes Nativos
|
|
|
|
### Carousel (`c-tns-wrapper`)
|
|
|
|
```html
|
|
<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:
|
|
```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>
|
|
```
|
|
|
|
### AOS (Animate On Scroll)
|
|
|
|
```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`
|
|
|
|
Después de cambios dinámicos: `AOS.refresh()` en JavaScript.
|
|
|
|
### Lazy Loading
|
|
|
|
```html
|
|
<!-- 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="">
|
|
```
|
|
|
|
---
|
|
|
|
## 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
|