@moni-labs/moni-ui 0.3.0 → 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.
Files changed (251) hide show
  1. package/custom-elements.json +5230 -1007
  2. package/dist/components/_base/moni-element.d.ts +16 -9
  3. package/dist/components/_base/moni-element.d.ts.map +1 -1
  4. package/dist/components/_base/moni-element.js +63 -9
  5. package/dist/components/index.js +2 -2
  6. package/dist/components/loading-shapes.d.ts +19 -0
  7. package/dist/components/loading-shapes.d.ts.map +1 -1
  8. package/dist/components/loading-shapes.js +19 -0
  9. package/dist/components/moni-app-bar.d.ts +72 -62
  10. package/dist/components/moni-app-bar.d.ts.map +1 -1
  11. package/dist/components/moni-app-bar.js +72 -62
  12. package/dist/components/moni-badge.d.ts +64 -65
  13. package/dist/components/moni-badge.d.ts.map +1 -1
  14. package/dist/components/moni-badge.js +64 -66
  15. package/dist/components/moni-bottom-sheet.d.ts +129 -61
  16. package/dist/components/moni-bottom-sheet.d.ts.map +1 -1
  17. package/dist/components/moni-bottom-sheet.js +217 -131
  18. package/dist/components/moni-button-group.d.ts +107 -29
  19. package/dist/components/moni-button-group.d.ts.map +1 -1
  20. package/dist/components/moni-button-group.js +107 -29
  21. package/dist/components/moni-button-segment.d.ts +53 -12
  22. package/dist/components/moni-button-segment.d.ts.map +1 -1
  23. package/dist/components/moni-button-segment.js +53 -12
  24. package/dist/components/moni-button.d.ts +116 -38
  25. package/dist/components/moni-button.d.ts.map +1 -1
  26. package/dist/components/moni-button.js +128 -38
  27. package/dist/components/moni-card.d.ts +68 -50
  28. package/dist/components/moni-card.d.ts.map +1 -1
  29. package/dist/components/moni-card.js +68 -50
  30. package/dist/components/moni-carousel.d.ts +484 -53
  31. package/dist/components/moni-carousel.d.ts.map +1 -1
  32. package/dist/components/moni-carousel.js +1024 -245
  33. package/dist/components/moni-checkbox.d.ts +49 -46
  34. package/dist/components/moni-checkbox.d.ts.map +1 -1
  35. package/dist/components/moni-checkbox.js +55 -48
  36. package/dist/components/moni-chip.d.ts +100 -33
  37. package/dist/components/moni-chip.d.ts.map +1 -1
  38. package/dist/components/moni-chip.js +104 -34
  39. package/dist/components/moni-color-field.d.ts +99 -24
  40. package/dist/components/moni-color-field.d.ts.map +1 -1
  41. package/dist/components/moni-color-field.js +99 -24
  42. package/dist/components/moni-context-menu.d.ts +72 -26
  43. package/dist/components/moni-context-menu.d.ts.map +1 -1
  44. package/dist/components/moni-context-menu.js +76 -30
  45. package/dist/components/moni-dialog.d.ts +84 -59
  46. package/dist/components/moni-dialog.d.ts.map +1 -1
  47. package/dist/components/moni-dialog.js +105 -58
  48. package/dist/components/moni-divider.d.ts +39 -27
  49. package/dist/components/moni-divider.d.ts.map +1 -1
  50. package/dist/components/moni-divider.js +42 -30
  51. package/dist/components/moni-expansion.d.ts +32 -22
  52. package/dist/components/moni-expansion.d.ts.map +1 -1
  53. package/dist/components/moni-expansion.js +32 -22
  54. package/dist/components/moni-fab-menu.d.ts +61 -23
  55. package/dist/components/moni-fab-menu.d.ts.map +1 -1
  56. package/dist/components/moni-fab-menu.js +69 -31
  57. package/dist/components/moni-fab.d.ts +86 -28
  58. package/dist/components/moni-fab.d.ts.map +1 -1
  59. package/dist/components/moni-fab.js +92 -34
  60. package/dist/components/moni-file-field.d.ts +118 -30
  61. package/dist/components/moni-file-field.d.ts.map +1 -1
  62. package/dist/components/moni-file-field.js +118 -30
  63. package/dist/components/moni-icon.d.ts +40 -35
  64. package/dist/components/moni-icon.d.ts.map +1 -1
  65. package/dist/components/moni-icon.js +43 -38
  66. package/dist/components/moni-list-item.d.ts +46 -33
  67. package/dist/components/moni-list-item.d.ts.map +1 -1
  68. package/dist/components/moni-list-item.js +61 -41
  69. package/dist/components/moni-list.d.ts +30 -20
  70. package/dist/components/moni-list.d.ts.map +1 -1
  71. package/dist/components/moni-list.js +36 -21
  72. package/dist/components/moni-loading-indicator.d.ts +54 -22
  73. package/dist/components/moni-loading-indicator.d.ts.map +1 -1
  74. package/dist/components/moni-loading-indicator.js +54 -22
  75. package/dist/components/moni-menu-item.d.ts +23 -16
  76. package/dist/components/moni-menu-item.d.ts.map +1 -1
  77. package/dist/components/moni-menu-item.js +23 -16
  78. package/dist/components/moni-menu.d.ts +81 -28
  79. package/dist/components/moni-menu.d.ts.map +1 -1
  80. package/dist/components/moni-menu.js +81 -30
  81. package/dist/components/moni-morph-modal.d.ts +141 -0
  82. package/dist/components/moni-morph-modal.d.ts.map +1 -1
  83. package/dist/components/moni-morph-modal.js +295 -58
  84. package/dist/components/moni-nav-item.d.ts +59 -29
  85. package/dist/components/moni-nav-item.d.ts.map +1 -1
  86. package/dist/components/moni-nav-item.js +59 -29
  87. package/dist/components/moni-nav.d.ts +94 -35
  88. package/dist/components/moni-nav.d.ts.map +1 -1
  89. package/dist/components/moni-nav.js +94 -35
  90. package/dist/components/moni-progress.d.ts +99 -55
  91. package/dist/components/moni-progress.d.ts.map +1 -1
  92. package/dist/components/moni-progress.js +99 -55
  93. package/dist/components/moni-radio.d.ts +62 -50
  94. package/dist/components/moni-radio.d.ts.map +1 -1
  95. package/dist/components/moni-radio.js +65 -52
  96. package/dist/components/moni-ripple.d.ts +74 -59
  97. package/dist/components/moni-ripple.d.ts.map +1 -1
  98. package/dist/components/moni-ripple.js +74 -59
  99. package/dist/components/moni-segmented-button.d.ts +91 -16
  100. package/dist/components/moni-segmented-button.d.ts.map +1 -1
  101. package/dist/components/moni-segmented-button.js +94 -17
  102. package/dist/components/moni-select-option.d.ts +30 -23
  103. package/dist/components/moni-select-option.d.ts.map +1 -1
  104. package/dist/components/moni-select-option.js +30 -23
  105. package/dist/components/moni-select.d.ts +300 -35
  106. package/dist/components/moni-select.d.ts.map +1 -1
  107. package/dist/components/moni-select.js +323 -39
  108. package/dist/components/moni-side-sheet.d.ts +94 -32
  109. package/dist/components/moni-side-sheet.d.ts.map +1 -1
  110. package/dist/components/moni-side-sheet.js +94 -32
  111. package/dist/components/moni-slider.d.ts +152 -41
  112. package/dist/components/moni-slider.d.ts.map +1 -1
  113. package/dist/components/moni-slider.js +154 -42
  114. package/dist/components/moni-snackbar.d.ts +53 -43
  115. package/dist/components/moni-snackbar.d.ts.map +1 -1
  116. package/dist/components/moni-snackbar.js +53 -43
  117. package/dist/components/moni-split-button.d.ts +63 -18
  118. package/dist/components/moni-split-button.d.ts.map +1 -1
  119. package/dist/components/moni-split-button.js +63 -18
  120. package/dist/components/moni-step.d.ts +36 -24
  121. package/dist/components/moni-step.d.ts.map +1 -1
  122. package/dist/components/moni-step.js +41 -29
  123. package/dist/components/moni-stepper.d.ts +64 -27
  124. package/dist/components/moni-stepper.d.ts.map +1 -1
  125. package/dist/components/moni-stepper.js +72 -34
  126. package/dist/components/moni-switch.d.ts +57 -46
  127. package/dist/components/moni-switch.d.ts.map +1 -1
  128. package/dist/components/moni-switch.js +59 -47
  129. package/dist/components/moni-tab.d.ts +26 -18
  130. package/dist/components/moni-tab.d.ts.map +1 -1
  131. package/dist/components/moni-tab.js +29 -21
  132. package/dist/components/moni-tabs.d.ts +34 -28
  133. package/dist/components/moni-tabs.d.ts.map +1 -1
  134. package/dist/components/moni-tabs.js +34 -28
  135. package/dist/components/moni-text-field.d.ts +139 -29
  136. package/dist/components/moni-text-field.d.ts.map +1 -1
  137. package/dist/components/moni-text-field.js +157 -31
  138. package/dist/components/moni-textarea.d.ts +127 -29
  139. package/dist/components/moni-textarea.d.ts.map +1 -1
  140. package/dist/components/moni-textarea.js +131 -32
  141. package/dist/components/moni-time-picker.d.ts +114 -24
  142. package/dist/components/moni-time-picker.d.ts.map +1 -1
  143. package/dist/components/moni-time-picker.js +127 -33
  144. package/dist/components/moni-toolbar.d.ts +37 -23
  145. package/dist/components/moni-toolbar.d.ts.map +1 -1
  146. package/dist/components/moni-toolbar.js +37 -23
  147. package/dist/components/moni-tooltip.d.ts +100 -36
  148. package/dist/components/moni-tooltip.d.ts.map +1 -1
  149. package/dist/components/moni-tooltip.js +104 -40
  150. package/dist/components/moni-typography.d.ts +68 -30
  151. package/dist/components/moni-typography.d.ts.map +1 -1
  152. package/dist/components/moni-typography.js +70 -32
  153. package/dist/styles/tokens.css +7 -7
  154. package/dist/types/events.d.ts +17 -0
  155. package/dist/types/events.d.ts.map +1 -0
  156. package/dist/types/events.js +1 -0
  157. package/dist/utils/event-emitter.d.ts +13 -0
  158. package/dist/utils/event-emitter.d.ts.map +1 -0
  159. package/dist/utils/event-emitter.js +13 -0
  160. package/package.json +2 -1
  161. package/src/components/_base/moni-element.ts +73 -9
  162. package/src/components/index.ts +2 -2
  163. package/src/components/loading-shapes.ts +19 -0
  164. package/src/components/moni-app-bar.test.ts +10 -10
  165. package/src/components/moni-app-bar.ts +72 -62
  166. package/src/components/moni-badge.ts +64 -66
  167. package/src/components/moni-bottom-sheet.test.ts +39 -45
  168. package/src/components/moni-bottom-sheet.ts +222 -131
  169. package/src/components/moni-button-group.test.ts +14 -14
  170. package/src/components/moni-button-group.ts +107 -29
  171. package/src/components/moni-button-segment.ts +53 -12
  172. package/src/components/moni-button.test.ts +28 -26
  173. package/src/components/moni-button.ts +139 -39
  174. package/src/components/moni-card.test.ts +6 -6
  175. package/src/components/moni-card.ts +70 -50
  176. package/src/components/moni-carousel.test.ts +12 -12
  177. package/src/components/moni-carousel.ts +1129 -269
  178. package/src/components/moni-checkbox.test.ts +7 -7
  179. package/src/components/moni-checkbox.ts +62 -48
  180. package/src/components/moni-chip.test.ts +17 -17
  181. package/src/components/moni-chip.ts +112 -34
  182. package/src/components/moni-color-field.test.ts +5 -5
  183. package/src/components/moni-color-field.ts +108 -24
  184. package/src/components/moni-context-menu.test.ts +11 -11
  185. package/src/components/moni-context-menu.ts +76 -30
  186. package/src/components/moni-dialog.ts +108 -59
  187. package/src/components/moni-divider.test.ts +4 -4
  188. package/src/components/moni-divider.ts +42 -30
  189. package/src/components/moni-expansion.ts +32 -22
  190. package/src/components/moni-fab-menu.test.ts +11 -11
  191. package/src/components/moni-fab-menu.ts +69 -31
  192. package/src/components/moni-fab.test.ts +16 -16
  193. package/src/components/moni-fab.ts +100 -34
  194. package/src/components/moni-file-field.test.ts +8 -8
  195. package/src/components/moni-file-field.ts +132 -30
  196. package/src/components/moni-icon.test.ts +11 -11
  197. package/src/components/moni-icon.ts +43 -38
  198. package/src/components/moni-list-item.test.ts +10 -10
  199. package/src/components/moni-list-item.ts +61 -41
  200. package/src/components/moni-list.ts +37 -21
  201. package/src/components/moni-loading-indicator.test.ts +3 -3
  202. package/src/components/moni-loading-indicator.ts +54 -22
  203. package/src/components/moni-menu-item.ts +23 -16
  204. package/src/components/moni-menu.test.ts +7 -7
  205. package/src/components/moni-menu.ts +87 -30
  206. package/src/components/moni-morph-modal.test.ts +18 -17
  207. package/src/components/moni-morph-modal.ts +327 -60
  208. package/src/components/moni-nav-item.ts +59 -29
  209. package/src/components/moni-nav.test.ts +11 -11
  210. package/src/components/moni-nav.ts +94 -35
  211. package/src/components/moni-progress.test.ts +9 -9
  212. package/src/components/moni-progress.ts +100 -56
  213. package/src/components/moni-radio.test.ts +8 -8
  214. package/src/components/moni-radio.ts +71 -51
  215. package/src/components/moni-ripple.ts +74 -59
  216. package/src/components/moni-segmented-button.test.ts +4 -4
  217. package/src/components/moni-segmented-button.ts +94 -17
  218. package/src/components/moni-select-option.ts +30 -23
  219. package/src/components/moni-select.test.ts +11 -11
  220. package/src/components/moni-select.ts +361 -50
  221. package/src/components/moni-side-sheet.test.ts +7 -7
  222. package/src/components/moni-side-sheet.ts +94 -32
  223. package/src/components/moni-slider.test.ts +7 -7
  224. package/src/components/moni-slider.ts +165 -42
  225. package/src/components/moni-snackbar.test.ts +8 -8
  226. package/src/components/moni-snackbar.ts +53 -43
  227. package/src/components/moni-split-button.ts +63 -18
  228. package/src/components/moni-step.test.ts +7 -7
  229. package/src/components/moni-step.ts +41 -29
  230. package/src/components/moni-stepper.test.ts +7 -7
  231. package/src/components/moni-stepper.ts +72 -34
  232. package/src/components/moni-switch.test.ts +12 -12
  233. package/src/components/moni-switch.ts +66 -47
  234. package/src/components/moni-tab.test.ts +5 -5
  235. package/src/components/moni-tab.ts +29 -21
  236. package/src/components/moni-tabs.ts +34 -28
  237. package/src/components/moni-text-field.test.ts +13 -13
  238. package/src/components/moni-text-field.ts +181 -30
  239. package/src/components/moni-textarea.test.ts +17 -17
  240. package/src/components/moni-textarea.ts +155 -31
  241. package/src/components/moni-time-picker.test.ts +5 -5
  242. package/src/components/moni-time-picker.ts +128 -33
  243. package/src/components/moni-toolbar.test.ts +8 -8
  244. package/src/components/moni-toolbar.ts +37 -23
  245. package/src/components/moni-tooltip.test.ts +15 -15
  246. package/src/components/moni-tooltip.ts +107 -40
  247. package/src/components/moni-typography.test.ts +10 -10
  248. package/src/components/moni-typography.ts +73 -32
  249. package/src/styles/tokens.css +7 -7
  250. package/src/types/events.ts +17 -0
  251. package/src/utils/event-emitter.ts +26 -0
@@ -44,7 +44,7 @@ type Placement =
44
44
  | 'viewport-bottom-right';
45
45
 
46
46
  /**
47
- * Internal coordinate and styling state of the morph target before animation.
47
+ * Estado interno de coordenadas y estilos del objetivo de morphing antes de la animación.
48
48
  *
49
49
  * @internal
50
50
  */
@@ -56,7 +56,7 @@ interface TargetState {
56
56
  }
57
57
 
58
58
  /**
59
- * References to the internal icon and text elements animated during the morph.
59
+ * Referencias a los elementos internos de icono y texto animados durante el morphing.
60
60
  *
61
61
  * @internal
62
62
  */
@@ -66,49 +66,49 @@ interface MorphElements {
66
66
  }
67
67
 
68
68
  /**
69
- * Material Design 3 Morph Modal component.
69
+ * Componente Material Design 3 Morph Modal.
70
70
  *
71
- * A highly interactive dialog that uses GSAP FLIP (First, Last, Invert, Play)
72
- * animations to seamlessly "morph" any clicked element on the page into a full
73
- * modal surface, and morph it back when closed.
71
+ * Un diálogo altamente interactivo que usa animaciones GSAP FLIP (First, Last, Invert, Play)
72
+ * para "transformar" (morph) sin problemas cualquier elemento clickeado en la página en una superficie
73
+ * modal completa, y transformarlo de vuelta cuando se cierra.
74
74
  *
75
- * **Motion choreography:**
76
- * This component implements the complex M3 container transform pattern. When
77
- * triggered, the source element visually expands into the modal. The modal's
78
- * background color, border radius, and typography fade and cross-dissolve
79
- * perfectly with the origin element.
75
+ * **Coreografía de movimiento:**
76
+ * Este componente implementa el patrón complejo de transformación de contenedor de M3. Cuando
77
+ * se activa, el elemento origen se expande visualmente hacia el modal. El color de fondo,
78
+ * el radio del borde y la tipografía del modal se desvanecen y se funden
79
+ * perfectamente con el elemento de origen.
80
80
  *
81
- * **Triggering mechanism:**
82
- * Consumers must provide the `triggerEvent` property containing the original
83
- * pointer/click `Event` that initiated the open action. The component extracts
84
- * the `event.target` (or uses `clientX/Y` as a fallback) to determine the exact
85
- * origin coordinates for the GSAP FLIP animation.
81
+ * **Mecanismo de activación:**
82
+ * Los consumidores deben proporcionar la propiedad `triggerEvent` que contenga el `Event`
83
+ * de puntero/clic original que inició la acción de apertura. El componente extrae
84
+ * el `event.target` (o usa `clientX/Y` como respaldo) para determinar las coordenadas
85
+ * de origen exactas para la animación GSAP FLIP.
86
86
  *
87
- * **Dialog behavior:**
88
- * Internally, it uses the native `<dialog>` element. It traps focus, supports
89
- * `Escape` to close, and handles scrim/backdrop clicks (which trigger the
90
- * reverse morph animation before actually closing the native dialog).
87
+ * **Comportamiento del diálogo:**
88
+ * Internamente, utiliza el elemento nativo `<dialog>`. Atrapa el foco, soporta
89
+ * `Escape` para cerrar, y maneja clics en el fondo/scrim (que activan la
90
+ * animación de morphing inverso antes de cerrar realmente el diálogo nativo).
91
91
  *
92
92
  * @example
93
93
  * ```html
94
- * <moni-morph-modal id="myModal" title="Details">
95
- * <p>This modal morphed from the button you just clicked.</p>
94
+ * <moni-morph-modal id="myModal" title="Detalles">
95
+ * <p>Este modal se transformó desde el botón que acabas de clickear.</p>
96
96
  * </moni-morph-modal>
97
97
  *
98
- * <moni-button id="openBtn">Open Details</moni-button>
98
+ * <moni-button id="openBtn">Abrir Detalles</moni-button>
99
99
  *
100
100
  * <script>
101
101
  * const modal = document.getElementById('myModal');
102
102
  * document.getElementById('openBtn').addEventListener('click', (e) => {
103
- * modal.triggerEvent = e; // Pass the event so it knows where to morph from
103
+ * modal.triggerEvent = e; // Pasa el evento para que sepa desde dónde transformarse
104
104
  * modal.open = true;
105
105
  * });
106
106
  * </script>
107
107
  * ```
108
108
  *
109
- * @slot default - The main body content of the modal.
110
- * @slot header - Custom header content (overrides `title` attribute).
111
- * @slot actions - Action buttons displayed at the bottom right.
109
+ * @slot default - El contenido del cuerpo principal del modal.
110
+ * @slot header - Contenido de encabezado personalizado (sobrescribe el atributo `title`).
111
+ * @slot actions - Botones de acción mostrados en la parte inferior derecha.
112
112
  */
113
113
 
114
114
  const litBool = {
@@ -118,31 +118,123 @@ const litBool = {
118
118
 
119
119
  @customElement('moni-morph-modal')
120
120
  export class MoniMorphModal extends MoniElement {
121
+ /**
122
+ * Selector del elemento disparador desde el cual transformarse (ej. '#fab' o '.my-button').
123
+ * Si se omite, el modal aún puede activarse programáticamente estableciendo `open = true`.
124
+ * @type {string}
125
+ */
121
126
  @property({ reflect: true }) target = '';
127
+
128
+ /**
129
+ * Controla el estado abierto/cerrado del modal.
130
+ * Puede establecerse directamente, o se gestiona internamente cuando se hace clic en el elemento `target`.
131
+ * @type {boolean}
132
+ */
122
133
  @property({ type: Boolean, reflect: true, converter: litBool }) open = false;
134
+
135
+ /**
136
+ * Si es true, renderiza un fondo y atrapa el foco, actuando como un verdadero diálogo modal.
137
+ * @type {boolean}
138
+ * @default true
139
+ */
123
140
  @property({ type: Boolean, reflect: true, converter: litBool }) modal = true;
141
+
142
+ /**
143
+ * Posición preferida del modal en relación con la ventana gráfica.
144
+ * @type {Placement}
145
+ * @default 'center'
146
+ */
124
147
  @property({ reflect: true }) placement: Placement = 'center';
148
+
149
+ /**
150
+ * El ancho del modal cuando está completamente expandido.
151
+ * @type {string}
152
+ * @default '22rem'
153
+ */
125
154
  @property({ reflect: true, attribute: 'expanded-width' }) expandedWidth =
126
155
  '22rem';
156
+
157
+ /**
158
+ * La altura del modal cuando está completamente expandido.
159
+ * @type {string}
160
+ * @default '18rem'
161
+ */
127
162
  @property({ reflect: true, attribute: 'expanded-height' }) expandedHeight =
128
163
  '18rem';
164
+
165
+ /**
166
+ * Si es true, hacer clic fuera del modal lo cerrará.
167
+ * @type {boolean}
168
+ * @default true
169
+ */
129
170
  @property({ type: Boolean, reflect: true, attribute: 'close-on-click-outside', converter: litBool })
130
171
  closeOnClickOutside = true;
172
+
173
+ /**
174
+ * Si es true, presionar la tecla Escape cerrará el modal.
175
+ * @type {boolean}
176
+ * @default true
177
+ */
131
178
  @property({ type: Boolean, reflect: true, attribute: 'close-on-esc', converter: litBool })
132
179
  closeOnEsc = true;
180
+
181
+ /**
182
+ * Si es true, muestra un botón de icono de cierre por defecto en el encabezado.
183
+ * @type {boolean}
184
+ */
133
185
  @property({ type: Boolean, reflect: true, attribute: 'show-close-button', converter: litBool })
134
186
  showCloseButton = false;
187
+
188
+ /**
189
+ * Si es true, intenta fundir/transformar una etiqueta del disparador hacia el encabezado del modal.
190
+ * @type {boolean}
191
+ */
135
192
  @property({ type: Boolean, reflect: true, attribute: 'morph-label', converter: litBool })
136
193
  morphLabel = false;
194
+
195
+ /**
196
+ * Un selector CSS personalizado utilizado para encontrar el elemento de etiqueta específico dentro del target a transformar.
197
+ * @type {string}
198
+ */
137
199
  @property({ reflect: true, attribute: 'morph-label-selector' })
138
200
  morphLabelSelector = '';
201
+
202
+ /**
203
+ * Si es true, renderiza un fondo atenuado detrás del modal.
204
+ * @type {boolean}
205
+ * @default true
206
+ */
139
207
  @property({ type: Boolean, reflect: true, attribute: 'has-backdrop', converter: litBool })
140
208
  hasBackdrop = true;
209
+
210
+ /**
211
+ * Si es true, oculta el elemento disparador (target) mientras el modal está abierto.
212
+ * @type {boolean}
213
+ */
141
214
  @property({ type: Boolean, reflect: true, attribute: 'hide-target', converter: litBool })
142
215
  hideTarget = false;
216
+
217
+ /**
218
+ * Si es true, fuerza al modal a cubrir visualmente la ubicación original del target antes de expandirse.
219
+ * @type {boolean}
220
+ */
143
221
  @property({ type: Boolean, reflect: true, attribute: 'cover-target', converter: litBool })
144
222
  coverTarget = false;
145
223
 
224
+ /**
225
+ * Si es true, el modal calcula automáticamente su tamaño final basándose en el contenido interno (ignora expandedWidth y expandedHeight).
226
+ * @type {boolean}
227
+ */
228
+ @property({ type: Boolean, reflect: true, attribute: 'auto-size', converter: litBool })
229
+ autoSize = false;
230
+
231
+ /**
232
+ * Si es true, aplica un efecto de desenfoque (blur) al contenido durante la animación de entrada y salida.
233
+ * @type {boolean}
234
+ */
235
+ @property({ type: Boolean, reflect: true, attribute: 'blur-content', converter: litBool })
236
+ blurContent = false;
237
+
146
238
  @query('.panel') private _panel!: HTMLDivElement;
147
239
  @query('.panel-inner') private _inner!: HTMLDivElement;
148
240
  @query('.backdrop') private _backdrop!: HTMLDivElement;
@@ -161,8 +253,20 @@ export class MoniMorphModal extends MoniElement {
161
253
  private static _openStack: MoniMorphModal[] = [];
162
254
  private _targetStylesCache = new Map<HTMLElement, { opacity: string; color: string }>();
163
255
 
256
+ /**
257
+ * Handler encapsulado para el evento click en el trigger.
258
+ * Dispara la animación de apertura.
259
+ */
164
260
  private _onTargetClick = () => this.show();
261
+ /**
262
+ * Handler encapsulado para interceptar teclas a nivel global (ej: Escape).
263
+ */
165
264
  private _onDocKeydown = (e: KeyboardEvent) => this._handleDocKeydown(e);
265
+ /**
266
+ * Handler de clics globales para detectar interacciones fuera del modal (Click outside).
267
+ * Sólo cierra el modal si `closeOnClickOutside` es true y el usuario no hizo clic
268
+ * dentro de las dimensiones del panel animado ni del botón original.
269
+ */
166
270
  private _onDocClick = (e: MouseEvent) => {
167
271
  if (!this.open || !this._visible || !this.closeOnClickOutside) return;
168
272
  if (e.composedPath().includes(this._panel)) return;
@@ -194,6 +298,12 @@ export class MoniMorphModal extends MoniElement {
194
298
  super.disconnectedCallback();
195
299
  }
196
300
 
301
+ /**
302
+ * Hook del ciclo de vida reactivo (Lit).
303
+ * Observa cambios asíncronos en el atributo `target` para re-bindear el disparador,
304
+ * y reacciona a cambios programáticos de la propiedad `open` para ejecutar
305
+ * imperativamente la animación de apertura (`show`) o cierre (`hide`).
306
+ */
197
307
  override updated(changed: Map<string, unknown>) {
198
308
  super.updated(changed);
199
309
  if (changed.has('target')) {
@@ -205,15 +315,21 @@ export class MoniMorphModal extends MoniElement {
205
315
  }
206
316
  }
207
317
 
318
+ /**
319
+ * Busca en el DOM el elemento que dispara el modal (el trigger) basándose en la propiedad `target`.
320
+ * Establece el listener de click que iniciará la animación de morphing.
321
+ */
208
322
  private _resolveTarget(): void {
209
323
  if (!this.target) return;
210
324
 
211
325
  const el = document.querySelector<HTMLElement>(this.target);
212
326
  if (!el) {
213
- console.warn(`[moni-morph-modal] target "${this.target}" not found`);
327
+ // Es normal que no se encuentre el target si está condicionado por un #if en Svelte
328
+ // Se intentará resolver nuevamente al llamar a show()
214
329
  return;
215
330
  }
216
331
 
332
+ // Si el target cambió dinámicamente, limpiamos el listener anterior para evitar memory leaks
217
333
  if (this._targetEl && this._targetEl !== el) {
218
334
  this._targetEl.removeEventListener('click', this._onTargetClick);
219
335
  }
@@ -221,8 +337,16 @@ export class MoniMorphModal extends MoniElement {
221
337
  this._targetEl.addEventListener('click', this._onTargetClick);
222
338
  }
223
339
 
340
+ /**
341
+ * Dispara la apertura del modal y coordina la animación de FLIP (First, Last, Invert, Play) / Morphing.
342
+ * 1. Oculta visualmente el target original.
343
+ * 2. Posiciona el modal colapsado exactamente sobre el target original (mismas dimensiones, color, y radios).
344
+ * 3. Expande fluidamente el modal a sus dimensiones finales mediante transformaciones CSS.
345
+ */
224
346
  show(): void {
347
+ // Prevenimos ejecuciones concurrentes si ya se está animando o está visible
225
348
  if (this._isAnimating || this._visible) return;
349
+
226
350
  if (!this._targetEl) this._resolveTarget();
227
351
  if (!this._targetEl) {
228
352
  console.warn('[moni-morph-modal] cannot show: target not found');
@@ -230,17 +354,23 @@ export class MoniMorphModal extends MoniElement {
230
354
  }
231
355
 
232
356
  this._isAnimating = true;
357
+
358
+ // Flag interno para evitar loops infinitos en el hook `updated()`
233
359
  this._handlingOpenChange = true;
234
360
  this.open = true;
235
361
  this._handlingOpenChange = false;
236
362
 
363
+ // Apilamos el modal globalmente para manejar correctamente el z-index en modales anidados
237
364
  MoniMorphModal._openStack.push(this);
238
365
  this._zIndex = 1000 + MoniMorphModal._openStack.length;
366
+
367
+ // Aplicamos variables CSS inline para gestionar las transiciones del backdrop y el modal
239
368
  this.style.zIndex = String(this._zIndex);
240
369
  this.style.setProperty('--_z-index', String(this._zIndex));
241
370
  this.style.setProperty('--_backdrop-duration', '0.38s');
242
371
  this.style.setProperty('--_backdrop-ease', 'cubic-bezier(0.15, 0.85, 0.2, 1)');
243
372
 
373
+ // Fase "First": Ocultamos o cubrimos el trigger original según la configuración del desarrollador
244
374
  this._hideTargetContent();
245
375
 
246
376
  void this.updateComplete.then(async () => {
@@ -248,13 +378,20 @@ export class MoniMorphModal extends MoniElement {
248
378
  this._isAnimating = false;
249
379
  return;
250
380
  }
381
+
382
+ // Añadimos clase para prevenir interacciones durante la animación
251
383
  this._panel.classList.add('is-animating');
252
384
 
385
+ // Esperamos un frame para asegurar que el navegador aplicó el `display: block`
386
+ // necesario antes de poder leer las dimensiones.
253
387
  await new Promise((resolve) => requestAnimationFrame(resolve));
254
388
 
389
+ // 1. "First": Calculamos el estado visual actual del trigger
255
390
  const targetState = this._getTargetState(this._targetEl);
256
391
  const panel = this._panel;
257
392
 
393
+ // 2. "Invert": Posicionamos el modal como un clon visual exacto del trigger.
394
+ // Desactivamos la sombra y forzamos el color/borde para coincidir con el botón/FAB.
258
395
  this._applyRect(panel, targetState.rect);
259
396
  panel.style.backgroundColor = targetState.backgroundColor;
260
397
  panel.style.color = targetState.color;
@@ -269,23 +406,35 @@ export class MoniMorphModal extends MoniElement {
269
406
  this._backdrop.style.zIndex = String(this._zIndex - 1);
270
407
  }
271
408
 
409
+ // Limpiamos elementos fantasma previos si el usuario interactuó muy rápido
272
410
  this._cleanupMorphElements();
411
+
412
+ // Respetamos la preferencia de sistema de animaciones reducidas por accesibilidad (a11y)
273
413
  const prefersReducedMotion = window.matchMedia(
274
414
  '(prefers-reduced-motion: reduce)'
275
415
  ).matches;
416
+
417
+ // 3. Verificamos si debemos orquestar la animación compleja del label flotante cruzado
276
418
  let shouldMorph = this._shouldMorphLabel() && !prefersReducedMotion;
277
419
  const morphTargets: HTMLElement[] = [];
278
420
  let source: any = null;
279
421
  const headerWrapper = this.shadowRoot!.querySelector('.header-morph-wrapper') as HTMLElement;
280
422
 
281
423
  if (shouldMorph) {
424
+ // Preparamos la estructura del header para que el texto real entre con un fade in
282
425
  this._animateLabelOpen();
426
+
427
+ // Buscamos el ícono y el texto dentro del botón de origen (trigger)
283
428
  source = this._resolveMorphSource();
429
+
430
+ // Si existe un nodo de texto, creamos un elemento flotante (fantasma) en el modal
284
431
  if (source.text) {
285
432
  const el = this._createMorphText(source.text.content, source.text.sourceEl);
286
433
  this._positionElement(el, source.text.rect);
287
434
  morphTargets.push(el);
288
435
  }
436
+
437
+ // Hacemos lo mismo si el botón original contenía un icono
289
438
  if (source.icon) {
290
439
  const el = this._createMorphIcon(source.icon.element);
291
440
  this._positionElement(el, source.icon.rect);
@@ -295,9 +444,11 @@ export class MoniMorphModal extends MoniElement {
295
444
  }
296
445
 
297
446
  if (shouldMorph && headerWrapper) {
447
+ // Ocultamos temporalmente el contenido final del header mientras animamos los elementos flotantes
298
448
  headerWrapper.style.opacity = '0';
299
449
  }
300
450
 
451
+ // Capturamos el estado "First" del panel (tamaño pequeño de botón) y de los elementos flotantes
301
452
  const state = Flip.getState(panel, {
302
453
  props: 'backgroundColor,borderRadius,color,boxShadow'
303
454
  });
@@ -305,21 +456,53 @@ export class MoniMorphModal extends MoniElement {
305
456
  props: 'color,fontSize,fontWeight,lineHeight'
306
457
  });
307
458
 
308
- const finalRect = this._computeFinalRect(targetState.rect);
459
+ // Fase "Last": Calculamos las dimensiones expandidas del modal basándonos en la configuración
460
+ let naturalSize: { width: number; height: number } | undefined;
461
+ if (this.autoSize) {
462
+ const prevW = panel.style.getPropertyValue('--_panel-width');
463
+ const prevH = panel.style.getPropertyValue('--_panel-height');
464
+ const prevPanelW = panel.style.width;
465
+ const prevPanelH = panel.style.height;
466
+
467
+ panel.style.width = 'auto';
468
+ panel.style.height = 'auto';
469
+ panel.style.setProperty('--_panel-width', 'max-content');
470
+ panel.style.setProperty('--_panel-height', 'max-content');
471
+
472
+ const bodyEl = this.shadowRoot!.querySelector('.body') as HTMLElement;
473
+ const prevOverflow = bodyEl ? bodyEl.style.overflow : '';
474
+ if (bodyEl) bodyEl.style.overflow = 'visible';
475
+
476
+ // Forzamos al panel-inner a expandirse para medir su contenido real
477
+ naturalSize = { width: this._inner.scrollWidth, height: this._inner.scrollHeight };
478
+
479
+ panel.style.setProperty('--_panel-width', prevW);
480
+ panel.style.setProperty('--_panel-height', prevH);
481
+ panel.style.width = prevPanelW;
482
+ panel.style.height = prevPanelH;
483
+
484
+ if (bodyEl) bodyEl.style.overflow = prevOverflow;
485
+ }
486
+
487
+ const finalRect = this._computeFinalRect(targetState.rect, naturalSize);
309
488
  panel.style.setProperty('--_panel-width', `${finalRect.width}px`);
310
489
  panel.style.setProperty('--_panel-height', `${finalRect.height}px`);
311
490
  this._applyRect(panel, finalRect);
491
+
492
+ // Removemos las forzadas de estilo del trigger para que herede las propias del modal
312
493
  panel.style.backgroundColor = '';
313
494
  panel.style.color = '';
314
495
  panel.style.borderRadius = '';
315
496
  panel.style.boxShadow = '';
316
497
 
317
498
  if (shouldMorph && source) {
499
+ // Medimos dónde van a terminar el icono y el texto dentro del modal ya expandido
318
500
  const textRect = this._measureHeaderTextRect();
319
501
  const iconRect = this._measureHeaderIconRect();
320
502
  const headerComputed = window.getComputedStyle(this._header);
321
503
 
322
504
  if (this._morphEls.text && textRect) {
505
+ // Movemos el texto flotante a su destino y le pedimos que adopte los estilos del header
323
506
  this._positionElement(this._morphEls.text, textRect);
324
507
  this._morphEls.text.style.color = headerComputed.color || 'var(--on-surface)';
325
508
  this._morphEls.text.style.fontSize = headerComputed.fontSize || 'inherit';
@@ -328,6 +511,7 @@ export class MoniMorphModal extends MoniElement {
328
511
  }
329
512
 
330
513
  if (this._morphEls.icon && iconRect) {
514
+ // Hacemos lo mismo con el icono
331
515
  this._positionElement(this._morphEls.icon, iconRect);
332
516
  this._morphEls.icon.style.color = headerComputed.color || 'var(--on-surface)';
333
517
  }
@@ -341,32 +525,38 @@ export class MoniMorphModal extends MoniElement {
341
525
  onComplete: () => {
342
526
  this._isAnimating = false;
343
527
  this._visible = true;
344
- this._panel.classList.remove('is-animating');
345
528
  this._cleanupMorphElements();
346
529
  if (headerWrapper) headerWrapper.style.opacity = '1';
347
530
  }
348
531
  });
349
532
 
350
533
  if (morphTargets.length > 0) {
351
- Flip.from(morphState, {
534
+ if (this.blurContent) {
535
+ gsap.set(morphTargets, { filter: 'blur(16px)' });
536
+ }
537
+ const flipConfig: any = {
352
538
  targets: morphTargets,
353
539
  duration: 0.38,
354
540
  ease: 'morph-open',
355
541
  zIndex: this._zIndex + 10
356
- });
542
+ };
543
+ if (this.blurContent) {
544
+ flipConfig.filter = 'blur(0px)';
545
+ flipConfig.clearProps = 'filter';
546
+ }
547
+ Flip.from(morphState, flipConfig);
357
548
  }
358
549
 
359
- const bodyChildren = Array.from(this.shadowRoot!.querySelectorAll('.body > *'));
360
- const fadeTargets = [
361
- ...bodyChildren,
362
- this.shadowRoot!.querySelector('footer'),
363
- this.shadowRoot!.querySelector('.close-btn')
364
- ].filter(Boolean) as HTMLElement[];
550
+ const body = this.shadowRoot!.querySelector('.body');
551
+ const footer = this.shadowRoot!.querySelector('footer');
552
+ const closeBtn = this.shadowRoot!.querySelector('.close-btn');
553
+ const fadeTargets = [body, footer, closeBtn].filter(Boolean) as HTMLElement[];
365
554
 
366
- gsap.set(fadeTargets, { opacity: 0, y: 8, filter: 'blur(8px)' });
555
+ const initialBlur = this.blurContent ? 'blur(16px)' : 'blur(0px)';
556
+ gsap.set(fadeTargets, { opacity: 0, y: 8, filter: initialBlur });
367
557
  gsap.fromTo(
368
558
  fadeTargets,
369
- { opacity: 0, y: 8, filter: 'blur(8px)' },
559
+ { opacity: 0, y: 8, filter: initialBlur },
370
560
  {
371
561
  opacity: 1,
372
562
  y: 0,
@@ -377,12 +567,20 @@ export class MoniMorphModal extends MoniElement {
377
567
  ease: 'power2.out',
378
568
  onComplete: () => {
379
569
  gsap.set(fadeTargets, { clearProps: 'opacity,transform,filter' });
570
+ this._panel.classList.remove('is-animating');
380
571
  }
381
572
  }
382
573
  );
383
574
  });
384
575
  }
385
576
 
577
+ /**
578
+ * Solución a un caso borde de FLIP Animations:
579
+ * Si el `.panel` original carece de un color de fondo sólido, la interpolación
580
+ * de GSAP puede comportarse erráticamente. Este método "asciende" el árbol del DOM
581
+ * hasta encontrar un padre con un color de fondo sólido y lo aplica, garantizando
582
+ * que el modal no colapse de forma transparente de regreso hacia el gatillo.
583
+ */
386
584
  private _ensureSolidPanelBackground() {
387
585
  if (!this._panel) return;
388
586
  let bg = window.getComputedStyle(this._panel).backgroundColor;
@@ -412,20 +610,19 @@ export class MoniMorphModal extends MoniElement {
412
610
  this.style.setProperty('--_backdrop-duration', '0.3s');
413
611
  this.style.setProperty('--_backdrop-ease', 'cubic-bezier(0.4, 0, 0.2, 1)');
414
612
 
415
- const bodyChildren = Array.from(this.shadowRoot!.querySelectorAll('.body > *'));
416
- const fadeTargets = [
417
- ...bodyChildren,
418
- this.shadowRoot!.querySelector('footer'),
419
- this.shadowRoot!.querySelector('.close-btn')
420
- ].filter(Boolean) as HTMLElement[];
613
+ const body = this.shadowRoot!.querySelector('.body');
614
+ const footer = this.shadowRoot!.querySelector('footer');
615
+ const closeBtn = this.shadowRoot!.querySelector('.close-btn');
616
+ const fadeTargets = [body, footer, closeBtn].filter(Boolean) as HTMLElement[];
421
617
  const headerWrapper = this.shadowRoot!.querySelector('.header-morph-wrapper') as HTMLElement;
422
618
 
619
+ const targetBlur = this.blurContent ? 'blur(12px)' : 'blur(0px)';
423
620
  gsap.fromTo(
424
621
  fadeTargets,
425
622
  { opacity: 1, filter: 'blur(0px)' },
426
623
  {
427
624
  opacity: 0,
428
- filter: 'blur(4px)',
625
+ filter: targetBlur,
429
626
  duration: 0.12,
430
627
  ease: 'power2.in',
431
628
  onComplete: () => {
@@ -478,7 +675,34 @@ export class MoniMorphModal extends MoniElement {
478
675
  });
479
676
 
480
677
  const targetState = this._getTargetState(this._targetEl!);
481
- const finalRect = this._computeFinalRect(targetState.rect);
678
+
679
+ let naturalSize: { width: number; height: number } | undefined;
680
+ if (this.autoSize) {
681
+ const prevW = panel.style.getPropertyValue('--_panel-width');
682
+ const prevH = panel.style.getPropertyValue('--_panel-height');
683
+ const prevPanelW = panel.style.width;
684
+ const prevPanelH = panel.style.height;
685
+
686
+ panel.style.width = 'auto';
687
+ panel.style.height = 'auto';
688
+ panel.style.setProperty('--_panel-width', 'max-content');
689
+ panel.style.setProperty('--_panel-height', 'max-content');
690
+
691
+ const bodyEl = this.shadowRoot!.querySelector('.body') as HTMLElement;
692
+ const prevOverflow = bodyEl ? bodyEl.style.overflow : '';
693
+ if (bodyEl) bodyEl.style.overflow = 'visible';
694
+
695
+ naturalSize = { width: this._inner.scrollWidth, height: this._inner.scrollHeight };
696
+
697
+ panel.style.setProperty('--_panel-width', prevW);
698
+ panel.style.setProperty('--_panel-height', prevH);
699
+ panel.style.width = prevPanelW;
700
+ panel.style.height = prevPanelH;
701
+
702
+ if (bodyEl) bodyEl.style.overflow = prevOverflow;
703
+ }
704
+
705
+ const finalRect = this._computeFinalRect(targetState.rect, naturalSize);
482
706
  panel.style.setProperty('--_panel-width', `${finalRect.width}px`);
483
707
  panel.style.setProperty('--_panel-height', `${finalRect.height}px`);
484
708
 
@@ -541,12 +765,19 @@ export class MoniMorphModal extends MoniElement {
541
765
  });
542
766
 
543
767
  if (morphTargets.length > 0) {
544
- Flip.from(morphState, {
768
+ if (this.blurContent) {
769
+ gsap.set(morphTargets, { filter: 'blur(0px)' });
770
+ }
771
+ const flipConfig: any = {
545
772
  targets: morphTargets,
546
773
  duration: 0.3,
547
774
  ease: 'morph-close',
548
775
  zIndex: this._zIndex + 10
549
- });
776
+ };
777
+ if (this.blurContent) {
778
+ flipConfig.filter = 'blur(12px)';
779
+ }
780
+ Flip.from(morphState, flipConfig);
550
781
  }
551
782
  }
552
783
  }
@@ -566,6 +797,12 @@ export class MoniMorphModal extends MoniElement {
566
797
  this.hide();
567
798
  }
568
799
 
800
+ /**
801
+ * Fase "First" de la animación (Cierre del Trigger):
802
+ * Determina cómo desaparecer u ocultar el botón original mientras el modal ocupa su lugar.
803
+ * Mantiene el cache de opacidades y colores originales para poder restaurarlos
804
+ * idénticamente al cerrar el modal (evitando saltos visuales).
805
+ */
569
806
  private _hideTargetContent() {
570
807
  if (!this._targetEl) return;
571
808
 
@@ -594,6 +831,10 @@ export class MoniMorphModal extends MoniElement {
594
831
  }
595
832
  }
596
833
 
834
+ /**
835
+ * Restaura puramente los estilos originales del botón Trigger tras terminar
836
+ * la animación de cierre, leyendo las opacidades/colores almacenados en caché.
837
+ */
597
838
  private _restoreTargetContent() {
598
839
  if (!this._targetEl) return;
599
840
 
@@ -667,11 +908,11 @@ export class MoniMorphModal extends MoniElement {
667
908
  }
668
909
 
669
910
  private _animateLabelOpen(): void {
670
- // No-op: label morphing is handled by the unified FLIP timeline in show()
911
+ // No-op: el morphing de la etiqueta es manejado por el timeline unificado FLIP en show()
671
912
  }
672
913
 
673
914
  private _animateLabelClose(): void {
674
- // No-op: label morphing is handled by the unified FLIP timeline in hide()
915
+ // No-op: el morphing de la etiqueta es manejado por el timeline unificado FLIP en hide()
675
916
  }
676
917
 
677
918
  private _createMorphText(sourceText: string, sourceEl: Element): HTMLElement {
@@ -1025,13 +1266,13 @@ export class MoniMorphModal extends MoniElement {
1025
1266
  el.style.height = `${rect.height}px`;
1026
1267
  }
1027
1268
 
1028
- private _computeFinalRect(targetRect: DOMRect): DOMRect {
1269
+ private _computeFinalRect(targetRect: DOMRect, naturalSize?: { width: number; height: number }): DOMRect {
1029
1270
  const vw = window.innerWidth;
1030
1271
  const vh = window.innerHeight;
1031
1272
  const padding = 16;
1032
1273
 
1033
- let width = this._parseSize(this.expandedWidth, vw);
1034
- let height = this._parseSize(this.expandedHeight, vh);
1274
+ let width = this.autoSize && naturalSize ? naturalSize.width : this._parseSize(this.expandedWidth, vw);
1275
+ let height = this.autoSize && naturalSize ? naturalSize.height : this._parseSize(this.expandedHeight, vh);
1035
1276
 
1036
1277
  width = Math.min(width, vw - padding * 2);
1037
1278
  height = Math.min(height, vh - padding * 2);
@@ -1206,7 +1447,7 @@ export class MoniMorphModal extends MoniElement {
1206
1447
  overflow: hidden;
1207
1448
  background-color: var(--surface-container-high);
1208
1449
  color: var(--on-surface);
1209
- border-radius: 1.75rem;
1450
+ border-radius: var(--moni-morph-panel-radius, 1.75rem);
1210
1451
  box-shadow: var(--elevate2);
1211
1452
  z-index: var(--_z-index, 100);
1212
1453
  }
@@ -1235,13 +1476,18 @@ export class MoniMorphModal extends MoniElement {
1235
1476
 
1236
1477
  .body {
1237
1478
  flex: 1;
1238
- overflow-y: auto;
1239
- padding: 0 1.5rem;
1240
- scrollbar-gutter: stable;
1479
+ overflow-y: var(--moni-morph-body-overflow-y, auto);
1480
+ overflow-x: var(--moni-morph-body-overflow-x, hidden);
1481
+ padding: var(--moni-morph-body-padding, 0 1.5rem);
1482
+ scrollbar-gutter: var(--moni-morph-body-scrollbar-gutter, stable);
1241
1483
  }
1242
1484
 
1243
1485
  .panel.is-animating .body {
1244
- overflow: hidden;
1486
+ overflow: hidden !important;
1487
+ }
1488
+
1489
+ .panel.is-animating .body::-webkit-scrollbar {
1490
+ display: none;
1245
1491
  }
1246
1492
 
1247
1493
  footer {
@@ -1291,6 +1537,27 @@ export class MoniMorphModal extends MoniElement {
1291
1537
  `
1292
1538
  ];
1293
1539
 
1540
+ /**
1541
+ * Renderiza el caparazón del modal morph: fondo opcional + contenedor de panel.
1542
+ *
1543
+ * **Bandera `_visible`:**
1544
+ * `visible = this._visible || this.open` mantiene vivo el DOM mientras se reproduce
1545
+ * la animación de cierre de GSAP. `_visible` se establece en `true` al inicio de la apertura y
1546
+ * en `false` solo después de que se dispara el `onComplete` de la animación de cierre. Sin esto,
1547
+ * Lit eliminaría el panel del DOM antes de que la animación termine.
1548
+ *
1549
+ * **Condicional del fondo (backdrop):**
1550
+ * El div `.backdrop` solo se renderiza cuando `hasBackdrop=true`. Cuando `modal=false`,
1551
+ * el `background-color` del fondo se establece en `transparent` para que el fondo superpuesto
1552
+ * (scrim) sea invisible, pero los eventos del puntero aún se consumen (clic para cerrar).
1553
+ *
1554
+ * **Objetivo de morphing de GSAP:**
1555
+ * `.panel` es el objetivo de la animación GSAP. Al abrir: se anima desde el
1556
+ * `getBoundingClientRect()` del elemento anclaje (posición + tamaño) a su tamaño CSS natural
1557
+ * mediante `gsap.fromTo()`. Al cerrar: a la inversa. `.panel-inner` recibe una
1558
+ * contra-escala para evitar que el contenido interno se escale junto con el panel,
1559
+ * creando una ilusión física de "expandirse desde el origen".
1560
+ */
1294
1561
  override render() {
1295
1562
  const visible = this._visible || this.open;
1296
1563
  const headerEmpty = !this._hasHeader && !this.showCloseButton;