@gemboss/gem-framework 0.6.0 → 0.9.0

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.
Files changed (57) hide show
  1. package/README.md +63 -1
  2. package/dist/SOURCE.json +2 -2
  3. package/dist/gem-framework.css +206 -147
  4. package/dist/gem-runtime.js +93 -15
  5. package/dist/index.js +629 -178
  6. package/dist/plugins/before_after.d.ts +9 -0
  7. package/dist/plugins/before_after.js +37 -0
  8. package/dist/plugins/carousel.d.ts +9 -0
  9. package/dist/plugins/carousel.js +49 -0
  10. package/dist/plugins/cart-drawer.d.ts +9 -0
  11. package/dist/plugins/cart-drawer.js +69 -0
  12. package/dist/plugins/context-adapter.d.ts +9 -0
  13. package/dist/plugins/context-adapter.js +17 -0
  14. package/dist/plugins/countdown.d.ts +9 -0
  15. package/dist/plugins/countdown.js +164 -0
  16. package/dist/plugins/disclosure.d.ts +9 -0
  17. package/dist/plugins/disclosure.js +40 -0
  18. package/dist/plugins/hero-slideshow.d.ts +9 -0
  19. package/dist/plugins/hero-slideshow.js +57 -0
  20. package/dist/plugins/localization.d.ts +9 -0
  21. package/dist/plugins/localization.js +37 -0
  22. package/dist/plugins/media-gallery.d.ts +9 -0
  23. package/dist/plugins/media-gallery.js +57 -0
  24. package/dist/plugins/mobile-menu.d.ts +9 -0
  25. package/dist/plugins/mobile-menu.js +51 -0
  26. package/dist/plugins/offer-engine.d.ts +9 -0
  27. package/dist/plugins/offer-engine.js +57 -0
  28. package/dist/plugins/predictive-search.d.ts +9 -0
  29. package/dist/plugins/predictive-search.js +77 -0
  30. package/dist/plugins/promo-popup.d.ts +9 -0
  31. package/dist/plugins/promo-popup.js +117 -0
  32. package/dist/plugins/quantity-input.d.ts +9 -0
  33. package/dist/plugins/quantity-input.js +44 -0
  34. package/dist/plugins/quick-view.d.ts +9 -0
  35. package/dist/plugins/quick-view.js +69 -0
  36. package/dist/plugins/recently-viewed.d.ts +9 -0
  37. package/dist/plugins/recently-viewed.js +71 -0
  38. package/dist/plugins/reveal-on-scroll.d.ts +9 -0
  39. package/dist/plugins/reveal-on-scroll.js +37 -0
  40. package/dist/plugins/runtime-slot-loader.d.ts +9 -0
  41. package/dist/plugins/runtime-slot-loader.js +29 -0
  42. package/dist/plugins/seasonal-effects.d.ts +9 -0
  43. package/dist/plugins/seasonal-effects.js +17 -0
  44. package/dist/plugins/sticky-atc.d.ts +9 -0
  45. package/dist/plugins/sticky-atc.js +43 -0
  46. package/dist/plugins/tabs.d.ts +9 -0
  47. package/dist/plugins/tabs.js +50 -0
  48. package/dist/plugins/variant-selects.d.ts +9 -0
  49. package/dist/plugins/variant-selects.js +70 -0
  50. package/dist/popup-shell.css +91 -0
  51. package/dist/primitives.css +1430 -0
  52. package/dist/runtime-core.d.ts +51 -0
  53. package/dist/runtime-core.js +176 -0
  54. package/dist/section-gate.js +315 -86
  55. package/dist/sections.css +1116 -0
  56. package/dist/tokens.css +67 -0
  57. package/package.json +21 -1
@@ -549,35 +549,46 @@
549
549
  registerPlugin('promo-popup', ({ api }) => {
550
550
  const popups = document.querySelectorAll('[data-gem-promo-popup]');
551
551
  let bound = 0;
552
+ const shown = [];
552
553
  const storagePrefix = 'gem:promo-popup-dismissed:';
553
554
  const now = () => Date.now ? Date.now() : new Date().getTime();
554
- const readDismissedUntil = (key) => {
555
- try { return Number(target.localStorage && target.localStorage.getItem(storagePrefix + key) || '0'); }
555
+ const readDismissal = (key) => {
556
+ try { const value = Number(target.localStorage && target.localStorage.getItem(storagePrefix + key) || '0'); return Number.isFinite(value) && value > 0 ? value : 0; }
556
557
  catch { return 0; }
557
558
  };
558
- const writeDismissedUntil = (key, timestamp) => {
559
+ const writeDismissedAt = (key, timestamp) => {
559
560
  try { if (target.localStorage) target.localStorage.setItem(storagePrefix + key, String(timestamp)); }
560
561
  catch { /* storage can be unavailable in private contexts */ }
561
562
  };
563
+ const isDismissed = (key, windowMs) => {
564
+ const stored = readDismissal(key);
565
+ if (!stored) return false;
566
+ const at = now();
567
+ if (stored > at) { if (stored - windowMs <= at) writeDismissedAt(key, stored - windowMs); return true; }
568
+ return at - stored < windowMs;
569
+ };
562
570
  popups.forEach((popup, index) => {
563
571
  if (!mark(popup, 'promo-popup')) return;
564
572
  const key = popup.id || popup.getAttribute && popup.getAttribute('data-gem-section-library-id') || String(index);
565
573
  const delaySeconds = Number(popup.getAttribute && (popup.getAttribute('data-gem-promo-delay') || '0'));
566
574
  const frequencyDays = Number(popup.getAttribute && (popup.getAttribute('data-gem-promo-frequency-days') || '7'));
575
+ const windowMs = (Number.isFinite(frequencyDays) ? Math.max(1, frequencyDays) : 7) * 86400000;
567
576
  const dialog = popup.querySelector && popup.querySelector('[data-gem-promo-popup-dialog]');
568
577
  const overlay = popup.querySelector && popup.querySelector('[data-gem-promo-popup-overlay]');
569
- const open = (reason = 'timer') => {
570
- if (readDismissedUntil(key) > now()) { popup.dataset.gemPromoPopupState = 'dismissed'; return false; }
578
+ const show = (reason) => {
571
579
  popup.hidden = false;
572
580
  if (dialog) dialog.hidden = false;
573
581
  if (overlay) overlay.hidden = false;
574
582
  popup.dataset.gemPromoPopupState = 'open';
575
583
  api.emit('gem:promo-popup:open', { source: 'gem-runtime', id: popup.id || undefined, reason });
584
+ };
585
+ const open = (reason = 'timer') => {
586
+ if (isDismissed(key, windowMs)) { popup.dataset.gemPromoPopupState = 'dismissed'; return false; }
587
+ show(reason);
576
588
  return true;
577
589
  };
578
590
  const close = (reason = 'dismiss') => {
579
- const days = Number.isFinite(frequencyDays) ? Math.max(1, frequencyDays) : 7;
580
- writeDismissedUntil(key, now() + days * 86400000);
591
+ writeDismissedAt(key, now());
581
592
  if (dialog) dialog.hidden = true;
582
593
  if (overlay) overlay.hidden = true;
583
594
  popup.dataset.gemPromoPopupState = 'closed';
@@ -590,10 +601,50 @@
590
601
  if (canListen(overlay)) overlay.addEventListener('click', () => close('overlay'));
591
602
  popup.dataset.gemPromoPopupState = 'ready';
592
603
  const delay = Math.max(0, Number.isFinite(delaySeconds) ? delaySeconds : 0) * 1000;
593
- target.setTimeout ? target.setTimeout(() => open('timer'), delay) : open('timer');
604
+ const trigger = String(popup.getAttribute && popup.getAttribute('data-gem-promo-trigger') || 'delay').trim();
605
+ const rawPercent = String(popup.getAttribute && popup.getAttribute('data-gem-promo-scroll-percent') || '').trim();
606
+ const parsedPercent = rawPercent ? Number(rawPercent) : NaN;
607
+ const scrollPercent = Number.isFinite(parsedPercent) ? Math.min(90, Math.max(10, parsedPercent)) : 50;
608
+ const canHover = () => { try { return Boolean(target.matchMedia && target.matchMedia('(hover: hover) and (pointer: fine)').matches); } catch { return false; } };
609
+ const armed = trigger === 'exit_intent' && canHover() ? 'exit_intent' : trigger === 'scroll' ? 'scroll' : 'delay';
610
+ if (popup.setAttribute) popup.setAttribute('data-gem-promo-armed', armed);
611
+ let opened = false;
612
+ const disarm = [];
613
+ const openOnce = (reason) => { if (opened) return; opened = true; disarm.splice(0).forEach((off) => off()); open(reason); };
614
+ shown.push((reason) => { opened = true; disarm.splice(0).forEach((off) => off()); if (popup.dataset.gemPromoPopupState !== 'open') show(reason); });
615
+ if (armed === 'exit_intent') {
616
+ const onMouseOut = (event) => { if (event.relatedTarget || event.clientY >= 10) return; openOnce('exit_intent'); };
617
+ if (canListen(document)) {
618
+ document.addEventListener('mouseout', onMouseOut);
619
+ disarm.push(() => document.removeEventListener && document.removeEventListener('mouseout', onMouseOut));
620
+ }
621
+ } else if (armed === 'scroll') {
622
+ const room = () => Number(document.documentElement && document.documentElement.scrollHeight || 0) - Number(target.innerHeight || 0);
623
+ const check = () => { const r = room(); if (r > 0 && (Number(target.scrollY || target.pageYOffset || 0) / r) * 100 >= scrollPercent) openOnce('scroll'); };
624
+ let ticking = false;
625
+ const onScroll = () => {
626
+ if (ticking) return;
627
+ ticking = true;
628
+ const frame = target.requestAnimationFrame ? (fn) => target.requestAnimationFrame(fn) : (fn) => target.setTimeout && target.setTimeout(fn, 16);
629
+ frame(() => { ticking = false; check(); });
630
+ };
631
+ if (canListen(target)) {
632
+ target.addEventListener('scroll', onScroll, { passive: true });
633
+ target.addEventListener('resize', check);
634
+ target.addEventListener('load', check);
635
+ disarm.push(() => { if (!target.removeEventListener) return; target.removeEventListener('scroll', onScroll); target.removeEventListener('resize', check); target.removeEventListener('load', check); });
636
+ }
637
+ if (target.setTimeout) {
638
+ const fallback = target.setTimeout(() => { if (room() <= 0) openOnce('timer'); }, delay);
639
+ disarm.push(() => target.clearTimeout && target.clearTimeout(fallback));
640
+ }
641
+ check();
642
+ } else {
643
+ target.setTimeout ? target.setTimeout(() => openOnce('timer'), delay) : openOnce('timer');
644
+ }
594
645
  bound += 1;
595
646
  });
596
- api.promoPopup = { open: () => popups.forEach((popup) => { popup.hidden = false; popup.dataset.gemPromoPopupState = 'open'; }) };
647
+ api.promoPopup = { open: () => shown.forEach((show) => show('api')) };
597
648
  api.emit('gem:promo-popup:ready', { source: 'gem-runtime', count: bound });
598
649
  });
599
650
  registerPlugin('sticky-atc', ({ api }) => {
@@ -607,7 +658,10 @@
607
658
  };
608
659
  bars.forEach((bar) => {
609
660
  if (!mark(bar, 'sticky-atc')) return;
610
- const threshold = Number(bar.getAttribute && (bar.getAttribute('data-gem-sticky-threshold') || '240'));
661
+ const cssThreshold = () => { try { return target.getComputedStyle ? Number(target.getComputedStyle(bar).getPropertyValue('--gem-sticky-atc-threshold')) : 0; } catch { return 0; } };
662
+ const threshold = bar.hasAttribute && bar.hasAttribute('data-gem-sticky-atc-threshold')
663
+ ? (Number(bar.getAttribute('data-gem-sticky-atc-threshold')) || cssThreshold() || 360)
664
+ : Number(bar.getAttribute && (bar.getAttribute('data-gem-sticky-threshold') || '240'));
611
665
  const update = () => {
612
666
  const scrollY = Number(target.scrollY || target.pageYOffset || 0);
613
667
  setState(bar, scrollY >= threshold ? 'visible' : 'hidden');
@@ -674,6 +728,9 @@
674
728
  let endAt = 0;
675
729
  let handle = null;
676
730
  let state = 'idle';
731
+ let started = false;
732
+ let observer = null;
733
+ const release = () => { if (observer) { observer.disconnect(); observer = null; } };
677
734
  const detail = () => ({ source: 'gem-runtime', id: timer.id || undefined, mode: 'duration', duration: total, remainingSeconds: Math.max(0, Math.ceil(left / 1000)) });
678
735
  const render = () => {
679
736
  const s = Math.max(0, Math.ceil(left / 1000));
@@ -689,6 +746,7 @@
689
746
  const halt = () => { if (handle !== null && target.clearInterval) target.clearInterval(handle); handle = null; };
690
747
  const finish = () => {
691
748
  halt();
749
+ release();
692
750
  left = 0;
693
751
  state = 'complete';
694
752
  render();
@@ -705,20 +763,20 @@
705
763
  endAt = Date.now() + left;
706
764
  render();
707
765
  if (target.setInterval) handle = target.setInterval(tick, 250);
708
- api.emit('gem:countdown:start', detail());
766
+ if (!started) { started = true; api.emit('gem:countdown:start', detail()); }
709
767
  };
710
768
  const pause = (next) => {
711
769
  if (state === 'running') { left = Math.max(0, endAt - Date.now()); halt(); }
712
770
  if (state === 'complete' || state === 'stopped') return;
713
771
  state = next;
772
+ if (next === 'stopped') release();
714
773
  render();
715
774
  };
716
775
  render();
717
776
  if (stopEvent && canListen(scope)) scope.addEventListener(stopEvent, () => pause('stopped'));
718
777
  if (mode === 'visible' && target.IntersectionObserver) {
719
- const observer = new target.IntersectionObserver((entries) => {
778
+ observer = new target.IntersectionObserver((entries) => {
720
779
  for (const entry of entries) entry.isIntersecting ? run() : pause('paused');
721
- if (state === 'complete' || state === 'stopped') observer.disconnect();
722
780
  });
723
781
  observer.observe(timer);
724
782
  } else if (mode === 'mount' || mode === 'visible') run();
@@ -735,7 +793,9 @@
735
793
  const targetTime = countdownEnd(timer.getAttribute && timer.getAttribute('data-gem-countdown-target'));
736
794
  const section = timer.closest ? timer.closest('[data-gem-section-id]') : null;
737
795
  const band = section && section.hasAttribute && section.hasAttribute('data-gem-countdown-end') ? section : null;
796
+ const hasSecondsCell = Boolean(timer.querySelector && (timer.querySelector('[data-gem-cd-secs]') || timer.querySelector('[data-gem-countdown-seconds]')));
738
797
  let handle = null;
798
+ let announced = '';
739
799
  const update = () => {
740
800
  if (!Number.isFinite(targetTime)) {
741
801
  timer.dataset.gemCountdownState = 'invalid';
@@ -746,19 +806,37 @@
746
806
  const days = Math.floor(remainingSeconds / 86400);
747
807
  const hours = Math.floor((remainingSeconds % 86400) / 3600);
748
808
  const minutes = Math.floor((remainingSeconds % 3600) / 60);
809
+ const seconds = remainingSeconds % 60;
749
810
  timer.dataset.gemCountdownState = ms > 0 ? 'running' : 'complete';
750
811
  timer.dataset.gemCountdownRemainingSeconds = String(remainingSeconds);
751
812
  write(timer, '[data-gem-countdown-days]', days);
752
813
  write(timer, '[data-gem-countdown-hours]', hours);
753
814
  write(timer, '[data-gem-countdown-minutes]', minutes);
815
+ write(timer, '[data-gem-countdown-seconds]', seconds);
816
+ write(timer, '[data-gem-cd-days]', days);
817
+ write(timer, '[data-gem-cd-hours]', hours);
818
+ write(timer, '[data-gem-cd-mins]', minutes);
819
+ write(timer, '[data-gem-cd-secs]', seconds);
754
820
  if (ms <= 0) {
755
821
  if (band) band.setAttribute('data-gem-countdown-ended', 'true');
822
+ if (timer.setAttribute && !(timer.hasAttribute && timer.hasAttribute('data-gem-section-id'))) timer.setAttribute('data-gem-countdown-ended', 'true');
756
823
  if (handle !== null && target.clearInterval) { target.clearInterval(handle); handle = null; }
757
824
  }
758
- api.emit(ms > 0 ? 'gem:countdown:start' : 'gem:countdown:complete', { source: 'gem-runtime', id: timer.id || undefined, remainingSeconds });
825
+ const event = ms > 0 ? 'gem:countdown:start' : 'gem:countdown:complete';
826
+ if (event === announced && (event === 'gem:countdown:complete' || hasSecondsCell)) return;
827
+ announced = event;
828
+ api.emit(event, { source: 'gem-runtime', id: timer.id || undefined, remainingSeconds });
829
+ };
830
+ const onTheMinute = () => {
831
+ const ms = targetTime - Date.now();
832
+ if (!(ms > 0) || !target.setTimeout) return;
833
+ target.setTimeout(() => { update(); onTheMinute(); }, Math.min((ms % 60000) + 5, ms));
759
834
  };
760
835
  update();
761
- if (target.setInterval && Number.isFinite(targetTime) && targetTime - Date.now() > 0) handle = target.setInterval(update, 60000);
836
+ if (Number.isFinite(targetTime) && targetTime - Date.now() > 0) {
837
+ if (!hasSecondsCell) onTheMinute();
838
+ else if (target.setInterval) handle = target.setInterval(update, 1000);
839
+ }
762
840
  const left = targetTime - Date.now();
763
841
  if (target.setTimeout && Number.isFinite(left) && left > 0 && left < 2147483647) target.setTimeout(update, left);
764
842
  bound += 1;