@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.
- package/README.md +63 -1
- package/dist/SOURCE.json +2 -2
- package/dist/gem-framework.css +206 -147
- package/dist/gem-runtime.js +93 -15
- package/dist/index.js +629 -178
- package/dist/plugins/before_after.d.ts +9 -0
- package/dist/plugins/before_after.js +37 -0
- package/dist/plugins/carousel.d.ts +9 -0
- package/dist/plugins/carousel.js +49 -0
- package/dist/plugins/cart-drawer.d.ts +9 -0
- package/dist/plugins/cart-drawer.js +69 -0
- package/dist/plugins/context-adapter.d.ts +9 -0
- package/dist/plugins/context-adapter.js +17 -0
- package/dist/plugins/countdown.d.ts +9 -0
- package/dist/plugins/countdown.js +164 -0
- package/dist/plugins/disclosure.d.ts +9 -0
- package/dist/plugins/disclosure.js +40 -0
- package/dist/plugins/hero-slideshow.d.ts +9 -0
- package/dist/plugins/hero-slideshow.js +57 -0
- package/dist/plugins/localization.d.ts +9 -0
- package/dist/plugins/localization.js +37 -0
- package/dist/plugins/media-gallery.d.ts +9 -0
- package/dist/plugins/media-gallery.js +57 -0
- package/dist/plugins/mobile-menu.d.ts +9 -0
- package/dist/plugins/mobile-menu.js +51 -0
- package/dist/plugins/offer-engine.d.ts +9 -0
- package/dist/plugins/offer-engine.js +57 -0
- package/dist/plugins/predictive-search.d.ts +9 -0
- package/dist/plugins/predictive-search.js +77 -0
- package/dist/plugins/promo-popup.d.ts +9 -0
- package/dist/plugins/promo-popup.js +117 -0
- package/dist/plugins/quantity-input.d.ts +9 -0
- package/dist/plugins/quantity-input.js +44 -0
- package/dist/plugins/quick-view.d.ts +9 -0
- package/dist/plugins/quick-view.js +69 -0
- package/dist/plugins/recently-viewed.d.ts +9 -0
- package/dist/plugins/recently-viewed.js +71 -0
- package/dist/plugins/reveal-on-scroll.d.ts +9 -0
- package/dist/plugins/reveal-on-scroll.js +37 -0
- package/dist/plugins/runtime-slot-loader.d.ts +9 -0
- package/dist/plugins/runtime-slot-loader.js +29 -0
- package/dist/plugins/seasonal-effects.d.ts +9 -0
- package/dist/plugins/seasonal-effects.js +17 -0
- package/dist/plugins/sticky-atc.d.ts +9 -0
- package/dist/plugins/sticky-atc.js +43 -0
- package/dist/plugins/tabs.d.ts +9 -0
- package/dist/plugins/tabs.js +50 -0
- package/dist/plugins/variant-selects.d.ts +9 -0
- package/dist/plugins/variant-selects.js +70 -0
- package/dist/popup-shell.css +91 -0
- package/dist/primitives.css +1430 -0
- package/dist/runtime-core.d.ts +51 -0
- package/dist/runtime-core.js +176 -0
- package/dist/section-gate.js +315 -86
- package/dist/sections.css +1116 -0
- package/dist/tokens.css +67 -0
- package/package.json +21 -1
package/dist/gem-runtime.js
CHANGED
|
@@ -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
|
|
555
|
-
try {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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: () =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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;
|