@widelab-nc/widelab 1.1.37 → 1.1.39

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@widelab-nc/widelab",
3
- "version": "1.1.37",
3
+ "version": "1.1.39",
4
4
  "description": "Widelab starter template based on Finsweet template + add-ons.",
5
5
  "homepage": "https://widelab.co",
6
6
  "license": "ISC",
package/src/index.js CHANGED
@@ -35,6 +35,68 @@ function scrollToHashOnLoad(offsetY = 0) {
35
35
 
36
36
  // Functions definitions
37
37
 
38
+ // === Skills: ScrollTrigger + GSAP init (definition only) ===
39
+ // You will call this yourself later, e.g. initSkillsTriggers({ section: '#skills' })
40
+ // Client logos reveal inside a section (definition only; you call it later)
41
+ function initClientLogosAnimation(rootEl, userOpts = {}) {
42
+ // ---- guards ----
43
+ if (!window.gsap || !window.ScrollTrigger) {
44
+ console.warn('[client-logos] GSAP/ScrollTrigger missing');
45
+ return { ok: false, reason: 'gsap-or-plugin-missing' };
46
+ }
47
+
48
+ const gsap = window.gsap;
49
+ const ScrollTrigger = window.ScrollTrigger;
50
+ try { gsap.registerPlugin(ScrollTrigger); } catch (_) {}
51
+
52
+ // ---- config ----
53
+ const opts = {
54
+ // adjust these to your real structure if needed
55
+ listSelector: '.clients_wrapper-bento',
56
+ itemSelector: '.clients_wrapper-bento .bento_item, .client-logo, .logo',
57
+ start: 'top 80%',
58
+ end: 'bottom top',
59
+ markers: false,
60
+ ...userOpts
61
+ };
62
+
63
+ // ---- scope ----
64
+ const root = rootEl && rootEl.nodeType === 1 ? rootEl : document;
65
+ const list = root.querySelector(opts.listSelector);
66
+ const items = list ? Array.from(list.querySelectorAll(opts.itemSelector)) : [];
67
+
68
+ // nothing to animate? bail quietly (prevents "GSAP target … not found")
69
+ if (!list || !items.length) {
70
+ return { ok: false, reason: 'targets-not-found', listFound: !!list, count: items.length };
71
+ }
72
+
73
+ // kill old triggers bound to this list (idempotent reruns)
74
+ ScrollTrigger.getAll().forEach(st => {
75
+ if (st && st.trigger === list) st.kill(false);
76
+ });
77
+
78
+ // ---- animation ----
79
+ gsap.from(items, {
80
+ y: 16,
81
+ opacity: 0,
82
+ duration: 0.6,
83
+ ease: 'power2.out',
84
+ stagger: 0.06,
85
+ scrollTrigger: {
86
+ trigger: list,
87
+ start: opts.start,
88
+ end: opts.end,
89
+ toggleActions: 'play none none reverse',
90
+ markers: opts.markers,
91
+ invalidateOnRefresh: true
92
+ }
93
+ });
94
+
95
+ // refresh after layout settles
96
+ requestAnimationFrame(() => { try { ScrollTrigger.refresh(); } catch(_) {} });
97
+
98
+ return { ok: true, count: items.length };
99
+ }
38
100
  // Contact form open animation
39
101
  let contactFormOpen;
40
102
  function contactFormOpenAnimation() {
@@ -142,50 +204,54 @@ function contactFormOpenAnimation() {
142
204
  }
143
205
  }
144
206
 
145
- // Page load animation
146
- function pageLoadAnimation() {
147
- const loaderAnimationIn = gsap.timeline({});
148
- loaderAnimationIn
149
- .add(function() {
150
- history.scrollRestoration = 'manual';
151
- lenis.scrollTo(0);
152
- })
153
- loaderAnimationIn.to(
154
- '.loader_ract.is-left',
155
- {
156
- delay: 0.2,
157
- duration: 0.7,
158
- x: '100%',
159
- ease: 'loader2',
160
- },
161
- '>'
162
- );
163
- loaderAnimationIn.to(
164
- '.loader_ract.is-right',
165
- {
166
- duration: 0.7,
167
- x: '-100%',
168
- ease: 'loader2',
169
- },
170
- '<'
171
- );
172
- loaderAnimationIn.add(function() {
173
- lenis.start();
174
- }, '>');
175
- loaderAnimationIn.to('.loader_wrapper', { display: 'none', duration: 0.1 }, '>');
176
- // Update on screen resize
177
- window.addEventListener('resize', function () {
178
- setTimeout(function () {
179
- gsap.set('.loader_wrapper', { display: 'none' });
180
- }, 50);
181
- });
182
- }
207
+ // Page load animation (no-op below 992px)
208
+ function pageLoadAnimation() {
209
+ if (window.innerWidth < 992) return; // guard for mobile/tablet
210
+
211
+ const loaderAnimationIn = gsap.timeline({});
212
+ loaderAnimationIn.add(function () {
213
+ history.scrollRestoration = 'manual';
214
+ lenis.scrollTo(0);
215
+ });
216
+ loaderAnimationIn.to(
217
+ '.loader_ract.is-left',
218
+ {
219
+ delay: 0.2,
220
+ duration: 0.7,
221
+ x: '100%',
222
+ ease: 'loader2',
223
+ },
224
+ '>'
225
+ );
226
+ loaderAnimationIn.to(
227
+ '.loader_ract.is-right',
228
+ {
229
+ duration: 0.7,
230
+ x: '-100%',
231
+ ease: 'loader2',
232
+ },
233
+ '<'
234
+ );
235
+ loaderAnimationIn.add(function () {
236
+ lenis.start();
237
+ }, '>');
238
+ loaderAnimationIn.to('.loader_wrapper', { display: 'none', duration: 0.1 }, '>');
239
+ // Keep your resize fix
240
+ window.addEventListener('resize', function () {
241
+ setTimeout(function () {
242
+ gsap.set('.loader_wrapper', { display: 'none' });
243
+ }, 50);
244
+ });
245
+ }
246
+
247
+ // Page transition animation (no-op below 992px)
248
+ function pageTransitionAnimation() {
249
+ if (window.innerWidth < 992) return; // guard for mobile/tablet
183
250
 
184
- // Page transition animation
185
- function pageTransitionAnimation() {
186
251
  const transitionLinks = document.querySelectorAll(
187
- 'a:not([/**/href^="#"]):not([href^="javascript:"]):not([href=""]):not([href^="mailto:"]):not([href^="tel:"])'
252
+ 'a:not([href^="#"]):not([href^="javascript:"]):not([href=""]):not([href^="mailto:"]):not([href^="tel:"])'
188
253
  );
254
+
189
255
  transitionLinks.forEach((link) => {
190
256
  if (link.getAttribute('target') !== '_blank') {
191
257
  link.addEventListener('click', (e) => {
@@ -201,15 +267,12 @@ function contactFormOpenAnimation() {
201
267
  duration: 0.4,
202
268
  width: '0%',
203
269
  ease: 'loader2',
204
- }),
205
- '>';
206
- // play the timeline
270
+ });
207
271
  loaderAnimationOut.play();
208
272
  });
209
273
  }
210
274
  });
211
- }
212
-
275
+ }
213
276
  // Header on scroll animation
214
277
  const infoBar = document.querySelector('.info-bar');
215
278
  const showHideHeader = gsap.timeline({ paused: true,})
@@ -239,6 +302,114 @@ if (infoBar) {
239
302
  });
240
303
  }
241
304
 
305
+ // === SKILLS TRIGGER HARD-FIX (drop-in) ===
306
+ (function () {
307
+ const DEBUG = false; // flip to true to see console + markers
308
+
309
+ if (!window.gsap || !window.ScrollTrigger) {
310
+ console.error('[skills-fix] GSAP/ScrollTrigger missing');
311
+ return;
312
+ }
313
+ gsap.registerPlugin(ScrollTrigger);
314
+
315
+ // 1) Pick the VISIBLE "skills" section (ignore duplicates/hidden)
316
+ const candidates = Array.from(document.querySelectorAll('#skills, .section.is-skills'));
317
+ const el = candidates.find(n => {
318
+ const r = n.getBoundingClientRect();
319
+ const style = getComputedStyle(n);
320
+ return r.width > 0 && r.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
321
+ });
322
+ if (!el) {
323
+ console.error('[skills-fix] skills section not found or not visible');
324
+ return;
325
+ }
326
+
327
+ // 2) Diagnose ancestors that break ScrollTrigger (transform / overflow != visible)
328
+ const badAncestors = [];
329
+ let p = el.parentElement;
330
+ while (p && p !== document.body) {
331
+ const cs = getComputedStyle(p);
332
+ const badTransform = cs.transform && cs.transform !== 'none';
333
+ const badOverflow = (cs.overflow !== 'visible' || cs.overflowY !== 'visible' || cs.overflowX !== 'visible');
334
+ if (badTransform || badOverflow) badAncestors.push({ node: p, badTransform, badOverflow });
335
+ p = p.parentElement;
336
+ }
337
+
338
+ // 3) Neutralize only the dangerous bits, minimally
339
+ badAncestors.forEach(({ node, badTransform }) => {
340
+ // overflow hidden na wrapperach zostawiamy (często layout), ScrollTrigger sobie radzi;
341
+ // PRZEWAŻNIE zabija go transform na rodzicu — zdejmujemy go klasą.
342
+ if (badTransform) node.classList.add('st-fix-transform');
343
+ });
344
+
345
+ // 4) Inject minimal CSS to neutralize transforms (non-destructive)
346
+ const styleTag = document.createElement('style');
347
+ styleTag.textContent = `
348
+ .st-fix-transform { transform: none !important; }
349
+ `;
350
+ document.head.appendChild(styleTag);
351
+
352
+ // 5) Kill duplicated triggers on the same element
353
+ ScrollTrigger.getAll().forEach(st => { if (st.trigger === el) st.kill(false); });
354
+
355
+ // 6) The exact action you expect
356
+ const run = (target) => {
357
+ try { window.sectionBgChange && window.sectionBgChange(target); } catch (e) {}
358
+ try { window.badgeColorChange && window.badgeColorChange(target); } catch (e) {}
359
+ if (DEBUG) console.log('[skills-fix] run()', target);
360
+ };
361
+
362
+ // 7) Fire immediately if already on screen (new layout often loads in-view)
363
+ const r = el.getBoundingClientRect();
364
+ if (r.top < innerHeight && r.bottom > 0) run(el);
365
+
366
+ // 8) Create robust trigger (same feel as old), with optional markers
367
+ ScrollTrigger.create({
368
+ trigger: el,
369
+ start: 'top 80%',
370
+ end: 'bottom top',
371
+ onEnter: self => run(self.trigger),
372
+ onEnterBack: self => run(self.trigger),
373
+ markers: !!DEBUG,
374
+ fastScrollEnd: true,
375
+ invalidateOnRefresh: true
376
+ });
377
+
378
+ // 9) Refresh after things that commonly shift layout on new home
379
+ const doRefresh = () => {
380
+ if (DEBUG) console.log('[skills-fix] refresh()');
381
+ ScrollTrigger.refresh();
382
+ };
383
+
384
+ // after images
385
+ if (document.readyState === 'complete') requestAnimationFrame(doRefresh);
386
+ else window.addEventListener('load', () => requestAnimationFrame(doRefresh));
387
+
388
+ // after Webflow IX (if present)
389
+ try {
390
+ if (window.Webflow && Webflow.require) {
391
+ const ix2 = Webflow.require('ix2');
392
+ if (ix2 && ix2.ready) setTimeout(doRefresh, 300); // allow IX to apply styles
393
+ }
394
+ } catch (e) {}
395
+
396
+ // after Alpine renders (x-init etc.)
397
+ document.addEventListener('alpine:init', () => setTimeout(doRefresh, 100));
398
+ document.addEventListener('DOMContentLoaded', () => setTimeout(doRefresh, 150));
399
+
400
+ // As a last resort: one more refresh when user starts scrolling
401
+ let firstScroll = false;
402
+ const onFirstScroll = () => { if (!firstScroll) { firstScroll = true; doRefresh(); } };
403
+ window.addEventListener('scroll', onFirstScroll, { passive: true, once: true });
404
+
405
+ if (DEBUG) {
406
+ console.group('[skills-fix] diagnostics');
407
+ console.log('picked element:', el);
408
+ console.log('bad ancestors:', badAncestors);
409
+ console.groupEnd();
410
+ }
411
+ })();
412
+
242
413
  // Header scroll trigger
243
414
  window.standardHeaderScrollAnimation = function() {
244
415
  ScrollTrigger.create({
@@ -360,76 +531,89 @@ window.customCursorTextUpdate = function(el) {
360
531
  );
361
532
  }
362
533
 
363
- // Case study mask animation + pill fade-in (last step)
534
+
535
+ // Case study mask animation + pill fade-in (full, iOS-safe, L->R reveal)
364
536
  window.caseStudyInteraction = function (e) {
365
- // Video mask + pill timeline
366
537
  const mask = e.querySelector('.portfolio_item_image-mask');
367
538
  const pill = e.querySelector('.cs_pill');
368
-
369
- // Build a single timeline so the pill fades in AFTER the mask animation
370
- const tl = gsap.timeline({
371
- paused: true,
372
- defaults: { ease: 'loader2' }
373
- });
374
-
375
- // Ensure pill starts hidden
376
- if (pill) gsap.set(pill, { autoAlpha: 0 }); // autoAlpha controls visibility + opacity
377
-
378
- // Mask reveals first...
379
- tl.from(mask, {
380
- width: '100%',
381
- duration: 0.35
382
- });
383
-
384
- // ...then pill fades in as the last step
385
- if (pill) {
386
- tl.to(pill, {
387
- autoAlpha: 1,
388
- duration: 0.25
539
+ const triggerEl = e.querySelector('.portfolio_item_image-wrapper') || e;
540
+
541
+ // iOS detection for safe fallbacks
542
+ const isIOS =
543
+ /iPad|iPhone|iPod/.test(navigator.userAgent) ||
544
+ (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
545
+
546
+ // --- Initial states ---
547
+ if (pill) gsap.set(pill, { autoAlpha: 0 });
548
+ if (mask) {
549
+ // We fully cover first, then shrink to reveal from LEFT to RIGHT.
550
+ // To reveal L->R we set origin to RIGHT and scaleX: 1 -> 0
551
+ gsap.set(mask, {
552
+ clearProps: 'width', // ignore any inline width
553
+ scaleX: 1,
554
+ transformOrigin: 'right center',
555
+ willChange: 'transform'
389
556
  });
390
557
  }
391
558
 
392
- // Trigger on scroll
559
+ // --- Timeline: mask reveal then pill ---
560
+ const tl = gsap.timeline({ paused: true, defaults: { ease: 'loader2' } });
561
+ if (mask) tl.to(mask, { scaleX: 0, duration: 0.35 }); // L->R reveal
562
+ if (pill) tl.to(pill, { autoAlpha: 1, duration: 0.25 }, '>-0.05');
563
+
564
+ // --- Scroll trigger (use wrapper for reliable bounds on iOS) ---
393
565
  ScrollTrigger.create({
394
566
  animation: tl,
395
- trigger: mask,
567
+ trigger: triggerEl,
396
568
  start: 'top 80%',
397
- onEnter: () => tl.play()
569
+ once: true,
398
570
  });
399
571
 
400
- // Video on hover actions
572
+ // --- Video handling ---
401
573
  const videoElements = e.getElementsByTagName('video');
574
+
402
575
  if (window.innerWidth > 992) {
403
- const videoElement = videoElements.length > 0 ? videoElements[0] : null;
404
- if (!videoElement) return;
576
+ const video = videoElements.length ? videoElements[0] : null;
577
+ if (!video) return;
405
578
 
406
- videoElement.addEventListener('loadeddata', () => {
407
- videoElement.pause();
408
- });
579
+ // Safari compositing hints
580
+ video.style.transform = 'translateZ(0)';
581
+
582
+ const hoverIN = () => video.play();
583
+ const hoverOUT = () => { video.pause(); /* video.currentTime = 0; */ };
409
584
 
585
+ video.addEventListener('loadeddata', () => { video.pause(); });
410
586
  e.addEventListener('mouseenter', hoverIN, false);
411
587
  e.addEventListener('mouseleave', hoverOUT, false);
412
588
 
413
- function hoverIN() {
414
- videoElement.play();
415
- }
416
-
417
- function hoverOUT() {
418
- videoElement.pause();
419
- // videoElement.currentTime = 0;
420
- }
421
589
  } else {
422
- // Mobile: keep poster only
423
- Array.from(videoElements).forEach((videoElem) => {
424
- const poster = videoElem.getAttribute('poster');
425
- videoElem.innerHTML = '';
426
- videoElem.setAttribute('poster', poster);
427
- videoElem.load();
428
- videoElem.pause();
590
+ // Mobile: poster-only, but keep sources on Android; replace with <img> on iOS
591
+ Array.from(videoElements).forEach((v) => {
592
+ const poster = v.getAttribute('poster');
593
+
594
+ if (isIOS && poster) {
595
+ const img = new Image();
596
+ img.src = poster;
597
+ img.alt = '';
598
+ img.className = v.className;
599
+ // Keep sizing from CSS on wrapper; this prevents 0-height glitches
600
+ v.replaceWith(img);
601
+ } else {
602
+ // Android: keep sources to preserve intrinsic size and poster rendering
603
+ v.removeAttribute('autoplay');
604
+ v.pause();
605
+ v.preload = 'none';
606
+ // v.load(); // enable only if you notice stale frames
607
+ }
429
608
  });
430
609
  }
610
+
611
+ // After layout settles (posters/images), refresh triggers
612
+ setTimeout(() => ScrollTrigger.refresh(), 120);
431
613
  };
432
614
 
615
+
616
+
433
617
  // Reviews slider
434
618
  window.reviewsSlider = function(e) {
435
619
  // Put content of static slide into list
@@ -2023,6 +2207,7 @@ window.closeInfoBar = function() {
2023
2207
  lenis.destroy();
2024
2208
  }
2025
2209
  customGsapEasing();
2210
+
2026
2211
  reloadOnBack();
2027
2212
  revealFooter();
2028
2213
  pageLoadAnimation();
@@ -2031,4 +2216,5 @@ window.closeInfoBar = function() {
2031
2216
  contactFormOpenAnimation();
2032
2217
  disableScrollMenu();
2033
2218
  scrollToHashOnLoad();
2219
+ initClientLogosAnimation();
2034
2220
  });