@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.
@@ -1,79 +1,79 @@
1
- ---
2
- interface Props {
3
- clientName?: string;
4
- location?: string;
5
- href?: string;
6
- logoSrc?: string;
7
- logoDarkSrc?: string;
8
- className?: string;
9
- clientNameI18nKey?: string;
10
- locationI18nKey?: string;
11
- }
12
-
13
- const {
14
- clientName = "SD-M GmbH",
15
- location = "Alemania",
16
- href = "https://sd-kpi.com/",
17
- logoSrc = "/brand/sd-m-logo.png",
18
- logoDarkSrc = "/brand/sd-m-logo-dark.png",
19
- className = "",
20
- clientNameI18nKey = "client_trust.client_name",
21
- locationI18nKey = "client_trust.client_location",
22
- } = Astro.props;
23
- ---
24
-
25
- <a
26
- href={href}
27
- target="_blank"
28
- rel="noopener noreferrer"
29
- class={`group inline-flex items-center gap-4 sm:gap-5 px-6 sm:px-8 py-3.5 sm:py-4 rounded-2xl bg-slateSurface/60 border border-slateBorder hover:border-pistachio/50 hover:bg-slateCard/80 hover:scale-105 active:scale-95 transition-all duration-300 shadow-sm cursor-pointer font-sans ${className}`}
30
- aria-label={`${clientName}${location ? ` - ${location}` : ''}`}
31
- >
32
- {logoDarkSrc ? (
33
- <picture>
34
- <source srcset={logoDarkSrc} />
35
- <img
36
- src={logoSrc}
37
- alt={clientName}
38
- class="h-7 sm:h-8 w-auto object-contain opacity-90 group-hover:opacity-100 transition-opacity"
39
- width="120"
40
- height="44"
41
- loading="lazy"
42
- />
43
- </picture>
44
- ) : logoSrc ? (
45
- <img
46
- src={logoSrc}
47
- alt={clientName}
48
- class="h-7 sm:h-8 w-auto object-contain opacity-90 group-hover:opacity-100 transition-opacity"
49
- width="120"
50
- height="44"
51
- loading="lazy"
52
- />
53
- ) : (
54
- <div class="w-8 h-8 rounded-lg bg-slateSurface border border-slateBorder flex items-center justify-center text-pistachio shrink-0">
55
- <svg class="w-5 h-5 text-pistachio transition-transform duration-300 group-hover:scale-105" viewBox="0 0 32 32" fill="none">
56
- <path d="M7 21L12 16L16 19L24 10" stroke="#E0F9B5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
57
- <circle cx="24" cy="10" r="2" fill="#E0F9B5" />
58
- <path d="M19 10H24V15" stroke="#E0F9B5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
59
- </svg>
60
- </div>
61
- )}
62
-
63
- <div class="text-left border-l border-slateBorder/60 pl-4 sm:pl-5">
64
- <span
65
- class="block font-display font-bold text-base sm:text-lg text-white group-hover:text-pistachio transition-colors leading-tight"
66
- data-i18n={clientNameI18nKey}
67
- >
68
- {clientName}
69
- </span>
70
- {location && (
71
- <span
72
- class="block text-sm font-sans text-muted leading-tight mt-0.5"
73
- data-i18n={locationI18nKey}
74
- >
75
- {location}
76
- </span>
77
- )}
78
- </div>
79
- </a>
1
+ ---
2
+ interface Props {
3
+ clientName?: string;
4
+ location?: string;
5
+ href?: string;
6
+ logoSrc?: string;
7
+ logoDarkSrc?: string;
8
+ className?: string;
9
+ clientNameI18nKey?: string;
10
+ locationI18nKey?: string;
11
+ }
12
+
13
+ const {
14
+ clientName = "SD-M GmbH",
15
+ location = "Alemania",
16
+ href = "https://sd-kpi.com/",
17
+ logoSrc = "/brand/sd-m-logo.png",
18
+ logoDarkSrc = "/brand/sd-m-logo-dark.png",
19
+ className = "",
20
+ clientNameI18nKey = "client_trust.client_name",
21
+ locationI18nKey = "client_trust.client_location",
22
+ } = Astro.props;
23
+ ---
24
+
25
+ <a
26
+ href={href}
27
+ target="_blank"
28
+ rel="noopener noreferrer"
29
+ class={`group inline-flex items-center gap-4 sm:gap-5 px-6 sm:px-8 py-3.5 sm:py-4 rounded-2xl bg-slateSurface/60 border border-slateBorder hover:border-pistachio/50 hover:bg-slateCard/80 hover:scale-105 active:scale-95 transition-all duration-300 shadow-sm cursor-pointer font-sans ${className}`}
30
+ aria-label={`${clientName}${location ? ` - ${location}` : ''}`}
31
+ >
32
+ {logoDarkSrc ? (
33
+ <picture>
34
+ <source srcset={logoDarkSrc} />
35
+ <img
36
+ src={logoSrc}
37
+ alt={clientName}
38
+ class="h-7 sm:h-8 w-auto object-contain opacity-90 group-hover:opacity-100 transition-opacity"
39
+ width="120"
40
+ height="44"
41
+ loading="lazy"
42
+ />
43
+ </picture>
44
+ ) : logoSrc ? (
45
+ <img
46
+ src={logoSrc}
47
+ alt={clientName}
48
+ class="h-7 sm:h-8 w-auto object-contain opacity-90 group-hover:opacity-100 transition-opacity"
49
+ width="120"
50
+ height="44"
51
+ loading="lazy"
52
+ />
53
+ ) : (
54
+ <div class="w-8 h-8 rounded-lg bg-slateSurface border border-slateBorder flex items-center justify-center text-pistachio shrink-0">
55
+ <svg class="w-5 h-5 text-pistachio transition-transform duration-300 group-hover:scale-105" viewBox="0 0 32 32" fill="none">
56
+ <path d="M7 21L12 16L16 19L24 10" stroke="#E0F9B5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
57
+ <circle cx="24" cy="10" r="2" fill="#E0F9B5" />
58
+ <path d="M19 10H24V15" stroke="#E0F9B5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
59
+ </svg>
60
+ </div>
61
+ )}
62
+
63
+ <div class="text-left border-l border-slateBorder/60 pl-4 sm:pl-5">
64
+ <span
65
+ class="block font-display font-bold text-base sm:text-lg text-white group-hover:text-pistachio transition-colors leading-tight"
66
+ data-i18n={clientNameI18nKey}
67
+ >
68
+ {clientName}
69
+ </span>
70
+ {location && (
71
+ <span
72
+ class="block text-sm font-sans text-muted leading-tight mt-0.5"
73
+ data-i18n={locationI18nKey}
74
+ >
75
+ {location}
76
+ </span>
77
+ )}
78
+ </div>
79
+ </a>
@@ -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>