@synthex-engineers/refined-tech 1.0.1 → 1.0.3

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.
@@ -16,104 +16,224 @@ const {
16
16
  align = 'center',
17
17
  className = '',
18
18
  } = Astro.props;
19
-
20
- const alignClasses = {
21
- left: 'left-0 translate-x-0',
22
- center: 'left-1/2 -translate-x-1/2',
23
- right: 'right-0 translate-x-0',
24
- }[align];
25
-
26
- const arrowAlignClasses = {
27
- left: 'left-3',
28
- center: 'left-1/2 -translate-x-1/2',
29
- right: 'right-3',
30
- }[align];
31
19
  ---
32
20
 
33
- <span class={`relative inline-flex items-center group/bubble ml-1.5 align-middle group-hover/bubble:z-50 group-focus-within/bubble:z-50 ${className}`}>
21
+ <span
22
+ class={`relative inline-flex items-center group/bubble ml-1.5 align-middle ${className}`}
23
+ data-align={align}
24
+ >
34
25
  <button
35
26
  type="button"
36
- class="info-bubble-trigger w-4 h-4 rounded-full bg-white/10 hover:bg-brand-pistachio/20 text-white/70 hover:text-brand-pistachio border border-white/20 hover:border-brand-pistachio/40 text-xs font-mono font-bold inline-flex items-center justify-center cursor-help transition-all duration-200 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-brand-pistachio"
27
+ class="info-bubble-trigger w-4 h-4 rounded-full bg-white/10 hover:bg-pistachio/20 text-white/70 hover:text-pistachio border border-white/20 hover:border-pistachio/40 text-xs font-mono font-bold inline-flex items-center justify-center cursor-help transition-all duration-200 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-pistachio shrink-0 select-none"
37
28
  aria-label={`${title}: ${description}`}
29
+ aria-expanded="false"
38
30
  >
39
31
  ?
40
32
  </button>
41
33
 
42
- <!-- Popover Flotante Accesible (Hover & Focus) con fondo sólido opaco y contención -->
34
+ <!-- Popover Flotante Accesible (Hover, Focus & Click/Tap): sale justo arriba a 8px sin tapar el botón ? -->
43
35
  <span
44
36
  role="tooltip"
45
- class={`info-bubble-tooltip absolute bottom-full ${alignClasses} mb-2.5 w-64 sm:w-72 p-3.5 rounded-xl bg-charcoal border border-white/20 text-left shadow-2xl z-[100] pointer-events-none opacity-0 translate-y-1 group-hover/bubble:opacity-100 group-hover/bubble:pointer-events-auto group-hover/bubble:translate-y-0 group-focus-within/bubble:opacity-100 group-focus-within/bubble:pointer-events-auto group-focus-within/bubble:translate-y-0 transition-all duration-200 backdrop-blur-xl`}
37
+ class="info-bubble-tooltip absolute bottom-[calc(100%+8px)] left-1/2 -translate-x-1/2 w-64 sm:w-72 max-w-[calc(100vw-32px)] p-3.5 rounded-xl border border-white/20 text-left shadow-2xl z-[100] pointer-events-none opacity-0 invisible transition-opacity duration-200"
46
38
  >
47
- <span class="block text-xs font-display font-bold text-brand-pistachio mb-1" data-i18n={titleKey}>
39
+ <span class="block text-xs font-display font-bold text-pistachio mb-1" data-i18n={titleKey}>
48
40
  {title}
49
41
  </span>
50
42
  <span class="block text-xs font-body text-white/90 leading-relaxed" data-i18n={descKey}>
51
43
  {description}
52
44
  </span>
53
45
 
54
- <!-- Flecha indicadora inferior -->
46
+ <!-- Flecha indicadora apuntando al botón ? -->
55
47
  <span
56
- class={`info-bubble-arrow absolute top-full ${arrowAlignClasses} -mt-[1px] border-4 border-transparent border-t-charcoal`}
48
+ class="info-bubble-arrow absolute top-full left-1/2 -translate-x-1/2 -mt-[1px] border-[5px] border-transparent border-t-[#141414] pointer-events-none"
57
49
  ></span>
58
50
  </span>
59
51
  </span>
60
52
 
53
+ <style is:global>
54
+ /* Visibilidad y z-index garantizados independientemente de Tailwind */
55
+ .group\/bubble:hover .info-bubble-tooltip,
56
+ .group\/bubble:focus-within .info-bubble-tooltip,
57
+ .group\/bubble[data-state="open"] .info-bubble-tooltip,
58
+ .group\/bubble.is-open .info-bubble-tooltip {
59
+ opacity: 1 !important;
60
+ pointer-events: auto !important;
61
+ visibility: visible !important;
62
+ }
63
+
64
+ .group\/bubble:hover,
65
+ .group\/bubble:focus-within,
66
+ .group\/bubble[data-state="open"],
67
+ .group\/bubble.is-open {
68
+ z-index: 60 !important;
69
+ }
70
+
71
+ .info-bubble-tooltip {
72
+ background-color: #141414 !important;
73
+ }
74
+
75
+ /* Inversión vertical automática si no cabe por arriba */
76
+ .info-bubble-tooltip.is-inverted {
77
+ bottom: auto !important;
78
+ top: calc(100% + 8px) !important;
79
+ }
80
+
81
+ .info-bubble-arrow {
82
+ border-top-color: #141414 !important;
83
+ }
84
+
85
+ .info-bubble-tooltip.is-inverted .info-bubble-arrow {
86
+ top: auto !important;
87
+ bottom: 100% !important;
88
+ margin-top: 0 !important;
89
+ margin-bottom: -1px !important;
90
+ border-top-color: transparent !important;
91
+ border-bottom-color: #141414 !important;
92
+ }
93
+ </style>
94
+
61
95
  <script>
62
- function initHelpTooltips() {
63
- const triggers = document.querySelectorAll<HTMLElement>('.info-bubble-trigger');
64
-
65
- triggers.forEach((trigger) => {
66
- if ((trigger as any).__infoBubbleBound) return;
67
- (trigger as any).__infoBubbleBound = true;
68
-
69
- const tooltip = trigger.parentElement?.querySelector<HTMLElement>('.info-bubble-tooltip');
70
- if (!tooltip) return;
71
-
72
- function repositionTooltip() {
73
- if (!tooltip) return;
74
- tooltip.style.removeProperty('left');
75
- tooltip.style.removeProperty('right');
76
- tooltip.style.removeProperty('transform');
77
-
78
- requestAnimationFrame(() => {
79
- const rect = tooltip.getBoundingClientRect();
80
- const viewportWidth = window.innerWidth;
81
- const margin = 16;
82
-
83
- // Corrección si se sale por la derecha
84
- if (rect.right > viewportWidth - margin) {
85
- const overflow = rect.right - viewportWidth + margin;
86
- tooltip.style.transform = `translateX(-${overflow}px)`;
87
- } else if (rect.left < margin) {
88
- const overflow = margin - rect.left;
89
- tooltip.style.transform = `translateX(${overflow}px)`;
90
- }
91
-
92
- // Inversión vertical si no cabe por arriba
93
- const arrow = tooltip.querySelector<HTMLElement>('.info-bubble-arrow');
94
- if (rect.top < 60) {
95
- tooltip.classList.remove('bottom-full', 'mb-2.5');
96
- tooltip.classList.add('top-full', 'mt-2.5');
97
- if (arrow) {
98
- arrow.classList.remove('top-full', '-mt-[1px]', 'border-t-charcoal');
99
- arrow.classList.add('bottom-full', '-mb-[1px]', 'border-b-charcoal');
100
- }
101
- } else {
102
- tooltip.classList.remove('top-full', 'mt-2.5');
103
- tooltip.classList.add('bottom-full', 'mb-2.5');
104
- if (arrow) {
105
- arrow.classList.remove('bottom-full', '-mb-[1px]', 'border-b-charcoal');
106
- arrow.classList.add('top-full', '-mt-[1px]', 'border-t-charcoal');
107
- }
108
- }
109
- });
96
+ function setupHelpTooltipSystem() {
97
+ if ((window as any).__synthexHelpTooltipInitialized) return;
98
+ (window as any).__synthexHelpTooltipInitialized = true;
99
+
100
+ let activeBubble: HTMLElement | null = null;
101
+
102
+ function closeActiveBubble() {
103
+ if (activeBubble) {
104
+ activeBubble.removeAttribute('data-state');
105
+ activeBubble.classList.remove('is-open');
106
+ const trigger = activeBubble.querySelector<HTMLButtonElement>('.info-bubble-trigger');
107
+ if (trigger) trigger.setAttribute('aria-expanded', 'false');
108
+ activeBubble = null;
109
+ }
110
+ }
111
+
112
+ function reposition(bubble: HTMLElement) {
113
+ const trigger = bubble.querySelector<HTMLElement>('.info-bubble-trigger');
114
+ const tooltip = bubble.querySelector<HTMLElement>('.info-bubble-tooltip');
115
+ const arrow = bubble.querySelector<HTMLElement>('.info-bubble-arrow');
116
+ if (!trigger || !tooltip) return;
117
+
118
+ // 1. Reset base
119
+ tooltip.classList.remove('is-inverted');
120
+ tooltip.style.left = '50%';
121
+ tooltip.style.transform = 'translateX(-50%)';
122
+ if (arrow) {
123
+ arrow.style.left = '50%';
124
+ arrow.style.transform = 'translateX(-50%)';
110
125
  }
111
126
 
112
- trigger.addEventListener('mouseenter', repositionTooltip);
113
- trigger.addEventListener('focus', repositionTooltip);
127
+ // 2. Medir cotas en el viewport
128
+ const triggerRect = trigger.getBoundingClientRect();
129
+ const tooltipRect = tooltip.getBoundingClientRect();
130
+ const viewportWidth = window.innerWidth;
131
+ const margin = 16;
132
+
133
+ // 3. Comprobar si cabe verticalmente arriba (mínimo 60px libres desde el top)
134
+ if (triggerRect.top - tooltipRect.height - 12 < 60) {
135
+ tooltip.classList.add('is-inverted');
136
+ }
137
+
138
+ // 4. Cálculo horizontal para no salirse de los límites de la pantalla
139
+ const triggerCenterX = triggerRect.left + (triggerRect.width / 2);
140
+ const idealLeft = triggerCenterX - (tooltipRect.width / 2);
141
+ const idealRight = idealLeft + tooltipRect.width;
142
+
143
+ let shiftX = 0;
144
+ if (idealRight > viewportWidth - margin) {
145
+ shiftX = (viewportWidth - margin) - idealRight;
146
+ } else if (idealLeft < margin) {
147
+ shiftX = margin - idealLeft;
148
+ }
149
+
150
+ if (shiftX !== 0) {
151
+ tooltip.style.transform = `translateX(calc(-50% + ${shiftX}px))`;
152
+
153
+ // Mantener la flecha apuntando exactamente al centro del botón ?
154
+ if (arrow) {
155
+ const arrowLeft = (tooltipRect.width / 2) - shiftX;
156
+ const clampedArrowLeft = Math.max(14, Math.min(tooltipRect.width - 14, arrowLeft));
157
+ arrow.style.left = `${clampedArrowLeft}px`;
158
+ arrow.style.transform = 'translateX(-50%)';
159
+ }
160
+ }
161
+ }
162
+
163
+ // Delegación universal de Clic / Tap
164
+ document.addEventListener('click', (e) => {
165
+ const target = e.target as HTMLElement | null;
166
+ const trigger = target?.closest<HTMLButtonElement>('.info-bubble-trigger');
167
+
168
+ if (trigger) {
169
+ e.preventDefault();
170
+ e.stopPropagation();
171
+ const bubble = trigger.closest<HTMLElement>('.group\\/bubble');
172
+ if (!bubble) return;
173
+
174
+ const isOpen = bubble.getAttribute('data-state') === 'open' || bubble.classList.contains('is-open');
175
+ closeActiveBubble();
176
+
177
+ if (!isOpen) {
178
+ bubble.setAttribute('data-state', 'open');
179
+ bubble.classList.add('is-open');
180
+ trigger.setAttribute('aria-expanded', 'true');
181
+ activeBubble = bubble;
182
+ reposition(bubble);
183
+ }
184
+ return;
185
+ }
186
+
187
+ // Si se hace clic dentro del tooltip, mantenerlo abierto
188
+ if (target?.closest('.info-bubble-tooltip')) {
189
+ return;
190
+ }
191
+
192
+ // Clic fuera -> cerrar
193
+ closeActiveBubble();
114
194
  });
195
+
196
+ // Delegación universal de Hover (MouseEnter & MouseOver en fase de captura)
197
+ document.addEventListener('mouseover', (e) => {
198
+ const target = e.target as HTMLElement | null;
199
+ const trigger = target?.closest<HTMLButtonElement>('.info-bubble-trigger');
200
+ if (trigger) {
201
+ const bubble = trigger.closest<HTMLElement>('.group\\/bubble');
202
+ if (bubble) reposition(bubble);
203
+ }
204
+ }, true);
205
+
206
+ // Delegación universal de Foco Accesible
207
+ document.addEventListener('focusin', (e) => {
208
+ const target = e.target as HTMLElement | null;
209
+ const trigger = target?.closest<HTMLButtonElement>('.info-bubble-trigger');
210
+ if (trigger) {
211
+ const bubble = trigger.closest<HTMLElement>('.group\\/bubble');
212
+ if (bubble) reposition(bubble);
213
+ }
214
+ });
215
+
216
+ // Cierre accesible con tecla Escape
217
+ document.addEventListener('keydown', (e) => {
218
+ if (e.key === 'Escape') {
219
+ closeActiveBubble();
220
+ }
221
+ });
222
+
223
+ // Recalcular o cerrar en scroll/resize
224
+ window.addEventListener('resize', () => {
225
+ if (activeBubble) reposition(activeBubble);
226
+ }, { passive: true });
227
+
228
+ window.addEventListener('scroll', () => {
229
+ if (activeBubble) closeActiveBubble();
230
+ }, { passive: true });
115
231
  }
116
232
 
117
- initHelpTooltips();
118
- document.addEventListener('astro:page-load', initHelpTooltips);
233
+ setupHelpTooltipSystem();
234
+ if (document.readyState === 'loading') {
235
+ document.addEventListener('DOMContentLoaded', setupHelpTooltipSystem);
236
+ }
237
+ document.addEventListener('astro:page-load', setupHelpTooltipSystem);
238
+ window.addEventListener('synthex:page-ready', setupHelpTooltipSystem);
119
239
  </script>
@@ -0,0 +1,240 @@
1
+ ---
2
+ // Preloader Vectorial Cinemático: Metamorfosis Geométrica al Logotipo Horizontal Synthex
3
+ ---
4
+
5
+ <div
6
+ id="preloader-curtain"
7
+ class="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-charcoal select-none overflow-hidden"
8
+ aria-hidden="true"
9
+ >
10
+ <div class="relative flex flex-col items-center justify-center p-8">
11
+ <!-- Contenedor Maestro SVG con Formas Geométricas que mutan al Logo Horizontal -->
12
+ <svg
13
+ id="preloader-master-svg"
14
+ xmlns="http://www.w3.org/2000/svg"
15
+ viewBox="0 0 350 100"
16
+ class="w-72 sm:w-80 md:w-[420px] overflow-visible"
17
+ >
18
+ <defs>
19
+ <!-- Trazados Destino Ocultos: Isotipo Oficial </> en Verde Pistacho -->
20
+ <!-- 0: Bracket Izquierdo < -->
21
+ <path
22
+ id="target-bracket-left"
23
+ d="M 40 29 L 18 50 L 40 71 L 44.5 66.5 L 27 50 L 44.5 33.5 Z"
24
+ />
25
+ <!-- 1: Barra Diagonal / -->
26
+ <path
27
+ id="target-slash"
28
+ d="M 50.5 70 L 64.5 30 L 68.5 31.5 L 54.5 71.5 Z"
29
+ />
30
+ <!-- 2: Bracket Derecho > -->
31
+ <path
32
+ id="target-bracket-right"
33
+ d="M 74 29 L 96 50 L 74 71 L 69.5 66.5 L 87 50 L 69.5 33.5 Z"
34
+ />
35
+
36
+ <!-- Trazados Destino Ocultos: Glifos Tipográficos Outfit Bold de 'Synthex' -->
37
+ <!-- 3: S -->
38
+ <path
39
+ id="target-char-s"
40
+ d="M135.12 68.59L135.12 68.59Q130.42 68.59 127.02 67.08Q123.62 65.57 120.86 62.38L120.86 62.38L126.37 56.93Q128.21 59.09 130.42 60.22Q132.64 61.36 135.66 61.36L135.66 61.36Q138.36 61.36 139.84 60.39Q141.33 59.41 141.33 57.69L141.33 57.69Q141.33 55.90 140.03 54.82Q138.74 53.74 136.71 53.02Q134.69 52.29 132.39 51.45Q130.10 50.61 128.05 49.34Q125.99 48.07 124.72 45.97Q123.46 43.86 123.46 40.51L123.46 40.51Q123.46 37.00 125.16 34.49Q126.86 31.98 129.88 30.63Q132.91 29.28 136.85 29.28L136.85 29.28Q140.84 29.28 144.16 30.71Q147.49 32.14 149.65 34.52L149.65 34.52L144.14 39.97Q142.41 38.19 140.66 37.33Q138.90 36.46 136.69 36.46L136.69 36.46Q134.42 36.46 133.15 37.30Q131.88 38.14 131.88 39.70L131.88 39.70Q131.88 41.38 133.15 42.35Q134.42 43.32 136.47 44.05Q138.52 44.78 140.82 45.62Q143.11 46.45 145.14 47.78Q147.16 49.10 148.46 51.29Q149.75 53.47 149.75 56.93L149.75 56.93Q149.75 62.44 145.87 65.52Q141.98 68.59 135.12 68.59Z"
41
+ />
42
+ <!-- 4: y -->
43
+ <path
44
+ id="target-char-y"
45
+ d="M164.23 79.07L155.75 79.07L161.69 66.22L151.70 41.76L160.61 41.76L165.85 57.74L171.19 41.76L180.10 41.76L168.87 68.11L164.23 79.07Z"
46
+ />
47
+ <!-- 5: n -->
48
+ <path
49
+ id="target-char-n"
50
+ d="M191.50 68L183.23 68L183.23 41.76L191.50 41.76L191.50 43.86Q194.41 41.22 198.84 41.22L198.84 41.22Q201.70 41.22 203.97 42.59Q206.24 43.97 207.53 46.24Q208.83 48.51 208.83 51.21L208.83 51.21L208.83 68L200.57 68L200.57 53.04Q200.57 50.99 199.30 49.72Q198.03 48.45 196.09 48.45L196.09 48.45Q194.09 48.45 192.79 49.72Q191.50 50.99 191.50 53.04L191.50 53.04L191.50 68Z"
51
+ />
52
+ <!-- 6: t -->
53
+ <path
54
+ id="target-char-t"
55
+ d="M226.11 68L217.85 68L217.85 48.78L211.91 48.78L211.91 41.76L217.85 41.76L217.85 30.90L226.11 30.90L226.11 41.76L232.05 41.76L232.05 48.78L226.11 48.78L226.11 68Z"
56
+ />
57
+ <!-- 7: h -->
58
+ <path
59
+ id="target-char-h"
60
+ d="M243.93 68L235.67 68L235.67 28.80L243.93 28.80L243.93 43.70Q246.74 41.22 251.00 41.22L251.00 41.22Q254.03 41.22 256.35 42.48Q258.67 43.75 259.97 46.02Q261.26 48.29 261.26 51.21L261.26 51.21L261.26 68L253.00 68L253.00 53.04Q253.00 50.99 251.73 49.72Q250.46 48.45 248.52 48.45L248.52 48.45Q246.52 48.45 245.23 49.72Q243.93 50.99 243.93 53.04L243.93 53.04L243.93 68Z"
61
+ />
62
+ <!-- 8: e -->
63
+ <path
64
+ id="target-char-e"
65
+ d="M279.52 68.59L279.52 68.59Q275.25 68.59 271.93 66.84Q268.61 65.08 266.72 61.98Q264.83 58.87 264.83 54.88L264.83 54.88Q264.83 50.94 266.69 47.86Q268.55 44.78 271.74 42.97Q274.93 41.16 278.92 41.16L278.92 41.16Q282.76 41.16 285.75 42.86Q288.75 44.56 290.45 47.51Q292.15 50.45 292.15 54.28L292.15 54.28Q292.15 55.74 291.77 57.58L291.77 57.58L272.98 57.63Q273.63 59.85 275.30 61.03Q276.98 62.22 279.46 62.22L279.46 62.22Q283.40 62.22 285.73 59.52L285.73 59.52L290.26 64.06Q288.32 66.33 285.56 67.46Q282.81 68.59 279.52 68.59ZM278.98 47.48L278.98 47.48Q276.71 47.48 275.14 48.67Q273.58 49.86 272.98 51.96L272.98 51.96L284.43 51.91Q283.40 47.48 278.98 47.48Z"
66
+ />
67
+ <!-- 9: x -->
68
+ <path
69
+ id="target-char-x"
70
+ d="M302.52 68L293.50 68L302.90 54.61L293.99 41.76L303.65 41.76L308.03 48.88L312.46 41.76L321.42 41.76L312.67 54.45L321.96 68L312.40 68L307.54 60.17L302.52 68Z"
71
+ />
72
+ </defs>
73
+
74
+ <!-- FORMAS GEOMÉTRICAS ELEMENTALES ACTIVAS (▲ ■ ●) QUE MUTAN AL LOGO -->
75
+ <g id="preloader-morph-group" class="opacity-0">
76
+ <!-- 0: Triángulo que muta al Bracket Izquierdo < (cx: 31, cy: 50) -->
77
+ <path
78
+ id="morph-shape-0"
79
+ class="morph-item"
80
+ d="M 18 50 L 44 32 L 44 68 Z"
81
+ fill="#E0F9B5"
82
+ />
83
+
84
+ <!-- 1: Rectángulo vertical que muta a la Barra / (cx: 58, cy: 50) -->
85
+ <path
86
+ id="morph-shape-1"
87
+ class="morph-item"
88
+ d="M 54 30 L 63 30 L 63 70 L 54 70 Z"
89
+ fill="#E0F9B5"
90
+ />
91
+
92
+ <!-- 2: Triángulo que muta al Bracket Derecho > (cx: 85, cy: 50) -->
93
+ <path
94
+ id="morph-shape-2"
95
+ class="morph-item"
96
+ d="M 96 50 L 70 68 L 70 32 Z"
97
+ fill="#E0F9B5"
98
+ />
99
+
100
+ <!-- 3: Círculo que muta a 'S' (cx: 135, cy: 50, r: 16) -->
101
+ <path
102
+ id="morph-shape-3"
103
+ class="morph-item"
104
+ d="M 135 34 A 16 16 0 1 0 135 66 A 16 16 0 1 0 135 34 Z"
105
+ fill="#FFFFFF"
106
+ />
107
+
108
+ <!-- 4: Triángulo invertido que muta a 'y' (cx: 166, cy: 55) -->
109
+ <path
110
+ id="morph-shape-4"
111
+ class="morph-item"
112
+ d="M 152 40 L 180 40 L 166 75 Z"
113
+ fill="#FFFFFF"
114
+ />
115
+
116
+ <!-- 5: Cuadrado que muta a 'n' (cx: 196, cy: 54, w: 26, h: 26) -->
117
+ <path
118
+ id="morph-shape-5"
119
+ class="morph-item"
120
+ d="M 183 41 L 209 41 L 209 67 L 183 67 Z"
121
+ fill="#FFFFFF"
122
+ />
123
+
124
+ <!-- 6: Rectángulo vertical que muta a 't' (cx: 222, cy: 50) -->
125
+ <path
126
+ id="morph-shape-6"
127
+ class="morph-item"
128
+ d="M 215 31 L 229 31 L 229 68 L 215 68 Z"
129
+ fill="#FFFFFF"
130
+ />
131
+
132
+ <!-- 7: Cuadrado alto que muta a 'h' (cx: 248, cy: 49) -->
133
+ <path
134
+ id="morph-shape-7"
135
+ class="morph-item"
136
+ d="M 235 29 L 261 29 L 261 68 L 235 68 Z"
137
+ fill="#FFFFFF"
138
+ />
139
+
140
+ <!-- 8: Círculo que muta a 'e' (cx: 278, cy: 55, r: 14) -->
141
+ <path
142
+ id="morph-shape-8"
143
+ class="morph-item"
144
+ d="M 278 41 A 14 14 0 1 0 278 69 A 14 14 0 1 0 278 41 Z"
145
+ fill="#FFFFFF"
146
+ />
147
+
148
+ <!-- 9: Rombo / Diamante que muta a 'x' (cx: 308, cy: 55, r: 14) -->
149
+ <path
150
+ id="morph-shape-9"
151
+ class="morph-item"
152
+ d="M 308 41 L 322 55 L 308 69 L 294 55 Z"
153
+ fill="#FFFFFF"
154
+ />
155
+ </g>
156
+ </svg>
157
+ </div>
158
+ </div>
159
+
160
+ <script>
161
+ import gsap from 'gsap';
162
+ import { MorphSVGPlugin } from 'gsap/MorphSVGPlugin';
163
+
164
+ gsap.registerPlugin(MorphSVGPlugin);
165
+
166
+ const curtain = document.getElementById('preloader-curtain');
167
+ const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
168
+ const alreadySeen = sessionStorage.getItem('synthex_preloaded_session');
169
+
170
+ function notifyPageReady() {
171
+ (window as any).__synthexPageReady = true;
172
+ window.dispatchEvent(new CustomEvent('synthex:page-ready'));
173
+ }
174
+
175
+ if (curtain) {
176
+ if (prefersReducedMotion || alreadySeen) {
177
+ curtain.style.display = 'none';
178
+ document.documentElement.classList.add('synthex-preloaded');
179
+ requestAnimationFrame(() => {
180
+ notifyPageReady();
181
+ });
182
+ } else {
183
+ const morphGroup = document.getElementById('preloader-morph-group');
184
+ const targets = [
185
+ '#target-bracket-left',
186
+ '#target-slash',
187
+ '#target-bracket-right',
188
+ '#target-char-s',
189
+ '#target-char-y',
190
+ '#target-char-n',
191
+ '#target-char-t',
192
+ '#target-char-h',
193
+ '#target-char-e',
194
+ '#target-char-x',
195
+ ];
196
+
197
+ const tl = gsap.timeline({
198
+ onComplete: () => {
199
+ // Desplegar suavemente el telón hacia arriba
200
+ gsap.to(curtain, {
201
+ yPercent: -100,
202
+ duration: 0.7,
203
+ ease: 'power4.inOut',
204
+ onStart: () => {
205
+ notifyPageReady();
206
+ },
207
+ onComplete: () => {
208
+ curtain.style.display = 'none';
209
+ sessionStorage.setItem('synthex_preloaded_session', 'true');
210
+ document.documentElement.classList.add('synthex-preloaded');
211
+ },
212
+ });
213
+ },
214
+ });
215
+
216
+ // 1. Entrada sutil de las figuras geométricas elementales (▲ ■ ●)
217
+ tl.to(morphGroup, {
218
+ opacity: 1,
219
+ duration: 0.35,
220
+ ease: 'power2.out',
221
+ });
222
+
223
+ // 2. Metamorfosis geométrica con MorphSVG (cada figura transmuta al glifo oficial)
224
+ targets.forEach((targetSelector, index) => {
225
+ tl.to(
226
+ `#morph-shape-${index}`,
227
+ {
228
+ morphSVG: targetSelector,
229
+ duration: 0.85,
230
+ ease: 'power2.inOut',
231
+ },
232
+ 0.2 + index * 0.035
233
+ );
234
+ });
235
+
236
+ // 3. Breve pausa ceremonial tras completarse el logotipo
237
+ tl.to({}, { duration: 0.35 });
238
+ }
239
+ }
240
+ </script>
@@ -1,19 +1,23 @@
1
1
  ---
2
2
  interface Props {
3
+ as?: 'h1' | 'h2' | 'h3' | 'span';
3
4
  prefixText?: string;
4
5
  highlightText?: string;
5
6
  strokeColor?: string;
6
7
  className?: string;
8
+ headingClass?: string;
7
9
  prefixI18nKey?: string;
8
10
  highlightI18nKey?: string;
9
11
  id?: string;
10
12
  }
11
13
 
12
14
  const {
15
+ as: Tag = 'h2',
13
16
  prefixText = "Ingeniería con",
14
17
  highlightText = "total transparencia.",
15
18
  strokeColor = "#E0F9B5",
16
19
  className = "",
20
+ headingClass = "text-2xl sm:text-3xl md:text-4xl font-display font-bold text-white tracking-tight leading-snug",
17
21
  prefixI18nKey,
18
22
  highlightI18nKey,
19
23
  id = "wavy-heading-" + Math.random().toString(36).substring(2, 7),
@@ -21,15 +25,15 @@ const {
21
25
  ---
22
26
 
23
27
  <div class={`w-full text-center ${className}`}>
24
- <h2 class="font-display font-bold text-white tracking-tight leading-snug">
28
+ <Tag class={headingClass}>
25
29
  {prefixText && (
26
- <span data-i18n={prefixI18nKey}>{prefixText}</span>
30
+ <span class="inline relative z-10" data-i18n={prefixI18nKey}>{prefixText}</span>
27
31
  )}{" "}
28
- <span class="relative inline-block text-pistachio">
32
+ <span class="services-hero-highlight inline-block relative text-pistachio">
29
33
  <span class="relative z-10" data-i18n={highlightI18nKey}>{highlightText}</span>
30
34
  <svg
31
35
  id={`${id}-svg`}
32
- class="wavy-underline-svg absolute left-0 -bottom-2 w-full h-3 sm:h-4 overflow-visible pointer-events-none"
36
+ class="wavy-underline-svg absolute left-0 -bottom-2 sm:-bottom-3 w-full h-3 sm:h-4 overflow-visible pointer-events-none z-10"
33
37
  style="opacity: 0; visibility: hidden;"
34
38
  viewBox="0 0 300 16"
35
39
  preserveAspectRatio="none"
@@ -47,7 +51,7 @@ const {
47
51
  />
48
52
  </svg>
49
53
  </span>
50
- </h2>
54
+ </Tag>
51
55
  </div>
52
56
 
53
57
  <script>
@@ -58,6 +62,9 @@ const {
58
62
  const svgs = document.querySelectorAll<SVGSVGElement>('.wavy-underline-svg');
59
63
 
60
64
  svgs.forEach((svg) => {
65
+ if ((svg as any).__wavyBound) return;
66
+ (svg as any).__wavyBound = true;
67
+
61
68
  const path = svg.querySelector<SVGPathElement>('.wavy-underline-path');
62
69
  if (!path) return;
63
70
 
@@ -87,4 +94,5 @@ const {
87
94
 
88
95
  initWavyHeadings();
89
96
  document.addEventListener('astro:page-load', initWavyHeadings);
97
+ window.addEventListener('synthex:page-ready', initWavyHeadings);
90
98
  </script>
@@ -2,6 +2,8 @@ export const ButtonOutline: any;
2
2
  export const ButtonPrimary: any;
3
3
  export const ButtonSecondary: any;
4
4
  export const ClientTrustCard: any;
5
+ export const FooterBounceCurve: any;
6
+ export const Preloader: any;
5
7
  export const HelpTooltip: any;
6
8
  export const LanguageSelectorPill: any;
7
9
  export const ProcessStepCard: any;
@@ -2,6 +2,8 @@ export { default as ButtonOutline } from './components/ButtonOutline.astro';
2
2
  export { default as ButtonPrimary } from './components/ButtonPrimary.astro';
3
3
  export { default as ButtonSecondary } from './components/ButtonSecondary.astro';
4
4
  export { default as ClientTrustCard } from './components/ClientTrustCard.astro';
5
+ export { default as FooterBounceCurve } from './components/FooterBounceCurve.astro';
6
+ export { default as Preloader } from './components/Preloader.astro';
5
7
  export { default as HelpTooltip } from './components/HelpTooltip.astro';
6
8
  export { default as LanguageSelectorPill } from './components/LanguageSelectorPill.astro';
7
9
  export { default as ProcessStepCard } from './components/ProcessStepCard.astro';