@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.
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 -64
  4. package/dist/gem-runtime.js +158 -12
  5. package/dist/index.js +747 -112
  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 +432 -21
  55. package/dist/sections.css +1116 -0
  56. package/dist/tokens.css +67 -0
  57. package/package.json +21 -1
@@ -0,0 +1,9 @@
1
+ // @gemboss/gem-framework/plugins/before_after: types of the ES module next to this file. Generated; do not edit here.
2
+ import type { MountOptions, Unmount } from "../runtime-core.js";
3
+
4
+ export type { MountOptions, Unmount };
5
+ export declare const key: "before_after";
6
+ export declare const readyEvent: "gem:before-after:ready";
7
+ /** Bind every "before_after" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
8
+ export declare function mount(root?: ParentNode | null, options?: MountOptions): Unmount;
9
+ export default mount;
@@ -0,0 +1,37 @@
1
+ // @gemboss/gem-framework/plugins/before_after: the gem-runtime plugin "before_after" as an ES module. Generated from
2
+ // packages/gem-framework/src/runtime.ts, the same lines gem-runtime.js carries; do not edit here.
3
+ import { mountPlugin } from "../runtime-core.js";
4
+
5
+ export const key = "before_after";
6
+ export const readyEvent = "gem:before-after:ready";
7
+
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
9
+ registerPlugin('before_after', ({ api }) => {
10
+ const frames = document.querySelectorAll('[data-gem-before-after-frame], .gem-before-after__frame');
11
+ let bound = 0;
12
+ const clamp = (value) => Math.max(0, Math.min(100, Number.isFinite(value) ? value : 50));
13
+ frames.forEach((frame) => {
14
+ if (!mark(frame, 'before_after')) return;
15
+ const range = frame.querySelector && frame.querySelector('[data-gem-before-after-range], input[type="range"]');
16
+ const setPosition = (value, reason = 'init') => {
17
+ const position = clamp(Number(value));
18
+ if (frame.style && frame.style.setProperty) frame.style.setProperty('--gem-before-after-position', position + '%');
19
+ frame.dataset.gemBeforeAfterPosition = String(position);
20
+ if (range && range.value !== undefined) range.value = String(position);
21
+ api.emit('gem:before-after:change', { source: 'gem-runtime', position, reason, id: frame.id || undefined });
22
+ };
23
+ const initial = range && range.value !== undefined ? range.value : frame.getAttribute && frame.getAttribute('data-gem-before-after-position');
24
+ setPosition(initial || 50);
25
+ if (canListen(range)) __gemListen(range, 'input', () => setPosition(range.value, 'input'));
26
+ bound += 1;
27
+ });
28
+ api.emit('gem:before-after:ready', { source: 'gem-runtime', count: bound });
29
+ });
30
+ }
31
+
32
+ /** Bind every "before_after" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
33
+ export function mount(root, options) {
34
+ return mountPlugin(key, install, root, options);
35
+ }
36
+
37
+ export default mount;
@@ -0,0 +1,9 @@
1
+ // @gemboss/gem-framework/plugins/carousel: types of the ES module next to this file. Generated; do not edit here.
2
+ import type { MountOptions, Unmount } from "../runtime-core.js";
3
+
4
+ export type { MountOptions, Unmount };
5
+ export declare const key: "carousel";
6
+ export declare const readyEvent: "gem:carousel:ready";
7
+ /** Bind every "carousel" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
8
+ export declare function mount(root?: ParentNode | null, options?: MountOptions): Unmount;
9
+ export default mount;
@@ -0,0 +1,49 @@
1
+ // @gemboss/gem-framework/plugins/carousel: the gem-runtime plugin "carousel" as an ES module. Generated from
2
+ // packages/gem-framework/src/runtime.ts, the same lines gem-runtime.js carries; do not edit here.
3
+ import { mountPlugin } from "../runtime-core.js";
4
+
5
+ export const key = "carousel";
6
+ export const readyEvent = "gem:carousel:ready";
7
+
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
9
+ registerPlugin('carousel', ({ api }) => {
10
+ const carousels = document.querySelectorAll('[data-gem-carousel], .gem-carousel');
11
+ let bound = 0;
12
+ carousels.forEach((carousel) => {
13
+ if (!mark(carousel, 'carousel')) return;
14
+ const viewport = carousel.querySelector && (carousel.querySelector('[data-gem-carousel-viewport]') || carousel.querySelector('.gem-carousel__viewport'));
15
+ const slides = carousel.querySelectorAll('[data-gem-carousel-slide], .gem-carousel__slide');
16
+ const previous = carousel.querySelector && carousel.querySelector('[data-gem-carousel-prev]');
17
+ const next = carousel.querySelector && carousel.querySelector('[data-gem-carousel-next]');
18
+ const slideCount = slides.length || 0;
19
+ const currentIndex = () => {
20
+ if (!viewport || !slideCount) return 0;
21
+ const width = Number(viewport.clientWidth || viewport.offsetWidth || 1);
22
+ return Math.max(0, Math.min(slideCount - 1, Math.round(Number(viewport.scrollLeft || 0) / width)));
23
+ };
24
+ const setIndex = (index, reason = 'api') => {
25
+ const nextIndex = Math.max(0, Math.min(slideCount - 1, Number.isFinite(index) ? index : 0));
26
+ carousel.dataset.gemCarouselIndex = String(nextIndex);
27
+ carousel.dataset.gemCarouselCount = String(slideCount);
28
+ if (viewport && viewport.scrollTo) viewport.scrollTo({ left: Number(viewport.clientWidth || viewport.offsetWidth || 0) * nextIndex, behavior: 'smooth' });
29
+ api.emit('gem:carousel:change', { source: 'gem-runtime', id: carousel.id || undefined, index: nextIndex, count: slideCount, reason });
30
+ };
31
+ slides.forEach((slide, index) => { if (slide.dataset) slide.dataset.gemCarouselIndex = String(index); });
32
+ carousel.dataset.gemCarouselState = slideCount > 1 ? 'ready' : 'single';
33
+ carousel.dataset.gemCarouselCount = String(slideCount);
34
+ carousel.dataset.gemCarouselIndex = String(currentIndex());
35
+ if (canListen(previous)) __gemListen(previous, 'click', (event) => { event.preventDefault && event.preventDefault(); setIndex(currentIndex() - 1, 'previous'); });
36
+ if (canListen(next)) __gemListen(next, 'click', (event) => { event.preventDefault && event.preventDefault(); setIndex(currentIndex() + 1, 'next'); });
37
+ if (canListen(viewport)) __gemListen(viewport, 'scroll', () => { carousel.dataset.gemCarouselIndex = String(currentIndex()); }, { passive: true });
38
+ bound += 1;
39
+ });
40
+ api.emit('gem:carousel:ready', { source: 'gem-runtime', count: bound });
41
+ });
42
+ }
43
+
44
+ /** Bind every "carousel" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
45
+ export function mount(root, options) {
46
+ return mountPlugin(key, install, root, options);
47
+ }
48
+
49
+ export default mount;
@@ -0,0 +1,9 @@
1
+ // @gemboss/gem-framework/plugins/cart-drawer: types of the ES module next to this file. Generated; do not edit here.
2
+ import type { MountOptions, Unmount } from "../runtime-core.js";
3
+
4
+ export type { MountOptions, Unmount };
5
+ export declare const key: "cart-drawer";
6
+ export declare const readyEvent: "gem:cart:ready";
7
+ /** Bind every "cart-drawer" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
8
+ export declare function mount(root?: ParentNode | null, options?: MountOptions): Unmount;
9
+ export default mount;
@@ -0,0 +1,69 @@
1
+ // @gemboss/gem-framework/plugins/cart-drawer: the gem-runtime plugin "cart-drawer" as an ES module. Generated from
2
+ // packages/gem-framework/src/runtime.ts, the same lines gem-runtime.js carries; do not edit here.
3
+ import { mountPlugin } from "../runtime-core.js";
4
+
5
+ export const key = "cart-drawer";
6
+ export const readyEvent = "gem:cart:ready";
7
+
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
9
+ registerPlugin('cart-drawer', ({ api }) => {
10
+ const drawers = document.querySelectorAll('[data-gem-cart-drawer], [data-gem-cart-canvas], .gem-cart-drawer');
11
+ const cartLinks = document.querySelectorAll('[data-gem-cart-drawer-trigger], [data-gem-cart-toggle], a[href*="/cart"]');
12
+ const cartForms = document.querySelectorAll('form[action*="/cart/add"], form[action*="/cart"]');
13
+ const drawer = drawers[0];
14
+ const setDrawerA11y = (isOpen) => {
15
+ if (!drawer) return;
16
+ drawer.setAttribute && drawer.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
17
+ const panel = drawer.querySelector && drawer.querySelector('[data-gem-cart-canvas-panel], .gem-cart-drawer__panel');
18
+ if (panel && panel.setAttribute) panel.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
19
+ };
20
+ const openDrawer = (reason, href) => {
21
+ if (drawer && drawer.tagName === 'DETAILS') drawer.open = true;
22
+ if (drawer && drawer.dataset) drawer.dataset.gemCartDrawerState = 'open';
23
+ setDrawerA11y(true);
24
+ api.emit('gem:cart:open', { source: 'gem-runtime', reason, href });
25
+ };
26
+ const closeDrawer = (reason) => {
27
+ if (drawer && drawer.tagName === 'DETAILS') drawer.open = false;
28
+ if (drawer && drawer.dataset) drawer.dataset.gemCartDrawerState = 'closed';
29
+ setDrawerA11y(false);
30
+ api.emit('gem:cart:close', { source: 'gem-runtime', reason });
31
+ };
32
+ let bound = 0;
33
+ drawers.forEach((drawer) => {
34
+ if (!mark(drawer, 'cart-drawer')) return;
35
+ drawer.dataset.gemCartDrawerState = drawer.hasAttribute('open') ? 'open' : 'ready';
36
+ setDrawerA11y(drawer.hasAttribute('open'));
37
+ drawer.querySelectorAll('[data-gem-cart-drawer-close], [data-gem-cart-canvas-close]').forEach((control) => { if (!canListen(control)) return; __gemListen(control, 'click', (event) => {
38
+ event.preventDefault && event.preventDefault();
39
+ closeDrawer('close-control');
40
+ }); });
41
+ if (canListen(drawer)) __gemListen(drawer, 'toggle', () => { drawer.dataset.gemCartDrawerState = drawer.hasAttribute('open') ? 'open' : 'closed'; });
42
+ bound += 1;
43
+ });
44
+ cartLinks.forEach((link) => {
45
+ if (!mark(link, 'cart-link')) return;
46
+ if (!canListen(link)) return;
47
+ __gemListen(link, 'click', (event) => {
48
+ if (drawer && (link.hasAttribute('data-gem-cart-drawer-trigger') || link.hasAttribute('data-gem-cart-toggle'))) event.preventDefault && event.preventDefault();
49
+ openDrawer('cart-link', link.href || undefined);
50
+ });
51
+ bound += 1;
52
+ });
53
+ if (canListen(document)) __gemListen(document, 'keydown', (event) => { if (event.key === 'Escape' && drawer && drawer.hasAttribute('open')) closeDrawer('escape'); });
54
+ cartForms.forEach((form) => {
55
+ if (!mark(form, 'cart-form')) return;
56
+ if (!canListen(form)) return;
57
+ __gemListen(form, 'submit', () => api.emit('gem:cart:submit', { source: 'gem-runtime', action: form.getAttribute('action') || undefined }));
58
+ bound += 1;
59
+ });
60
+ api.emit('gem:cart:ready', { source: 'gem-runtime', count: bound });
61
+ });
62
+ }
63
+
64
+ /** Bind every "cart-drawer" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
65
+ export function mount(root, options) {
66
+ return mountPlugin(key, install, root, options);
67
+ }
68
+
69
+ export default mount;
@@ -0,0 +1,9 @@
1
+ // @gemboss/gem-framework/plugins/context-adapter: types of the ES module next to this file. Generated; do not edit here.
2
+ import type { MountOptions, Unmount } from "../runtime-core.js";
3
+
4
+ export type { MountOptions, Unmount };
5
+ export declare const key: "context-adapter";
6
+ export declare const readyEvent: "gem:context:ready";
7
+ /** Bind every "context-adapter" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
8
+ export declare function mount(root?: ParentNode | null, options?: MountOptions): Unmount;
9
+ export default mount;
@@ -0,0 +1,17 @@
1
+ // @gemboss/gem-framework/plugins/context-adapter: the gem-runtime plugin "context-adapter" as an ES module. Generated from
2
+ // packages/gem-framework/src/runtime.ts, the same lines gem-runtime.js carries; do not edit here.
3
+ import { mountPlugin } from "../runtime-core.js";
4
+
5
+ export const key = "context-adapter";
6
+ export const readyEvent = "gem:context:ready";
7
+
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
9
+ registerNoopPlugin("context-adapter", "gem:context:ready");
10
+ }
11
+
12
+ /** Bind every "context-adapter" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
13
+ export function mount(root, options) {
14
+ return mountPlugin(key, install, root, options);
15
+ }
16
+
17
+ export default mount;
@@ -0,0 +1,9 @@
1
+ // @gemboss/gem-framework/plugins/countdown: types of the ES module next to this file. Generated; do not edit here.
2
+ import type { MountOptions, Unmount } from "../runtime-core.js";
3
+
4
+ export type { MountOptions, Unmount };
5
+ export declare const key: "countdown";
6
+ export declare const readyEvent: "gem:countdown:ready";
7
+ /** Bind every "countdown" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
8
+ export declare function mount(root?: ParentNode | null, options?: MountOptions): Unmount;
9
+ export default mount;
@@ -0,0 +1,164 @@
1
+ // @gemboss/gem-framework/plugins/countdown: the gem-runtime plugin "countdown" as an ES module. Generated from
2
+ // packages/gem-framework/src/runtime.ts, the same lines gem-runtime.js carries; do not edit here.
3
+ import { mountPlugin } from "../runtime-core.js";
4
+
5
+ export const key = "countdown";
6
+ export const readyEvent = "gem:countdown:ready";
7
+
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
9
+ registerPlugin('countdown', ({ api }) => {
10
+ const timers = document.querySelectorAll('[data-gem-countdown], .gem-countdown');
11
+ let bound = 0;
12
+ const pad = (value) => String(Math.max(0, value)).padStart(2, '0');
13
+ const COUNTDOWN_ISO = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/i;
14
+ const countdownEnd = (value) => {
15
+ if (typeof value !== 'string') return NaN;
16
+ const raw = value.trim();
17
+ const m = COUNTDOWN_ISO.exec(raw);
18
+ if (!m) return NaN;
19
+ const y = Number(m[1]), mo = Number(m[2]), d = Number(m[3]);
20
+ const day = new Date(Date.UTC(y, mo - 1, d));
21
+ if (day.getUTCFullYear() !== y || day.getUTCMonth() !== mo - 1 || day.getUTCDate() !== d) return NaN;
22
+ if (m[4] === undefined) return Date.parse(m[1] + '-' + m[2] + '-' + m[3] + 'T23:59:59Z');
23
+ const iso = m[1] + '-' + m[2] + '-' + m[3] + 'T' + raw.slice(11);
24
+ return Date.parse(m[7] ? iso : iso + 'Z');
25
+ };
26
+ const write = (timer, selector, value) => {
27
+ const targetNode = timer.querySelector && timer.querySelector(selector);
28
+ if (targetNode) targetNode.textContent = pad(value);
29
+ };
30
+ const clockText = (s) => {
31
+ const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
32
+ return (h ? h + ':' + pad(m) : String(m)) + ':' + pad(sec);
33
+ };
34
+ const bindDuration = (timer, total) => {
35
+ const scope = (timer.closest && timer.closest('[data-gem-section-id]')) || document;
36
+ const mode = String(timer.getAttribute('data-gem-countdown-start') || 'mount').trim() || 'mount';
37
+ const stopEvent = String(timer.getAttribute('data-gem-countdown-stop') || '').trim();
38
+ let left = total * 1000;
39
+ let endAt = 0;
40
+ let handle = null;
41
+ let state = 'idle';
42
+ let started = false;
43
+ let observer = null;
44
+ const release = () => { if (observer) { observer.disconnect(); observer = null; } };
45
+ const detail = () => ({ source: 'gem-runtime', id: timer.id || undefined, mode: 'duration', duration: total, remainingSeconds: Math.max(0, Math.ceil(left / 1000)) });
46
+ const render = () => {
47
+ const s = Math.max(0, Math.ceil(left / 1000));
48
+ timer.dataset.gemCountdownState = state;
49
+ timer.dataset.gemCountdownRemainingSeconds = String(s);
50
+ write(timer, '[data-gem-countdown-days]', Math.floor(s / 86400));
51
+ write(timer, '[data-gem-countdown-hours]', Math.floor((s % 86400) / 3600));
52
+ write(timer, '[data-gem-countdown-minutes]', Math.floor((s % 3600) / 60));
53
+ write(timer, '[data-gem-countdown-seconds]', s % 60);
54
+ const readout = timer.querySelector && timer.querySelector('[data-gem-countdown-clock]');
55
+ if (readout) readout.textContent = clockText(s);
56
+ };
57
+ const halt = () => { if (handle !== null && target.clearInterval) target.clearInterval(handle); handle = null; };
58
+ const finish = () => {
59
+ halt();
60
+ release();
61
+ left = 0;
62
+ state = 'complete';
63
+ render();
64
+ timer.setAttribute('data-gem-countdown-ended', 'true');
65
+ if (timer.dispatchEvent && timer.isConnected !== false) {
66
+ timer.dispatchEvent(new CustomEvent('gem:countdown:complete', { bubbles: true, detail: detail() }));
67
+ for (const callback of api.listeners['gem:countdown:complete'] || []) callback(detail());
68
+ } else api.emit('gem:countdown:complete', detail());
69
+ };
70
+ const tick = () => { left = endAt - Date.now(); if (left <= 0) finish(); else render(); };
71
+ const run = () => {
72
+ if (state === 'running' || state === 'complete' || state === 'stopped') return;
73
+ state = 'running';
74
+ endAt = Date.now() + left;
75
+ render();
76
+ if (target.setInterval) handle = target.setInterval(tick, 250);
77
+ if (!started) { started = true; api.emit('gem:countdown:start', detail()); }
78
+ };
79
+ const pause = (next) => {
80
+ if (state === 'running') { left = Math.max(0, endAt - Date.now()); halt(); }
81
+ if (state === 'complete' || state === 'stopped') return;
82
+ state = next;
83
+ if (next === 'stopped') release();
84
+ render();
85
+ };
86
+ render();
87
+ if (stopEvent && canListen(scope)) __gemListen(scope, stopEvent, () => pause('stopped'));
88
+ if (mode === 'visible' && target.IntersectionObserver) {
89
+ observer = new target.IntersectionObserver((entries) => {
90
+ for (const entry of entries) entry.isIntersecting ? run() : pause('paused');
91
+ });
92
+ observer.observe(timer);
93
+ } else if (mode === 'mount' || mode === 'visible') run();
94
+ else if (canListen(scope)) __gemListen(scope, mode, run);
95
+ };
96
+ timers.forEach((timer) => {
97
+ if (!mark(timer, 'countdown')) return;
98
+ const duration = Number(timer.getAttribute && timer.getAttribute('data-gem-countdown-duration'));
99
+ if (Number.isFinite(duration) && duration > 0) {
100
+ bindDuration(timer, Math.min(Math.round(duration), 86400));
101
+ bound += 1;
102
+ return;
103
+ }
104
+ const targetTime = countdownEnd(timer.getAttribute && timer.getAttribute('data-gem-countdown-target'));
105
+ const section = timer.closest ? timer.closest('[data-gem-section-id]') : null;
106
+ const band = section && section.hasAttribute && section.hasAttribute('data-gem-countdown-end') ? section : null;
107
+ const hasSecondsCell = Boolean(timer.querySelector && (timer.querySelector('[data-gem-cd-secs]') || timer.querySelector('[data-gem-countdown-seconds]')));
108
+ let handle = null;
109
+ let announced = '';
110
+ const update = () => {
111
+ if (!Number.isFinite(targetTime)) {
112
+ timer.dataset.gemCountdownState = 'invalid';
113
+ return;
114
+ }
115
+ const ms = targetTime - Date.now();
116
+ const remainingSeconds = Math.max(0, Math.floor(ms / 1000));
117
+ const days = Math.floor(remainingSeconds / 86400);
118
+ const hours = Math.floor((remainingSeconds % 86400) / 3600);
119
+ const minutes = Math.floor((remainingSeconds % 3600) / 60);
120
+ const seconds = remainingSeconds % 60;
121
+ timer.dataset.gemCountdownState = ms > 0 ? 'running' : 'complete';
122
+ timer.dataset.gemCountdownRemainingSeconds = String(remainingSeconds);
123
+ write(timer, '[data-gem-countdown-days]', days);
124
+ write(timer, '[data-gem-countdown-hours]', hours);
125
+ write(timer, '[data-gem-countdown-minutes]', minutes);
126
+ write(timer, '[data-gem-countdown-seconds]', seconds);
127
+ write(timer, '[data-gem-cd-days]', days);
128
+ write(timer, '[data-gem-cd-hours]', hours);
129
+ write(timer, '[data-gem-cd-mins]', minutes);
130
+ write(timer, '[data-gem-cd-secs]', seconds);
131
+ if (ms <= 0) {
132
+ if (band) band.setAttribute('data-gem-countdown-ended', 'true');
133
+ if (timer.setAttribute && !(timer.hasAttribute && timer.hasAttribute('data-gem-section-id'))) timer.setAttribute('data-gem-countdown-ended', 'true');
134
+ if (handle !== null && target.clearInterval) { target.clearInterval(handle); handle = null; }
135
+ }
136
+ const event = ms > 0 ? 'gem:countdown:start' : 'gem:countdown:complete';
137
+ if (event === announced && (event === 'gem:countdown:complete' || hasSecondsCell)) return;
138
+ announced = event;
139
+ api.emit(event, { source: 'gem-runtime', id: timer.id || undefined, remainingSeconds });
140
+ };
141
+ const onTheMinute = () => {
142
+ const ms = targetTime - Date.now();
143
+ if (!(ms > 0) || !target.setTimeout) return;
144
+ target.setTimeout(() => { update(); onTheMinute(); }, Math.min((ms % 60000) + 5, ms));
145
+ };
146
+ update();
147
+ if (Number.isFinite(targetTime) && targetTime - Date.now() > 0) {
148
+ if (!hasSecondsCell) onTheMinute();
149
+ else if (target.setInterval) handle = target.setInterval(update, 1000);
150
+ }
151
+ const left = targetTime - Date.now();
152
+ if (target.setTimeout && Number.isFinite(left) && left > 0 && left < 2147483647) target.setTimeout(update, left);
153
+ bound += 1;
154
+ });
155
+ api.emit('gem:countdown:ready', { source: 'gem-runtime', count: bound });
156
+ });
157
+ }
158
+
159
+ /** Bind every "countdown" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
160
+ export function mount(root, options) {
161
+ return mountPlugin(key, install, root, options);
162
+ }
163
+
164
+ export default mount;
@@ -0,0 +1,9 @@
1
+ // @gemboss/gem-framework/plugins/disclosure: types of the ES module next to this file. Generated; do not edit here.
2
+ import type { MountOptions, Unmount } from "../runtime-core.js";
3
+
4
+ export type { MountOptions, Unmount };
5
+ export declare const key: "disclosure";
6
+ export declare const readyEvent: "gem:disclosure:ready";
7
+ /** Bind every "disclosure" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
8
+ export declare function mount(root?: ParentNode | null, options?: MountOptions): Unmount;
9
+ export default mount;
@@ -0,0 +1,40 @@
1
+ // @gemboss/gem-framework/plugins/disclosure: the gem-runtime plugin "disclosure" as an ES module. Generated from
2
+ // packages/gem-framework/src/runtime.ts, the same lines gem-runtime.js carries; do not edit here.
3
+ import { mountPlugin } from "../runtime-core.js";
4
+
5
+ export const key = "disclosure";
6
+ export const readyEvent = "gem:disclosure:ready";
7
+
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
9
+ registerPlugin('disclosure', ({ api }) => {
10
+ const items = document.querySelectorAll('details[data-gem-disclosure], .gem-accordion details, .gem-tabs details, .gem-footer-shell details, [data-gem-footer-shell] details');
11
+ let bound = 0;
12
+ items.forEach((item) => {
13
+ if (!mark(item, 'disclosure')) return;
14
+ item.dataset.gemDisclosureState = item.open ? 'open' : 'closed';
15
+ if (!canListen(item)) return;
16
+ __gemListen(item, 'toggle', () => {
17
+ item.dataset.gemDisclosureState = item.open ? 'open' : 'closed';
18
+ api.emit(item.open ? 'gem:disclosure:open' : 'gem:disclosure:close', { source: 'gem-runtime', id: item.id || undefined });
19
+ });
20
+ __gemListen(item, 'keydown', (event) => {
21
+ if (event.key !== 'Escape' || !item.open) return;
22
+ event.preventDefault && event.preventDefault();
23
+ item.open = false;
24
+ item.dataset.gemDisclosureState = 'closed';
25
+ const summary = item.querySelector && item.querySelector('summary');
26
+ if (summary && summary.focus) summary.focus();
27
+ api.emit('gem:disclosure:close', { source: 'gem-runtime', id: item.id || undefined, reason: 'escape' });
28
+ });
29
+ bound += 1;
30
+ });
31
+ api.emit('gem:disclosure:ready', { source: 'gem-runtime', count: bound });
32
+ });
33
+ }
34
+
35
+ /** Bind every "disclosure" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
36
+ export function mount(root, options) {
37
+ return mountPlugin(key, install, root, options);
38
+ }
39
+
40
+ export default mount;
@@ -0,0 +1,9 @@
1
+ // @gemboss/gem-framework/plugins/hero-slideshow: types of the ES module next to this file. Generated; do not edit here.
2
+ import type { MountOptions, Unmount } from "../runtime-core.js";
3
+
4
+ export type { MountOptions, Unmount };
5
+ export declare const key: "hero-slideshow";
6
+ export declare const readyEvent: "gem:hero-slideshow:ready";
7
+ /** Bind every "hero-slideshow" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
8
+ export declare function mount(root?: ParentNode | null, options?: MountOptions): Unmount;
9
+ export default mount;
@@ -0,0 +1,57 @@
1
+ // @gemboss/gem-framework/plugins/hero-slideshow: the gem-runtime plugin "hero-slideshow" as an ES module. Generated from
2
+ // packages/gem-framework/src/runtime.ts, the same lines gem-runtime.js carries; do not edit here.
3
+ import { mountPlugin } from "../runtime-core.js";
4
+
5
+ export const key = "hero-slideshow";
6
+ export const readyEvent = "gem:hero-slideshow:ready";
7
+
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
9
+ registerPlugin('hero-slideshow', ({ api }) => {
10
+ const roots = document.querySelectorAll('[data-gem-hero-slideshow]');
11
+ let bound = 0;
12
+ roots.forEach((root) => {
13
+ if (!mark(root, 'hero-slideshow')) return;
14
+ const slides = Array.prototype.slice.call(root.querySelectorAll('[data-gem-hero-slide]'));
15
+ if (slides.length < 2) return;
16
+ const dots = Array.prototype.slice.call(root.querySelectorAll('[data-gem-hero-dot]'));
17
+ const previous = root.querySelector && root.querySelector('[data-gem-hero-prev]');
18
+ const next = root.querySelector && root.querySelector('[data-gem-hero-next]');
19
+ const pause = root.querySelector && root.querySelector('[data-gem-hero-pause]');
20
+ const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
21
+ const autoplay = !!(root.dataset && root.dataset.autoplay === 'true') && !reduce;
22
+ const interval = Math.max(Number((root.dataset && root.dataset.autoplayInterval) || 6000), 4000);
23
+ let active = 0;
24
+ for (let i = 0; i < slides.length; i += 1) { if ((slides[i].className || '').indexOf('gem-hero-slideshow__slide--active') > -1) { active = i; break; } }
25
+ let timer = null; let paused = false;
26
+ const show = (index) => {
27
+ active = (index + slides.length) % slides.length;
28
+ slides.forEach((s, i) => { const on = i === active; if (s.classList) s.classList.toggle('gem-hero-slideshow__slide--active', on); s.hidden = !on; });
29
+ dots.forEach((d, i) => { const on = i === active; if (d.classList) d.classList.toggle('gem-hero-slideshow__dot--active', on); if (d.setAttribute) { d.setAttribute('aria-selected', String(on)); d.setAttribute('tabindex', on ? '0' : '-1'); } });
30
+ api.emit('gem:hero-slideshow:change', { source: 'gem-runtime', id: root.id || undefined, index: active, count: slides.length });
31
+ };
32
+ const stop = () => { if (timer) { window.clearInterval(timer); timer = null; } };
33
+ const start = () => { stop(); if (autoplay && !paused) timer = window.setInterval(() => { show(active + 1); }, interval); };
34
+ if (canListen(previous)) __gemListen(previous, 'click', (e) => { if (e.preventDefault) e.preventDefault(); show(active - 1); start(); });
35
+ if (canListen(next)) __gemListen(next, 'click', (e) => { if (e.preventDefault) e.preventDefault(); show(active + 1); start(); });
36
+ dots.forEach((d, i) => { if (canListen(d)) __gemListen(d, 'click', () => { show(Number((d.dataset && d.dataset.slideIndex) || i)); start(); }); });
37
+ const focusDot = (i) => { const n = dots.length; if (!n) return; const k = ((i % n) + n) % n; show(k); start(); if (dots[k].focus) dots[k].focus(); };
38
+ dots.forEach((d, i) => { if (canListen(d)) __gemListen(d, 'keydown', (e) => {
39
+ const k = e.key;
40
+ if (k === 'ArrowRight' || k === 'ArrowDown') { if (e.preventDefault) e.preventDefault(); focusDot(i + 1); }
41
+ else if (k === 'ArrowLeft' || k === 'ArrowUp') { if (e.preventDefault) e.preventDefault(); focusDot(i - 1); }
42
+ else if (k === 'Home') { if (e.preventDefault) e.preventDefault(); focusDot(0); }
43
+ else if (k === 'End') { if (e.preventDefault) e.preventDefault(); focusDot(dots.length - 1); }
44
+ }); });
45
+ if (canListen(pause)) __gemListen(pause, 'click', () => { paused = !paused; if (pause.setAttribute) pause.setAttribute('aria-pressed', String(paused)); start(); });
46
+ show(active); start(); bound += 1;
47
+ });
48
+ api.emit('gem:hero-slideshow:ready', { source: 'gem-runtime', count: bound });
49
+ });
50
+ }
51
+
52
+ /** Bind every "hero-slideshow" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
53
+ export function mount(root, options) {
54
+ return mountPlugin(key, install, root, options);
55
+ }
56
+
57
+ export default mount;
@@ -0,0 +1,9 @@
1
+ // @gemboss/gem-framework/plugins/localization: types of the ES module next to this file. Generated; do not edit here.
2
+ import type { MountOptions, Unmount } from "../runtime-core.js";
3
+
4
+ export type { MountOptions, Unmount };
5
+ export declare const key: "localization";
6
+ export declare const readyEvent: "gem:localization:ready";
7
+ /** Bind every "localization" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
8
+ export declare function mount(root?: ParentNode | null, options?: MountOptions): Unmount;
9
+ export default mount;
@@ -0,0 +1,37 @@
1
+ // @gemboss/gem-framework/plugins/localization: the gem-runtime plugin "localization" as an ES module. Generated from
2
+ // packages/gem-framework/src/runtime.ts, the same lines gem-runtime.js carries; do not edit here.
3
+ import { mountPlugin } from "../runtime-core.js";
4
+
5
+ export const key = "localization";
6
+ export const readyEvent = "gem:localization:ready";
7
+
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
9
+ registerPlugin('localization', ({ api }) => {
10
+ const forms = document.querySelectorAll('form[action*="/localization"], form[action*="localization"], [data-gem-footer-localization], [data-gem-header-localization], .gem-navigation-shell__localization, .gem-shell-footer__localization, .gem-shell-header__localization-form');
11
+ let bound = 0;
12
+ forms.forEach((form) => {
13
+ if (!mark(form, 'localization')) return;
14
+ form.dataset && (form.dataset.gemLocalizationState = 'ready');
15
+ form.querySelectorAll && form.querySelectorAll('select[name="country_code"], select[name="locale_code"]').forEach((select) => {
16
+ if (!canListen(select)) return;
17
+ __gemListen(select, 'change', () => {
18
+ form.dataset && (form.dataset.gemLocalizationState = 'changed');
19
+ api.emit('gem:localization:change', { source: 'gem-runtime', name: select.name || undefined, value: select.value || undefined });
20
+ });
21
+ });
22
+ canListen(form) && __gemListen(form, 'submit', () => {
23
+ form.dataset && (form.dataset.gemLocalizationState = 'submit');
24
+ api.emit('gem:localization:submit', { source: 'gem-runtime' });
25
+ });
26
+ bound += 1;
27
+ });
28
+ api.emit('gem:localization:ready', { source: 'gem-runtime', count: bound });
29
+ });
30
+ }
31
+
32
+ /** Bind every "localization" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
33
+ export function mount(root, options) {
34
+ return mountPlugin(key, install, root, options);
35
+ }
36
+
37
+ export default mount;
@@ -0,0 +1,9 @@
1
+ // @gemboss/gem-framework/plugins/media-gallery: types of the ES module next to this file. Generated; do not edit here.
2
+ import type { MountOptions, Unmount } from "../runtime-core.js";
3
+
4
+ export type { MountOptions, Unmount };
5
+ export declare const key: "media-gallery";
6
+ export declare const readyEvent: "gem:media:ready";
7
+ /** Bind every "media-gallery" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
8
+ export declare function mount(root?: ParentNode | null, options?: MountOptions): Unmount;
9
+ export default mount;
@@ -0,0 +1,57 @@
1
+ // @gemboss/gem-framework/plugins/media-gallery: the gem-runtime plugin "media-gallery" as an ES module. Generated from
2
+ // packages/gem-framework/src/runtime.ts, the same lines gem-runtime.js carries; do not edit here.
3
+ import { mountPlugin } from "../runtime-core.js";
4
+
5
+ export const key = "media-gallery";
6
+ export const readyEvent = "gem:media:ready";
7
+
8
+ function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
9
+ registerPlugin('media-gallery', ({ api }) => {
10
+ const galleries = document.querySelectorAll('[data-gem-media-gallery], .gem-media-gallery, .gem-product-media-gallery');
11
+ let bound = 0;
12
+ galleries.forEach((gallery) => {
13
+ if (!mark(gallery, 'media-gallery')) return;
14
+ const items = gallery.querySelectorAll('[data-gem-media-item], img, video, model-viewer');
15
+ const thumbControls = gallery.querySelectorAll('[data-gem-media-thumb]');
16
+ const controls = thumbControls.length ? thumbControls : items;
17
+ gallery.dataset.gemMediaState = 'ready';
18
+ items.forEach((item, index) => {
19
+ if (!item.dataset) return;
20
+ item.dataset.gemMediaIndex = String(index);
21
+ });
22
+ const activate = (target, reason = 'click') => {
23
+ if (!target) return;
24
+ const index = target.dataset && target.dataset.gemMediaIndex ? Number(target.dataset.gemMediaIndex) : undefined;
25
+ if (!Number.isFinite(index)) return;
26
+ controls.forEach((control) => {
27
+ const isActive = control.dataset && Number(control.dataset.gemMediaIndex) === index;
28
+ if (isActive) control.setAttribute('aria-current', 'true'); else control.removeAttribute('aria-current');
29
+ if (control.dataset) control.dataset.gemMediaActive = isActive ? 'true' : 'false';
30
+ });
31
+ gallery.dataset.gemMediaActiveIndex = String(index);
32
+ api.emit('gem:media:change', { source: 'gem-runtime', index, galleryId: gallery.id || undefined, reason });
33
+ };
34
+ const firstActive = gallery.querySelector('[aria-current="true"], [data-gem-media-thumb]');
35
+ if (firstActive) activate(firstActive, 'init');
36
+ if (canListen(gallery)) __gemListen(gallery, 'click', (event) => {
37
+ const target = event.target && event.target.closest ? event.target.closest('[data-gem-media-item], img, video, model-viewer') : null;
38
+ if (!target) return;
39
+ activate(target, 'click');
40
+ });
41
+ controls.forEach((control) => { if (!canListen(control)) return; __gemListen(control, 'keydown', (event) => {
42
+ if (!['Enter', ' '].includes(event.key)) return;
43
+ event.preventDefault && event.preventDefault();
44
+ activate(control, 'keyboard');
45
+ }); });
46
+ bound += 1;
47
+ });
48
+ api.emit('gem:media:ready', { source: 'gem-runtime', count: bound });
49
+ });
50
+ }
51
+
52
+ /** Bind every "media-gallery" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
53
+ export function mount(root, options) {
54
+ return mountPlugin(key, install, root, options);
55
+ }
56
+
57
+ export default mount;