@moni-labs/moni-ui 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +5181 -1043
- package/dist/components/_base/moni-element.d.ts +16 -9
- package/dist/components/_base/moni-element.d.ts.map +1 -1
- package/dist/components/_base/moni-element.js +63 -9
- package/dist/components/index.js +2 -2
- package/dist/components/loading-shapes.d.ts +19 -0
- package/dist/components/loading-shapes.d.ts.map +1 -1
- package/dist/components/loading-shapes.js +19 -0
- package/dist/components/moni-app-bar.d.ts +72 -62
- package/dist/components/moni-app-bar.d.ts.map +1 -1
- package/dist/components/moni-app-bar.js +72 -62
- package/dist/components/moni-badge.d.ts +64 -65
- package/dist/components/moni-badge.d.ts.map +1 -1
- package/dist/components/moni-badge.js +64 -66
- package/dist/components/moni-bottom-sheet.d.ts +129 -61
- package/dist/components/moni-bottom-sheet.d.ts.map +1 -1
- package/dist/components/moni-bottom-sheet.js +217 -131
- package/dist/components/moni-button-group.d.ts +107 -29
- package/dist/components/moni-button-group.d.ts.map +1 -1
- package/dist/components/moni-button-group.js +107 -29
- package/dist/components/moni-button-segment.d.ts +53 -12
- package/dist/components/moni-button-segment.d.ts.map +1 -1
- package/dist/components/moni-button-segment.js +53 -12
- package/dist/components/moni-button.d.ts +116 -38
- package/dist/components/moni-button.d.ts.map +1 -1
- package/dist/components/moni-button.js +128 -38
- package/dist/components/moni-card.d.ts +68 -50
- package/dist/components/moni-card.d.ts.map +1 -1
- package/dist/components/moni-card.js +68 -50
- package/dist/components/moni-carousel.d.ts +471 -53
- package/dist/components/moni-carousel.d.ts.map +1 -1
- package/dist/components/moni-carousel.js +862 -264
- package/dist/components/moni-checkbox.d.ts +49 -46
- package/dist/components/moni-checkbox.d.ts.map +1 -1
- package/dist/components/moni-checkbox.js +55 -48
- package/dist/components/moni-chip.d.ts +100 -33
- package/dist/components/moni-chip.d.ts.map +1 -1
- package/dist/components/moni-chip.js +104 -34
- package/dist/components/moni-color-field.d.ts +99 -24
- package/dist/components/moni-color-field.d.ts.map +1 -1
- package/dist/components/moni-color-field.js +99 -24
- package/dist/components/moni-context-menu.d.ts +72 -26
- package/dist/components/moni-context-menu.d.ts.map +1 -1
- package/dist/components/moni-context-menu.js +76 -30
- package/dist/components/moni-dialog.d.ts +84 -59
- package/dist/components/moni-dialog.d.ts.map +1 -1
- package/dist/components/moni-dialog.js +105 -58
- package/dist/components/moni-divider.d.ts +39 -27
- package/dist/components/moni-divider.d.ts.map +1 -1
- package/dist/components/moni-divider.js +42 -30
- package/dist/components/moni-expansion.d.ts +32 -22
- package/dist/components/moni-expansion.d.ts.map +1 -1
- package/dist/components/moni-expansion.js +32 -22
- package/dist/components/moni-fab-menu.d.ts +61 -23
- package/dist/components/moni-fab-menu.d.ts.map +1 -1
- package/dist/components/moni-fab-menu.js +69 -31
- package/dist/components/moni-fab.d.ts +86 -28
- package/dist/components/moni-fab.d.ts.map +1 -1
- package/dist/components/moni-fab.js +92 -34
- package/dist/components/moni-file-field.d.ts +118 -30
- package/dist/components/moni-file-field.d.ts.map +1 -1
- package/dist/components/moni-file-field.js +118 -30
- package/dist/components/moni-icon.d.ts +40 -35
- package/dist/components/moni-icon.d.ts.map +1 -1
- package/dist/components/moni-icon.js +43 -38
- package/dist/components/moni-list-item.d.ts +46 -33
- package/dist/components/moni-list-item.d.ts.map +1 -1
- package/dist/components/moni-list-item.js +61 -41
- package/dist/components/moni-list.d.ts +30 -20
- package/dist/components/moni-list.d.ts.map +1 -1
- package/dist/components/moni-list.js +36 -21
- package/dist/components/moni-loading-indicator.d.ts +54 -22
- package/dist/components/moni-loading-indicator.d.ts.map +1 -1
- package/dist/components/moni-loading-indicator.js +54 -22
- package/dist/components/moni-menu-item.d.ts +23 -16
- package/dist/components/moni-menu-item.d.ts.map +1 -1
- package/dist/components/moni-menu-item.js +23 -16
- package/dist/components/moni-menu.d.ts +81 -28
- package/dist/components/moni-menu.d.ts.map +1 -1
- package/dist/components/moni-menu.js +81 -30
- package/dist/components/moni-morph-modal.d.ts +141 -0
- package/dist/components/moni-morph-modal.d.ts.map +1 -1
- package/dist/components/moni-morph-modal.js +295 -58
- package/dist/components/moni-nav-item.d.ts +59 -29
- package/dist/components/moni-nav-item.d.ts.map +1 -1
- package/dist/components/moni-nav-item.js +59 -29
- package/dist/components/moni-nav.d.ts +94 -35
- package/dist/components/moni-nav.d.ts.map +1 -1
- package/dist/components/moni-nav.js +94 -35
- package/dist/components/moni-progress.d.ts +99 -55
- package/dist/components/moni-progress.d.ts.map +1 -1
- package/dist/components/moni-progress.js +99 -55
- package/dist/components/moni-radio.d.ts +62 -50
- package/dist/components/moni-radio.d.ts.map +1 -1
- package/dist/components/moni-radio.js +65 -52
- package/dist/components/moni-ripple.d.ts +74 -59
- package/dist/components/moni-ripple.d.ts.map +1 -1
- package/dist/components/moni-ripple.js +74 -59
- package/dist/components/moni-segmented-button.d.ts +91 -16
- package/dist/components/moni-segmented-button.d.ts.map +1 -1
- package/dist/components/moni-segmented-button.js +94 -17
- package/dist/components/moni-select-option.d.ts +30 -23
- package/dist/components/moni-select-option.d.ts.map +1 -1
- package/dist/components/moni-select-option.js +30 -23
- package/dist/components/moni-select.d.ts +300 -35
- package/dist/components/moni-select.d.ts.map +1 -1
- package/dist/components/moni-select.js +323 -39
- package/dist/components/moni-side-sheet.d.ts +94 -32
- package/dist/components/moni-side-sheet.d.ts.map +1 -1
- package/dist/components/moni-side-sheet.js +94 -32
- package/dist/components/moni-slider.d.ts +152 -41
- package/dist/components/moni-slider.d.ts.map +1 -1
- package/dist/components/moni-slider.js +154 -42
- package/dist/components/moni-snackbar.d.ts +53 -43
- package/dist/components/moni-snackbar.d.ts.map +1 -1
- package/dist/components/moni-snackbar.js +53 -43
- package/dist/components/moni-split-button.d.ts +63 -18
- package/dist/components/moni-split-button.d.ts.map +1 -1
- package/dist/components/moni-split-button.js +63 -18
- package/dist/components/moni-step.d.ts +36 -24
- package/dist/components/moni-step.d.ts.map +1 -1
- package/dist/components/moni-step.js +41 -29
- package/dist/components/moni-stepper.d.ts +64 -27
- package/dist/components/moni-stepper.d.ts.map +1 -1
- package/dist/components/moni-stepper.js +72 -34
- package/dist/components/moni-switch.d.ts +57 -46
- package/dist/components/moni-switch.d.ts.map +1 -1
- package/dist/components/moni-switch.js +59 -47
- package/dist/components/moni-tab.d.ts +26 -18
- package/dist/components/moni-tab.d.ts.map +1 -1
- package/dist/components/moni-tab.js +29 -21
- package/dist/components/moni-tabs.d.ts +34 -28
- package/dist/components/moni-tabs.d.ts.map +1 -1
- package/dist/components/moni-tabs.js +34 -28
- package/dist/components/moni-text-field.d.ts +139 -29
- package/dist/components/moni-text-field.d.ts.map +1 -1
- package/dist/components/moni-text-field.js +157 -31
- package/dist/components/moni-textarea.d.ts +127 -29
- package/dist/components/moni-textarea.d.ts.map +1 -1
- package/dist/components/moni-textarea.js +131 -32
- package/dist/components/moni-time-picker.d.ts +114 -24
- package/dist/components/moni-time-picker.d.ts.map +1 -1
- package/dist/components/moni-time-picker.js +127 -33
- package/dist/components/moni-toolbar.d.ts +37 -23
- package/dist/components/moni-toolbar.d.ts.map +1 -1
- package/dist/components/moni-toolbar.js +37 -23
- package/dist/components/moni-tooltip.d.ts +100 -36
- package/dist/components/moni-tooltip.d.ts.map +1 -1
- package/dist/components/moni-tooltip.js +104 -40
- package/dist/components/moni-typography.d.ts +68 -30
- package/dist/components/moni-typography.d.ts.map +1 -1
- package/dist/components/moni-typography.js +70 -32
- package/dist/styles/tokens.css +7 -7
- package/dist/types/events.d.ts +17 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +1 -0
- package/dist/utils/event-emitter.d.ts +13 -0
- package/dist/utils/event-emitter.d.ts.map +1 -0
- package/dist/utils/event-emitter.js +13 -0
- package/package.json +2 -1
- package/src/components/_base/moni-element.ts +73 -9
- package/src/components/index.ts +2 -2
- package/src/components/loading-shapes.ts +19 -0
- package/src/components/moni-app-bar.test.ts +10 -10
- package/src/components/moni-app-bar.ts +72 -62
- package/src/components/moni-badge.ts +64 -66
- package/src/components/moni-bottom-sheet.test.ts +39 -45
- package/src/components/moni-bottom-sheet.ts +222 -131
- package/src/components/moni-button-group.test.ts +14 -14
- package/src/components/moni-button-group.ts +107 -29
- package/src/components/moni-button-segment.ts +53 -12
- package/src/components/moni-button.test.ts +28 -26
- package/src/components/moni-button.ts +139 -39
- package/src/components/moni-card.test.ts +6 -6
- package/src/components/moni-card.ts +70 -50
- package/src/components/moni-carousel.test.ts +12 -12
- package/src/components/moni-carousel.ts +953 -290
- package/src/components/moni-checkbox.test.ts +7 -7
- package/src/components/moni-checkbox.ts +62 -48
- package/src/components/moni-chip.test.ts +17 -17
- package/src/components/moni-chip.ts +112 -34
- package/src/components/moni-color-field.test.ts +5 -5
- package/src/components/moni-color-field.ts +108 -24
- package/src/components/moni-context-menu.test.ts +11 -11
- package/src/components/moni-context-menu.ts +76 -30
- package/src/components/moni-dialog.ts +108 -59
- package/src/components/moni-divider.test.ts +4 -4
- package/src/components/moni-divider.ts +42 -30
- package/src/components/moni-expansion.ts +32 -22
- package/src/components/moni-fab-menu.test.ts +11 -11
- package/src/components/moni-fab-menu.ts +69 -31
- package/src/components/moni-fab.test.ts +16 -16
- package/src/components/moni-fab.ts +100 -34
- package/src/components/moni-file-field.test.ts +8 -8
- package/src/components/moni-file-field.ts +132 -30
- package/src/components/moni-icon.test.ts +11 -11
- package/src/components/moni-icon.ts +43 -38
- package/src/components/moni-list-item.test.ts +10 -10
- package/src/components/moni-list-item.ts +61 -41
- package/src/components/moni-list.ts +37 -21
- package/src/components/moni-loading-indicator.test.ts +3 -3
- package/src/components/moni-loading-indicator.ts +54 -22
- package/src/components/moni-menu-item.ts +23 -16
- package/src/components/moni-menu.test.ts +7 -7
- package/src/components/moni-menu.ts +87 -30
- package/src/components/moni-morph-modal.test.ts +18 -17
- package/src/components/moni-morph-modal.ts +327 -60
- package/src/components/moni-nav-item.ts +59 -29
- package/src/components/moni-nav.test.ts +11 -11
- package/src/components/moni-nav.ts +94 -35
- package/src/components/moni-progress.test.ts +9 -9
- package/src/components/moni-progress.ts +100 -56
- package/src/components/moni-radio.test.ts +8 -8
- package/src/components/moni-radio.ts +71 -51
- package/src/components/moni-ripple.ts +74 -59
- package/src/components/moni-segmented-button.test.ts +4 -4
- package/src/components/moni-segmented-button.ts +94 -17
- package/src/components/moni-select-option.ts +30 -23
- package/src/components/moni-select.test.ts +11 -11
- package/src/components/moni-select.ts +361 -50
- package/src/components/moni-side-sheet.test.ts +7 -7
- package/src/components/moni-side-sheet.ts +94 -32
- package/src/components/moni-slider.test.ts +7 -7
- package/src/components/moni-slider.ts +165 -42
- package/src/components/moni-snackbar.test.ts +8 -8
- package/src/components/moni-snackbar.ts +53 -43
- package/src/components/moni-split-button.ts +63 -18
- package/src/components/moni-step.test.ts +7 -7
- package/src/components/moni-step.ts +41 -29
- package/src/components/moni-stepper.test.ts +7 -7
- package/src/components/moni-stepper.ts +72 -34
- package/src/components/moni-switch.test.ts +12 -12
- package/src/components/moni-switch.ts +66 -47
- package/src/components/moni-tab.test.ts +5 -5
- package/src/components/moni-tab.ts +29 -21
- package/src/components/moni-tabs.ts +34 -28
- package/src/components/moni-text-field.test.ts +13 -13
- package/src/components/moni-text-field.ts +181 -30
- package/src/components/moni-textarea.test.ts +17 -17
- package/src/components/moni-textarea.ts +155 -31
- package/src/components/moni-time-picker.test.ts +5 -5
- package/src/components/moni-time-picker.ts +128 -33
- package/src/components/moni-toolbar.test.ts +8 -8
- package/src/components/moni-toolbar.ts +37 -23
- package/src/components/moni-tooltip.test.ts +15 -15
- package/src/components/moni-tooltip.ts +107 -40
- package/src/components/moni-typography.test.ts +10 -10
- package/src/components/moni-typography.ts +73 -32
- package/src/styles/tokens.css +7 -7
- package/src/types/events.ts +17 -0
- package/src/utils/event-emitter.ts +26 -0
|
@@ -8,133 +8,318 @@
|
|
|
8
8
|
import { html, css } from 'lit';
|
|
9
9
|
import { customElement, property, query, state, queryAll } from 'lit/decorators.js';
|
|
10
10
|
import { MoniElement, sharedStyles } from './_base/index.js';
|
|
11
|
+
import { emitMoniEvent } from '../utils/event-emitter.js';
|
|
11
12
|
import { gsap } from 'gsap';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
+
* Modelo de datos para un solo elemento en un `<moni-carousel>`.
|
|
15
16
|
*/
|
|
16
17
|
export interface CarouselItem {
|
|
17
|
-
/**
|
|
18
|
+
/** Título a mostrar sobre el elemento. */
|
|
18
19
|
title: string;
|
|
19
|
-
/** URL
|
|
20
|
+
/** URL o ruta a la imagen de fondo. */
|
|
20
21
|
img: string;
|
|
21
|
-
/**
|
|
22
|
+
/** Objetivo de enlace opcional. Si se proporciona, el elemento se renderiza como un `<a>`. */
|
|
22
23
|
href?: string;
|
|
23
|
-
/**
|
|
24
|
+
/** Atributo target del enlace (ej. `'_blank'`). Solo se aplica si se establece `href`. */
|
|
24
25
|
target?: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
|
-
* Material Design 3 Expressive Carousel
|
|
29
|
+
* Componente Material Design 3 Expressive Carousel.
|
|
29
30
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
31
|
+
* Los carruseles muestran una colección de elementos relacionados en una lista horizontal desplazable.
|
|
32
|
+
* Permiten a los usuarios navegar rápidamente a través de elementos como imágenes, tarjetas, o productos.
|
|
32
33
|
*
|
|
33
|
-
* **
|
|
34
|
+
* **Referencia de la especificación M3:** `m3-docs/components/carousel/specs.md`
|
|
34
35
|
*
|
|
35
|
-
* **
|
|
36
|
-
* - `multi-browse` (
|
|
37
|
-
*
|
|
38
|
-
* - `hero` —
|
|
39
|
-
*
|
|
40
|
-
* - `uncontained` —
|
|
41
|
-
*
|
|
36
|
+
* **Variantes de diseño:**
|
|
37
|
+
* - `multi-browse` (por defecto) — Muestra una mezcla de elementos grandes, medianos y pequeños (asomando).
|
|
38
|
+
* Mejor para explorar un gran número de elementos.
|
|
39
|
+
* - `hero` — Se enfoca en un elemento primario grande mientras muestra una fracción del
|
|
40
|
+
* siguiente elemento. Mejor para destacar contenido importante.
|
|
41
|
+
* - `uncontained` — Diseño estándar donde todos los elementos tienen el mismo ancho y
|
|
42
|
+
* se desbordan por los bordes del contenedor.
|
|
42
43
|
*
|
|
43
|
-
* **
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* **Animación y Gestos:**
|
|
45
|
+
* Este componente usa GSAP para animaciones suaves de arrastre, deslizamiento y ajuste,
|
|
46
|
+
* reflejando las especificaciones de movimiento de alta fidelidad de M3 Expressive. Maneja
|
|
47
|
+
* gestos táctiles para móviles y arrastre del ratón para escritorio.
|
|
47
48
|
*
|
|
48
|
-
* **Auto-
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
49
|
+
* **Auto-tamaño (modo `auto`):**
|
|
50
|
+
* Cuando `auto=true` (por defecto), el carrusel mide su propio ancho y
|
|
51
|
+
* calcula dinámicamente los tamaños óptimos para los elementos grandes, medianos y pequeños
|
|
52
|
+
* basados en el `layout` activo para asegurar que encajen perfectamente sin huecos incómodos
|
|
53
|
+
* ni recortes en los bordes.
|
|
53
54
|
*
|
|
54
55
|
* @example
|
|
55
56
|
* ```html
|
|
56
|
-
* <!--
|
|
57
|
+
* <!-- Uso declarativo a través de propiedades DOM (recomendado) -->
|
|
57
58
|
* <moni-carousel layout="hero"></moni-carousel>
|
|
58
59
|
* <script>
|
|
59
60
|
* const carousel = document.querySelector('moni-carousel');
|
|
60
61
|
* carousel.items = [
|
|
61
|
-
* { title: '
|
|
62
|
-
* { title: '
|
|
62
|
+
* { title: 'Elemento 1', img: '/img1.jpg', href: '/link1' },
|
|
63
|
+
* { title: 'Elemento 2', img: '/img2.jpg' }
|
|
63
64
|
* ];
|
|
64
65
|
* </script>
|
|
65
66
|
*
|
|
66
|
-
* <!--
|
|
67
|
+
* <!-- Uso basado en slots (para SSR o contenido estático simple) -->
|
|
67
68
|
* <moni-carousel layout="uncontained">
|
|
68
69
|
* <div slot="item">
|
|
69
70
|
* <img src="/img1.jpg" />
|
|
70
|
-
* <h3>
|
|
71
|
+
* <h3>Elemento Estático</h3>
|
|
71
72
|
* </div>
|
|
72
73
|
* </moni-carousel>
|
|
73
74
|
* ```
|
|
74
75
|
*
|
|
75
|
-
* @slot item -
|
|
76
|
-
*
|
|
76
|
+
* @slot item - Alternativa a la propiedad `items`. Inserta elementos HTML individuales
|
|
77
|
+
* en lugar de pasar objetos de datos.
|
|
77
78
|
*
|
|
78
|
-
* @csspart carousel -
|
|
79
|
-
* @csspart track -
|
|
80
|
-
* @csspart item -
|
|
81
|
-
* @csspart img -
|
|
82
|
-
* @csspart title -
|
|
79
|
+
* @csspart carousel - El envoltorio exterior.
|
|
80
|
+
* @csspart track - El elemento de la pista desplazable.
|
|
81
|
+
* @csspart item - Contenedores de elementos del carrusel individuales.
|
|
82
|
+
* @csspart img - Los elementos de imagen dentro de los elementos.
|
|
83
|
+
* @csspart title - Los elementos de texto de título dentro de los elementos.
|
|
83
84
|
*/
|
|
84
85
|
@customElement('moni-carousel')
|
|
85
86
|
export class MoniCarousel extends MoniElement {
|
|
87
|
+
/**
|
|
88
|
+
* Array de elementos a mostrar en el carrusel.
|
|
89
|
+
* Cada elemento requiere al menos `title` e `img`. El opcional `href` renderiza la tarjeta como un enlace.
|
|
90
|
+
* @default []
|
|
91
|
+
*/
|
|
86
92
|
@property({ type: Array }) items: CarouselItem[] = [];
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Variante de diseño visual. Determina cuántos elementos son visibles y cómo escalan.
|
|
96
|
+
* - `'multi-browse'` (por defecto) — Elementos grandes + medianos + pequeños (asomando).
|
|
97
|
+
* - `'hero'` — Uno o más elementos grandes + elemento pequeño asomando.
|
|
98
|
+
* - `'uncontained'` — Elementos de igual ancho que se desbordan por el borde.
|
|
99
|
+
* @default 'multi-browse'
|
|
100
|
+
*/
|
|
87
101
|
@property({ reflect: true }) layout: 'multi-browse' | 'hero' | 'uncontained' = 'multi-browse';
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Cuando es `true`, el carrusel calcula los tamaños óptimos de los elementos automáticamente
|
|
105
|
+
* basado en el ancho del contenedor y la variante de `layout` activa.
|
|
106
|
+
* Cuando es `false`, usa los valores explícitos `largeWidth`, `mediumWidth`, `smallWidth`.
|
|
107
|
+
* @default true
|
|
108
|
+
*/
|
|
88
109
|
@property({ type: Boolean, reflect: true }) auto = true;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Ancho en píxeles para el elemento grande (enfoque principal) del carrusel.
|
|
113
|
+
* Solo se usa cuando `auto=false`.
|
|
114
|
+
* @default 220
|
|
115
|
+
*/
|
|
89
116
|
@property({ type: Number, attribute: 'large-width' }) largeWidth = 220;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Ancho en píxeles para el elemento de tamaño mediano (secundario) del carrusel.
|
|
120
|
+
* Solo se usa cuando `auto=false` y `layout='multi-browse'`.
|
|
121
|
+
* @default 96
|
|
122
|
+
*/
|
|
90
123
|
@property({ type: Number, attribute: 'medium-width' }) mediumWidth = 96;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Ancho en píxeles para el elemento pequeño (asomando) del carrusel.
|
|
127
|
+
* Solo se usa cuando `auto=false`.
|
|
128
|
+
* @default 48
|
|
129
|
+
*/
|
|
91
130
|
@property({ type: Number, attribute: 'small-width' }) smallWidth = 48;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Espacio en píxeles entre los elementos del carrusel.
|
|
134
|
+
* @default 8
|
|
135
|
+
*/
|
|
92
136
|
@property({ type: Number }) gap = 8;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Relleno horizontal en píxeles aplicado al inicio de la pista del carrusel.
|
|
140
|
+
* @default 16
|
|
141
|
+
*/
|
|
93
142
|
@property({ type: Number }) padding = 16;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Radio del borde en píxeles aplicado a cada tarjeta.
|
|
146
|
+
* @default 28
|
|
147
|
+
*/
|
|
94
148
|
@property({ type: Number, attribute: 'border-radius' }) borderRadius = 28;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Cuando es `true`, renderiza un enlace "Mostrar todo" en el encabezado.
|
|
152
|
+
* @default false
|
|
153
|
+
*/
|
|
95
154
|
@property({ type: Boolean, attribute: 'show-all' }) showAll = false;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Texto de etiqueta para el enlace "Mostrar todo".
|
|
158
|
+
* @default 'Show all'
|
|
159
|
+
*/
|
|
96
160
|
@property({ attribute: 'show-all-text' }) showAllText = 'Show all';
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Encabezado de sección opcional renderizado sobre la pista del carrusel.
|
|
164
|
+
* @default ''
|
|
165
|
+
*/
|
|
97
166
|
@property({ attribute: 'header-text' }) headerText = '';
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Cuando es `true`, oculta los botones de flecha de navegación anterior/siguiente.
|
|
170
|
+
* @default false
|
|
171
|
+
*/
|
|
98
172
|
@property({ type: Boolean, attribute: 'hide-nav' }) hideNav = false;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Cuando es `true`, habilita el bucle infinito sin interrupciones clonando la lista de elementos
|
|
176
|
+
* a través de un búfer de desplazamiento virtual grande, luego reposicionando silenciosamente
|
|
177
|
+
* el desplazamiento cuando el usuario se acerca a cualquier borde.
|
|
178
|
+
* @default false
|
|
179
|
+
*/
|
|
99
180
|
@property({ type: Boolean, reflect: true }) infinite = false;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Cuando es `true`, avanza automáticamente el carrusel en el intervalo definido
|
|
184
|
+
* por `autoplayInterval`. La reproducción automática se detiene durante las interacciones de arrastre activas.
|
|
185
|
+
* @default false
|
|
186
|
+
*/
|
|
100
187
|
@property({ type: Boolean, reflect: true }) autoplay = false;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Milisegundos entre los avances automáticos de diapositivas cuando `autoplay=true`.
|
|
191
|
+
* @default 3000
|
|
192
|
+
*/
|
|
101
193
|
@property({ type: Number, attribute: 'autoplay-interval' }) autoplayInterval = 3000;
|
|
102
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Ancho medido del elemento host del carrusel en píxeles.
|
|
197
|
+
* Actualizado por el `ResizeObserver` en cada redimensionamiento del contenedor y usado por
|
|
198
|
+
* `computedLayout` para calcular cuántos elementos caben en la pantalla.
|
|
199
|
+
*/
|
|
103
200
|
@state() private _containerWidth = 0;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Elementos analizados desde el slot Light DOM (uso declarativo).
|
|
204
|
+
* Poblado por `_handleSlotChange` cuando los usuarios insertan elementos `<img>` o `<a><img></a>`.
|
|
205
|
+
*/
|
|
104
206
|
@state() private _slottedItems: CarouselItem[] = [];
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Si el slot `show-all` contiene contenido proyectado por el usuario.
|
|
210
|
+
* Controla si se renderiza el área "Mostrar todo" del encabezado.
|
|
211
|
+
*/
|
|
105
212
|
@state() private _hasSlottedShowAll = false;
|
|
106
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Devuelve la fuente del elemento activo.
|
|
216
|
+
* La propiedad programática `items` tiene prioridad sobre los hijos Light DOM en slots.
|
|
217
|
+
* Recurre a `_slottedItems` cuando no se proporcionan elementos programáticos.
|
|
218
|
+
*/
|
|
107
219
|
get effectiveItems(): CarouselItem[] {
|
|
108
220
|
return this.items && this.items.length > 0 ? this.items : this._slottedItems;
|
|
109
221
|
}
|
|
110
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Referencia directa al div contenedor de desplazamiento.
|
|
225
|
+
* Usado para leer `scrollLeft`, modificar `scrollBehavior`, y calcular `scrollWidth`.
|
|
226
|
+
*/
|
|
111
227
|
@query('.scroll-container') private _scrollContainer!: HTMLDivElement;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* NodeList en vivo de todos los elementos de tarjeta renderizados.
|
|
231
|
+
* Usado por el motor de diseño para aplicar actualizaciones de transform/width por cuadro a través de GSAP quickSetters.
|
|
232
|
+
*/
|
|
112
233
|
@queryAll('.card') private _cards!: NodeListOf<HTMLDivElement>;
|
|
113
234
|
|
|
235
|
+
/** Instancia ResizeObserver que observa las dimensiones del elemento host. */
|
|
114
236
|
private _resizeObserver: ResizeObserver | null = null;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
private
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
private
|
|
121
|
-
|
|
122
|
-
|
|
237
|
+
|
|
238
|
+
/** Estado interno de visibilidad (por índice lógico) para evitar falsos eventos al ciclar el scroll infinito */
|
|
239
|
+
private _logicalItemInViewport: boolean[] = [];
|
|
240
|
+
|
|
241
|
+
/** `true` mientras el usuario arrastra activamente (ratón o toque presionado). */
|
|
242
|
+
private _isDragging = false;
|
|
243
|
+
|
|
244
|
+
/** Coordenada X en el inicio del gesto de arrastre actual (relativa al contenedor de desplazamiento). */
|
|
245
|
+
private _dragStartX = 0;
|
|
246
|
+
|
|
247
|
+
/** Valor `scrollLeft` del contenedor en el momento en que comenzó el arrastre. */
|
|
248
|
+
private _scrollLeftAtDragStart = 0;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Velocidad del puntero en píxeles por milisegundo.
|
|
252
|
+
* Calculada gradualmente durante el arrastre y usada para el impulso/inercia al soltar.
|
|
253
|
+
*/
|
|
254
|
+
private _pointerVelocity = 0;
|
|
255
|
+
|
|
256
|
+
/** Marca de tiempo (de `performance.now()`) del último evento de movimiento del puntero/toque. */
|
|
257
|
+
private _lastPointerTime = 0;
|
|
258
|
+
|
|
259
|
+
/** Coordenada X del puntero en el cuadro del evento de movimiento anterior. */
|
|
260
|
+
private _lastPointerX = 0;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Distancia horizontal absoluta acumulada (en píxeles) recorrida durante el arrastre actual.
|
|
264
|
+
* Usada para distinguir un deslizamiento intencional de un clic accidental (umbral: 8px).
|
|
265
|
+
*/
|
|
266
|
+
private _totalDraggedDistance = 0;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* ID de `requestAnimationFrame` activo del bucle de desplazamiento por inercia.
|
|
270
|
+
* Almacenado para que pueda cancelarse cuando el usuario inicia un nuevo arrastre.
|
|
271
|
+
*/
|
|
272
|
+
private _animationFrameId: number | null = null;
|
|
273
|
+
|
|
274
|
+
/** `true` cuando el ancho de la ventana gráfica coincide con el punto de ruptura móvil (`max-width: 600px`). */
|
|
123
275
|
private _isMobile = false;
|
|
124
276
|
|
|
125
|
-
//
|
|
277
|
+
// ─── Estado de interpolación GSAP Ticker ──────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* La posición de desplazamiento hacia la que el ticker de GSAP está interpolando (lerping) **en cada cuadro**.
|
|
281
|
+
* Expresada en unidades de `itemSize` (no píxeles) para matemáticas independientes del diseño.
|
|
282
|
+
*/
|
|
126
283
|
private _tickerTarget = 0;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* La posición de desplazamiento interpolada actual usada para impulsar el diseño visual.
|
|
287
|
+
* Interpola hacia `_tickerTarget` en cada cuadro, creando el efecto de suavizado.
|
|
288
|
+
*/
|
|
127
289
|
private _tickerCurrent = 0;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* `true` mientras el callback del ticker de GSAP (`_tick`) está registrado y en ejecución.
|
|
293
|
+
* Evita añadir el callback del ticker múltiples veces en llamadas concurrentes.
|
|
294
|
+
*/
|
|
128
295
|
private _isTicking = false;
|
|
129
296
|
|
|
130
|
-
//
|
|
297
|
+
// ─── Estado de Bucle Infinito & Reproducción Automática ─────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Protege la inicialización de una sola vez de la posición del búfer de desplazamiento infinito.
|
|
301
|
+
* Se establece en `true` una vez que el contenedor de desplazamiento se ha reposicionado
|
|
302
|
+
* silenciosamente al centro del búfer virtual después del primer renderizado.
|
|
303
|
+
*/
|
|
131
304
|
private _infiniteInitialized = false;
|
|
305
|
+
|
|
306
|
+
/** ID del temporizador devuelto por `setInterval()` para la función de reproducción automática. */
|
|
132
307
|
private _autoplayTimer: any = null;
|
|
133
308
|
|
|
134
|
-
//
|
|
309
|
+
// ─── Cachés de GSAP quickSetter ───────────────────────────────────────────────
|
|
310
|
+
// Las funciones quickSetter evitan la sobrecarga de llamar a `gsap.set()` en cada cuadro.
|
|
311
|
+
// Pre-construidas una vez después de cada ciclo de renderizado; invalidadas cuando cambia el número de tarjetas.
|
|
312
|
+
|
|
313
|
+
/** Array de quickSetters de GSAP para el CSS `transform: translateX()` de cada tarjeta. */
|
|
135
314
|
private _cardSetX: ((value: number) => void)[] = [];
|
|
315
|
+
|
|
316
|
+
/** Array de quickSetters de GSAP para el `width` en línea de cada tarjeta. */
|
|
136
317
|
private _cardSetWidth: ((value: number) => void)[] = [];
|
|
318
|
+
|
|
319
|
+
/** Array de quickSetters de GSAP para el parallax `translateX` del `<img>` de cada tarjeta. */
|
|
137
320
|
private _imgSetX: ((value: number) => void)[] = [];
|
|
321
|
+
|
|
322
|
+
/** Array de quickSetters de GSAP para la `opacity` del elemento `.card-title` de cada tarjeta. */
|
|
138
323
|
private _titleSetOpacity: ((value: number) => void)[] = [];
|
|
139
324
|
|
|
140
325
|
static override styles = [
|
|
@@ -196,6 +381,8 @@ export class MoniCarousel extends MoniElement {
|
|
|
196
381
|
overflow-x: auto;
|
|
197
382
|
overflow-y: hidden;
|
|
198
383
|
scroll-snap-type: x mandatory;
|
|
384
|
+
scroll-padding-left: var(--carousel-padding, 16px);
|
|
385
|
+
scroll-padding-right: var(--carousel-padding, 16px);
|
|
199
386
|
scrollbar-width: none;
|
|
200
387
|
-webkit-overflow-scrolling: touch;
|
|
201
388
|
cursor: grab;
|
|
@@ -364,6 +551,12 @@ export class MoniCarousel extends MoniElement {
|
|
|
364
551
|
`
|
|
365
552
|
];
|
|
366
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Hook del ciclo de vida (Lit).
|
|
556
|
+
* Inicializa el `ResizeObserver` para reaccionar a cambios en el tamaño del contenedor,
|
|
557
|
+
* lo que es crucial para recalcular el `computedLayout` (cuántas cards caben en el viewport).
|
|
558
|
+
* También añade listeners globales pasivos para eventos de redimensionamiento de ventana.
|
|
559
|
+
*/
|
|
367
560
|
override connectedCallback() {
|
|
368
561
|
super.connectedCallback();
|
|
369
562
|
if (typeof ResizeObserver !== 'undefined') {
|
|
@@ -388,14 +581,19 @@ export class MoniCarousel extends MoniElement {
|
|
|
388
581
|
}
|
|
389
582
|
}
|
|
390
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Hook del ciclo de vida (Lit).
|
|
586
|
+
* Limpia y desconecta el `ResizeObserver`, el `IntersectionObserver` y elimina
|
|
587
|
+
* los event listeners globales para evitar fugas de memoria (memory leaks).
|
|
588
|
+
*/
|
|
391
589
|
override disconnectedCallback() {
|
|
392
590
|
if (this._resizeObserver) {
|
|
393
591
|
this._resizeObserver.disconnect();
|
|
394
592
|
}
|
|
395
593
|
// Cancel any pending RAF or ticker
|
|
396
|
-
if (this.
|
|
397
|
-
cancelAnimationFrame(this.
|
|
398
|
-
this.
|
|
594
|
+
if (this._animationFrameId !== null) {
|
|
595
|
+
cancelAnimationFrame(this._animationFrameId);
|
|
596
|
+
this._animationFrameId = null;
|
|
399
597
|
}
|
|
400
598
|
gsap.ticker.remove(this._tick);
|
|
401
599
|
this._isTicking = false;
|
|
@@ -408,6 +606,11 @@ export class MoniCarousel extends MoniElement {
|
|
|
408
606
|
super.disconnectedCallback();
|
|
409
607
|
}
|
|
410
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Hook del ciclo de vida (Lit) ejecutado tras el primer render.
|
|
611
|
+
* Inspecciona el Shadow DOM para ubicar el slot por defecto y procesar
|
|
612
|
+
* las cards iniciales (`_handleSlotChange` invocado manualmente).
|
|
613
|
+
*/
|
|
411
614
|
override firstUpdated() {
|
|
412
615
|
const defaultSlot = this.shadowRoot?.querySelector('slot:not([name])');
|
|
413
616
|
if (defaultSlot) {
|
|
@@ -417,6 +620,7 @@ export class MoniCarousel extends MoniElement {
|
|
|
417
620
|
if (showAllSlot) {
|
|
418
621
|
this._handleShowAllSlotChange({ target: showAllSlot } as any);
|
|
419
622
|
}
|
|
623
|
+
|
|
420
624
|
this._buildQuickSetters();
|
|
421
625
|
this.updateLayout(true);
|
|
422
626
|
|
|
@@ -425,6 +629,15 @@ export class MoniCarousel extends MoniElement {
|
|
|
425
629
|
}
|
|
426
630
|
}
|
|
427
631
|
|
|
632
|
+
/**
|
|
633
|
+
* Hook del ciclo de vida (Lit). Se invoca tras cada actualización reactiva.
|
|
634
|
+
*
|
|
635
|
+
* Lógica principal:
|
|
636
|
+
* - Si cambian propiedades estructurales (`autoplay`, `infinite`, `gap`), reconstruye el
|
|
637
|
+
* layout y detiene/inicia el timer de autoplay.
|
|
638
|
+
* - Delega en `_checkInfiniteWrap()` para reposicionar el scroll si estamos cerca
|
|
639
|
+
* de los bordes del carrusel infinito.
|
|
640
|
+
*/
|
|
428
641
|
override updated(changedProperties: Map<string, any>) {
|
|
429
642
|
super.updated(changedProperties);
|
|
430
643
|
if (
|
|
@@ -465,166 +678,282 @@ export class MoniCarousel extends MoniElement {
|
|
|
465
678
|
}
|
|
466
679
|
}
|
|
467
680
|
|
|
468
|
-
|
|
469
|
-
|
|
681
|
+
/**
|
|
682
|
+
* Motor de cálculo del Layout.
|
|
683
|
+
*
|
|
684
|
+
* Basado en las proporciones de Material 3 para carruseles:
|
|
685
|
+
* Calcula el ancho disponible (`W_c`) y determina el número de ítems grandes (`L`),
|
|
686
|
+
* medianos (`M`) y pequeños (`S`) que caben en pantalla, usando la fórmula de la
|
|
687
|
+
* especificación (multi-browse, hero o uncontained).
|
|
688
|
+
*
|
|
689
|
+
* Retorna `{ L, M, S, N }` donde `N` es el divisor base para calcular tamaños proporcionales.
|
|
690
|
+
*/
|
|
691
|
+
get computedLayout() {
|
|
692
|
+
/**
|
|
693
|
+
* `containerWidth` — ancho total en píxeles del elemento host del carrusel.
|
|
694
|
+
* Recurre a `getBoundingClientRect()` si el ResizeObserver aún no se ha disparado,
|
|
695
|
+
* y en última instancia por defecto a 360px (un ancho común de ventana gráfica móvil).
|
|
696
|
+
*/
|
|
697
|
+
const containerWidth = this._containerWidth || this.getBoundingClientRect().width || 360;
|
|
470
698
|
const gap = this.gap;
|
|
471
699
|
const padding = this.padding;
|
|
472
|
-
const W_a = W_c - 2 * padding;
|
|
473
700
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
701
|
+
/**
|
|
702
|
+
* `availableWidth` — el ancho de pista utilizable después de restar el padding
|
|
703
|
+
* inicial y final del ancho total del contenedor. Este es el espacio que el algoritmo de diseño
|
|
704
|
+
* divide en ranuras de elementos grandes, medianos y pequeños.
|
|
705
|
+
* Fórmula: availableWidth = containerWidth - 2 × padding
|
|
706
|
+
*/
|
|
707
|
+
const availableWidth = containerWidth - 2 * padding;
|
|
708
|
+
|
|
709
|
+
/** `largeItemWidth` — ancho calculado o configurado (px) de los elementos de enfoque principal. */
|
|
710
|
+
let largeItemWidth = this.largeWidth;
|
|
711
|
+
/** `mediumItemWidth` — ancho calculado o configurado (px) de los elementos secundarios. */
|
|
712
|
+
let mediumItemWidth = this.mediumWidth;
|
|
713
|
+
/** `smallItemWidth` — ancho calculado o configurado (px) de los elementos pequeños/asomando. */
|
|
714
|
+
let smallItemWidth = this.smallWidth;
|
|
715
|
+
/**
|
|
716
|
+
* `largeItemCount` — el número de elementos grandes visibles simultáneamente en la ventana gráfica.
|
|
717
|
+
* La especificación M3 define la fórmula de la ventana gráfica como:
|
|
718
|
+
* availableWidth = largeItemCount × largeItemWidth + mediumItemWidth + smallItemWidth + (largeItemCount + 1) × gap
|
|
719
|
+
*/
|
|
720
|
+
let largeItemCount = 1;
|
|
478
721
|
|
|
479
722
|
if (this.auto) {
|
|
480
723
|
if (this.layout === 'uncontained') {
|
|
481
|
-
//
|
|
482
|
-
// We target
|
|
483
|
-
|
|
724
|
+
// En diseño 'uncontained', todos los elementos tienen el mismo ancho y se desbordan por el borde.
|
|
725
|
+
// We target a comfortable width around 260px, but cap it to availableWidth.
|
|
726
|
+
largeItemWidth = Math.min(availableWidth, 260);
|
|
484
727
|
} else if (this.layout === 'hero') {
|
|
485
|
-
// Hero
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
728
|
+
// M3 Hero layout formula:
|
|
729
|
+
// availableWidth = largeItemCount × largeItemWidth + smallItemWidth + largeItemCount × gap
|
|
730
|
+
// We fix smallItemWidth = 48px (M3 spec minimum peeking width), then solve for largeItemWidth.
|
|
731
|
+
smallItemWidth = 48;
|
|
732
|
+
largeItemCount = Math.max(1, Math.floor((availableWidth - smallItemWidth) / (250 + gap)));
|
|
733
|
+
largeItemWidth = (availableWidth - smallItemWidth - largeItemCount * gap) / largeItemCount;
|
|
734
|
+
|
|
735
|
+
// Límite de seguridad: si el elemento grande fuera menor a 150px, forzar modo de un solo elemento.
|
|
736
|
+
if (largeItemWidth < 150) {
|
|
737
|
+
largeItemCount = 1;
|
|
738
|
+
largeItemWidth = availableWidth - smallItemWidth - gap;
|
|
494
739
|
}
|
|
495
740
|
} else {
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
741
|
+
// M3 Multi-Browse layout formula:
|
|
742
|
+
// availableWidth = largeItemCount × largeItemWidth + mediumItemWidth + smallItemWidth + (largeItemCount + 1) × gap
|
|
743
|
+
// mediumItemWidth = 0.45 × largeItemWidth (derived from M3 spec proportions)
|
|
744
|
+
// Solving: largeItemWidth = (availableWidth - smallItemWidth - (largeItemCount + 1) × gap) / (largeItemCount + 0.45)
|
|
745
|
+
smallItemWidth = 48;
|
|
746
|
+
|
|
747
|
+
// Optimización: probar valores de largeItemCount de 1 a 10 y elegir aquel cuyo
|
|
748
|
+
// largeItemWidth esté más cerca del objetivo ideal de 220px.
|
|
749
|
+
let bestLargeItemCount = 1;
|
|
750
|
+
let bestLargeItemWidth = 220;
|
|
501
751
|
let bestDiff = Infinity;
|
|
502
752
|
|
|
503
|
-
for (let
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
753
|
+
for (let candidateCount = 1; candidateCount <= 10; candidateCount++) {
|
|
754
|
+
// Solve for largeItemWidth given this candidateCount:
|
|
755
|
+
const candidateLargeWidth = (availableWidth - smallItemWidth - (candidateCount + 1) * gap) / (candidateCount + 0.45);
|
|
756
|
+
|
|
757
|
+
// Only accept widths within a reasonable human-perceivable range [140px, 340px].
|
|
758
|
+
if (candidateLargeWidth >= 140 && candidateLargeWidth <= 340) {
|
|
759
|
+
const diff = Math.abs(candidateLargeWidth - 220);
|
|
507
760
|
if (diff < bestDiff) {
|
|
508
761
|
bestDiff = diff;
|
|
509
|
-
|
|
510
|
-
|
|
762
|
+
bestLargeItemCount = candidateCount;
|
|
763
|
+
bestLargeItemWidth = candidateLargeWidth;
|
|
511
764
|
}
|
|
512
765
|
}
|
|
513
766
|
}
|
|
514
767
|
|
|
515
768
|
if (bestDiff === Infinity) {
|
|
516
|
-
|
|
517
|
-
|
|
769
|
+
// Fallback: no valid solution found; use single large item with minimum spacing.
|
|
770
|
+
largeItemCount = 1;
|
|
771
|
+
largeItemWidth = (availableWidth - smallItemWidth - 2 * gap) / 1.45;
|
|
518
772
|
} else {
|
|
519
|
-
|
|
520
|
-
|
|
773
|
+
largeItemCount = bestLargeItemCount;
|
|
774
|
+
largeItemWidth = bestLargeItemWidth;
|
|
521
775
|
}
|
|
522
|
-
|
|
776
|
+
// Especificación M3: el elemento mediano es el 45% del ancho del elemento grande.
|
|
777
|
+
mediumItemWidth = 0.45 * largeItemWidth;
|
|
523
778
|
}
|
|
524
779
|
} else {
|
|
525
|
-
//
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
780
|
+
// ── Manual sizing mode (auto=false) ──────────────────────────────────
|
|
781
|
+
// Usar los valores explícitos de propiedad proporcionados por el consumidor.
|
|
782
|
+
largeItemWidth = this.largeWidth;
|
|
783
|
+
mediumItemWidth = this.mediumWidth;
|
|
784
|
+
smallItemWidth = this.smallWidth;
|
|
529
785
|
|
|
530
786
|
if (this.layout === 'hero') {
|
|
531
|
-
|
|
787
|
+
largeItemCount = 1;
|
|
532
788
|
} else if (this.layout === 'uncontained') {
|
|
533
|
-
|
|
789
|
+
// En modo uncontained, calcular cuántos elementos grandes caben en el ancho disponible.
|
|
790
|
+
largeItemCount = Math.max(1, Math.floor(availableWidth / (largeItemWidth + gap)));
|
|
534
791
|
} else {
|
|
535
|
-
|
|
792
|
+
largeItemCount = 1;
|
|
536
793
|
}
|
|
537
794
|
}
|
|
538
795
|
|
|
539
|
-
|
|
796
|
+
// Retornar los cuatro valores calculados como propiedades nombradas.
|
|
797
|
+
// Callers use destructuring: const { largeItemWidth, mediumItemWidth, smallItemWidth, largeItemCount } = this.computedLayout;
|
|
798
|
+
return { largeItemWidth, mediumItemWidth, smallItemWidth, largeItemCount };
|
|
540
799
|
}
|
|
541
800
|
|
|
542
|
-
|
|
543
|
-
|
|
801
|
+
/**
|
|
802
|
+
* Calcula el ancho total (incluyendo el gap) de un ítem de tamaño 'L' (Large).
|
|
803
|
+
* Este valor se utiliza como unidad de medida (step) para el Scroll Snapping
|
|
804
|
+
* y el cálculo de desplazamiento de páginas (sets).
|
|
805
|
+
*/
|
|
806
|
+
get itemSize() {
|
|
807
|
+
// El paso de ajuste es el ancho completo de la ranura: ancho del elemento más una unidad de espacio.
|
|
808
|
+
// Usado como la unidad de medida para el ajuste de desplazamiento, cálculos de impulso,
|
|
809
|
+
// y el posicionamiento del búfer de desplazamiento infinito.
|
|
810
|
+
return this.computedLayout.largeItemWidth + this.gap;
|
|
544
811
|
}
|
|
545
812
|
|
|
546
|
-
|
|
547
|
-
|
|
813
|
+
/**
|
|
814
|
+
* Algoritmo Central del Carrusel M3.
|
|
815
|
+
*
|
|
816
|
+
* Calcula el ancho visual, la posición horizontal y la opacidad para una sola tarjeta del carrusel
|
|
817
|
+
* basándose en su posición normalizada en relación con la ventana gráfica actualmente visible.
|
|
818
|
+
*
|
|
819
|
+
* Este es el corazón del efecto "Masked Carousel" de M3: a medida que una tarjeta transita de
|
|
820
|
+
* fuera de pantalla a en pantalla (posición -1 → 0 → N), su ancho se interpola de
|
|
821
|
+
* `smallItemWidth` → `largeItemWidth`, y su opacidad aumenta.
|
|
822
|
+
*
|
|
823
|
+
* Semántica de posición:
|
|
824
|
+
* - `normalizedPosition < -1` → tarjeta está fuera de pantalla a la izquierda (oculta)
|
|
825
|
+
* - `normalizedPosition = 0` → tarjeta está en la ranura grande más a la izquierda (enfoque principal)
|
|
826
|
+
* - `normalizedPosition = N-1` → tarjeta está en la ranura grande más a la derecha
|
|
827
|
+
* - `normalizedPosition > N` → tarjeta está fuera de pantalla a la derecha (oculta)
|
|
828
|
+
*
|
|
829
|
+
* @param normalizedPosition - El índice de posición fraccional de la tarjeta en relación con el progreso
|
|
830
|
+
* de desplazamiento actual. Un valor de `0` significa que la tarjeta está perfectamente alineada con la ranura
|
|
831
|
+
* grande inicial; los valores fraccionales representan transiciones parciales.
|
|
832
|
+
* @returns Objeto con `{ width, x, opacity }` para aplicar directamente al elemento de la tarjeta.
|
|
833
|
+
*/
|
|
834
|
+
private _getCardLayout(normalizedPosition: number) {
|
|
835
|
+
const { largeItemWidth, mediumItemWidth, smallItemWidth, largeItemCount } = this.computedLayout;
|
|
548
836
|
const gap = this.gap;
|
|
549
837
|
const padding = this.padding;
|
|
550
838
|
|
|
551
|
-
|
|
552
|
-
let
|
|
839
|
+
// Default: full large-item width, primary slot position, fully visible.
|
|
840
|
+
let width = largeItemWidth;
|
|
841
|
+
let horizontalPosition = padding;
|
|
553
842
|
let opacity = 1;
|
|
843
|
+
let visible = true;
|
|
554
844
|
|
|
555
845
|
if (this.layout === 'uncontained') {
|
|
556
|
-
width
|
|
557
|
-
|
|
846
|
+
// Uncontained: all items same width, positioned linearly without masking.
|
|
847
|
+
width = largeItemWidth;
|
|
848
|
+
horizontalPosition = padding + normalizedPosition * (largeItemWidth + gap);
|
|
558
849
|
opacity = 1;
|
|
850
|
+
const containerWidth = this._containerWidth || this.getBoundingClientRect().width || 360;
|
|
851
|
+
visible = (horizontalPosition + width > 0) && (horizontalPosition < containerWidth);
|
|
559
852
|
} else if (this.layout === 'hero') {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
853
|
+
// ── Anclas de posición para el diseño 'hero' ──────────────────────────
|
|
854
|
+
// `xAfterLastLargeSlot` — la posición X en píxeles inmediatamente después del último elemento grande.
|
|
855
|
+
// Aquí es donde comienza el elemento pequeño asomando.
|
|
856
|
+
const xAfterLastLargeSlot = padding + largeItemCount * (largeItemWidth + gap);
|
|
857
|
+
// `xBeforeFirstLargeSlot` — la posición X en píxeles del elemento pequeño asomando a la izquierda.
|
|
858
|
+
// Alineado al borde negativo exacto para que repose completamente fuera del contenedor.
|
|
859
|
+
const xBeforeFirstLargeSlot = -smallItemWidth;
|
|
860
|
+
|
|
861
|
+
if (normalizedPosition <= -1) {
|
|
862
|
+
// La tarjeta está oculta fuera de la pantalla a la izquierda, apilada detrás del elemento asomando.
|
|
863
|
+
width = smallItemWidth;
|
|
864
|
+
horizontalPosition = xBeforeFirstLargeSlot + (normalizedPosition - -1) * (smallItemWidth + Math.max(gap, padding));
|
|
566
865
|
opacity = 0;
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
866
|
+
visible = false;
|
|
867
|
+
} else if (normalizedPosition <= 0) {
|
|
868
|
+
// La tarjeta está en transición de entrada desde la izquierda: morphing de pequeño a grande.
|
|
869
|
+
// `normalizedPosition + 1` reasigna el rango [-1, 0] a [0, 1] para la interpolación.
|
|
870
|
+
width = gsap.utils.interpolate(smallItemWidth, largeItemWidth, normalizedPosition + 1);
|
|
871
|
+
horizontalPosition = gsap.utils.interpolate(xBeforeFirstLargeSlot, padding, normalizedPosition + 1);
|
|
872
|
+
opacity = gsap.utils.interpolate(0, 1, normalizedPosition + 1);
|
|
873
|
+
visible = true;
|
|
874
|
+
} else if (normalizedPosition <= largeItemCount - 1) {
|
|
875
|
+
// La tarjeta está en una de las ranuras grandes (completamente visible).
|
|
876
|
+
width = largeItemWidth;
|
|
877
|
+
horizontalPosition = padding + normalizedPosition * (largeItemWidth + gap);
|
|
574
878
|
opacity = 1;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
879
|
+
visible = true;
|
|
880
|
+
} else if (normalizedPosition <= largeItemCount) {
|
|
881
|
+
// La tarjeta está en transición de salida a la derecha: morphing de grande a pequeño (asomando).
|
|
882
|
+
// `transitionProgress` reasigna [N-1, N] a [0, 1].
|
|
883
|
+
const transitionProgress = normalizedPosition - (largeItemCount - 1);
|
|
884
|
+
const xLastLargeSlot = padding + (largeItemCount - 1) * (largeItemWidth + gap);
|
|
885
|
+
width = gsap.utils.interpolate(largeItemWidth, smallItemWidth, transitionProgress);
|
|
886
|
+
horizontalPosition = gsap.utils.interpolate(xLastLargeSlot, xAfterLastLargeSlot, transitionProgress);
|
|
887
|
+
opacity = gsap.utils.interpolate(1, 0, transitionProgress);
|
|
888
|
+
visible = true;
|
|
581
889
|
} else {
|
|
582
|
-
|
|
583
|
-
|
|
890
|
+
// La tarjeta está completamente fuera de la pantalla a la derecha, más allá de la ranura de asomo.
|
|
891
|
+
const containerWidth = this._containerWidth || this.getBoundingClientRect().width || 360;
|
|
892
|
+
width = smallItemWidth;
|
|
893
|
+
const distanceToClear = Math.max(smallItemWidth + padding, containerWidth - xAfterLastLargeSlot);
|
|
894
|
+
horizontalPosition = xAfterLastLargeSlot + (normalizedPosition - largeItemCount) * distanceToClear;
|
|
584
895
|
opacity = 0;
|
|
896
|
+
visible = false;
|
|
585
897
|
}
|
|
586
898
|
} else {
|
|
587
|
-
//
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
899
|
+
// `xMediumSlot` — X position where the medium item begins (after the last large item).
|
|
900
|
+
const xMediumSlot = padding + largeItemCount * (largeItemWidth + gap);
|
|
901
|
+
// `xSmallSlot` — X position where the small peeking item begins (after the medium item).
|
|
902
|
+
const xSmallSlot = xMediumSlot + mediumItemWidth + gap;
|
|
903
|
+
// `xBeforeFirstLargeSlot` — X position of the peeking small item off the left edge.
|
|
904
|
+
const xBeforeFirstLargeSlot = -smallItemWidth;
|
|
905
|
+
|
|
906
|
+
if (normalizedPosition <= -1) {
|
|
907
|
+
// La tarjeta está oculta fuera de la pantalla a la izquierda.
|
|
908
|
+
width = smallItemWidth;
|
|
909
|
+
horizontalPosition = xBeforeFirstLargeSlot + (normalizedPosition - -1) * (smallItemWidth + Math.max(gap, padding));
|
|
595
910
|
opacity = 0;
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
911
|
+
visible = false;
|
|
912
|
+
} else if (normalizedPosition <= 0) {
|
|
913
|
+
// La tarjeta está en transición de entrada desde la izquierda (pequeño → grande).
|
|
914
|
+
width = gsap.utils.interpolate(smallItemWidth, largeItemWidth, normalizedPosition + 1);
|
|
915
|
+
horizontalPosition = gsap.utils.interpolate(xBeforeFirstLargeSlot, padding, normalizedPosition + 1);
|
|
916
|
+
opacity = gsap.utils.interpolate(0, 1, normalizedPosition + 1);
|
|
917
|
+
visible = true;
|
|
918
|
+
} else if (normalizedPosition <= largeItemCount - 1) {
|
|
919
|
+
// La tarjeta está en una de las ranuras grandes (completamente visible).
|
|
920
|
+
width = largeItemWidth;
|
|
921
|
+
horizontalPosition = padding + normalizedPosition * (largeItemWidth + gap);
|
|
603
922
|
opacity = 1;
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
923
|
+
visible = true;
|
|
924
|
+
} else if (normalizedPosition <= largeItemCount) {
|
|
925
|
+
// La tarjeta está en transición de grande a mediano (entrando a la ranura mediana).
|
|
926
|
+
const transitionProgress = normalizedPosition - (largeItemCount - 1);
|
|
927
|
+
const xLastLargeSlot = padding + (largeItemCount - 1) * (largeItemWidth + gap);
|
|
928
|
+
width = gsap.utils.interpolate(largeItemWidth, mediumItemWidth, transitionProgress);
|
|
929
|
+
horizontalPosition = gsap.utils.interpolate(xLastLargeSlot, xMediumSlot, transitionProgress);
|
|
930
|
+
// La opacidad se desvanece a 0 a medida que la tarjeta se encoge en la ranura mediana — el efecto de máscara.
|
|
931
|
+
opacity = gsap.utils.interpolate(1, 0, transitionProgress);
|
|
932
|
+
visible = true;
|
|
933
|
+
} else if (normalizedPosition <= largeItemCount + 1) {
|
|
934
|
+
// La tarjeta está en la ranura mediana, en transición hacia la ranura pequeña/asomando.
|
|
935
|
+
const transitionProgress = normalizedPosition - largeItemCount;
|
|
936
|
+
width = gsap.utils.interpolate(mediumItemWidth, smallItemWidth, transitionProgress);
|
|
937
|
+
horizontalPosition = gsap.utils.interpolate(xMediumSlot, xSmallSlot, transitionProgress);
|
|
938
|
+
opacity = 0; // Ya oculta (enmascarada) una vez en la ranura mediana.
|
|
939
|
+
visible = true;
|
|
615
940
|
} else {
|
|
616
|
-
|
|
617
|
-
|
|
941
|
+
// La tarjeta está completamente fuera de la pantalla a la derecha.
|
|
942
|
+
const containerWidth = this._containerWidth || this.getBoundingClientRect().width || 360;
|
|
943
|
+
width = smallItemWidth;
|
|
944
|
+
const distanceToClear = Math.max(smallItemWidth + padding, containerWidth - xSmallSlot);
|
|
945
|
+
horizontalPosition = xSmallSlot + (normalizedPosition - (largeItemCount + 1)) * distanceToClear;
|
|
618
946
|
opacity = 0;
|
|
947
|
+
visible = false;
|
|
619
948
|
}
|
|
620
949
|
}
|
|
621
950
|
|
|
622
|
-
return { width, x, opacity };
|
|
951
|
+
return { width, x: horizontalPosition, opacity, visible };
|
|
623
952
|
}
|
|
624
953
|
|
|
625
954
|
/**
|
|
626
|
-
*
|
|
627
|
-
* quickSetter
|
|
955
|
+
* Reconstruye las cachés de quickSetter después de que cambian las tarjetas.
|
|
956
|
+
* quickSetter evita la sobrecarga de gsap.set() para las propiedades actualizadas en cada cuadro.
|
|
628
957
|
*/
|
|
629
958
|
private _buildQuickSetters() {
|
|
630
959
|
if (!this._cards || this._cards.length === 0) return;
|
|
@@ -633,6 +962,9 @@ export class MoniCarousel extends MoniElement {
|
|
|
633
962
|
this._cardSetWidth = [];
|
|
634
963
|
this._imgSetX = [];
|
|
635
964
|
this._titleSetOpacity = [];
|
|
965
|
+
|
|
966
|
+
// Reset logical viewport state array to match original items count
|
|
967
|
+
this._logicalItemInViewport = new Array(this.effectiveItems.length).fill(false);
|
|
636
968
|
|
|
637
969
|
this._cards.forEach((card) => {
|
|
638
970
|
this._cardSetX.push(gsap.quickSetter(card, 'x', 'px') as (v: number) => void);
|
|
@@ -651,25 +983,57 @@ export class MoniCarousel extends MoniElement {
|
|
|
651
983
|
}
|
|
652
984
|
|
|
653
985
|
/**
|
|
654
|
-
*
|
|
655
|
-
*
|
|
986
|
+
* Callback del Ticker de GSAP — el bucle de animación principal para el carrusel.
|
|
987
|
+
*
|
|
988
|
+
* Esta función está registrada con `gsap.ticker.add()` y se ejecuta en cada
|
|
989
|
+
* cuadro de animación (sincronizado con `requestAnimationFrame`). Implementa
|
|
990
|
+
* **interpolación exponencial (smoothstep)** para interpolar `_tickerCurrent` hacia
|
|
991
|
+
* `_tickerTarget`, creando una desaceleración de suavizado físicamente plausible.
|
|
992
|
+
*
|
|
993
|
+
* La fórmula de interpolación: `current += (target - current) × easeStrength × deltaTimeRatio`
|
|
994
|
+
*
|
|
995
|
+
* - `easeStrength = 0.08` en móvil: acoplamiento más estrecho para evitar que el carrusel
|
|
996
|
+
* se sienta "lento" o desincronizado con un dedo.
|
|
997
|
+
* - `easeStrength = 0.15` en escritorio: ligeramente más suelto para una sensación más cinematográfica.
|
|
998
|
+
* - `deltaTimeRatio` normaliza la velocidad de animación a través de diferentes velocidades de cuadro
|
|
999
|
+
* (60fps vs 120fps), manteniendo la atenuación percibida constante.
|
|
1000
|
+
*
|
|
1001
|
+
* Una vez que la diferencia entre el objetivo y el actual cae por debajo de 0.0005 (medio
|
|
1002
|
+
* sub-píxel), la animación se considera completa y el ticker se elimina
|
|
1003
|
+
* para conservar los recursos de CPU/GPU entre interacciones.
|
|
656
1004
|
*/
|
|
657
1005
|
private _tick = () => {
|
|
658
|
-
|
|
659
|
-
//
|
|
660
|
-
const
|
|
661
|
-
|
|
1006
|
+
// `deltaTimeRatio` normalizes frame-rate differences. At 60fps it is ≈1;
|
|
1007
|
+
// at 120fps it is ≈0.5, preventing double-speed animations on high-refresh screens.
|
|
1008
|
+
const deltaTimeRatio = gsap.ticker.deltaRatio();
|
|
1009
|
+
|
|
1010
|
+
// Tighter lerp factor on mobile for finger-tracking accuracy;
|
|
1011
|
+
// looser on desktop for cinematic smoothness.
|
|
1012
|
+
const easeStrength = this._isMobile ? 0.08 : 0.15;
|
|
1013
|
+
|
|
1014
|
+
// Lerp exponencial: mueve una fracción de la distancia restante en cada cuadro.
|
|
1015
|
+
this._tickerCurrent += (this._tickerTarget - this._tickerCurrent) * easeStrength * deltaTimeRatio;
|
|
662
1016
|
|
|
1017
|
+
// Umbral de convergencia: detener la animación cuando sea visualmente indistinguible del objetivo.
|
|
1018
|
+
// 0.0005 is well below sub-pixel precision, so no visible jitter will occur.
|
|
663
1019
|
if (Math.abs(this._tickerTarget - this._tickerCurrent) < 0.0005) {
|
|
664
1020
|
this._tickerCurrent = this._tickerTarget;
|
|
665
1021
|
gsap.ticker.remove(this._tick);
|
|
666
1022
|
this._isTicking = false;
|
|
1023
|
+
// Después de que la animación se asiente, comprobar si necesitamos reposicionar
|
|
1024
|
+
// silenciosamente el búfer de desplazamiento para un bucle infinito sin interrupciones.
|
|
667
1025
|
this._checkInfiniteWrap();
|
|
668
1026
|
}
|
|
669
1027
|
|
|
1028
|
+
// Renderizar todas las tarjetas en la nueva posición de desplazamiento interpolada.
|
|
670
1029
|
this._applyLayout(this._tickerCurrent);
|
|
671
1030
|
};
|
|
672
1031
|
|
|
1032
|
+
/**
|
|
1033
|
+
* Lee la posición real del scroll nativo y arranca el ticker de animación si no está corriendo.
|
|
1034
|
+
* Esta separación es crucial para el rendimiento: leemos el DOM aquí (scrollLeft) pero
|
|
1035
|
+
* escribimos los estilos en `_tick` -> `_applyLayout`.
|
|
1036
|
+
*/
|
|
673
1037
|
private _scheduleLayout = () => {
|
|
674
1038
|
if (!this._scrollContainer) return;
|
|
675
1039
|
this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize;
|
|
@@ -679,33 +1043,36 @@ export class MoniCarousel extends MoniElement {
|
|
|
679
1043
|
}
|
|
680
1044
|
};
|
|
681
1045
|
|
|
1046
|
+
/**
|
|
1047
|
+
* Función principal de sincronización de estado.
|
|
1048
|
+
* Refresca la lógica del scroll infinito y fuerza una recolección de los setters de GSAP
|
|
1049
|
+
* si la cantidad de tarjetas cambió. Puede saltarse el ticker y ser instantánea (ej. al redimensionar la ventana).
|
|
1050
|
+
*/
|
|
682
1051
|
updateLayout = (instant = false) => {
|
|
683
1052
|
if (!this._scrollContainer || !this._cards || this._cards.length === 0) return;
|
|
684
1053
|
|
|
685
|
-
//
|
|
1054
|
+
// Re-cacheamos las referencias DOM de GSAP si la cantidad de tarjetas varió (e.g., inserción dinámica de nodos)
|
|
686
1055
|
if (this._cardSetX.length !== this._cards.length) {
|
|
687
1056
|
this._buildQuickSetters();
|
|
688
1057
|
}
|
|
689
1058
|
|
|
1059
|
+
// Lógica de montaje inicial para el carrusel infinito
|
|
690
1060
|
if (this.infinite && !this._infiniteInitialized && this.effectiveItems.length > 0) {
|
|
691
1061
|
const C = this.effectiveItems.length;
|
|
692
1062
|
const V = C * this._visualCardsMultiplier;
|
|
693
1063
|
const K = this._snapSetsCount;
|
|
694
1064
|
const centerSetIndex = Math.floor(K / 2);
|
|
695
1065
|
|
|
696
|
-
//
|
|
697
|
-
// A double-rAF or setTimeout ensures the layout engine has processed the new snap-items
|
|
1066
|
+
// Doble rAF asegura que el navegador completó el reflow de CSS (scrollWidth) antes de inyectar el scroll
|
|
698
1067
|
requestAnimationFrame(() => {
|
|
699
1068
|
setTimeout(() => {
|
|
700
1069
|
if (this._scrollContainer) {
|
|
701
|
-
//
|
|
1070
|
+
// Forzamos el recálculo interno del layout
|
|
702
1071
|
this._scrollContainer.scrollWidth;
|
|
1072
|
+
// Desplazamos silenciosamente al usuario hacia la mitad del contenedor clonado
|
|
703
1073
|
this._scrollContainer.scrollLeft = centerSetIndex * V * this.itemSize;
|
|
704
1074
|
|
|
705
|
-
//
|
|
706
|
-
// But with setTimeout it should be fully ready.
|
|
707
|
-
|
|
708
|
-
// Force the visual ticker to match instantly to avoid an animation zoom from 0
|
|
1075
|
+
// Forzamos al ticker visual a asimilar la posición de golpe, evitando un "flash" de animación desde 0
|
|
709
1076
|
this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize;
|
|
710
1077
|
this._tickerCurrent = this._tickerTarget;
|
|
711
1078
|
this._applyLayout(this._tickerCurrent);
|
|
@@ -717,6 +1084,7 @@ export class MoniCarousel extends MoniElement {
|
|
|
717
1084
|
|
|
718
1085
|
this._tickerTarget = this._scrollContainer.scrollLeft / this.itemSize;
|
|
719
1086
|
|
|
1087
|
+
// Si pasamos instant=true, bypassamos el smoothing temporalmente
|
|
720
1088
|
if (instant || (!this._isTicking && this._tickerCurrent === 0)) {
|
|
721
1089
|
this._tickerCurrent = this._tickerTarget;
|
|
722
1090
|
this._applyLayout(this._tickerCurrent);
|
|
@@ -728,41 +1096,113 @@ export class MoniCarousel extends MoniElement {
|
|
|
728
1096
|
}
|
|
729
1097
|
};
|
|
730
1098
|
|
|
731
|
-
|
|
1099
|
+
/**
|
|
1100
|
+
* Bucle de renderizado principal — aplica el diseño calculado a cada tarjeta en el DOM.
|
|
1101
|
+
*
|
|
1102
|
+
* Llamado en cada cuadro del ticker de GSAP a través de `_tick()`. Itera sobre todos los elementos físicos
|
|
1103
|
+
* de las tarjetas y aplica ancho, posición, paralaje y opacidad del título
|
|
1104
|
+
* usando quickSetters de GSAP pre-almacenados en caché para un rendimiento máximo de velocidad de fotogramas.
|
|
1105
|
+
*
|
|
1106
|
+
* **Bucle de desplazamiento infinito:**
|
|
1107
|
+
* Cuando `infinite=true`, la posición normalizada se calcula usando aritmética
|
|
1108
|
+
* modular para que las tarjetas se repitan sin problemas alrededor del centro virtual del búfer.
|
|
1109
|
+
* Una corrección de envoltura asegura que cada tarjeta permanezca dentro de una distancia de medio ancho de búfer
|
|
1110
|
+
* de la posición de desplazamiento actual.
|
|
1111
|
+
*
|
|
1112
|
+
* **Optimización fuera de pantalla:**
|
|
1113
|
+
* Las tarjetas que están a más de 15 posiciones fuera de la pantalla se mueven a `x = -9999` y sus
|
|
1114
|
+
* títulos se ocultan. Esto evita que el navegador gaste el presupuesto de composición
|
|
1115
|
+
* en elementos invisibles.
|
|
1116
|
+
*
|
|
1117
|
+
* @param scrollProgress - La posición de desplazamiento interpolada actual en unidades de `itemSize`.
|
|
1118
|
+
* Un valor de `1.0` significa que el contenedor de desplazamiento ha avanzado exactamente el ancho de un elemento + espacio.
|
|
1119
|
+
*/
|
|
1120
|
+
private _applyLayout(scrollProgress: number) {
|
|
732
1121
|
const isMobile = this._isMobile;
|
|
733
1122
|
const cardCount = this._cards.length;
|
|
734
|
-
|
|
735
|
-
|
|
1123
|
+
|
|
1124
|
+
const visibleLogicalItems = new Map<number, HTMLDivElement>();
|
|
1125
|
+
const allLogicalItems = new Map<number, HTMLDivElement>();
|
|
1126
|
+
|
|
1127
|
+
for (let cardIndex = 0; cardIndex < cardCount; cardIndex++) {
|
|
1128
|
+
// `normalizedPosition` representa cuántas ranuras de elementos esta tarjeta está
|
|
1129
|
+
// desplazada desde el punto focal actual (0 = ranura grande primaria).
|
|
1130
|
+
let normalizedPosition = cardIndex - scrollProgress;
|
|
1131
|
+
|
|
1132
|
+
const card = this._cards[cardIndex];
|
|
1133
|
+
const dataIndex = Number(card.dataset.index || 0);
|
|
1134
|
+
const logicalIndex = this.infinite ? dataIndex % this.effectiveItems.length : dataIndex;
|
|
1135
|
+
|
|
1136
|
+
// Mapear cada índice lógico a un nodo físico para garantizar que tengamos
|
|
1137
|
+
// una referencia DOM incluso cuando la tarjeta esté saliendo (leave event).
|
|
1138
|
+
if (!allLogicalItems.has(logicalIndex)) {
|
|
1139
|
+
allLogicalItems.set(logicalIndex, card);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
736
1142
|
if (this.infinite) {
|
|
737
|
-
//
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
if (
|
|
1143
|
+
// Envolver la posición usando aritmética modular para mantener cada tarjeta dentro de
|
|
1144
|
+
// una distancia de ±medio-búfer de la posición de desplazamiento actual.
|
|
1145
|
+
normalizedPosition = cardIndex - (scrollProgress % cardCount);
|
|
1146
|
+
if (normalizedPosition > cardCount / 2) normalizedPosition -= cardCount;
|
|
1147
|
+
if (normalizedPosition < -cardCount / 2) normalizedPosition += cardCount;
|
|
741
1148
|
}
|
|
742
1149
|
|
|
743
|
-
//
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1150
|
+
// Omitir renderizado para tarjetas muy fuera del viewport visible para reducir
|
|
1151
|
+
// la carga de composición del navegador (un margen de seguridad de ~30 tarjetas alrededor de la pantalla).
|
|
1152
|
+
if (normalizedPosition < -15 || normalizedPosition > 15) {
|
|
1153
|
+
// Move the card completely off-screen and hide its title.
|
|
1154
|
+
this._cardSetX[cardIndex](-9999);
|
|
1155
|
+
this._titleSetOpacity[cardIndex](0);
|
|
747
1156
|
continue;
|
|
748
1157
|
}
|
|
749
1158
|
|
|
750
|
-
|
|
1159
|
+
// Get the precise pixel width and position for this card at its current normalized position.
|
|
1160
|
+
const cardLayoutResult = this._getCardLayout(normalizedPosition);
|
|
1161
|
+
|
|
1162
|
+
// Map logical visibility
|
|
1163
|
+
if (cardLayoutResult.visible) {
|
|
1164
|
+
if (!visibleLogicalItems.has(logicalIndex)) {
|
|
1165
|
+
visibleLogicalItems.set(logicalIndex, card);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
751
1168
|
|
|
752
|
-
//
|
|
753
|
-
this._cardSetX[
|
|
754
|
-
this._cardSetWidth[
|
|
1169
|
+
// Apply position and width via quickSetters (~3× faster than gsap.set() per call).
|
|
1170
|
+
this._cardSetX[cardIndex](cardLayoutResult.x);
|
|
1171
|
+
this._cardSetWidth[cardIndex](cardLayoutResult.width);
|
|
755
1172
|
|
|
756
|
-
//
|
|
1173
|
+
// Parallax effect: shift the image slightly in the opposite direction of scroll
|
|
1174
|
+
// to create depth. Clamped to ±1 to prevent over-shifting at extreme positions.
|
|
1175
|
+
// Skipped on mobile to conserve per-frame computation budget.
|
|
757
1176
|
if (!isMobile) {
|
|
758
|
-
const
|
|
759
|
-
|
|
1177
|
+
const clampedParallaxPosition = Math.max(-1, Math.min(1, normalizedPosition));
|
|
1178
|
+
// Image shifts up to ±25px to create the parallax depth illusion.
|
|
1179
|
+
this._imgSetX[cardIndex](clampedParallaxPosition * -25);
|
|
760
1180
|
}
|
|
761
1181
|
|
|
762
|
-
this._titleSetOpacity[
|
|
1182
|
+
this._titleSetOpacity[cardIndex](cardLayoutResult.opacity);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Calculate diff over logical items to dispatch exact events globally without jump glitches
|
|
1186
|
+
for (let i = 0; i < this.effectiveItems.length; i++) {
|
|
1187
|
+
const isNowVisible = visibleLogicalItems.has(i);
|
|
1188
|
+
if (this._logicalItemInViewport[i] !== isNowVisible) {
|
|
1189
|
+
this._logicalItemInViewport[i] = isNowVisible;
|
|
1190
|
+
const eventName = isNowVisible ? 'moni-item-entered-viewport' : 'moni-item-left-viewport';
|
|
1191
|
+
// Usamos el nodo físico visible si entra, o el nodo físico de respaldo si sale
|
|
1192
|
+
const physicalCard = visibleLogicalItems.get(i) || allLogicalItems.get(i) || null;
|
|
1193
|
+
emitMoniEvent<CarouselItem>(this, eventName, {
|
|
1194
|
+
detail: { item: this.effectiveItems[i], index: i, card: physicalCard }
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
763
1197
|
}
|
|
764
1198
|
}
|
|
765
1199
|
|
|
1200
|
+
/**
|
|
1201
|
+
* Calcula el multiplicador necesario para rellenar visualmente el carrusel infinito.
|
|
1202
|
+
* Si la lista original de ítems es corta (ej: 3 ítems) pero el viewport puede
|
|
1203
|
+
* mostrar 5, necesitamos multiplicar la lista original para cubrir el ancho de
|
|
1204
|
+
* la pantalla e interactuar de forma ininterrumpida (Infinity Wrap).
|
|
1205
|
+
*/
|
|
766
1206
|
private get _visualCardsMultiplier() {
|
|
767
1207
|
if (!this.infinite || this.effectiveItems.length === 0) return 1;
|
|
768
1208
|
const C = this.effectiveItems.length;
|
|
@@ -770,6 +1210,12 @@ export class MoniCarousel extends MoniElement {
|
|
|
770
1210
|
return Math.max(1, Math.ceil(15 / C));
|
|
771
1211
|
}
|
|
772
1212
|
|
|
1213
|
+
/**
|
|
1214
|
+
* Calcula cuántos "sets" completos de las cards originales se deben clonar
|
|
1215
|
+
* hacia los lados para habilitar el Infinite Scroll.
|
|
1216
|
+
* Usualmente es de 3 a 5 "sets" multiplicados para crear un buffer circular
|
|
1217
|
+
* invisible para el usuario final.
|
|
1218
|
+
*/
|
|
773
1219
|
private get _snapSetsCount() {
|
|
774
1220
|
if (!this.infinite || this.effectiveItems.length === 0) return 1;
|
|
775
1221
|
const C = this.effectiveItems.length;
|
|
@@ -781,6 +1227,11 @@ export class MoniCarousel extends MoniElement {
|
|
|
781
1227
|
return Math.max(3, K);
|
|
782
1228
|
}
|
|
783
1229
|
|
|
1230
|
+
/**
|
|
1231
|
+
* Determina el conteo total físico de cards (`li`) que serán montados en el DOM.
|
|
1232
|
+
* Es el producto de `effectiveItems.length` multiplicado por `_snapSetsCount`.
|
|
1233
|
+
* Para carruseles finitos (no infinite), esto simplemente retorna la cantidad original.
|
|
1234
|
+
*/
|
|
784
1235
|
private get _snapItemsCount() {
|
|
785
1236
|
if (this.infinite) {
|
|
786
1237
|
const C = this.effectiveItems.length;
|
|
@@ -791,6 +1242,15 @@ export class MoniCarousel extends MoniElement {
|
|
|
791
1242
|
return this.effectiveItems.length;
|
|
792
1243
|
}
|
|
793
1244
|
|
|
1245
|
+
/**
|
|
1246
|
+
* Lógica de Bucle Infinito (Infinite Wrap).
|
|
1247
|
+
*
|
|
1248
|
+
* Analiza el valor actual del `scrollLeft` del contenedor. Si el usuario se
|
|
1249
|
+
* acerca demasiado al límite del buffer (físico izquierdo o derecho) de los ítems
|
|
1250
|
+
* clonados, este método reposiciona instantáneamente (saltándose animaciones/scroll-behavior)
|
|
1251
|
+
* el scrollLeft hacia el bloque central equivalente.
|
|
1252
|
+
* El usuario nunca nota el salto visual porque los ítems en esas coordenadas lucen idénticos.
|
|
1253
|
+
*/
|
|
794
1254
|
private _checkInfiniteWrap() {
|
|
795
1255
|
if (!this.infinite || this.effectiveItems.length === 0 || !this._scrollContainer) return;
|
|
796
1256
|
|
|
@@ -825,14 +1285,14 @@ export class MoniCarousel extends MoniElement {
|
|
|
825
1285
|
// Restore original scroll behavior
|
|
826
1286
|
this._scrollContainer.style.scrollBehavior = oldBehavior;
|
|
827
1287
|
|
|
828
|
-
if (this.
|
|
829
|
-
this.
|
|
1288
|
+
if (this._isDragging) {
|
|
1289
|
+
this._scrollLeftAtDragStart += delta;
|
|
830
1290
|
}
|
|
831
1291
|
|
|
832
1292
|
if (oldSnap && oldSnap !== 'none') {
|
|
833
1293
|
// Re-enable snap on the next frame to avoid jumping
|
|
834
1294
|
requestAnimationFrame(() => {
|
|
835
|
-
if (this._scrollContainer && !this.
|
|
1295
|
+
if (this._scrollContainer && !this._isDragging) {
|
|
836
1296
|
this._scrollContainer.style.scrollSnapType = oldSnap;
|
|
837
1297
|
}
|
|
838
1298
|
});
|
|
@@ -840,18 +1300,27 @@ export class MoniCarousel extends MoniElement {
|
|
|
840
1300
|
}
|
|
841
1301
|
}
|
|
842
1302
|
|
|
1303
|
+
/**
|
|
1304
|
+
* Inicia el temporizador de reproducción automática (Autoplay).
|
|
1305
|
+
* Evalúa si el usuario no está interactuando activamente (`_isDown`)
|
|
1306
|
+
* antes de forzar el scroll hacia la siguiente vista.
|
|
1307
|
+
*/
|
|
843
1308
|
private _startAutoplay() {
|
|
844
1309
|
this._stopAutoplay();
|
|
845
1310
|
if (this.autoplay && this.effectiveItems.length > 0) {
|
|
846
1311
|
this._autoplayTimer = setInterval(() => {
|
|
847
1312
|
// We removed _isInteracting to make autoplay more robust. It only pauses on active drag.
|
|
848
|
-
if (!this.
|
|
1313
|
+
if (!this._isDragging) {
|
|
849
1314
|
this._scrollNext();
|
|
850
1315
|
}
|
|
851
1316
|
}, this.autoplayInterval) as unknown as number;
|
|
852
1317
|
}
|
|
853
1318
|
}
|
|
854
1319
|
|
|
1320
|
+
/**
|
|
1321
|
+
* Detiene y limpia el temporizador de reproducción automática (Autoplay).
|
|
1322
|
+
* Invocado durante interacciones del usuario (hover, drag) o al desmontar.
|
|
1323
|
+
*/
|
|
855
1324
|
private _stopAutoplay() {
|
|
856
1325
|
if (this._autoplayTimer) {
|
|
857
1326
|
clearInterval(this._autoplayTimer);
|
|
@@ -859,88 +1328,154 @@ export class MoniCarousel extends MoniElement {
|
|
|
859
1328
|
}
|
|
860
1329
|
}
|
|
861
1330
|
|
|
1331
|
+
/**
|
|
1332
|
+
* Manejador de evento: `mouseenter`.
|
|
1333
|
+
* Expuesto por si se requiere pausar el autoplay al hacer hover (actualmente en desuso).
|
|
1334
|
+
*/
|
|
862
1335
|
private _handleMouseEnter() {
|
|
863
1336
|
}
|
|
864
1337
|
|
|
1338
|
+
/**
|
|
1339
|
+
* Manejador de evento: `mouseleave`.
|
|
1340
|
+
* Finaliza forzosamente cualquier secuencia de drag o interacción táctil en progreso
|
|
1341
|
+
* si el puntero abandona el área de bounding box del carrusel.
|
|
1342
|
+
*/
|
|
865
1343
|
private _handleMouseLeave() {
|
|
866
1344
|
this._handleMouseUp();
|
|
867
1345
|
}
|
|
868
1346
|
|
|
869
|
-
|
|
1347
|
+
/**
|
|
1348
|
+
* Inicia la secuencia de arrastre (Pointer/Mouse Down).
|
|
1349
|
+
* - Elimina las animaciones GSAP en vuelo para evitar conflictos elásticos.
|
|
1350
|
+
* - Captura la posición de inicio X y resetea el acumulador de fricción y velocidad.
|
|
1351
|
+
* - Realiza una comprobación de Infinite Wrap preliminar por si el usuario
|
|
1352
|
+
* comienza a arrastrar desde el límite del buffer.
|
|
1353
|
+
*/
|
|
1354
|
+
private _handleMouseDown(e: MouseEvent) {
|
|
1355
|
+
// Kill any in-flight GSAP tween on the scroll container (e.g. momentum scroll
|
|
1356
|
+
// from a previous release) to prevent fighting with the new drag gesture.
|
|
870
1357
|
gsap.killTweensOf(this._scrollContainer);
|
|
1358
|
+
|
|
1359
|
+
// Check for infinite wrap before starting drag, in case the user
|
|
1360
|
+
// starts a new drag while near the buffer edge.
|
|
871
1361
|
this._checkInfiniteWrap();
|
|
872
|
-
|
|
873
|
-
this.
|
|
1362
|
+
|
|
1363
|
+
this._isDragging = true;
|
|
1364
|
+
this._totalDraggedDistance = 0;
|
|
1365
|
+
|
|
1366
|
+
// Disable native scroll-snap and smooth behavior during drag so the
|
|
1367
|
+
// scroll position tracks the pointer pixel-perfectly.
|
|
874
1368
|
this._scrollContainer.style.scrollBehavior = 'auto';
|
|
875
1369
|
this._scrollContainer.style.scrollSnapType = 'none';
|
|
876
1370
|
|
|
877
|
-
|
|
878
|
-
this.
|
|
879
|
-
this.
|
|
880
|
-
|
|
881
|
-
|
|
1371
|
+
// Capture the starting position relative to the scroll container's left edge.
|
|
1372
|
+
this._dragStartX = e.pageX - this._scrollContainer.offsetLeft;
|
|
1373
|
+
this._scrollLeftAtDragStart = this._scrollContainer.scrollLeft;
|
|
1374
|
+
|
|
1375
|
+
// Reset velocity tracking for fresh momentum calculation.
|
|
1376
|
+
this._pointerVelocity = 0;
|
|
1377
|
+
this._lastPointerX = e.pageX;
|
|
1378
|
+
this._lastPointerTime = performance.now();
|
|
882
1379
|
}
|
|
883
1380
|
|
|
884
|
-
|
|
885
|
-
|
|
1381
|
+
/**
|
|
1382
|
+
* Maneja el evento de movimiento del puntero durante un arrastre (Drag).
|
|
1383
|
+
*
|
|
1384
|
+
* @logic
|
|
1385
|
+
* 1. Calcula el delta (`walk`) respecto a la posición original X.
|
|
1386
|
+
* 2. Si excede el threshold de arrastre (3px), comienza a inyectar el scroll.
|
|
1387
|
+
* 3. En carruseles finitos (no infinite), si el usuario sobrepasa el inicio o el final,
|
|
1388
|
+
* aplica una fricción exponencial al `walk` para simular la "resistencia elástica" (Overscroll).
|
|
1389
|
+
* 4. Captura la velocidad (`_velocity`) calculando el delta dividido por el lapso de tiempo.
|
|
1390
|
+
*/
|
|
1391
|
+
private _handleMouseMove(e: MouseEvent) {
|
|
1392
|
+
if (!this._isDragging) return;
|
|
886
1393
|
e.preventDefault();
|
|
887
1394
|
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
1395
|
+
const currentPointerX = e.pageX;
|
|
1396
|
+
// `pointerOffsetInContainer` is the pointer's X relative to the scroll container's left edge.
|
|
1397
|
+
const pointerOffsetInContainer = currentPointerX - this._scrollContainer.offsetLeft;
|
|
891
1398
|
|
|
1399
|
+
// Apply a 1.25× multiplier for a faster-feeling drag that closely matches
|
|
1400
|
+
// the M3 Expressive motion spec for carousels.
|
|
1401
|
+
this._scrollContainer.scrollLeft = this._scrollLeftAtDragStart + (this._dragStartX - pointerOffsetInContainer) * 1.25;
|
|
1402
|
+
|
|
1403
|
+
// Calculate instantaneous velocity (px/ms) for the momentum/inertia calculation on release.
|
|
892
1404
|
const currentTime = performance.now();
|
|
893
|
-
const
|
|
894
|
-
if (
|
|
895
|
-
this.
|
|
1405
|
+
const elapsedMs = currentTime - this._lastPointerTime;
|
|
1406
|
+
if (elapsedMs > 0) {
|
|
1407
|
+
this._pointerVelocity = (currentPointerX - this._lastPointerX) / elapsedMs;
|
|
896
1408
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
this.
|
|
1409
|
+
|
|
1410
|
+
// Accumulate total distance to differentiate deliberate swipes from accidental click-drags.
|
|
1411
|
+
this._totalDraggedDistance += Math.abs(currentPointerX - this._lastPointerX);
|
|
1412
|
+
this._lastPointerX = currentPointerX;
|
|
1413
|
+
this._lastPointerTime = currentTime;
|
|
900
1414
|
}
|
|
901
1415
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1416
|
+
/**
|
|
1417
|
+
* Finaliza la secuencia de arrastre (Pointer/Mouse Up).
|
|
1418
|
+
*
|
|
1419
|
+
* @logic
|
|
1420
|
+
* Restaura el "Scroll Snap" que fue desactivado durante el drag (para permitir un
|
|
1421
|
+
* arrastre libre píxel por píxel). Adicionalmente, si el arrastre acumuló suficiente
|
|
1422
|
+
* velocidad (Inercia), inyecta esa inercia desplazando imperativamente el contenedor.
|
|
1423
|
+
*/
|
|
1424
|
+
private _handleMouseUp() {
|
|
1425
|
+
if (!this._isDragging) return;
|
|
1426
|
+
this._isDragging = false;
|
|
905
1427
|
|
|
1428
|
+
// If the last pointer event was more than 100ms ago, the user paused
|
|
1429
|
+
// before releasing — treat it as a zero-velocity drop (no momentum).
|
|
906
1430
|
const currentTime = performance.now();
|
|
907
|
-
if (currentTime - this.
|
|
908
|
-
this.
|
|
1431
|
+
if (currentTime - this._lastPointerTime > 100) {
|
|
1432
|
+
this._pointerVelocity = 0;
|
|
909
1433
|
}
|
|
910
1434
|
|
|
911
|
-
//
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
//
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1435
|
+
// ── Momentum / Inertia calculation ───────────────────────────────────
|
|
1436
|
+
// Dynamic momentum multiplier: faster swipes multiply further.
|
|
1437
|
+
// Base of 350px + an additional 300px per px/ms of velocity.
|
|
1438
|
+
const momentumMultiplier = 350 + Math.abs(this._pointerVelocity) * 300;
|
|
1439
|
+
// Project the target scroll position based on the velocity vector.
|
|
1440
|
+
let projectedScrollLeft = this._scrollContainer.scrollLeft - this._pointerVelocity * momentumMultiplier;
|
|
1441
|
+
|
|
1442
|
+
// Cap the throw distance at 15 items to prevent the user from flying past
|
|
1443
|
+
// the infinite scroll buffer boundaries in a single fast swipe.
|
|
1444
|
+
const maxMomentumThrowDistance = 15 * this.itemSize;
|
|
1445
|
+
if (projectedScrollLeft < this._scrollContainer.scrollLeft - maxMomentumThrowDistance) {
|
|
1446
|
+
projectedScrollLeft = this._scrollContainer.scrollLeft - maxMomentumThrowDistance;
|
|
1447
|
+
} else if (projectedScrollLeft > this._scrollContainer.scrollLeft + maxMomentumThrowDistance) {
|
|
1448
|
+
projectedScrollLeft = this._scrollContainer.scrollLeft + maxMomentumThrowDistance;
|
|
921
1449
|
}
|
|
922
1450
|
|
|
923
1451
|
if (this.layout !== 'uncontained') {
|
|
1452
|
+
// Disable snap while GSAP animates to the snap point to allow smooth fly-through.
|
|
924
1453
|
this._scrollContainer.style.scrollSnapType = 'none';
|
|
925
1454
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1455
|
+
// Snap to the nearest item index in the direction of momentum.
|
|
1456
|
+
let targetSnapIndex = Math.round(projectedScrollLeft / this.itemSize);
|
|
1457
|
+
const maxSnapIndex = this.effectiveItems.length > 0 ? this._snapItemsCount - 1 : 0;
|
|
1458
|
+
// Clamp to valid index range.
|
|
1459
|
+
targetSnapIndex = Math.max(0, Math.min(maxSnapIndex, targetSnapIndex));
|
|
1460
|
+
const snapTargetScrollLeft = targetSnapIndex * this.itemSize;
|
|
930
1461
|
|
|
931
|
-
// Dynamic duration: longer distance = more time,
|
|
932
|
-
|
|
933
|
-
const
|
|
934
|
-
const
|
|
1462
|
+
// Dynamic duration: longer distance = more time, capped at 0.85s.
|
|
1463
|
+
// This gives a natural "lighter = faster, heavier = slower" momentum feel.
|
|
1464
|
+
const currentSnapIndex = Math.round(this._scrollContainer.scrollLeft / this.itemSize);
|
|
1465
|
+
const cardsTraveled = Math.abs(targetSnapIndex - currentSnapIndex);
|
|
1466
|
+
const snapDuration = Math.min(0.85, 0.25 + cardsTraveled * 0.04);
|
|
935
1467
|
|
|
936
1468
|
this._scrollContainer.style.scrollBehavior = 'auto';
|
|
937
|
-
|
|
1469
|
+
|
|
1470
|
+
// Animate the scroll to the snap point using GSAP for precise easing control.
|
|
938
1471
|
gsap.to(this._scrollContainer, {
|
|
939
|
-
scrollLeft:
|
|
940
|
-
duration:
|
|
941
|
-
ease: 'power3.out',
|
|
1472
|
+
scrollLeft: snapTargetScrollLeft,
|
|
1473
|
+
duration: snapDuration,
|
|
1474
|
+
ease: 'power3.out', // Decelerating ease matching M3 Expressive spring curves.
|
|
942
1475
|
onComplete: () => {
|
|
943
|
-
|
|
1476
|
+
// Restore native CSS scroll-snap after the animation completes
|
|
1477
|
+
// so keyboard and touch-based scrolling can snap again.
|
|
1478
|
+
if (!this._isDragging && this._scrollContainer) {
|
|
944
1479
|
this._scrollContainer.style.scrollSnapType = 'x mandatory';
|
|
945
1480
|
}
|
|
946
1481
|
}
|
|
@@ -948,44 +1483,77 @@ export class MoniCarousel extends MoniElement {
|
|
|
948
1483
|
}
|
|
949
1484
|
}
|
|
950
1485
|
|
|
951
|
-
|
|
1486
|
+
/**
|
|
1487
|
+
* Inicia la secuencia de arrastre táctil (Touch Start).
|
|
1488
|
+
* Funciona análogamente a `_handleMouseDown`: detiene animaciones GSAP, evalúa el Infinite Wrap
|
|
1489
|
+
* y captura las coordenadas táctiles iniciales (`e.touches[0]`).
|
|
1490
|
+
*/
|
|
1491
|
+
private _handleTouchStart(e: TouchEvent) {
|
|
1492
|
+
// Mirror the mouse-down setup: kill flying animations, check for wrap,
|
|
1493
|
+
// then capture the starting touch coordinates.
|
|
952
1494
|
gsap.killTweensOf(this._scrollContainer);
|
|
953
1495
|
this._checkInfiniteWrap();
|
|
954
|
-
this.
|
|
955
|
-
this.
|
|
1496
|
+
this._isDragging = true;
|
|
1497
|
+
this._totalDraggedDistance = 0;
|
|
956
1498
|
this._scrollContainer.style.scrollBehavior = 'auto';
|
|
957
1499
|
this._scrollContainer.style.scrollSnapType = 'none';
|
|
958
1500
|
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
this.
|
|
962
|
-
this.
|
|
963
|
-
this.
|
|
964
|
-
this.
|
|
1501
|
+
// `e.touches[0]` is the first active touch point on the screen.
|
|
1502
|
+
const primaryTouch = e.touches[0];
|
|
1503
|
+
this._dragStartX = primaryTouch.pageX - this._scrollContainer.offsetLeft;
|
|
1504
|
+
this._scrollLeftAtDragStart = this._scrollContainer.scrollLeft;
|
|
1505
|
+
this._pointerVelocity = 0;
|
|
1506
|
+
this._lastPointerX = primaryTouch.pageX;
|
|
1507
|
+
this._lastPointerTime = performance.now();
|
|
965
1508
|
}
|
|
966
1509
|
|
|
967
|
-
|
|
968
|
-
|
|
1510
|
+
/**
|
|
1511
|
+
* Maneja el evento de movimiento táctil (Touch Move).
|
|
1512
|
+
* Actualiza el `scrollLeft` del contenedor imperativamente según el delta de arrastre.
|
|
1513
|
+
* Calcula la velocidad del dedo para la simulación de inercia posterior.
|
|
1514
|
+
*/
|
|
1515
|
+
private _handleTouchMove(e: TouchEvent) {
|
|
1516
|
+
if (!this._isDragging) return;
|
|
1517
|
+
|
|
1518
|
+
const primaryTouch = e.touches[0];
|
|
1519
|
+
const currentTouchX = primaryTouch.pageX;
|
|
1520
|
+
// Compute the touch offset relative to the scroll container's left edge.
|
|
1521
|
+
const touchOffsetInContainer = currentTouchX - this._scrollContainer.offsetLeft;
|
|
969
1522
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
const x = currentX - this._scrollContainer.offsetLeft;
|
|
973
|
-
this._scrollContainer.scrollLeft = this._scrollLeftStart + (this._startX - x);
|
|
1523
|
+
// Update scroll position directly (no multiplier for touch — native 1:1 feel).
|
|
1524
|
+
this._scrollContainer.scrollLeft = this._scrollLeftAtDragStart + (this._dragStartX - touchOffsetInContainer);
|
|
974
1525
|
|
|
1526
|
+
// Track velocity for momentum calculation on touch end.
|
|
975
1527
|
const currentTime = performance.now();
|
|
976
|
-
const
|
|
977
|
-
if (
|
|
978
|
-
this.
|
|
1528
|
+
const elapsedMs = currentTime - this._lastPointerTime;
|
|
1529
|
+
if (elapsedMs > 0) {
|
|
1530
|
+
this._pointerVelocity = (currentTouchX - this._lastPointerX) / elapsedMs;
|
|
979
1531
|
}
|
|
980
|
-
this.
|
|
981
|
-
this.
|
|
982
|
-
this.
|
|
1532
|
+
this._totalDraggedDistance += Math.abs(currentTouchX - this._lastPointerX);
|
|
1533
|
+
this._lastPointerX = currentTouchX;
|
|
1534
|
+
this._lastPointerTime = currentTime;
|
|
983
1535
|
}
|
|
984
1536
|
|
|
1537
|
+
/**
|
|
1538
|
+
* Finaliza la secuencia de arrastre táctil (Touch End).
|
|
1539
|
+
* Delega en `_handleMouseUp()` para aplicar inercia y restaurar el Snap.
|
|
1540
|
+
*/
|
|
985
1541
|
private _handleTouchEnd() {
|
|
986
1542
|
this._handleMouseUp();
|
|
987
1543
|
}
|
|
988
1544
|
|
|
1545
|
+
/**
|
|
1546
|
+
* Manejador del evento Scroll/Wheel.
|
|
1547
|
+
*
|
|
1548
|
+
* @logic
|
|
1549
|
+
* 1. Filtra para procesar únicamente el scroll vertical (`deltaY > deltaX`), típico de ruedas de ratón.
|
|
1550
|
+
* 2. Si el usuario está en los bordes extremos del carrusel, permite que el evento de scroll
|
|
1551
|
+
* haga bubble hacia arriba para hacer scroll a la página completa, logrando una interacción fluida.
|
|
1552
|
+
* 3. Si no, previene el scroll de la página (`e.preventDefault`) y lo convierte en scroll horizontal
|
|
1553
|
+
* para el carrusel.
|
|
1554
|
+
* 4. Desactiva temporalmente el "CSS Scroll Snap" para que el movimiento sea suave, restaurándolo
|
|
1555
|
+
* tras 150ms de inactividad (`_wheelTimeout`).
|
|
1556
|
+
*/
|
|
989
1557
|
private _handleWheel(e: WheelEvent) {
|
|
990
1558
|
// Solo interceptar scroll vertical puro (rueda de mouse típica)
|
|
991
1559
|
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
|
|
@@ -1015,55 +1583,91 @@ export class MoniCarousel extends MoniElement {
|
|
|
1015
1583
|
// Restablecer el snap después de un pequeño retraso
|
|
1016
1584
|
if ((this as any)._wheelTimeout) clearTimeout((this as any)._wheelTimeout);
|
|
1017
1585
|
(this as any)._wheelTimeout = setTimeout(() => {
|
|
1018
|
-
|
|
1586
|
+
// Handle uncontained snapping explicitly
|
|
1587
|
+
if (this.layout !== 'uncontained' && !this._isDragging) {
|
|
1588
|
+
// Fallback to CSS snap if not uncontained and not dragging
|
|
1019
1589
|
this._scrollContainer.style.scrollSnapType = 'x mandatory';
|
|
1020
1590
|
}
|
|
1021
1591
|
}, 150);
|
|
1022
1592
|
}
|
|
1023
1593
|
}
|
|
1024
1594
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1595
|
+
/**
|
|
1596
|
+
* Maneja los eventos de clic en las tarjetas individuales del carrusel.
|
|
1597
|
+
*
|
|
1598
|
+
* Distingue entre toques/clics genuinos y eventos accidentales de levantar el puntero
|
|
1599
|
+
* que ocurrieron después de un gesto de arrastre. Si la distancia total arrastrada excede
|
|
1600
|
+
* los 8px (el umbral mínimo de desplazamiento intencional de M3), el clic se suprime
|
|
1601
|
+
* para evitar una navegación involuntaria o el disparo de acciones.
|
|
1602
|
+
*
|
|
1603
|
+
* Despacha `'item-click'` con `{ item, index }` para que los consumidores puedan
|
|
1604
|
+
* responder a la selección de la tarjeta (ej. navegar a una página de detalles).
|
|
1605
|
+
*
|
|
1606
|
+
* @param e - El evento de clic del elemento de la tarjeta.
|
|
1607
|
+
* @param item - Los datos `CarouselItem` asociados con la tarjeta clicada.
|
|
1608
|
+
* @param cardDomIndex - El índice de la tarjeta en el DOM renderizado (puede diferir
|
|
1609
|
+
* del índice lógico en modo infinito debido a la clonación de elementos).
|
|
1610
|
+
*/
|
|
1611
|
+
private _handleCardClick(e: Event, item: CarouselItem, cardDomIndex: number) {
|
|
1612
|
+
// Suppress click if the gesture traveled more than 8px — it was a swipe, not a tap.
|
|
1613
|
+
if (this._totalDraggedDistance > 8) {
|
|
1027
1614
|
e.preventDefault();
|
|
1028
1615
|
e.stopPropagation();
|
|
1029
1616
|
return;
|
|
1030
1617
|
}
|
|
1031
1618
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
}
|
|
1039
|
-
);
|
|
1619
|
+
// In infinite mode, the DOM index may be a multiple of the original list length.
|
|
1620
|
+
// Modulo maps it back to the logical item index.
|
|
1621
|
+
const logicalItemIndex = cardDomIndex % this.effectiveItems.length;
|
|
1622
|
+
this.dispatchEvent(new CustomEvent('item-click', {
|
|
1623
|
+
bubbles: true,
|
|
1624
|
+
composed: true,
|
|
1625
|
+
detail: { item, index: logicalItemIndex }
|
|
1626
|
+
}));
|
|
1627
|
+
emitMoniEvent<CarouselItem>(this, 'moni-item-click', {
|
|
1628
|
+
detail: { item, index: logicalItemIndex }
|
|
1629
|
+
});
|
|
1040
1630
|
}
|
|
1041
1631
|
|
|
1632
|
+
/**
|
|
1633
|
+
* Acción Imperativa: Scroll hacia el elemento anterior.
|
|
1634
|
+
* Desplaza el contenedor una distancia equivalente al tamaño exacto de un ítem `L` (Large) + gap.
|
|
1635
|
+
* Restaura el comportamiento `smooth` de CSS nativo.
|
|
1636
|
+
*/
|
|
1042
1637
|
private _scrollPrevious() {
|
|
1043
1638
|
const target = this._scrollContainer.scrollLeft - this.itemSize;
|
|
1044
1639
|
this._scrollContainer.style.scrollBehavior = 'smooth';
|
|
1045
1640
|
this._scrollContainer.scrollTo({ left: target, behavior: 'smooth' });
|
|
1046
1641
|
}
|
|
1047
1642
|
|
|
1643
|
+
/**
|
|
1644
|
+
* Acción Imperativa: Scroll hacia el elemento siguiente.
|
|
1645
|
+
* Desplaza el contenedor una distancia equivalente al tamaño exacto de un ítem `L` (Large) + gap.
|
|
1646
|
+
*/
|
|
1048
1647
|
private _scrollNext() {
|
|
1049
1648
|
const target = this._scrollContainer.scrollLeft + this.itemSize;
|
|
1050
1649
|
this._scrollContainer.style.scrollBehavior = 'smooth';
|
|
1051
1650
|
this._scrollContainer.scrollTo({ left: target, behavior: 'smooth' });
|
|
1052
1651
|
}
|
|
1053
1652
|
|
|
1653
|
+
/**
|
|
1654
|
+
* Manejador del botón o enlace "Mostrar Todo" (Show All).
|
|
1655
|
+
* Intercepta clics en enlaces nulos (`#`) y despacha el evento CustomEvent `show-all-click`.
|
|
1656
|
+
*/
|
|
1054
1657
|
private _handleShowAllClick(e: Event) {
|
|
1055
1658
|
const target = e.target as HTMLElement;
|
|
1056
1659
|
if (target.tagName.toLowerCase() === 'a' && target.getAttribute('href') === '#') {
|
|
1057
1660
|
e.preventDefault();
|
|
1058
1661
|
}
|
|
1059
|
-
this.dispatchEvent(
|
|
1060
|
-
|
|
1061
|
-
bubbles: true,
|
|
1062
|
-
composed: true
|
|
1063
|
-
})
|
|
1064
|
-
);
|
|
1662
|
+
this.dispatchEvent(new CustomEvent('show-all-click', { bubbles: true, composed: true }));
|
|
1663
|
+
emitMoniEvent(this, 'moni-show-all-click');
|
|
1065
1664
|
}
|
|
1066
1665
|
|
|
1666
|
+
/**
|
|
1667
|
+
* Hook invocado por Lit cuando el contenido del slot `"show-all"` muta en el Light DOM.
|
|
1668
|
+
* Detecta si hay contenido proyectado para ocultar/mostrar la interfaz correspondiente,
|
|
1669
|
+
* proveyendo un fallback manual para entornos DOM limitados (como JSDOM en test unitarios).
|
|
1670
|
+
*/
|
|
1067
1671
|
private _handleShowAllSlotChange(e: Event) {
|
|
1068
1672
|
const slot = e.target as HTMLSlotElement;
|
|
1069
1673
|
let nodes = typeof slot.assignedElements === 'function' ? slot.assignedElements({ flatten: true }) : [];
|
|
@@ -1076,6 +1680,15 @@ export class MoniCarousel extends MoniElement {
|
|
|
1076
1680
|
this._hasSlottedShowAll = nodes.length > 0;
|
|
1077
1681
|
}
|
|
1078
1682
|
|
|
1683
|
+
/**
|
|
1684
|
+
* Hook invocado por Lit cuando los hijos directos proyectados en el `<slot>` por defecto cambian.
|
|
1685
|
+
*
|
|
1686
|
+
* @logic
|
|
1687
|
+
* Parsea el Light DOM proporcionado por el usuario (ej: lista de `<img>` o `<a><img></a>`).
|
|
1688
|
+
* Extrae los atributos `src`, `title`, y `href` de esos nodos nativos y los convierte en
|
|
1689
|
+
* un array estructurado interno de objetos `CarouselItem`, que el componente usará
|
|
1690
|
+
* subsecuentemente para renderizar su propio DOM Shadow infinito.
|
|
1691
|
+
*/
|
|
1079
1692
|
private _handleSlotChange(e: Event) {
|
|
1080
1693
|
const slot = e.target as HTMLSlotElement;
|
|
1081
1694
|
let nodes = typeof slot.assignedElements === 'function' ? slot.assignedElements({ flatten: true }) : [];
|
|
@@ -1117,46 +1730,95 @@ export class MoniCarousel extends MoniElement {
|
|
|
1117
1730
|
|
|
1118
1731
|
void this.updateComplete.then(() => {
|
|
1119
1732
|
this.updateLayout(true);
|
|
1733
|
+
emitMoniEvent(this, 'moni-slot-items-changed', { detail: { items: this._slottedItems } });
|
|
1120
1734
|
});
|
|
1121
1735
|
}
|
|
1122
1736
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1737
|
+
/**
|
|
1738
|
+
* Ensambla la estructura del Shadow DOM del carrusel para cada ciclo de renderizado de Lit.
|
|
1739
|
+
*
|
|
1740
|
+
* **Extracción de `computedLayout`:**
|
|
1741
|
+
* `largeItemWidth`, `mediumItemWidth` y `smallItemWidth` se desestructuran
|
|
1742
|
+
* del getter `computedLayout` (que devuelve tamaños según la especificación M3 basados en
|
|
1743
|
+
* el ancho del contenedor a través de `_containerWidth`). Se inyectan como propiedades personalizadas
|
|
1744
|
+
* CSS en la pista para que el CSS de tamaño de ranura pueda referenciarlos.
|
|
1745
|
+
*
|
|
1746
|
+
* **Puerta `showHeader`:**
|
|
1747
|
+
* El elemento `.header` solo se renderiza cuando hay algo que mostrar:
|
|
1748
|
+
* un `headerText`, un atributo `showAll`, o un elemento `[slot="show-all"]`.
|
|
1749
|
+
* Esto evita un espaciador de encabezado vacío cuando el carrusel no tiene fila de título.
|
|
1750
|
+
*
|
|
1751
|
+
* **Cálculo de `rightPadding`:**
|
|
1752
|
+
* Asegura que la última tarjeta pueda desplazarse a ras de la posición focal del elemento grande:
|
|
1753
|
+
* - Diseño `hero`: sin relleno adicional (el elemento derecho que asoma proporciona espacio natural).
|
|
1754
|
+
* - Diseños de ancho completo / lista: también relleno mínimo.
|
|
1755
|
+
* - Todos los demás diseños: `128px` (≈ ancho de la tarjeta pequeña) para que la última tarjeta pueda
|
|
1756
|
+
* alcanzar la ranura de enfoque principal sin que el usuario se desplace demasiado.
|
|
1757
|
+
*
|
|
1758
|
+
* **Ciclo de vida del ticker de GSAP:**
|
|
1759
|
+
* El ticker de GSAP (`gsap.ticker.add(_tick)`) se inicia en `firstUpdated()`
|
|
1760
|
+
* y se detiene en `disconnectedCallback()`. Este método de renderizado NO
|
|
1761
|
+
* interactúa con el ticker; el ticker impulsa las actualizaciones de posición por cuadro
|
|
1762
|
+
* independientemente del ciclo de actualización reactiva de Lit.
|
|
1763
|
+
*/
|
|
1764
|
+
override render() {
|
|
1765
|
+
const { largeItemWidth, mediumItemWidth, smallItemWidth } = this.computedLayout;
|
|
1766
|
+
|
|
1767
|
+
// Determine whether there are items to render (from either source).
|
|
1125
1768
|
const hasItems = this.effectiveItems && this.effectiveItems.length > 0;
|
|
1769
|
+
|
|
1770
|
+
// Check for a slotted "Show All" button or the declarative `show-all` attribute.
|
|
1126
1771
|
const hasSlottedShowAll = this._hasSlottedShowAll || !!this.querySelector('[slot="show-all"]');
|
|
1772
|
+
// Only render the header section if there is something to display in it.
|
|
1127
1773
|
const showHeader = this.headerText || this.showAll || hasSlottedShowAll;
|
|
1128
1774
|
|
|
1775
|
+
// `rightPadding` determines the trailing space in the scroll track so the
|
|
1776
|
+
// last item can scroll all the way to the primary (leftmost large) focus slot.
|
|
1129
1777
|
let rightPadding = 128;
|
|
1130
1778
|
if (this.layout === 'hero') {
|
|
1131
|
-
|
|
1779
|
+
// Hero: the peeking item on the right already provides natural trailing space.
|
|
1780
|
+
rightPadding = smallItemWidth + this.gap;
|
|
1132
1781
|
} else if (this.layout === 'uncontained') {
|
|
1782
|
+
// Uncontained: minimal trailing padding; items bleed off the edge.
|
|
1133
1783
|
rightPadding = 16;
|
|
1134
1784
|
} else {
|
|
1135
|
-
|
|
1785
|
+
// Multi-browse: both the medium and small slots provide the trailing buffer.
|
|
1786
|
+
rightPadding = mediumItemWidth + smallItemWidth + 2 * this.gap;
|
|
1136
1787
|
}
|
|
1137
1788
|
|
|
1138
1789
|
// Ensure the scroll track is long enough to allow the last item to scroll to the start (p = 0 focus).
|
|
1139
1790
|
// Safety margin of 400px avoids fractional rounding/zoom-scale snapping blockages in Firefox.
|
|
1140
|
-
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1143
|
-
|
|
1791
|
+
// Measure the container width for the scroll track overflow calculation.
|
|
1792
|
+
const containerWidth = this._containerWidth || this.getBoundingClientRect().width || 360;
|
|
1793
|
+
|
|
1794
|
+
// Minimum trailing padding needed so the last item can scroll to position 0 (primary focus).
|
|
1795
|
+
// The +400px safety margin prevents fractional zoom-scale snapping blockages in Firefox.
|
|
1796
|
+
const minRequiredRightPadding = containerWidth - largeItemWidth - this.padding + 400;
|
|
1797
|
+
if (rightPadding < minRequiredRightPadding) {
|
|
1798
|
+
rightPadding = minRequiredRightPadding;
|
|
1144
1799
|
}
|
|
1145
1800
|
|
|
1801
|
+
// Total number of snap items in the track (includes cloned sets for infinite mode).
|
|
1146
1802
|
let snapItemsCount = hasItems ? this._snapItemsCount : 0;
|
|
1803
|
+
|
|
1804
|
+
// `visualItems` is the array of items actually rendered as card elements in the Shadow DOM.
|
|
1805
|
+
// For infinite mode, this is a repeated version of `effectiveItems` scaled by `_visualCardsMultiplier`.
|
|
1147
1806
|
let visualItems = this.effectiveItems;
|
|
1148
|
-
|
|
1807
|
+
|
|
1149
1808
|
if (this.infinite && hasItems) {
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1809
|
+
const visualMultiplier = this._visualCardsMultiplier;
|
|
1810
|
+
|
|
1152
1811
|
visualItems = [];
|
|
1153
|
-
|
|
1812
|
+
// Concatenate the original item list `visualMultiplier` times to create
|
|
1813
|
+
// enough cards to fill the visual viewport without visible gaps.
|
|
1814
|
+
for (let repeatIndex = 0; repeatIndex < visualMultiplier; repeatIndex++) {
|
|
1154
1815
|
visualItems = visualItems.concat(this.effectiveItems);
|
|
1155
1816
|
}
|
|
1156
1817
|
}
|
|
1157
1818
|
|
|
1819
|
+
// Total pixel width of the snap track (sum of all item widths + all gaps between them).
|
|
1158
1820
|
const trackWidth = hasItems
|
|
1159
|
-
? snapItemsCount *
|
|
1821
|
+
? snapItemsCount * largeItemWidth + (snapItemsCount - 1) * this.gap
|
|
1160
1822
|
: 0;
|
|
1161
1823
|
|
|
1162
1824
|
const isScrollable = hasItems && this._scrollContainer &&
|
|
@@ -1167,7 +1829,7 @@ export class MoniCarousel extends MoniElement {
|
|
|
1167
1829
|
|
|
1168
1830
|
return html`
|
|
1169
1831
|
<div class="carousel-container" style="
|
|
1170
|
-
--carousel-snap-width: ${
|
|
1832
|
+
--carousel-snap-width: ${largeItemWidth}px;
|
|
1171
1833
|
--carousel-gap: ${this.gap}px;
|
|
1172
1834
|
--carousel-padding: ${this.padding}px;
|
|
1173
1835
|
--carousel-right-padding: ${rightPadding}px;
|
|
@@ -1227,6 +1889,7 @@ export class MoniCarousel extends MoniElement {
|
|
|
1227
1889
|
(item, idx) => html`
|
|
1228
1890
|
<div
|
|
1229
1891
|
class="card"
|
|
1892
|
+
data-index="${idx}"
|
|
1230
1893
|
@click=${(e: Event) => this._handleCardClick(e, item, idx)}
|
|
1231
1894
|
>
|
|
1232
1895
|
${item.href
|