@gemboss/gem-framework 0.4.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.
@@ -0,0 +1,880 @@
1
+ (() => {
2
+ const config = {"planId":"gem-framework-package","base":"gem-native","dawnAdapter":false,"plugins":[{"key":"cart-drawer","loadStrategy":"interaction","dependsOn":[]},{"key":"mobile-menu","loadStrategy":"interaction","dependsOn":[]},{"key":"quantity-input","loadStrategy":"interaction","dependsOn":[]},{"key":"variant-selects","loadStrategy":"interaction","dependsOn":[]},{"key":"predictive-search","loadStrategy":"critical","dependsOn":[]},{"key":"localization","loadStrategy":"interaction","dependsOn":[]},{"key":"disclosure","loadStrategy":"interaction","dependsOn":[]},{"key":"tabs","loadStrategy":"interaction","dependsOn":[]},{"key":"media-gallery","loadStrategy":"viewport","dependsOn":[]},{"key":"carousel","loadStrategy":"viewport","dependsOn":[]},{"key":"hero-slideshow","loadStrategy":"idle","dependsOn":[]},{"key":"quick-view","loadStrategy":"interaction","dependsOn":[]},{"key":"promo-popup","loadStrategy":"idle","dependsOn":[]},{"key":"before_after","loadStrategy":"viewport","dependsOn":[]},{"key":"runtime-slot-loader","loadStrategy":"idle","dependsOn":[]},{"key":"sticky-atc","loadStrategy":"viewport","dependsOn":[]},{"key":"recently-viewed","loadStrategy":"idle","dependsOn":[]},{"key":"countdown","loadStrategy":"viewport","dependsOn":[]},{"key":"reveal-on-scroll","loadStrategy":"viewport","dependsOn":[]},{"key":"seasonal-effects","loadStrategy":"idle","dependsOn":[]},{"key":"context-adapter","loadStrategy":"idle","dependsOn":[]},{"key":"offer-engine","loadStrategy":"interaction","dependsOn":[]}]};
3
+ const target = window;
4
+ const existing = target.GemTheme || {};
5
+ const listeners = existing.listeners || {};
6
+ const api = {
7
+ ...existing,
8
+ config,
9
+ listeners,
10
+ on(event, callback) {
11
+ (listeners[event] ||= []).push(callback);
12
+ return () => { listeners[event] = (listeners[event] || []).filter((item) => item !== callback); };
13
+ },
14
+ emit(event, detail = {}) {
15
+ document.dispatchEvent(new CustomEvent(event, { detail }));
16
+ for (const callback of listeners[event] || []) callback(detail);
17
+ },
18
+ };
19
+ target.GemTheme = api;
20
+ const pluginRegistry = {};
21
+ const loadedPlugins = {};
22
+ const hasPlugin = (key) => config.plugins.some((plugin) => plugin.key === key);
23
+ const pluginConfig = (key) => config.plugins.find((plugin) => plugin.key === key);
24
+ const registerPlugin = (key, setup) => { pluginRegistry[key] = setup; };
25
+ const loadPlugin = (key, reason = 'direct') => {
26
+ const spec = pluginConfig(key);
27
+ if (!spec || loadedPlugins[key]) return false;
28
+ for (const dependency of spec.dependsOn || []) loadPlugin(dependency, 'dependency');
29
+ const setup = pluginRegistry[key];
30
+ if (typeof setup === 'function') setup({ api, config, reason });
31
+ loadedPlugins[key] = true;
32
+ api.emit('gem:plugin:loaded', { key, reason, loadStrategy: spec.loadStrategy });
33
+ return true;
34
+ };
35
+ const onInteraction = (callback) => {
36
+ const events = ['click', 'focusin', 'submit', 'input', 'change'];
37
+ const handler = (event) => {
38
+ events.forEach((name) => document.removeEventListener && document.removeEventListener(name, handler, true));
39
+ callback(event);
40
+ };
41
+ events.forEach((name) => canListen(document) && document.addEventListener(name, handler, true));
42
+ };
43
+ const onIdle = (callback) => {
44
+ if (target.requestIdleCallback) target.requestIdleCallback(callback);
45
+ else setTimeout(callback, 0);
46
+ };
47
+ const onViewport = (callback) => {
48
+ if (!target.IntersectionObserver) { setTimeout(callback, 0); return; }
49
+ const sentinel = document.querySelector('[data-gem-plugin], [data-gem-component], .gem-section') || document.documentElement;
50
+ const observer = new target.IntersectionObserver((entries) => {
51
+ if (!entries.some((entry) => entry.isIntersecting)) return;
52
+ observer.disconnect();
53
+ callback();
54
+ }, { rootMargin: '160px' });
55
+ observer.observe(sentinel);
56
+ };
57
+ const schedulePlugin = (plugin) => {
58
+ if (!plugin || loadedPlugins[plugin.key]) return;
59
+ api.emit('gem:plugin:scheduled', { key: plugin.key, loadStrategy: plugin.loadStrategy });
60
+ if (plugin.loadStrategy === 'critical') loadPlugin(plugin.key, 'critical');
61
+ else if (plugin.loadStrategy === 'idle') onIdle(() => loadPlugin(plugin.key, 'idle'));
62
+ else if (plugin.loadStrategy === 'viewport') onViewport(() => loadPlugin(plugin.key, 'viewport'));
63
+ else onInteraction(() => loadPlugin(plugin.key, 'interaction'));
64
+ };
65
+ const canListen = (element) => Boolean(element && typeof element.addEventListener === 'function');
66
+ const registerNoopPlugin = (key, readyEvent) => {
67
+ registerPlugin(key, ({ api }) => {
68
+ if (readyEvent) api.emit(readyEvent, { source: 'gem-runtime' });
69
+ });
70
+ };
71
+ registerPlugin('cart-drawer', ({ api }) => {
72
+ const drawers = document.querySelectorAll('[data-gem-cart-drawer], [data-gem-cart-canvas], .gem-cart-drawer');
73
+ const cartLinks = document.querySelectorAll('[data-gem-cart-drawer-trigger], [data-gem-cart-toggle], a[href*="/cart"]');
74
+ const cartForms = document.querySelectorAll('form[action*="/cart/add"], form[action*="/cart"]');
75
+ const drawer = drawers[0];
76
+ const setDrawerA11y = (isOpen) => {
77
+ if (!drawer) return;
78
+ drawer.setAttribute && drawer.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
79
+ const panel = drawer.querySelector && drawer.querySelector('[data-gem-cart-canvas-panel], .gem-cart-drawer__panel');
80
+ if (panel && panel.setAttribute) panel.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
81
+ };
82
+ const openDrawer = (reason, href) => {
83
+ if (drawer && drawer.tagName === 'DETAILS') drawer.open = true;
84
+ if (drawer && drawer.dataset) drawer.dataset.gemCartDrawerState = 'open';
85
+ setDrawerA11y(true);
86
+ api.emit('gem:cart:open', { source: 'gem-runtime', reason, href });
87
+ };
88
+ const closeDrawer = (reason) => {
89
+ if (drawer && drawer.tagName === 'DETAILS') drawer.open = false;
90
+ if (drawer && drawer.dataset) drawer.dataset.gemCartDrawerState = 'closed';
91
+ setDrawerA11y(false);
92
+ api.emit('gem:cart:close', { source: 'gem-runtime', reason });
93
+ };
94
+ let bound = 0;
95
+ drawers.forEach((drawer) => {
96
+ if (!mark(drawer, 'cart-drawer')) return;
97
+ drawer.dataset.gemCartDrawerState = drawer.hasAttribute('open') ? 'open' : 'ready';
98
+ setDrawerA11y(drawer.hasAttribute('open'));
99
+ drawer.querySelectorAll('[data-gem-cart-drawer-close], [data-gem-cart-canvas-close]').forEach((control) => { if (!canListen(control)) return; control.addEventListener('click', (event) => {
100
+ event.preventDefault && event.preventDefault();
101
+ closeDrawer('close-control');
102
+ }); });
103
+ if (canListen(drawer)) drawer.addEventListener('toggle', () => { drawer.dataset.gemCartDrawerState = drawer.hasAttribute('open') ? 'open' : 'closed'; });
104
+ bound += 1;
105
+ });
106
+ cartLinks.forEach((link) => {
107
+ if (!mark(link, 'cart-link')) return;
108
+ if (!canListen(link)) return;
109
+ link.addEventListener('click', (event) => {
110
+ if (drawer && (link.hasAttribute('data-gem-cart-drawer-trigger') || link.hasAttribute('data-gem-cart-toggle'))) event.preventDefault && event.preventDefault();
111
+ openDrawer('cart-link', link.href || undefined);
112
+ });
113
+ bound += 1;
114
+ });
115
+ if (canListen(document)) document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && drawer && drawer.hasAttribute('open')) closeDrawer('escape'); });
116
+ cartForms.forEach((form) => {
117
+ if (!mark(form, 'cart-form')) return;
118
+ if (!canListen(form)) return;
119
+ form.addEventListener('submit', () => api.emit('gem:cart:submit', { source: 'gem-runtime', action: form.getAttribute('action') || undefined }));
120
+ bound += 1;
121
+ });
122
+ api.emit('gem:cart:ready', { source: 'gem-runtime', count: bound });
123
+ });
124
+ registerPlugin('mobile-menu', ({ api }) => {
125
+ const menus = document.querySelectorAll('[data-gem-mobile-menu], details.gem-shell-header__drawer');
126
+ let bound = 0;
127
+ menus.forEach((menu) => {
128
+ if (!mark(menu, 'mobile-menu')) return;
129
+ const setMenuA11y = () => {
130
+ menu.setAttribute && menu.setAttribute('aria-expanded', menu.open ? 'true' : 'false');
131
+ const panel = menu.querySelector && menu.querySelector('[data-gem-mobile-menu-panel], .gem-shell-header__drawer-panel');
132
+ if (panel && panel.setAttribute) panel.setAttribute('aria-hidden', menu.open ? 'false' : 'true');
133
+ };
134
+ menu.dataset.gemMenuState = menu.open ? 'open' : 'closed';
135
+ setMenuA11y();
136
+ if (!canListen(menu)) return;
137
+ menu.addEventListener('toggle', () => {
138
+ menu.dataset.gemMenuState = menu.open ? 'open' : 'closed';
139
+ setMenuA11y();
140
+ if (menu.open) {
141
+ const panel = menu.querySelector && menu.querySelector('[data-gem-mobile-menu-panel], .gem-shell-header__drawer-panel');
142
+ const firstFocusable = panel && panel.querySelector && panel.querySelector('a[href], button, select, input, textarea, summary, [tabindex]:not([tabindex="-1"])');
143
+ if (firstFocusable && firstFocusable.focus) firstFocusable.focus();
144
+ }
145
+ api.emit(menu.open ? 'gem:menu:open' : 'gem:menu:close', { source: 'gem-runtime', id: menu.id || undefined });
146
+ });
147
+ menu.addEventListener('keydown', (event) => {
148
+ if (event.key !== 'Escape' || !menu.open) return;
149
+ menu.open = false;
150
+ menu.dataset.gemMenuState = 'closed';
151
+ setMenuA11y();
152
+ const summary = menu.querySelector && menu.querySelector('summary');
153
+ if (summary && summary.focus) summary.focus();
154
+ });
155
+ bound += 1;
156
+ });
157
+ api.emit('gem:menu:ready', { source: 'gem-runtime', count: bound });
158
+ });
159
+ registerPlugin('quantity-input', ({ api }) => {
160
+ const controls = document.querySelectorAll('[data-gem-quantity-input], .gem-quantity-input');
161
+ let bound = 0;
162
+ const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
163
+ controls.forEach((control) => {
164
+ if (!mark(control, 'quantity-input')) return;
165
+ const input = control.querySelector && control.querySelector('input[type="number"]');
166
+ if (!input) return;
167
+ const setValue = (nextValue, reason) => {
168
+ const min = Number.isFinite(Number(input.min)) ? Number(input.min) : 0;
169
+ const max = Number.isFinite(Number(input.max)) && input.max !== '' ? Number(input.max) : Number.MAX_SAFE_INTEGER;
170
+ const value = clamp(Number(nextValue) || min, min, max);
171
+ input.value = String(value);
172
+ api.emit('gem:quantity:change', { source: 'gem-runtime', value, reason, name: input.name || undefined });
173
+ };
174
+ if (!canListen(control) || !canListen(input)) return;
175
+ control.addEventListener('click', (event) => {
176
+ const button = event.target && event.target.closest ? event.target.closest('[data-gem-quantity-change]') : null;
177
+ if (!button) return;
178
+ event.preventDefault && event.preventDefault();
179
+ const delta = Number(button.getAttribute('data-gem-quantity-change') || '0');
180
+ setValue(Number(input.value || 0) + delta, delta > 0 ? 'increase' : 'decrease');
181
+ });
182
+ input.addEventListener('change', () => setValue(input.value, 'input'));
183
+ bound += 1;
184
+ });
185
+ api.emit('gem:quantity:ready', { source: 'gem-runtime', count: bound });
186
+ });
187
+ registerPlugin('variant-selects', ({ api }) => {
188
+ const selects = document.querySelectorAll('[data-gem-variant-select], select[name="id"]');
189
+ let bound = 0;
190
+ selects.forEach((select) => {
191
+ if (!mark(select, 'variant-selects')) return;
192
+ let hasInitialized = false;
193
+ const update = () => {
194
+ const option = select.options && select.options[select.selectedIndex];
195
+ const available = !option || option.getAttribute('data-gem-variant-available') !== 'false';
196
+ const price = option && option.getAttribute('data-gem-variant-price');
197
+ const mediaId = option && option.getAttribute('data-gem-variant-media-id');
198
+ const form = select.closest && select.closest('form');
199
+ const scope = (form && form.closest && form.closest('[data-gem-section-id], .gem-section, [data-qa-surface]')) || (select.closest && select.closest('[data-gem-section-id], .gem-section, [data-qa-surface]')) || document;
200
+ const submit = form && form.querySelector && form.querySelector('[type="submit"]');
201
+ const status = form && form.querySelector && form.querySelector('[data-gem-variant-status]');
202
+ const priceOutput = scope && scope.querySelector && scope.querySelector('[data-gem-variant-price-output]');
203
+ const availabilityOutput = scope && scope.querySelector && scope.querySelector('[data-gem-variant-availability]');
204
+ if (form && form.dataset) form.dataset.gemSelectedVariantId = String(select.value || '');
205
+ if (form && form.dataset && price) form.dataset.gemSelectedVariantPrice = price;
206
+ if (form && form.dataset && mediaId) form.dataset.gemSelectedMediaId = mediaId;
207
+ if (submit) submit.disabled = !available;
208
+ if (submit) submit.setAttribute('aria-disabled', available ? 'false' : 'true');
209
+ if (status) status.textContent = available ? 'Selected variant available' : 'Selected variant unavailable';
210
+ if (hasInitialized && price && priceOutput) priceOutput.textContent = price;
211
+ if (availabilityOutput) {
212
+ availabilityOutput.textContent = available ? 'In stock' : 'Sold out';
213
+ availabilityOutput.classList && availabilityOutput.classList.toggle('gem-product-main__availability--in-stock', available);
214
+ availabilityOutput.classList && availabilityOutput.classList.toggle('gem-product-main__availability--sold-out', !available);
215
+ }
216
+ if (mediaId) {
217
+ const escapedMediaId = String(mediaId).replace(/["\\]/g, '\\$&');
218
+ const gallery = (scope && scope.querySelector && scope.querySelector('[data-gem-media-gallery], .gem-product-media-gallery')) || document.querySelector('[data-gem-media-gallery], .gem-product-media-gallery');
219
+ const thumb = gallery && gallery.querySelector && gallery.querySelector('[data-gem-media-id="' + escapedMediaId + '"]');
220
+ const mediaIndex = thumb && thumb.getAttribute && thumb.getAttribute('data-gem-media-index');
221
+ if (thumb && typeof thumb.click === 'function') thumb.click();
222
+ if (gallery && mediaIndex !== null && mediaIndex !== undefined) {
223
+ gallery.dataset && (gallery.dataset.gemMediaActiveIndex = String(mediaIndex));
224
+ gallery.querySelectorAll && gallery.querySelectorAll('[data-gem-media-thumb]').forEach((control) => {
225
+ const isActive = control.getAttribute && control.getAttribute('data-gem-media-index') === String(mediaIndex);
226
+ if (isActive) control.setAttribute('aria-current', 'true'); else control.removeAttribute('aria-current');
227
+ if (control.dataset) control.dataset.gemMediaActive = isActive ? 'true' : 'false';
228
+ });
229
+ }
230
+ }
231
+ api.emit('gem:variant:change', { source: 'gem-runtime', variantId: select.value || undefined, available, price, mediaId });
232
+ hasInitialized = true;
233
+ };
234
+ if (!canListen(select)) return;
235
+ select.addEventListener('change', update);
236
+ update();
237
+ bound += 1;
238
+ });
239
+ api.emit('gem:variant:ready', { source: 'gem-runtime', count: bound });
240
+ });
241
+ registerPlugin('predictive-search', ({ api }) => {
242
+ const forms = document.querySelectorAll('[data-gem-predictive-search], [data-gem-search-canvas], form[action*="/search"], form[role="search"]');
243
+ const toggles = document.querySelectorAll('[data-gem-search-toggle]');
244
+ const canvas = document.querySelector('[data-gem-search-canvas]');
245
+ let bound = 0;
246
+ const setCanvasState = (state, reason = 'api') => {
247
+ if (!canvas || !canvas.dataset) return false;
248
+ canvas.dataset.gemSearchCanvasState = state;
249
+ canvas.setAttribute && canvas.setAttribute('aria-hidden', state === 'open' ? 'false' : 'true');
250
+ if (state === 'open') {
251
+ const input = canvas.querySelector && canvas.querySelector('input[type="search"], input[name="q"]');
252
+ if (input && input.focus) input.focus();
253
+ }
254
+ api.emit(state === 'open' ? 'gem:search:open' : 'gem:search:close', { source: 'gem-runtime', reason });
255
+ return true;
256
+ };
257
+ forms.forEach((form) => {
258
+ if (!mark(form, 'predictive-search')) return;
259
+ const input = form.querySelector && form.querySelector('input[type="search"], input[name="q"]');
260
+ const results = form.querySelector && form.querySelector('[data-gem-predictive-search-results]');
261
+ const setState = (state, value = '') => {
262
+ if (form.dataset) form.dataset.gemPredictiveSearchState = state;
263
+ if (results) {
264
+ results.hidden = state === 'closed';
265
+ if (results.dataset) results.dataset.gemPredictiveSearchState = state;
266
+ results.setAttribute && results.setAttribute('aria-hidden', state === 'closed' ? 'true' : 'false');
267
+ if (state === 'typing') results.textContent = value ? 'Search suggestions for ' + value : 'Start typing to search';
268
+ if (state === 'empty') results.textContent = 'Start typing to search';
269
+ }
270
+ };
271
+ if (results) {
272
+ results.hidden = true;
273
+ results.setAttribute && results.setAttribute('role', 'status');
274
+ results.setAttribute && results.setAttribute('aria-live', 'polite');
275
+ }
276
+ setState('closed');
277
+ if (canListen(input)) input.addEventListener('focus', () => { setState(input.value ? 'typing' : 'empty', input.value || ''); api.emit('gem:search:open', { source: 'gem-runtime', value: input.value || '' }); });
278
+ const closeOnEscape = (event) => { if (event.key === 'Escape') { event.preventDefault && event.preventDefault(); event.stopPropagation && event.stopPropagation(); setState('closed'); api.emit('gem:search:close', { source: 'gem-runtime', reason: 'escape' }); } };
279
+ if (canListen(input)) input.addEventListener('input', () => { setState(input.value ? 'typing' : 'empty', input.value || ''); api.emit('gem:search:input', { source: 'gem-runtime', value: input.value || '' }); });
280
+ if (canListen(input)) input.addEventListener('keydown', closeOnEscape);
281
+ if (canListen(form)) form.addEventListener('keydown', closeOnEscape);
282
+ if (canListen(form)) form.addEventListener('submit', () => { setState('submit', input && input.value || ''); api.emit('gem:search:submit', { source: 'gem-runtime', value: input && input.value || '' }); });
283
+ bound += 1;
284
+ });
285
+ if (canvas) {
286
+ canvas.dataset && (canvas.dataset.gemSearchCanvasState = 'closed');
287
+ canvas.setAttribute && canvas.setAttribute('aria-hidden', 'true');
288
+ if (mark(canvas, 'search-canvas')) bound += 1;
289
+ if (canListen(canvas)) canvas.addEventListener('keydown', (event) => { if (event.key === 'Escape') setCanvasState('closed', 'escape'); });
290
+ }
291
+ toggles.forEach((toggle) => {
292
+ if (!mark(toggle, 'search-toggle')) return;
293
+ if (!canListen(toggle)) return;
294
+ toggle.addEventListener('click', (event) => {
295
+ if (canvas) event.preventDefault && event.preventDefault();
296
+ setCanvasState('open', 'toggle');
297
+ });
298
+ bound += 1;
299
+ });
300
+ api.emit('gem:search:ready', { source: 'gem-runtime', count: bound });
301
+ });
302
+ registerPlugin('localization', ({ api }) => {
303
+ 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');
304
+ let bound = 0;
305
+ forms.forEach((form) => {
306
+ if (!mark(form, 'localization')) return;
307
+ form.dataset && (form.dataset.gemLocalizationState = 'ready');
308
+ form.querySelectorAll && form.querySelectorAll('select[name="country_code"], select[name="locale_code"]').forEach((select) => {
309
+ if (!canListen(select)) return;
310
+ select.addEventListener('change', () => {
311
+ form.dataset && (form.dataset.gemLocalizationState = 'changed');
312
+ api.emit('gem:localization:change', { source: 'gem-runtime', name: select.name || undefined, value: select.value || undefined });
313
+ });
314
+ });
315
+ canListen(form) && form.addEventListener('submit', () => {
316
+ form.dataset && (form.dataset.gemLocalizationState = 'submit');
317
+ api.emit('gem:localization:submit', { source: 'gem-runtime' });
318
+ });
319
+ bound += 1;
320
+ });
321
+ api.emit('gem:localization:ready', { source: 'gem-runtime', count: bound });
322
+ });
323
+ registerPlugin('disclosure', ({ api }) => {
324
+ const items = document.querySelectorAll('details[data-gem-disclosure], .gem-accordion details, .gem-tabs details, .gem-footer-shell details, [data-gem-footer-shell] details');
325
+ let bound = 0;
326
+ items.forEach((item) => {
327
+ if (!mark(item, 'disclosure')) return;
328
+ item.dataset.gemDisclosureState = item.open ? 'open' : 'closed';
329
+ if (!canListen(item)) return;
330
+ item.addEventListener('toggle', () => {
331
+ item.dataset.gemDisclosureState = item.open ? 'open' : 'closed';
332
+ api.emit(item.open ? 'gem:disclosure:open' : 'gem:disclosure:close', { source: 'gem-runtime', id: item.id || undefined });
333
+ });
334
+ item.addEventListener('keydown', (event) => {
335
+ if (event.key !== 'Escape' || !item.open) return;
336
+ event.preventDefault && event.preventDefault();
337
+ item.open = false;
338
+ item.dataset.gemDisclosureState = 'closed';
339
+ const summary = item.querySelector && item.querySelector('summary');
340
+ if (summary && summary.focus) summary.focus();
341
+ api.emit('gem:disclosure:close', { source: 'gem-runtime', id: item.id || undefined, reason: 'escape' });
342
+ });
343
+ bound += 1;
344
+ });
345
+ api.emit('gem:disclosure:ready', { source: 'gem-runtime', count: bound });
346
+ });
347
+ registerPlugin('tabs', ({ api }) => {
348
+ const roots = document.querySelectorAll('[data-gem-tabs]');
349
+ let bound = 0;
350
+ roots.forEach((root) => {
351
+ if (!mark(root, 'tabs')) return;
352
+ const tabs = Array.prototype.slice.call(root.querySelectorAll('[data-gem-tab]'));
353
+ const panels = Array.prototype.slice.call(root.querySelectorAll('[data-gem-panel]'));
354
+ if (!tabs.length || !panels.length) return;
355
+ const select = (key, userInitiated) => {
356
+ tabs.forEach((t) => t.setAttribute('aria-selected', String(t.getAttribute('data-gem-tab') === key)));
357
+ let active = null;
358
+ panels.forEach((p) => {
359
+ if (p.getAttribute('data-gem-panel') === key) { p.removeAttribute('hidden'); active = p; }
360
+ else p.setAttribute('hidden', '');
361
+ });
362
+ if (userInitiated && active && active.getBoundingClientRect) {
363
+ const r = active.getBoundingClientRect();
364
+ const vh = window.innerHeight || document.documentElement.clientHeight;
365
+ if (!(r.top < vh * 0.85 && r.bottom > vh * 0.15) && active.scrollIntoView) {
366
+ active.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
367
+ }
368
+ }
369
+ api.emit('gem:tabs:change', { source: 'gem-runtime', key: key });
370
+ };
371
+ tabs.forEach((t) => {
372
+ if (!canListen(t)) return;
373
+ t.addEventListener('click', () => select(t.getAttribute('data-gem-tab'), true));
374
+ });
375
+ const initial = tabs.filter((t) => t.getAttribute('aria-selected') === 'true')[0] || tabs[0];
376
+ select(initial.getAttribute('data-gem-tab'), false);
377
+ bound += 1;
378
+ });
379
+ api.emit('gem:tabs:ready', { source: 'gem-runtime', count: bound });
380
+ });
381
+ registerPlugin('media-gallery', ({ api }) => {
382
+ const galleries = document.querySelectorAll('[data-gem-media-gallery], .gem-media-gallery, .gem-product-media-gallery');
383
+ let bound = 0;
384
+ galleries.forEach((gallery) => {
385
+ if (!mark(gallery, 'media-gallery')) return;
386
+ const items = gallery.querySelectorAll('[data-gem-media-item], img, video, model-viewer');
387
+ const thumbControls = gallery.querySelectorAll('[data-gem-media-thumb]');
388
+ const controls = thumbControls.length ? thumbControls : items;
389
+ gallery.dataset.gemMediaState = 'ready';
390
+ items.forEach((item, index) => {
391
+ if (!item.dataset) return;
392
+ item.dataset.gemMediaIndex = String(index);
393
+ });
394
+ const activate = (target, reason = 'click') => {
395
+ if (!target) return;
396
+ const index = target.dataset && target.dataset.gemMediaIndex ? Number(target.dataset.gemMediaIndex) : undefined;
397
+ if (!Number.isFinite(index)) return;
398
+ controls.forEach((control) => {
399
+ const isActive = control.dataset && Number(control.dataset.gemMediaIndex) === index;
400
+ if (isActive) control.setAttribute('aria-current', 'true'); else control.removeAttribute('aria-current');
401
+ if (control.dataset) control.dataset.gemMediaActive = isActive ? 'true' : 'false';
402
+ });
403
+ gallery.dataset.gemMediaActiveIndex = String(index);
404
+ api.emit('gem:media:change', { source: 'gem-runtime', index, galleryId: gallery.id || undefined, reason });
405
+ };
406
+ const firstActive = gallery.querySelector('[aria-current="true"], [data-gem-media-thumb]');
407
+ if (firstActive) activate(firstActive, 'init');
408
+ if (canListen(gallery)) gallery.addEventListener('click', (event) => {
409
+ const target = event.target && event.target.closest ? event.target.closest('[data-gem-media-item], img, video, model-viewer') : null;
410
+ if (!target) return;
411
+ activate(target, 'click');
412
+ });
413
+ controls.forEach((control) => { if (!canListen(control)) return; control.addEventListener('keydown', (event) => {
414
+ if (!['Enter', ' '].includes(event.key)) return;
415
+ event.preventDefault && event.preventDefault();
416
+ activate(control, 'keyboard');
417
+ }); });
418
+ bound += 1;
419
+ });
420
+ api.emit('gem:media:ready', { source: 'gem-runtime', count: bound });
421
+ });
422
+ registerPlugin('carousel', ({ api }) => {
423
+ const carousels = document.querySelectorAll('[data-gem-carousel], .gem-carousel');
424
+ let bound = 0;
425
+ carousels.forEach((carousel) => {
426
+ if (!mark(carousel, 'carousel')) return;
427
+ const viewport = carousel.querySelector && (carousel.querySelector('[data-gem-carousel-viewport]') || carousel.querySelector('.gem-carousel__viewport'));
428
+ const slides = carousel.querySelectorAll('[data-gem-carousel-slide], .gem-carousel__slide');
429
+ const previous = carousel.querySelector && carousel.querySelector('[data-gem-carousel-prev]');
430
+ const next = carousel.querySelector && carousel.querySelector('[data-gem-carousel-next]');
431
+ const slideCount = slides.length || 0;
432
+ const currentIndex = () => {
433
+ if (!viewport || !slideCount) return 0;
434
+ const width = Number(viewport.clientWidth || viewport.offsetWidth || 1);
435
+ return Math.max(0, Math.min(slideCount - 1, Math.round(Number(viewport.scrollLeft || 0) / width)));
436
+ };
437
+ const setIndex = (index, reason = 'api') => {
438
+ const nextIndex = Math.max(0, Math.min(slideCount - 1, Number.isFinite(index) ? index : 0));
439
+ carousel.dataset.gemCarouselIndex = String(nextIndex);
440
+ carousel.dataset.gemCarouselCount = String(slideCount);
441
+ if (viewport && viewport.scrollTo) viewport.scrollTo({ left: Number(viewport.clientWidth || viewport.offsetWidth || 0) * nextIndex, behavior: 'smooth' });
442
+ api.emit('gem:carousel:change', { source: 'gem-runtime', id: carousel.id || undefined, index: nextIndex, count: slideCount, reason });
443
+ };
444
+ slides.forEach((slide, index) => { if (slide.dataset) slide.dataset.gemCarouselIndex = String(index); });
445
+ carousel.dataset.gemCarouselState = slideCount > 1 ? 'ready' : 'single';
446
+ carousel.dataset.gemCarouselCount = String(slideCount);
447
+ carousel.dataset.gemCarouselIndex = String(currentIndex());
448
+ if (canListen(previous)) previous.addEventListener('click', (event) => { event.preventDefault && event.preventDefault(); setIndex(currentIndex() - 1, 'previous'); });
449
+ if (canListen(next)) next.addEventListener('click', (event) => { event.preventDefault && event.preventDefault(); setIndex(currentIndex() + 1, 'next'); });
450
+ if (canListen(viewport)) viewport.addEventListener('scroll', () => { carousel.dataset.gemCarouselIndex = String(currentIndex()); }, { passive: true });
451
+ bound += 1;
452
+ });
453
+ api.emit('gem:carousel:ready', { source: 'gem-runtime', count: bound });
454
+ });
455
+ registerPlugin('hero-slideshow', ({ api }) => {
456
+ const roots = document.querySelectorAll('[data-gem-hero-slideshow]');
457
+ let bound = 0;
458
+ roots.forEach((root) => {
459
+ if (!mark(root, 'hero-slideshow')) return;
460
+ const slides = Array.prototype.slice.call(root.querySelectorAll('[data-gem-hero-slide]'));
461
+ if (slides.length < 2) return;
462
+ const dots = Array.prototype.slice.call(root.querySelectorAll('[data-gem-hero-dot]'));
463
+ const previous = root.querySelector && root.querySelector('[data-gem-hero-prev]');
464
+ const next = root.querySelector && root.querySelector('[data-gem-hero-next]');
465
+ const pause = root.querySelector && root.querySelector('[data-gem-hero-pause]');
466
+ const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
467
+ const autoplay = !!(root.dataset && root.dataset.autoplay === 'true') && !reduce;
468
+ const interval = Math.max(Number((root.dataset && root.dataset.autoplayInterval) || 6000), 4000);
469
+ let active = 0;
470
+ for (let i = 0; i < slides.length; i += 1) { if ((slides[i].className || '').indexOf('gem-hero-slideshow__slide--active') > -1) { active = i; break; } }
471
+ let timer = null; let paused = false;
472
+ const show = (index) => {
473
+ active = (index + slides.length) % slides.length;
474
+ slides.forEach((s, i) => { const on = i === active; if (s.classList) s.classList.toggle('gem-hero-slideshow__slide--active', on); s.hidden = !on; });
475
+ 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'); } });
476
+ api.emit('gem:hero-slideshow:change', { source: 'gem-runtime', id: root.id || undefined, index: active, count: slides.length });
477
+ };
478
+ const stop = () => { if (timer) { window.clearInterval(timer); timer = null; } };
479
+ const start = () => { stop(); if (autoplay && !paused) timer = window.setInterval(() => { show(active + 1); }, interval); };
480
+ if (canListen(previous)) previous.addEventListener('click', (e) => { if (e.preventDefault) e.preventDefault(); show(active - 1); start(); });
481
+ if (canListen(next)) next.addEventListener('click', (e) => { if (e.preventDefault) e.preventDefault(); show(active + 1); start(); });
482
+ dots.forEach((d, i) => { if (canListen(d)) d.addEventListener('click', () => { show(Number((d.dataset && d.dataset.slideIndex) || i)); start(); }); });
483
+ 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(); };
484
+ dots.forEach((d, i) => { if (canListen(d)) d.addEventListener('keydown', (e) => {
485
+ const k = e.key;
486
+ if (k === 'ArrowRight' || k === 'ArrowDown') { if (e.preventDefault) e.preventDefault(); focusDot(i + 1); }
487
+ else if (k === 'ArrowLeft' || k === 'ArrowUp') { if (e.preventDefault) e.preventDefault(); focusDot(i - 1); }
488
+ else if (k === 'Home') { if (e.preventDefault) e.preventDefault(); focusDot(0); }
489
+ else if (k === 'End') { if (e.preventDefault) e.preventDefault(); focusDot(dots.length - 1); }
490
+ }); });
491
+ if (canListen(pause)) pause.addEventListener('click', () => { paused = !paused; if (pause.setAttribute) pause.setAttribute('aria-pressed', String(paused)); start(); });
492
+ show(active); start(); bound += 1;
493
+ });
494
+ api.emit('gem:hero-slideshow:ready', { source: 'gem-runtime', count: bound });
495
+ });
496
+ registerPlugin('quick-view', ({ api }) => {
497
+ const triggers = document.querySelectorAll('[data-gem-quick-view], [data-gem-action="quick-view"]');
498
+ const surface = document.querySelector('[data-gem-quick-view-surface], .gem-quick-view, .gem-modal[data-gem-component="quick-view"]');
499
+ let bound = 0;
500
+ const close = (reason = 'close-control') => {
501
+ if (surface) {
502
+ surface.dataset.gemQuickViewState = 'closed';
503
+ surface.removeAttribute && surface.removeAttribute('open');
504
+ surface.setAttribute && surface.setAttribute('aria-hidden', 'true');
505
+ }
506
+ api.emit('gem:quick-view:close', { source: 'gem-runtime', reason });
507
+ };
508
+ const openSurface = (detail) => {
509
+ if (surface) {
510
+ surface.dataset.gemQuickViewState = 'open';
511
+ surface.dataset.gemProductHandle = detail.productHandle || '';
512
+ surface.dataset.gemProductUrl = detail.productUrl || '';
513
+ surface.setAttribute && surface.setAttribute('open', '');
514
+ surface.setAttribute && surface.setAttribute('aria-hidden', 'false');
515
+ surface.setAttribute && surface.setAttribute('role', 'dialog');
516
+ surface.setAttribute && surface.setAttribute('aria-modal', 'true');
517
+ const content = surface.querySelector && surface.querySelector('[data-gem-quick-view-content]');
518
+ if (content && content.textContent !== undefined) content.textContent = 'Quick view for ' + (detail.productHandle || 'product');
519
+ else if (surface.textContent !== undefined) surface.textContent = 'Quick view for ' + (detail.productHandle || 'product');
520
+ const closeButton = surface.querySelector && surface.querySelector('[data-gem-quick-view-close]');
521
+ if (closeButton && closeButton.focus) closeButton.focus();
522
+ }
523
+ api.emit('gem:quick-view:open', { source: 'gem-runtime', ...detail, hasSurface: Boolean(surface) });
524
+ };
525
+ triggers.forEach((trigger) => {
526
+ if (!mark(trigger, 'quick-view')) return;
527
+ if (!canListen(trigger)) return;
528
+ trigger.addEventListener('click', (event) => {
529
+ const productUrl = trigger.getAttribute && (trigger.getAttribute('data-gem-product-url') || trigger.getAttribute('href'));
530
+ const productHandle = trigger.getAttribute && trigger.getAttribute('data-gem-product-handle');
531
+ if (!productUrl && !productHandle) return;
532
+ event.preventDefault && event.preventDefault();
533
+ openSurface({ productUrl, productHandle });
534
+ });
535
+ bound += 1;
536
+ });
537
+ if (surface && mark(surface, 'quick-view-surface')) {
538
+ surface.dataset.gemQuickViewState = 'closed';
539
+ surface.setAttribute && surface.setAttribute('aria-hidden', 'true');
540
+ if (canListen(surface)) surface.addEventListener('click', (event) => {
541
+ const target = event.target;
542
+ if (target && target.closest && target.closest('[data-gem-quick-view-close]')) close('close-control');
543
+ });
544
+ if (canListen(surface)) surface.addEventListener('keydown', (event) => { if (event.key === 'Escape') close('escape'); });
545
+ }
546
+ api.quickView = { open: (detail = {}) => openSurface({ ...detail }), close };
547
+ api.emit('gem:quick-view:ready', { source: 'gem-runtime', count: bound, hasSurface: Boolean(surface) });
548
+ });
549
+ registerPlugin('promo-popup', ({ api }) => {
550
+ const popups = document.querySelectorAll('[data-gem-promo-popup], .gem-promo-popup');
551
+ let bound = 0;
552
+ const storagePrefix = 'gem:promo-popup-dismissed:';
553
+ 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'); }
556
+ catch { return 0; }
557
+ };
558
+ const writeDismissedUntil = (key, timestamp) => {
559
+ try { if (target.localStorage) target.localStorage.setItem(storagePrefix + key, String(timestamp)); }
560
+ catch { /* storage can be unavailable in private contexts */ }
561
+ };
562
+ popups.forEach((popup, index) => {
563
+ if (!mark(popup, 'promo-popup')) return;
564
+ const key = popup.id || popup.getAttribute && popup.getAttribute('data-gem-section-library-id') || String(index);
565
+ const delaySeconds = Number(popup.getAttribute && (popup.getAttribute('data-gem-promo-delay') || '0'));
566
+ const frequencyDays = Number(popup.getAttribute && (popup.getAttribute('data-gem-promo-frequency-days') || '7'));
567
+ const dialog = popup.querySelector && popup.querySelector('[data-gem-promo-popup-dialog]');
568
+ 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; }
571
+ popup.hidden = false;
572
+ if (dialog) dialog.hidden = false;
573
+ if (overlay) overlay.hidden = false;
574
+ popup.dataset.gemPromoPopupState = 'open';
575
+ api.emit('gem:promo-popup:open', { source: 'gem-runtime', id: popup.id || undefined, reason });
576
+ return true;
577
+ };
578
+ const close = (reason = 'dismiss') => {
579
+ const days = Number.isFinite(frequencyDays) ? Math.max(1, frequencyDays) : 7;
580
+ writeDismissedUntil(key, now() + days * 86400000);
581
+ if (dialog) dialog.hidden = true;
582
+ if (overlay) overlay.hidden = true;
583
+ popup.dataset.gemPromoPopupState = 'closed';
584
+ api.emit('gem:promo-popup:close', { source: 'gem-runtime', id: popup.id || undefined, reason });
585
+ };
586
+ popup.querySelectorAll && popup.querySelectorAll('[data-gem-promo-popup-dismiss]').forEach((button) => {
587
+ if (!canListen(button)) return;
588
+ button.addEventListener('click', (event) => { event.preventDefault && event.preventDefault(); close('dismiss'); });
589
+ });
590
+ if (canListen(overlay)) overlay.addEventListener('click', () => close('overlay'));
591
+ popup.dataset.gemPromoPopupState = 'ready';
592
+ const delay = Math.max(0, Number.isFinite(delaySeconds) ? delaySeconds : 0) * 1000;
593
+ target.setTimeout ? target.setTimeout(() => open('timer'), delay) : open('timer');
594
+ bound += 1;
595
+ });
596
+ api.promoPopup = { open: () => popups.forEach((popup) => { popup.hidden = false; popup.dataset.gemPromoPopupState = 'open'; }) };
597
+ api.emit('gem:promo-popup:ready', { source: 'gem-runtime', count: bound });
598
+ });
599
+ registerPlugin('sticky-atc', ({ api }) => {
600
+ const bars = document.querySelectorAll('[data-gem-sticky-atc], .gem-sticky-atc');
601
+ let bound = 0;
602
+ const setState = (bar, state) => {
603
+ if (!bar || !bar.dataset) return;
604
+ if (bar.dataset.gemStickyAtcState === state) return;
605
+ bar.dataset.gemStickyAtcState = state;
606
+ api.emit(state === 'visible' ? 'gem:sticky-atc:show' : 'gem:sticky-atc:hide', { source: 'gem-runtime', id: bar.id || undefined });
607
+ };
608
+ bars.forEach((bar) => {
609
+ if (!mark(bar, 'sticky-atc')) return;
610
+ const threshold = Number(bar.getAttribute && (bar.getAttribute('data-gem-sticky-threshold') || '240'));
611
+ const update = () => {
612
+ const scrollY = Number(target.scrollY || target.pageYOffset || 0);
613
+ setState(bar, scrollY >= threshold ? 'visible' : 'hidden');
614
+ };
615
+ update();
616
+ if (canListen(target)) target.addEventListener('scroll', update, { passive: true });
617
+ const form = bar.querySelector && bar.querySelector('form');
618
+ if (canListen(form)) form.addEventListener('submit', () => api.emit('gem:sticky-atc:submit', { source: 'gem-runtime', id: bar.id || undefined }));
619
+ bound += 1;
620
+ });
621
+ api.emit('gem:sticky-atc:ready', { source: 'gem-runtime', count: bound });
622
+ });
623
+ registerPlugin('before_after', ({ api }) => {
624
+ const frames = document.querySelectorAll('[data-gem-before-after-frame], .gem-before-after__frame');
625
+ let bound = 0;
626
+ const clamp = (value) => Math.max(0, Math.min(100, Number.isFinite(value) ? value : 50));
627
+ frames.forEach((frame) => {
628
+ if (!mark(frame, 'before_after')) return;
629
+ const range = frame.querySelector && frame.querySelector('[data-gem-before-after-range], input[type="range"]');
630
+ const setPosition = (value, reason = 'init') => {
631
+ const position = clamp(Number(value));
632
+ if (frame.style && frame.style.setProperty) frame.style.setProperty('--gem-before-after-position', position + '%');
633
+ frame.dataset.gemBeforeAfterPosition = String(position);
634
+ if (range && range.value !== undefined) range.value = String(position);
635
+ api.emit('gem:before-after:change', { source: 'gem-runtime', position, reason, id: frame.id || undefined });
636
+ };
637
+ const initial = range && range.value !== undefined ? range.value : frame.getAttribute && frame.getAttribute('data-gem-before-after-position');
638
+ setPosition(initial || 50);
639
+ if (canListen(range)) range.addEventListener('input', () => setPosition(range.value, 'input'));
640
+ bound += 1;
641
+ });
642
+ api.emit('gem:before-after:ready', { source: 'gem-runtime', count: bound });
643
+ });
644
+ registerPlugin('countdown', ({ api }) => {
645
+ const timers = document.querySelectorAll('[data-gem-countdown], .gem-countdown');
646
+ let bound = 0;
647
+ const pad = (value) => String(Math.max(0, value)).padStart(2, '0');
648
+ const write = (timer, selector, value) => {
649
+ const targetNode = timer.querySelector && timer.querySelector(selector);
650
+ if (targetNode) targetNode.textContent = pad(value);
651
+ };
652
+ timers.forEach((timer) => {
653
+ if (!mark(timer, 'countdown')) return;
654
+ const targetIso = timer.getAttribute && timer.getAttribute('data-gem-countdown-target');
655
+ const targetTime = targetIso ? Date.parse(targetIso) : NaN;
656
+ const update = () => {
657
+ if (!Number.isFinite(targetTime)) {
658
+ timer.dataset.gemCountdownState = 'invalid';
659
+ return;
660
+ }
661
+ const remainingSeconds = Math.max(0, Math.floor((targetTime - Date.now()) / 1000));
662
+ const days = Math.floor(remainingSeconds / 86400);
663
+ const hours = Math.floor((remainingSeconds % 86400) / 3600);
664
+ const minutes = Math.floor((remainingSeconds % 3600) / 60);
665
+ timer.dataset.gemCountdownState = remainingSeconds > 0 ? 'running' : 'complete';
666
+ timer.dataset.gemCountdownRemainingSeconds = String(remainingSeconds);
667
+ write(timer, '[data-gem-countdown-days]', days);
668
+ write(timer, '[data-gem-countdown-hours]', hours);
669
+ write(timer, '[data-gem-countdown-minutes]', minutes);
670
+ api.emit(remainingSeconds > 0 ? 'gem:countdown:start' : 'gem:countdown:complete', { source: 'gem-runtime', id: timer.id || undefined, remainingSeconds });
671
+ };
672
+ update();
673
+ if (target.setInterval && Number.isFinite(targetTime)) target.setInterval(update, 60000);
674
+ bound += 1;
675
+ });
676
+ api.emit('gem:countdown:ready', { source: 'gem-runtime', count: bound });
677
+ });
678
+ registerPlugin('reveal-on-scroll', ({ api }) => {
679
+ const nodes = document.querySelectorAll('[data-gem-reveal], .gem-reveal');
680
+ let bound = 0;
681
+ const reveal = (node) => { node.dataset.gemRevealState = 'in'; };
682
+ if (!target.IntersectionObserver) { nodes.forEach(reveal); api.emit('gem:reveal:ready', { source: 'gem-runtime', count: 0 }); return; }
683
+ const observer = new target.IntersectionObserver((entries, obs) => {
684
+ entries.forEach((entry) => {
685
+ if (!entry.isIntersecting) return;
686
+ reveal(entry.target);
687
+ obs.unobserve(entry.target);
688
+ api.emit('gem:reveal:in', { source: 'gem-runtime', id: entry.target.id || undefined });
689
+ });
690
+ }, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
691
+ nodes.forEach((node) => {
692
+ if (!mark(node, 'reveal-on-scroll')) return;
693
+ if (!node.dataset.gemRevealState) node.dataset.gemRevealState = 'ready';
694
+ observer.observe(node);
695
+ bound += 1;
696
+ });
697
+ api.emit('gem:reveal:ready', { source: 'gem-runtime', count: bound });
698
+ });
699
+ registerPlugin('recently-viewed', ({ api }) => {
700
+ const storageKey = 'gem:recently-viewed-products';
701
+ const readItems = () => {
702
+ try { return JSON.parse(target.localStorage && target.localStorage.getItem(storageKey) || '[]'); }
703
+ catch { return []; }
704
+ };
705
+ const writeItems = (items) => {
706
+ try { if (target.localStorage) target.localStorage.setItem(storageKey, JSON.stringify(items.slice(0, 12))); }
707
+ catch { /* storage can be unavailable in private contexts */ }
708
+ };
709
+ const escapeHtml = (value) => String(value || '').replace(/[&<>'"]/g, (char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[char]));
710
+ const renderItem = (item) => {
711
+ const url = item && item.url ? String(item.url) : '#';
712
+ const label = item && (item.title || item.handle || item.url) ? String(item.title || item.handle || item.url) : 'Recently viewed product';
713
+ return '<a class="gem-card gem-recently-viewed__item" href="' + escapeHtml(url) + '"><strong>' + escapeHtml(label) + '</strong></a>';
714
+ };
715
+ const current = document.querySelector('[data-gem-current-product], [data-gem-product-handle]');
716
+ if (current) {
717
+ const handle = current.getAttribute && current.getAttribute('data-gem-product-handle');
718
+ const url = current.getAttribute && (current.getAttribute('data-gem-product-url') || current.getAttribute('href'));
719
+ const title = current.getAttribute && current.getAttribute('data-gem-product-title');
720
+ if (handle || url) {
721
+ const nextItem = { handle, url, title, recordedAt: new Date().toISOString() };
722
+ const items = [nextItem, ...readItems().filter((item) => item && item.handle !== handle && item.url !== url)];
723
+ writeItems(items);
724
+ api.emit('gem:recently-viewed:record', { source: 'gem-runtime', handle, url });
725
+ }
726
+ }
727
+ const rails = document.querySelectorAll('[data-gem-recently-viewed], .gem-recently-viewed');
728
+ let bound = 0;
729
+ rails.forEach((rail) => {
730
+ if (!mark(rail, 'recently-viewed')) return;
731
+ const limit = Number(rail.getAttribute && (rail.getAttribute('data-gem-recently-viewed-limit') || '4'));
732
+ const items = readItems().slice(0, Number.isFinite(limit) ? limit : 4);
733
+ const list = rail.querySelector && (rail.querySelector('[data-gem-recently-viewed-list]') || rail);
734
+ const empty = rail.querySelector && rail.querySelector('[data-gem-recently-viewed-empty]');
735
+ rail.dataset.gemRecentlyViewedCount = String(items.length);
736
+ if (items.length > 0) {
737
+ rail.dataset.gemRecentlyViewedState = 'ready';
738
+ if (list) {
739
+ list.hidden = false;
740
+ if (list.dataset) list.dataset.gemRecentlyViewedCount = String(items.length);
741
+ if ('innerHTML' in list) list.innerHTML = items.map(renderItem).join('');
742
+ else if ('textContent' in list) list.textContent = items.map((item) => item && (item.title || item.handle || item.url) || 'Recently viewed product').join(', ');
743
+ }
744
+ if (empty) empty.hidden = true;
745
+ } else {
746
+ rail.dataset.gemRecentlyViewedState = 'empty';
747
+ if (empty) empty.hidden = false;
748
+ }
749
+ bound += 1;
750
+ });
751
+ api.recentlyViewed = { list: readItems };
752
+ api.emit('gem:recently-viewed:ready', { source: 'gem-runtime', count: bound });
753
+ });
754
+ registerPlugin('runtime-slot-loader', ({ api }) => {
755
+ const slots = document.querySelectorAll('[data-gem-runtime-slot], [data-gem-footer-block-builder], .gem-navigation-shell__layout');
756
+ let bound = 0;
757
+ slots.forEach((slot, index) => {
758
+ if (!mark(slot, 'runtime-slot-loader')) return;
759
+ if (slot.dataset) {
760
+ slot.dataset.gemRuntimeSlotState = 'ready';
761
+ if (!slot.dataset.gemRuntimeSlotIndex) slot.dataset.gemRuntimeSlotIndex = String(index);
762
+ }
763
+ bound += 1;
764
+ });
765
+ api.emit('gem:slot:ready', { source: 'gem-runtime', count: bound });
766
+ });
767
+ registerPlugin('offer-engine', ({ api }) => {
768
+ (function(){
769
+ document.querySelectorAll('[data-gem-offer-engine]').forEach(function(root){
770
+ if(root.__gemOffer)return; root.__gemOffer=1;
771
+ var el=root.querySelector('[data-gem-offer-model]'); if(!el)return;
772
+ var model; try{model=JSON.parse(el.textContent);}catch(e){return;}
773
+ var dims=model.dimensions||{}, sel={};
774
+ Object.keys(dims).forEach(function(d){var o=(dims[d].options||[]),def=o.filter(function(x){return x.default;})[0]||o[0];sel[d]=(model.default_selection&&model.default_selection[d])||(def&&def.id);});
775
+ function m(w){return Object.keys(w).every(function(k){return sel[k]===w[k];});}
776
+ function sp(w){return Object.keys(w).length;}
777
+ function pick(list){return (list||[]).filter(function(r){return m(r.when);}).sort(function(a,b){return sp(b.when)-sp(a.when);})[0];}
778
+ function gift(g,t){if(!g||t==null)return null;var sym=g.currency_symbol||'$';var rem=Math.max(0,Math.round((g.threshold-t)*100)/100);var unl=rem<=0;var rs=sym+rem.toFixed(2);var pct=g.threshold>0?Math.max(0,Math.min(100,Math.round((t/g.threshold)*100))):100;var tpl=unl?(g.unlocked_text||'\uD83C\uDF81 {label} unlocked!'):(g.locked_text||'Add {remaining} more to unlock {label}');return {unlocked:unl,remaining:rs,remaining_amount:rem,percent:pct,text:tpl.replace('{remaining}',rs).replace('{label}',g.label)};}
779
+ function render(){
780
+ var row=pick(model.price_matrix)||{}, plan=pick(model.selling_plan_map);
781
+ var vis={}; Object.keys(dims).forEach(function(d){vis[d]=true;});
782
+ (model.conditions||[]).forEach(function(c){vis[c.show]=(sel[c.when_dim]===c.equals);});
783
+ (model.panels||[]).forEach(function(p){if(p.when)vis[p.id]=(sel[p.when.when_dim]===p.when.equals);});
784
+ var headline=row.total||row.price||'';
785
+ var gf=gift(model.gift,row.total_amount);
786
+ root.querySelectorAll('[data-gem-offer-out]').forEach(function(o){var k=o.getAttribute('data-gem-offer-out');
787
+ if(k==='price')o.textContent=row.price||'';
788
+ else if(k==='compare'){o.textContent=row.compare_at||'';o.style.display=row.compare_at?'':'none';}
789
+ else if(k==='save'){o.textContent=row.save_label||'';o.style.display=row.save_label?'':'none';}
790
+ else if(k==='gift'){o.textContent=gf?gf.text:'';if(o.classList)o.classList.toggle('unlocked',!!(gf&&gf.unlocked));}
791
+ else if(k==='cta')o.textContent=(model.cta_template||'Add to cart').replace('{price}',headline).replace('{total}',headline);});
792
+ root.querySelectorAll('[data-gem-offer-gift]').forEach(function(w){w.style.display=gf?'':'none';if(w.classList)w.classList.toggle('unlocked',!!(gf&&gf.unlocked));});
793
+ root.querySelectorAll('[data-gem-offer-gift-bar]').forEach(function(b){b.style.width=(gf?gf.percent:0)+'%';});
794
+ root.querySelectorAll('[data-gem-offer-group]').forEach(function(g){g.style.display=(vis[g.getAttribute('data-gem-offer-group')]===false)?'none':'';});
795
+ root.querySelectorAll('[data-gem-offer-dim]').forEach(function(b){var d=b.getAttribute('data-gem-offer-dim'),v=b.getAttribute('data-gem-offer-value');if(b.tagName==='OPTION'){b.selected=(sel[d]===v);}else{b.classList.toggle('sel',sel[d]===v);}});
796
+ var s=root.querySelector('[data-gem-offer-selling-plan]'); if(s)s.value=(plan&&plan.selling_plan_id)||'';
797
+ var ca=(plan&&plan.cart_attributes)||{};
798
+ root.querySelectorAll('[data-gem-offer-cart-field]').forEach(function(f){var n=f.getAttribute('data-gem-offer-cart-field');f.value=ca[n]||'';f.disabled=!(n in ca);});
799
+ root.dispatchEvent(new CustomEvent('gem:offer:change',{bubbles:true,detail:{selection:sel,row:row,selling_plan_id:(plan&&plan.selling_plan_id)||null,cart_attributes:ca,gift:gf}}));
800
+ }
801
+ root.addEventListener('click',function(e){var b=e.target.closest('[data-gem-offer-dim]');if(!b||b.tagName==='OPTION')return;sel[b.getAttribute('data-gem-offer-dim')]=b.getAttribute('data-gem-offer-value');render();});
802
+ root.addEventListener('change',function(e){var s=e.target.closest('select[data-gem-offer-select]');if(!s)return;var o=s.options[s.selectedIndex];sel[s.getAttribute('data-gem-offer-select')]=o.getAttribute('data-gem-offer-value');render();});
803
+ render();
804
+ });
805
+ })();
806
+ api.emit('gem:offer:ready', { source: 'gem-runtime' });
807
+ });
808
+ registerNoopPlugin("seasonal-effects", "gem:seasonal:ready");
809
+ registerNoopPlugin("context-adapter", "gem:context:ready");
810
+ const mark = (element, key) => {
811
+ const attr = 'data-gem-bound-' + key;
812
+ if (!element || element.hasAttribute(attr)) return false;
813
+ element.setAttribute(attr, 'true');
814
+ return true;
815
+ };
816
+ const wrapMethod = (element, method, event, detail = {}) => {
817
+ if (!element || typeof element[method] !== 'function') return false;
818
+ const attr = 'data-gem-wrap-' + method;
819
+ if (element.hasAttribute(attr)) return true;
820
+ const original = element[method].bind(element);
821
+ element[method] = (...args) => {
822
+ const result = original(...args);
823
+ api.emit(event, typeof detail === 'function' ? detail(...args) : detail);
824
+ return result;
825
+ };
826
+ element.setAttribute(attr, 'true');
827
+ return true;
828
+ };
829
+ const whenElementDefined = (name, callback) => {
830
+ if (!target.customElements || !target.customElements.whenDefined) { callback(); return; }
831
+ target.customElements.whenDefined(name).then(callback).catch(callback);
832
+ };
833
+ const wireDawnCartDrawer = () => {
834
+ if (!hasPlugin('cart-drawer')) return false;
835
+ const drawer = document.querySelector('cart-drawer');
836
+ if (!drawer || !mark(drawer, 'cart-drawer')) return false;
837
+ wrapMethod(drawer, 'open', 'gem:cart:open', { source: 'dawn' });
838
+ wrapMethod(drawer, 'close', 'gem:cart:close', { source: 'dawn' });
839
+ wrapMethod(drawer, 'renderContents', 'gem:cart:update', (state) => ({
840
+ source: 'dawn',
841
+ productId: state && state.id ? state.id : undefined,
842
+ }));
843
+ return true;
844
+ };
845
+ const wireDawnMenuDrawer = () => {
846
+ if (!hasPlugin('mobile-menu')) return false;
847
+ const drawers = document.querySelectorAll('menu-drawer, header-drawer');
848
+ let wired = false;
849
+ drawers.forEach((drawer) => {
850
+ if (!mark(drawer, 'mobile-menu')) return;
851
+ wrapMethod(drawer, 'openMenuDrawer', 'gem:menu:open', { source: 'dawn' });
852
+ wrapMethod(drawer, 'closeMenuDrawer', 'gem:menu:close', { source: 'dawn' });
853
+ wired = true;
854
+ });
855
+ return wired;
856
+ };
857
+ const wireDawnPredictiveSearch = () => {
858
+ if (!hasPlugin('predictive-search')) return false;
859
+ const search = document.querySelector('predictive-search');
860
+ if (!search || !mark(search, 'predictive-search')) return false;
861
+ const input = search.querySelector('input[type="search"], input[name="q"]');
862
+ const form = search.querySelector('form');
863
+ if (canListen(input)) input.addEventListener('focus', () => api.emit('gem:search:open', { source: 'dawn' }), { once: false });
864
+ if (canListen(form)) form.addEventListener('submit', () => api.emit('gem:search:submit', { source: 'dawn' }), { once: false });
865
+ return true;
866
+ };
867
+ const wireDawnAdapter = () => {
868
+ if (!config.dawnAdapter || config.base !== 'dawn') return;
869
+ const detected = [];
870
+ whenElementDefined('cart-drawer', () => { if (wireDawnCartDrawer()) detected.push('cart-drawer'); });
871
+ whenElementDefined('menu-drawer', () => { if (wireDawnMenuDrawer()) detected.push('mobile-menu'); });
872
+ whenElementDefined('header-drawer', () => { if (wireDawnMenuDrawer()) detected.push('mobile-menu'); });
873
+ whenElementDefined('predictive-search', () => { if (wireDawnPredictiveSearch()) detected.push('predictive-search'); });
874
+ setTimeout(() => api.emit('gem:dawn-adapter:ready', { detected: [...new Set(detected)] }), 0);
875
+ };
876
+ wireDawnAdapter();
877
+ config.plugins.forEach(schedulePlugin);
878
+ document.documentElement.dataset.gemRuntime = 'ready';
879
+ api.emit('gem:runtime:ready', { plugins: config.plugins.map((plugin) => plugin.key), loadedPlugins: Object.keys(loadedPlugins) });
880
+ })();