@synthex-engineers/refined-tech 1.0.0 → 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.
- package/dist/astro/components/FooterBounceCurve.astro +302 -0
- package/dist/astro/components/HelpTooltip.astro +239 -119
- package/dist/astro/components/Preloader.astro +240 -0
- package/dist/astro/components/WavyUnderlineHeading.astro +13 -5
- package/dist/astro/index.d.ts +2 -0
- package/dist/astro/index.js +2 -0
- package/dist/index.cjs +12 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/registry/components.json +12 -0
- package/dist/tokens/tailwind-preset.d.ts +1 -1
- package/package.json +8 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
---
|
|
2
|
+
// FooterBounceCurve.astro - Fondo oficial unificado en SVG con curva elástica deformable y textura de grano 35mm
|
|
3
|
+
interface Props {
|
|
4
|
+
/**
|
|
5
|
+
* Altura que ocupará el SVG. Permite cualquier valor CSS válido (ej: '100%', '100vh', '850px').
|
|
6
|
+
* Por defecto '100%' para acoplarse con exactitud al alto del contenedor padre (el footer).
|
|
7
|
+
*/
|
|
8
|
+
height?: string | number;
|
|
9
|
+
/**
|
|
10
|
+
* Clases CSS opcionales para el contenedor del SVG.
|
|
11
|
+
*/
|
|
12
|
+
class?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Selector o ID del elemento trigger para disparar el rebote con ScrollTrigger.
|
|
15
|
+
* Por defecto: 'contacto' (el ID oficial del footer).
|
|
16
|
+
*/
|
|
17
|
+
triggerId?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Amplitud vertical máxima de la deformación elástica en unidades de viewBox (por defecto: 70).
|
|
20
|
+
*/
|
|
21
|
+
bounceAmplitude?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
height = '100%',
|
|
26
|
+
class: className = '',
|
|
27
|
+
triggerId = 'contacto',
|
|
28
|
+
bounceAmplitude = 70,
|
|
29
|
+
} = Astro.props;
|
|
30
|
+
|
|
31
|
+
const containerHeight = typeof height === 'number' ? `${height}px` : height;
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
<div
|
|
35
|
+
id="footer-bounce-wrap"
|
|
36
|
+
class:list={[
|
|
37
|
+
"absolute inset-x-0 top-0 w-full min-w-full pointer-events-none select-none overflow-visible z-10",
|
|
38
|
+
className,
|
|
39
|
+
]}
|
|
40
|
+
style={{ height: containerHeight }}
|
|
41
|
+
aria-hidden="true"
|
|
42
|
+
data-trigger-id={triggerId}
|
|
43
|
+
data-amplitude={bounceAmplitude}
|
|
44
|
+
>
|
|
45
|
+
<svg
|
|
46
|
+
id="footer-bounce-svg"
|
|
47
|
+
class="w-full h-full block overflow-visible"
|
|
48
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
49
|
+
viewBox="0 0 1440 1000"
|
|
50
|
+
preserveAspectRatio="none"
|
|
51
|
+
>
|
|
52
|
+
<defs>
|
|
53
|
+
<!-- Gradiente oficial Synthex: Pistacho (#E0F9B5) a Blanco Nieve (#F4FDE4) -->
|
|
54
|
+
<linearGradient id="footer-bounce-gradient" x1="0" y1="0" x2="0" y2="1">
|
|
55
|
+
<stop offset="0%" stop-color="#E0F9B5" />
|
|
56
|
+
<stop offset="100%" stop-color="#F4FDE4" />
|
|
57
|
+
</linearGradient>
|
|
58
|
+
|
|
59
|
+
<!-- Filtro de Grano de Película 35mm integrado y recortado exactamente a la silueta del trazado -->
|
|
60
|
+
<filter
|
|
61
|
+
id="footer-bounce-grain"
|
|
62
|
+
x="0%"
|
|
63
|
+
y="-20%"
|
|
64
|
+
width="100%"
|
|
65
|
+
height="140%"
|
|
66
|
+
color-interpolation-filters="sRGB"
|
|
67
|
+
>
|
|
68
|
+
<feTurbulence
|
|
69
|
+
type="fractalNoise"
|
|
70
|
+
baseFrequency="0.75"
|
|
71
|
+
numOctaves="4"
|
|
72
|
+
stitchTiles="stitch"
|
|
73
|
+
result="noise"
|
|
74
|
+
/>
|
|
75
|
+
<feColorMatrix
|
|
76
|
+
type="saturate"
|
|
77
|
+
values="0"
|
|
78
|
+
in="noise"
|
|
79
|
+
result="grayNoise"
|
|
80
|
+
/>
|
|
81
|
+
<feComponentTransfer in="grayNoise" result="subtleNoise">
|
|
82
|
+
<feFuncR type="linear" slope="0.5" intercept="0.5" />
|
|
83
|
+
<feFuncG type="linear" slope="0.5" intercept="0.5" />
|
|
84
|
+
<feFuncB type="linear" slope="0.5" intercept="0.5" />
|
|
85
|
+
</feComponentTransfer>
|
|
86
|
+
<feBlend in="SourceGraphic" in2="subtleNoise" mode="multiply" result="blended" />
|
|
87
|
+
<feComposite in="blended" in2="SourceAlpha" operator="in" />
|
|
88
|
+
</filter>
|
|
89
|
+
</defs>
|
|
90
|
+
|
|
91
|
+
<!-- Trazado ÚNICO: Fondo Pistacho Deformable con Granulado 35mm Palpable -->
|
|
92
|
+
<path
|
|
93
|
+
id="bouncy-path"
|
|
94
|
+
class="footer-bounce-path"
|
|
95
|
+
fill="url(#footer-bounce-gradient)"
|
|
96
|
+
filter="url(#footer-bounce-grain)"
|
|
97
|
+
d="M 0,0 Q 720,0 1440,0 L 1440,1000 L 0,1000 Z"
|
|
98
|
+
/>
|
|
99
|
+
</svg>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<script>
|
|
103
|
+
import gsap from "gsap";
|
|
104
|
+
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
105
|
+
|
|
106
|
+
gsap.registerPlugin(ScrollTrigger);
|
|
107
|
+
|
|
108
|
+
let activeScrollTrigger: ScrollTrigger | null = null;
|
|
109
|
+
let activeCleanup: (() => void) | null = null;
|
|
110
|
+
let refreshDebounceTimer: any = null;
|
|
111
|
+
|
|
112
|
+
function scheduleRefresh() {
|
|
113
|
+
clearTimeout(refreshDebounceTimer);
|
|
114
|
+
refreshDebounceTimer = setTimeout(() => {
|
|
115
|
+
ScrollTrigger.refresh();
|
|
116
|
+
const lenis = (window as any).__lenis;
|
|
117
|
+
if (lenis && typeof lenis.resize === "function") {
|
|
118
|
+
lenis.resize();
|
|
119
|
+
}
|
|
120
|
+
}, 60);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function initFooterBounce() {
|
|
124
|
+
const wrap = document.getElementById("footer-bounce-wrap");
|
|
125
|
+
const bouncyPath = document.getElementById("bouncy-path");
|
|
126
|
+
if (!wrap || !bouncyPath) return;
|
|
127
|
+
|
|
128
|
+
// Destruir instancias previas y listeners para evitar fugas de memoria
|
|
129
|
+
if (activeCleanup) {
|
|
130
|
+
activeCleanup();
|
|
131
|
+
activeCleanup = null;
|
|
132
|
+
}
|
|
133
|
+
if (activeScrollTrigger) {
|
|
134
|
+
activeScrollTrigger.kill();
|
|
135
|
+
activeScrollTrigger = null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const rest = "M 0,0 Q 720,0 1440,0 L 1440,1000 L 0,1000 Z";
|
|
139
|
+
const isMobile = window.matchMedia("(max-width: 768px)").matches;
|
|
140
|
+
const baseStretch = parseInt(wrap.getAttribute("data-amplitude") || "70", 10) || 70;
|
|
141
|
+
const STRETCH_PIXELS = isMobile ? Math.min(baseStretch, 45) : baseStretch;
|
|
142
|
+
|
|
143
|
+
const prefersReducedMotion = window.matchMedia(
|
|
144
|
+
"(prefers-reduced-motion: reduce)",
|
|
145
|
+
).matches;
|
|
146
|
+
|
|
147
|
+
if (!prefersReducedMotion) {
|
|
148
|
+
let hasSnapped = false;
|
|
149
|
+
let activeTween: gsap.core.Tween | null = null;
|
|
150
|
+
|
|
151
|
+
const initialRect = wrap.getBoundingClientRect();
|
|
152
|
+
if (initialRect.top < window.innerHeight - STRETCH_PIXELS) {
|
|
153
|
+
hasSnapped = true;
|
|
154
|
+
bouncyPath.setAttribute("d", rest);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const updateMembrane = () => {
|
|
158
|
+
const rect = wrap.getBoundingClientRect();
|
|
159
|
+
const S = window.innerHeight - rect.top;
|
|
160
|
+
|
|
161
|
+
if (S <= 0) {
|
|
162
|
+
if (hasSnapped || activeTween) {
|
|
163
|
+
hasSnapped = false;
|
|
164
|
+
if (activeTween) {
|
|
165
|
+
activeTween.kill();
|
|
166
|
+
activeTween = null;
|
|
167
|
+
}
|
|
168
|
+
bouncyPath.setAttribute("d", rest);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (hasSnapped) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (S >= STRETCH_PIXELS) {
|
|
178
|
+
hasSnapped = true;
|
|
179
|
+
if (activeTween) {
|
|
180
|
+
activeTween.kill();
|
|
181
|
+
activeTween = null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const H = rect.height || 1000;
|
|
185
|
+
const snapC = (2 * STRETCH_PIXELS * 1000) / H;
|
|
186
|
+
const snapAnim = { c: snapC };
|
|
187
|
+
|
|
188
|
+
activeTween = gsap.to(snapAnim, {
|
|
189
|
+
c: 0,
|
|
190
|
+
duration: 1.8,
|
|
191
|
+
ease: "elastic.out(1.2, 0.35)",
|
|
192
|
+
overwrite: "auto",
|
|
193
|
+
onUpdate: () => {
|
|
194
|
+
bouncyPath.setAttribute(
|
|
195
|
+
"d",
|
|
196
|
+
`M 0,0 Q 720,${snapAnim.c} 1440,0 L 1440,1000 L 0,1000 Z`,
|
|
197
|
+
);
|
|
198
|
+
},
|
|
199
|
+
onComplete: () => {
|
|
200
|
+
bouncyPath.setAttribute("d", rest);
|
|
201
|
+
activeTween = null;
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const H = rect.height || 1000;
|
|
208
|
+
const currentC = (2 * S * 1000) / H;
|
|
209
|
+
bouncyPath.setAttribute(
|
|
210
|
+
"d",
|
|
211
|
+
`M 0,0 Q 720,${currentC} 1440,0 L 1440,1000 L 0,1000 Z`,
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
activeScrollTrigger = ScrollTrigger.create({
|
|
216
|
+
trigger: wrap,
|
|
217
|
+
start: "top bottom",
|
|
218
|
+
end: "bottom top",
|
|
219
|
+
onUpdate: updateMembrane,
|
|
220
|
+
onLeaveBack: () => {
|
|
221
|
+
hasSnapped = false;
|
|
222
|
+
if (activeTween) {
|
|
223
|
+
activeTween.kill();
|
|
224
|
+
activeTween = null;
|
|
225
|
+
}
|
|
226
|
+
bouncyPath.setAttribute("d", rest);
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
window.addEventListener("scroll", updateMembrane, { passive: true });
|
|
231
|
+
const lenis = (window as any).__lenis;
|
|
232
|
+
if (lenis && typeof lenis.on === "function") {
|
|
233
|
+
lenis.on("scroll", updateMembrane);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
activeCleanup = () => {
|
|
237
|
+
window.removeEventListener("scroll", updateMembrane);
|
|
238
|
+
const l = (window as any).__lenis;
|
|
239
|
+
if (l && typeof l.off === "function") {
|
|
240
|
+
l.off("scroll", updateMembrane);
|
|
241
|
+
}
|
|
242
|
+
if (activeTween) {
|
|
243
|
+
activeTween.kill();
|
|
244
|
+
activeTween = null;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
} else {
|
|
248
|
+
bouncyPath.setAttribute("d", rest);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
scheduleRefresh();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function startFooterBounce() {
|
|
255
|
+
initFooterBounce();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (typeof window !== "undefined" && "ResizeObserver" in window) {
|
|
259
|
+
const bodyResizeObserver = new ResizeObserver(() => {
|
|
260
|
+
scheduleRefresh();
|
|
261
|
+
});
|
|
262
|
+
if (document.body) {
|
|
263
|
+
bodyResizeObserver.observe(document.body);
|
|
264
|
+
} else {
|
|
265
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
266
|
+
bodyResizeObserver.observe(document.body);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
window.addEventListener("synthex:lang-changed", () => {
|
|
272
|
+
scheduleRefresh();
|
|
273
|
+
setTimeout(scheduleRefresh, 120);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
window.addEventListener("resize", scheduleRefresh);
|
|
277
|
+
|
|
278
|
+
document.addEventListener(
|
|
279
|
+
"toggle",
|
|
280
|
+
(e) => {
|
|
281
|
+
if ((e.target as HTMLElement)?.tagName === "DETAILS") {
|
|
282
|
+
scheduleRefresh();
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
true,
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
if (
|
|
289
|
+
(window as any).__synthexPageReady ||
|
|
290
|
+
sessionStorage.getItem("synthex_preloaded_session")
|
|
291
|
+
) {
|
|
292
|
+
startFooterBounce();
|
|
293
|
+
} else {
|
|
294
|
+
window.addEventListener("synthex:page-ready", startFooterBounce, {
|
|
295
|
+
once: true,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
document.addEventListener("astro:page-load", () => {
|
|
300
|
+
startFooterBounce();
|
|
301
|
+
});
|
|
302
|
+
</script>
|
|
@@ -1,119 +1,239 @@
|
|
|
1
|
-
---
|
|
2
|
-
interface Props {
|
|
3
|
-
title: string;
|
|
4
|
-
description: string;
|
|
5
|
-
titleKey?: string;
|
|
6
|
-
descKey?: string;
|
|
7
|
-
align?: 'left' | 'center' | 'right';
|
|
8
|
-
className?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const {
|
|
12
|
-
title,
|
|
13
|
-
description,
|
|
14
|
-
titleKey,
|
|
15
|
-
descKey,
|
|
16
|
-
align = 'center',
|
|
17
|
-
className = '',
|
|
18
|
-
} = Astro.props;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
center
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<span
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
trigger.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
titleKey?: string;
|
|
6
|
+
descKey?: string;
|
|
7
|
+
align?: 'left' | 'center' | 'right';
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
title,
|
|
13
|
+
description,
|
|
14
|
+
titleKey,
|
|
15
|
+
descKey,
|
|
16
|
+
align = 'center',
|
|
17
|
+
className = '',
|
|
18
|
+
} = Astro.props;
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<span
|
|
22
|
+
class={`relative inline-flex items-center group/bubble ml-1.5 align-middle ${className}`}
|
|
23
|
+
data-align={align}
|
|
24
|
+
>
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
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"
|
|
28
|
+
aria-label={`${title}: ${description}`}
|
|
29
|
+
aria-expanded="false"
|
|
30
|
+
>
|
|
31
|
+
?
|
|
32
|
+
</button>
|
|
33
|
+
|
|
34
|
+
<!-- Popover Flotante Accesible (Hover, Focus & Click/Tap): sale justo arriba a 8px sin tapar el botón ? -->
|
|
35
|
+
<span
|
|
36
|
+
role="tooltip"
|
|
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"
|
|
38
|
+
>
|
|
39
|
+
<span class="block text-xs font-display font-bold text-pistachio mb-1" data-i18n={titleKey}>
|
|
40
|
+
{title}
|
|
41
|
+
</span>
|
|
42
|
+
<span class="block text-xs font-body text-white/90 leading-relaxed" data-i18n={descKey}>
|
|
43
|
+
{description}
|
|
44
|
+
</span>
|
|
45
|
+
|
|
46
|
+
<!-- Flecha indicadora apuntando al botón ? -->
|
|
47
|
+
<span
|
|
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"
|
|
49
|
+
></span>
|
|
50
|
+
</span>
|
|
51
|
+
</span>
|
|
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
|
+
|
|
95
|
+
<script>
|
|
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%)';
|
|
125
|
+
}
|
|
126
|
+
|
|
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();
|
|
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 });
|
|
231
|
+
}
|
|
232
|
+
|
|
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);
|
|
239
|
+
</script>
|