@gemboss/gem-framework 0.5.2 → 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 -64
- package/dist/gem-runtime.js +158 -12
- package/dist/index.js +747 -112
- 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 +432 -21
- 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');
|
|
@@ -662,12 +716,86 @@
|
|
|
662
716
|
const targetNode = timer.querySelector && timer.querySelector(selector);
|
|
663
717
|
if (targetNode) targetNode.textContent = pad(value);
|
|
664
718
|
};
|
|
719
|
+
const clockText = (s) => {
|
|
720
|
+
const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
|
|
721
|
+
return (h ? h + ':' + pad(m) : String(m)) + ':' + pad(sec);
|
|
722
|
+
};
|
|
723
|
+
const bindDuration = (timer, total) => {
|
|
724
|
+
const scope = (timer.closest && timer.closest('[data-gem-section-id]')) || document;
|
|
725
|
+
const mode = String(timer.getAttribute('data-gem-countdown-start') || 'mount').trim() || 'mount';
|
|
726
|
+
const stopEvent = String(timer.getAttribute('data-gem-countdown-stop') || '').trim();
|
|
727
|
+
let left = total * 1000;
|
|
728
|
+
let endAt = 0;
|
|
729
|
+
let handle = null;
|
|
730
|
+
let state = 'idle';
|
|
731
|
+
let started = false;
|
|
732
|
+
let observer = null;
|
|
733
|
+
const release = () => { if (observer) { observer.disconnect(); observer = null; } };
|
|
734
|
+
const detail = () => ({ source: 'gem-runtime', id: timer.id || undefined, mode: 'duration', duration: total, remainingSeconds: Math.max(0, Math.ceil(left / 1000)) });
|
|
735
|
+
const render = () => {
|
|
736
|
+
const s = Math.max(0, Math.ceil(left / 1000));
|
|
737
|
+
timer.dataset.gemCountdownState = state;
|
|
738
|
+
timer.dataset.gemCountdownRemainingSeconds = String(s);
|
|
739
|
+
write(timer, '[data-gem-countdown-days]', Math.floor(s / 86400));
|
|
740
|
+
write(timer, '[data-gem-countdown-hours]', Math.floor((s % 86400) / 3600));
|
|
741
|
+
write(timer, '[data-gem-countdown-minutes]', Math.floor((s % 3600) / 60));
|
|
742
|
+
write(timer, '[data-gem-countdown-seconds]', s % 60);
|
|
743
|
+
const readout = timer.querySelector && timer.querySelector('[data-gem-countdown-clock]');
|
|
744
|
+
if (readout) readout.textContent = clockText(s);
|
|
745
|
+
};
|
|
746
|
+
const halt = () => { if (handle !== null && target.clearInterval) target.clearInterval(handle); handle = null; };
|
|
747
|
+
const finish = () => {
|
|
748
|
+
halt();
|
|
749
|
+
release();
|
|
750
|
+
left = 0;
|
|
751
|
+
state = 'complete';
|
|
752
|
+
render();
|
|
753
|
+
timer.setAttribute('data-gem-countdown-ended', 'true');
|
|
754
|
+
if (timer.dispatchEvent && timer.isConnected !== false) {
|
|
755
|
+
timer.dispatchEvent(new CustomEvent('gem:countdown:complete', { bubbles: true, detail: detail() }));
|
|
756
|
+
for (const callback of api.listeners['gem:countdown:complete'] || []) callback(detail());
|
|
757
|
+
} else api.emit('gem:countdown:complete', detail());
|
|
758
|
+
};
|
|
759
|
+
const tick = () => { left = endAt - Date.now(); if (left <= 0) finish(); else render(); };
|
|
760
|
+
const run = () => {
|
|
761
|
+
if (state === 'running' || state === 'complete' || state === 'stopped') return;
|
|
762
|
+
state = 'running';
|
|
763
|
+
endAt = Date.now() + left;
|
|
764
|
+
render();
|
|
765
|
+
if (target.setInterval) handle = target.setInterval(tick, 250);
|
|
766
|
+
if (!started) { started = true; api.emit('gem:countdown:start', detail()); }
|
|
767
|
+
};
|
|
768
|
+
const pause = (next) => {
|
|
769
|
+
if (state === 'running') { left = Math.max(0, endAt - Date.now()); halt(); }
|
|
770
|
+
if (state === 'complete' || state === 'stopped') return;
|
|
771
|
+
state = next;
|
|
772
|
+
if (next === 'stopped') release();
|
|
773
|
+
render();
|
|
774
|
+
};
|
|
775
|
+
render();
|
|
776
|
+
if (stopEvent && canListen(scope)) scope.addEventListener(stopEvent, () => pause('stopped'));
|
|
777
|
+
if (mode === 'visible' && target.IntersectionObserver) {
|
|
778
|
+
observer = new target.IntersectionObserver((entries) => {
|
|
779
|
+
for (const entry of entries) entry.isIntersecting ? run() : pause('paused');
|
|
780
|
+
});
|
|
781
|
+
observer.observe(timer);
|
|
782
|
+
} else if (mode === 'mount' || mode === 'visible') run();
|
|
783
|
+
else if (canListen(scope)) scope.addEventListener(mode, run);
|
|
784
|
+
};
|
|
665
785
|
timers.forEach((timer) => {
|
|
666
786
|
if (!mark(timer, 'countdown')) return;
|
|
787
|
+
const duration = Number(timer.getAttribute && timer.getAttribute('data-gem-countdown-duration'));
|
|
788
|
+
if (Number.isFinite(duration) && duration > 0) {
|
|
789
|
+
bindDuration(timer, Math.min(Math.round(duration), 86400));
|
|
790
|
+
bound += 1;
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
667
793
|
const targetTime = countdownEnd(timer.getAttribute && timer.getAttribute('data-gem-countdown-target'));
|
|
668
794
|
const section = timer.closest ? timer.closest('[data-gem-section-id]') : null;
|
|
669
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]')));
|
|
670
797
|
let handle = null;
|
|
798
|
+
let announced = '';
|
|
671
799
|
const update = () => {
|
|
672
800
|
if (!Number.isFinite(targetTime)) {
|
|
673
801
|
timer.dataset.gemCountdownState = 'invalid';
|
|
@@ -678,19 +806,37 @@
|
|
|
678
806
|
const days = Math.floor(remainingSeconds / 86400);
|
|
679
807
|
const hours = Math.floor((remainingSeconds % 86400) / 3600);
|
|
680
808
|
const minutes = Math.floor((remainingSeconds % 3600) / 60);
|
|
809
|
+
const seconds = remainingSeconds % 60;
|
|
681
810
|
timer.dataset.gemCountdownState = ms > 0 ? 'running' : 'complete';
|
|
682
811
|
timer.dataset.gemCountdownRemainingSeconds = String(remainingSeconds);
|
|
683
812
|
write(timer, '[data-gem-countdown-days]', days);
|
|
684
813
|
write(timer, '[data-gem-countdown-hours]', hours);
|
|
685
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);
|
|
686
820
|
if (ms <= 0) {
|
|
687
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');
|
|
688
823
|
if (handle !== null && target.clearInterval) { target.clearInterval(handle); handle = null; }
|
|
689
824
|
}
|
|
690
|
-
|
|
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));
|
|
691
834
|
};
|
|
692
835
|
update();
|
|
693
|
-
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
|
+
}
|
|
694
840
|
const left = targetTime - Date.now();
|
|
695
841
|
if (target.setTimeout && Number.isFinite(left) && left > 0 && left < 2147483647) target.setTimeout(update, left);
|
|
696
842
|
bound += 1;
|