@widelab-nc/widelab 1.1.37 → 1.1.38

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.38",
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() {
@@ -239,6 +301,114 @@ if (infoBar) {
239
301
  });
240
302
  }
241
303
 
304
+ // === SKILLS TRIGGER HARD-FIX (drop-in) ===
305
+ (function () {
306
+ const DEBUG = false; // flip to true to see console + markers
307
+
308
+ if (!window.gsap || !window.ScrollTrigger) {
309
+ console.error('[skills-fix] GSAP/ScrollTrigger missing');
310
+ return;
311
+ }
312
+ gsap.registerPlugin(ScrollTrigger);
313
+
314
+ // 1) Pick the VISIBLE "skills" section (ignore duplicates/hidden)
315
+ const candidates = Array.from(document.querySelectorAll('#skills, .section.is-skills'));
316
+ const el = candidates.find(n => {
317
+ const r = n.getBoundingClientRect();
318
+ const style = getComputedStyle(n);
319
+ return r.width > 0 && r.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
320
+ });
321
+ if (!el) {
322
+ console.error('[skills-fix] skills section not found or not visible');
323
+ return;
324
+ }
325
+
326
+ // 2) Diagnose ancestors that break ScrollTrigger (transform / overflow != visible)
327
+ const badAncestors = [];
328
+ let p = el.parentElement;
329
+ while (p && p !== document.body) {
330
+ const cs = getComputedStyle(p);
331
+ const badTransform = cs.transform && cs.transform !== 'none';
332
+ const badOverflow = (cs.overflow !== 'visible' || cs.overflowY !== 'visible' || cs.overflowX !== 'visible');
333
+ if (badTransform || badOverflow) badAncestors.push({ node: p, badTransform, badOverflow });
334
+ p = p.parentElement;
335
+ }
336
+
337
+ // 3) Neutralize only the dangerous bits, minimally
338
+ badAncestors.forEach(({ node, badTransform }) => {
339
+ // overflow hidden na wrapperach zostawiamy (często layout), ScrollTrigger sobie radzi;
340
+ // PRZEWAŻNIE zabija go transform na rodzicu — zdejmujemy go klasą.
341
+ if (badTransform) node.classList.add('st-fix-transform');
342
+ });
343
+
344
+ // 4) Inject minimal CSS to neutralize transforms (non-destructive)
345
+ const styleTag = document.createElement('style');
346
+ styleTag.textContent = `
347
+ .st-fix-transform { transform: none !important; }
348
+ `;
349
+ document.head.appendChild(styleTag);
350
+
351
+ // 5) Kill duplicated triggers on the same element
352
+ ScrollTrigger.getAll().forEach(st => { if (st.trigger === el) st.kill(false); });
353
+
354
+ // 6) The exact action you expect
355
+ const run = (target) => {
356
+ try { window.sectionBgChange && window.sectionBgChange(target); } catch (e) {}
357
+ try { window.badgeColorChange && window.badgeColorChange(target); } catch (e) {}
358
+ if (DEBUG) console.log('[skills-fix] run()', target);
359
+ };
360
+
361
+ // 7) Fire immediately if already on screen (new layout often loads in-view)
362
+ const r = el.getBoundingClientRect();
363
+ if (r.top < innerHeight && r.bottom > 0) run(el);
364
+
365
+ // 8) Create robust trigger (same feel as old), with optional markers
366
+ ScrollTrigger.create({
367
+ trigger: el,
368
+ start: 'top 80%',
369
+ end: 'bottom top',
370
+ onEnter: self => run(self.trigger),
371
+ onEnterBack: self => run(self.trigger),
372
+ markers: !!DEBUG,
373
+ fastScrollEnd: true,
374
+ invalidateOnRefresh: true
375
+ });
376
+
377
+ // 9) Refresh after things that commonly shift layout on new home
378
+ const doRefresh = () => {
379
+ if (DEBUG) console.log('[skills-fix] refresh()');
380
+ ScrollTrigger.refresh();
381
+ };
382
+
383
+ // after images
384
+ if (document.readyState === 'complete') requestAnimationFrame(doRefresh);
385
+ else window.addEventListener('load', () => requestAnimationFrame(doRefresh));
386
+
387
+ // after Webflow IX (if present)
388
+ try {
389
+ if (window.Webflow && Webflow.require) {
390
+ const ix2 = Webflow.require('ix2');
391
+ if (ix2 && ix2.ready) setTimeout(doRefresh, 300); // allow IX to apply styles
392
+ }
393
+ } catch (e) {}
394
+
395
+ // after Alpine renders (x-init etc.)
396
+ document.addEventListener('alpine:init', () => setTimeout(doRefresh, 100));
397
+ document.addEventListener('DOMContentLoaded', () => setTimeout(doRefresh, 150));
398
+
399
+ // As a last resort: one more refresh when user starts scrolling
400
+ let firstScroll = false;
401
+ const onFirstScroll = () => { if (!firstScroll) { firstScroll = true; doRefresh(); } };
402
+ window.addEventListener('scroll', onFirstScroll, { passive: true, once: true });
403
+
404
+ if (DEBUG) {
405
+ console.group('[skills-fix] diagnostics');
406
+ console.log('picked element:', el);
407
+ console.log('bad ancestors:', badAncestors);
408
+ console.groupEnd();
409
+ }
410
+ })();
411
+
242
412
  // Header scroll trigger
243
413
  window.standardHeaderScrollAnimation = function() {
244
414
  ScrollTrigger.create({
@@ -360,49 +530,65 @@ window.customCursorTextUpdate = function(el) {
360
530
  );
361
531
  }
362
532
 
533
+
534
+
363
535
  // Case study mask animation + pill fade-in (last step)
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');
539
+ const triggerEl = e.querySelector('.portfolio_item_image-wrapper') || e; // safer trigger
540
+
541
+ // --- iOS detection (reliable enough for layout fallbacks) ---
542
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
543
+ (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
544
+
545
+ // Ensure initial states
546
+ if (pill) gsap.set(pill, { autoAlpha: 0 }); // hide pill
547
+ if (mask) {
548
+ // Use transform-based reveal, much more reliable on iOS
549
+ gsap.set(mask, {
550
+ scaleX: 1,
551
+ transformOrigin: 'left center',
552
+ willChange: 'transform'
553
+ });
554
+ }
368
555
 
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
556
+ // Timeline: mask reveal -> pill fade-in
557
+ const tl = gsap.timeline({ paused: true, defaults: { ease: 'loader2' } });
558
+ if (mask) {
559
+ // Reveal by scaling mask from full (1) to zero (0)
560
+ tl.to(mask, { scaleX: 0, duration: 0.35 });
561
+ }
385
562
  if (pill) {
386
- tl.to(pill, {
387
- autoAlpha: 1,
388
- duration: 0.25
389
- });
563
+ tl.to(pill, { autoAlpha: 1, duration: 0.25 }, '>-0.05');
390
564
  }
391
565
 
392
- // Trigger on scroll
566
+ // Scroll trigger (use wrapper, not absolute mask)
393
567
  ScrollTrigger.create({
394
568
  animation: tl,
395
- trigger: mask,
569
+ trigger: triggerEl,
396
570
  start: 'top 80%',
397
- onEnter: () => tl.play()
571
+ once: true
398
572
  });
399
573
 
400
- // Video on hover actions
574
+ // --- Video handling ---
401
575
  const videoElements = e.getElementsByTagName('video');
576
+
402
577
  if (window.innerWidth > 992) {
403
578
  const videoElement = videoElements.length > 0 ? videoElements[0] : null;
404
579
  if (!videoElement) return;
405
580
 
581
+ // Force Safari compositing on the video
582
+ videoElement.style.transform = 'translateZ(0)';
583
+
584
+ // Desktop: hover play/pause
585
+ const hoverIN = () => videoElement.play();
586
+ const hoverOUT = () => {
587
+ videoElement.pause();
588
+ // videoElement.currentTime = 0; // optional reset
589
+ };
590
+
591
+ // Pause once metadata is ready
406
592
  videoElement.addEventListener('loadeddata', () => {
407
593
  videoElement.pause();
408
594
  });
@@ -410,26 +596,45 @@ window.caseStudyInteraction = function (e) {
410
596
  e.addEventListener('mouseenter', hoverIN, false);
411
597
  e.addEventListener('mouseleave', hoverOUT, false);
412
598
 
413
- function hoverIN() {
414
- videoElement.play();
415
- }
416
-
417
- function hoverOUT() {
418
- videoElement.pause();
419
- // videoElement.currentTime = 0;
420
- }
421
599
  } 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();
600
+ // Mobile
601
+ Array.from(videoElements).forEach((v) => {
602
+ const poster = v.getAttribute('poster');
603
+
604
+ if (isIOS) {
605
+ // iOS: safest is to replace <video> with an <img> using the poster
606
+ if (poster) {
607
+ const img = new Image();
608
+ img.src = poster;
609
+ img.alt = '';
610
+ img.className = v.className; // preserve classes if needed
611
+ // Keep sizing via CSS from the video
612
+ v.replaceWith(img);
613
+ } else {
614
+ // If no poster, fall back to pausing & keeping sources
615
+ v.removeAttribute('autoplay');
616
+ v.pause();
617
+ v.preload = 'none';
618
+ }
619
+ } else {
620
+ // Android: keep poster only, but DON'T remove sources (some browsers need metadata for sizing)
621
+ v.removeAttribute('autoplay');
622
+ v.pause();
623
+ v.preload = 'none';
624
+ // Keeping sources avoids 0-height issues in some engines
625
+ // v.load(); // optional: only if you observe stale frames
626
+ }
429
627
  });
430
628
  }
629
+
630
+ // After images/posters load, refresh ScrollTrigger (prevents late layout causing missed triggers)
631
+ // Safe to call; GSAP debounces
632
+ setTimeout(() => ScrollTrigger.refresh(), 100);
431
633
  };
432
634
 
635
+
636
+
637
+
433
638
  // Reviews slider
434
639
  window.reviewsSlider = function(e) {
435
640
  // Put content of static slide into list
@@ -2023,6 +2228,7 @@ window.closeInfoBar = function() {
2023
2228
  lenis.destroy();
2024
2229
  }
2025
2230
  customGsapEasing();
2231
+
2026
2232
  reloadOnBack();
2027
2233
  revealFooter();
2028
2234
  pageLoadAnimation();
@@ -2031,4 +2237,5 @@ window.closeInfoBar = function() {
2031
2237
  contactFormOpenAnimation();
2032
2238
  disableScrollMenu();
2033
2239
  scrollToHashOnLoad();
2240
+ initClientLogosAnimation();
2034
2241
  });