@widelab-nc/widelab 1.1.36 → 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.36",
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
@@ -16,8 +16,87 @@ function customGsapEasing() {
16
16
  CustomEase.create('loader2', '0.77, 0, 0.175, 1');
17
17
  }
18
18
 
19
+ // Smoothly scrolls to section if current URL contains a hash
20
+ function scrollToHashOnLoad(offsetY = 0) {
21
+ window.addEventListener('load', () => {
22
+ const hash = window.location.hash;
23
+ if (!hash) return;
24
+
25
+ const target = document.querySelector(hash);
26
+ if (!target) return;
27
+
28
+ gsap.to(window, {
29
+ scrollTo: { y: target, offsetY: offsetY },
30
+ duration: 1,
31
+ ease: 'power2.out'
32
+ });
33
+ });
34
+ }
35
+
19
36
  // Functions definitions
20
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
+ }
21
100
  // Contact form open animation
22
101
  let contactFormOpen;
23
102
  function contactFormOpenAnimation() {
@@ -222,6 +301,114 @@ if (infoBar) {
222
301
  });
223
302
  }
224
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
+
225
412
  // Header scroll trigger
226
413
  window.standardHeaderScrollAnimation = function() {
227
414
  ScrollTrigger.create({
@@ -343,54 +530,111 @@ window.customCursorTextUpdate = function(el) {
343
530
  );
344
531
  }
345
532
 
346
- // Case study mask animation
347
- window.caseStudyInteraction = function(e) {
348
- // Video mask animation
349
- const mask = e.querySelector('.portfolio_item_image-mask');
350
- const maskAnimation = gsap.from(mask, {
351
- width: '100%',
352
- duration: 0.35,
353
- ease: 'loader2',
354
- paused: true,
355
- });
356
- ScrollTrigger.create({
357
- animation: maskAnimation,
358
- trigger: mask,
359
- start: 'top 80%',
360
- onEnter: () => maskAnimation.play(),
361
- });
362
533
 
363
- // Video on hover actions
364
- const videoElements = e.getElementsByTagName('video');
365
- if (window.innerWidth > 992) {
366
- const videoElement = videoElements.length > 0 ? videoElements[0] : null;
367
- if (!videoElement) return;
368
534
 
369
- videoElement.addEventListener('loadeddata', () => {
370
- videoElement.pause();
371
- });
372
- e.addEventListener('mouseenter', hoverIN, false);
373
- e.addEventListener('mouseleave', hoverOUT, false);
535
+ // Case study mask animation + pill fade-in (last step)
536
+ window.caseStudyInteraction = function (e) {
537
+ const mask = e.querySelector('.portfolio_item_image-mask');
538
+ const pill = e.querySelector('.cs_pill');
539
+ const triggerEl = e.querySelector('.portfolio_item_image-wrapper') || e; // safer trigger
374
540
 
375
- function hoverIN() {
376
- videoElement.play();
377
- }
541
+ // --- iOS detection (reliable enough for layout fallbacks) ---
542
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
543
+ (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
378
544
 
379
- function hoverOUT() {
380
- videoElement.pause();
381
- //videoElement.currentTime = 0;
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
+ }
555
+
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
+ }
562
+ if (pill) {
563
+ tl.to(pill, { autoAlpha: 1, duration: 0.25 }, '>-0.05');
564
+ }
565
+
566
+ // Scroll trigger (use wrapper, not absolute mask)
567
+ ScrollTrigger.create({
568
+ animation: tl,
569
+ trigger: triggerEl,
570
+ start: 'top 80%',
571
+ once: true
572
+ });
573
+
574
+ // --- Video handling ---
575
+ const videoElements = e.getElementsByTagName('video');
576
+
577
+ if (window.innerWidth > 992) {
578
+ const videoElement = videoElements.length > 0 ? videoElements[0] : null;
579
+ if (!videoElement) return;
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
592
+ videoElement.addEventListener('loadeddata', () => {
593
+ videoElement.pause();
594
+ });
595
+
596
+ e.addEventListener('mouseenter', hoverIN, false);
597
+ e.addEventListener('mouseleave', hoverOUT, false);
598
+
599
+ } else {
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';
382
618
  }
383
- } else {
384
- videoElements.forEach((videoElem) => {
385
- const poster = videoElem.getAttribute('poster');
386
- videoElem.innerHTML = '';
387
- videoElem.setAttribute('poster', poster);
388
- videoElem.load();
389
- videoElem.pause();
390
- });
391
- }
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
+ }
627
+ });
392
628
  }
393
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);
633
+ };
634
+
635
+
636
+
637
+
394
638
  // Reviews slider
395
639
  window.reviewsSlider = function(e) {
396
640
  // Put content of static slide into list
@@ -446,61 +690,6 @@ window.customCursorTextUpdate = function(el) {
446
690
  });
447
691
  }
448
692
 
449
- // Reviews slider
450
- window.reviewsSlider = function(e) {
451
- // Put content of static slide into list
452
- const staticContent = e.querySelector(
453
- '.splide__slide.reviews_collection_item.is-static'
454
- ).innerHTML;
455
- const dynamicElement = e.querySelector('.reviews_collection_item');
456
- dynamicElement.innerHTML = staticContent;
457
- dynamicElement.classList.add('is-static');
458
-
459
- // Init reviews splide
460
- const reviewsSplide = new Splide(e, {
461
- type: 'slide',
462
- fixedWidth: '24.5rem',
463
- rewind: true,
464
- focus: 0,
465
- padding: { left: '2.5rem', right: 0 },
466
- gap: 0,
467
- omitEnd: true,
468
- arrows: false,
469
- drag: false,
470
- easing: 'cubic-bezier(0.83, 0, 0.17, 1)',
471
- snap: true,
472
- classes: {
473
- pagination: 'splide__pagination reviews-pagination',
474
- page: 'splide__pagination__page reviews-page',
475
- },
476
- breakpoints: {
477
- 992: { fixedWidth: '17.438rem', padding: { left: '1.75rem' }, drag: 'free', snap: false },
478
- 480: { fixedWidth: '16.25rem', padding: { left: '1.375rem' }},
479
- },
480
- }).mount();
481
-
482
- // Next slide on click
483
- if (window.innerWidth > 992) {
484
- reviewsSplide.on('click', function () {
485
- reviewsSplide.go('+1');
486
- });
487
- }
488
-
489
- // Animation in
490
- gsap.from(e, {
491
- scrollTrigger: {
492
- trigger: e,
493
- start: 'top bottom',
494
- end: 'bottom bottom-=15%',
495
- scrub: 1,
496
- once: true,
497
- },
498
- x: '10%',
499
- opacity: 0,
500
- ease: 'linear',
501
- });
502
- }
503
-
504
693
  // Facts slider
505
694
  window.factsSlider = function(e) {
506
695
  // Facts slider splide initialize
@@ -1684,6 +1873,7 @@ window.initBentoRevealScrub = function (container) {
1684
1873
  }
1685
1874
  );
1686
1875
  };
1876
+
1687
1877
  window.initPartnerVideos = function (container) {
1688
1878
  const items = Array.from(container.querySelectorAll('.partner_item'));
1689
1879
  const vids = items.map(i => i.querySelector('video')).filter(Boolean);
@@ -2038,6 +2228,7 @@ window.closeInfoBar = function() {
2038
2228
  lenis.destroy();
2039
2229
  }
2040
2230
  customGsapEasing();
2231
+
2041
2232
  reloadOnBack();
2042
2233
  revealFooter();
2043
2234
  pageLoadAnimation();
@@ -2045,4 +2236,6 @@ window.closeInfoBar = function() {
2045
2236
  customCursorOnClick();
2046
2237
  contactFormOpenAnimation();
2047
2238
  disableScrollMenu();
2239
+ scrollToHashOnLoad();
2240
+ initClientLogosAnimation();
2048
2241
  });