@gemboss/gem-framework 0.9.3 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/dist/SOURCE.json +2 -2
- package/dist/gem-runtime.js +45 -2
- package/dist/index.js +86 -2
- package/dist/plugins/hero-slideshow.js +5 -1
- package/dist/plugins/media-gallery.js +25 -0
- package/dist/plugins/sticky-atc.js +15 -1
- package/dist/section-gate.js +3145 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,6 +73,33 @@ Từ 0.8.2 mỗi module có `.d.ts` cạnh nó (`mount(root?: ParentNode | null,
|
|
|
73
73
|
|
|
74
74
|
## Nhật ký thay đổi
|
|
75
75
|
|
|
76
|
+
- **0.9.8** (06/10/2026): biến chỉ được tính là "khai trên :root" khi MỌI vế của selector đúng là gốc (`:root`, `html`,
|
|
77
|
+
`:host`, có thể kèm điều kiện như `:root[data-theme="dark"]`), không bao giờ là con của nó. Bản 0.9.7 chỉ xét đầu
|
|
78
|
+
selector, nên một luật `:root .gem-row { --gem-row-auto-sm: … }` sẽ biến biến nội bộ thành công khai. Stylesheet hiện
|
|
79
|
+
tại chưa có luật nào như vậy, nên kết quả không đổi (review của gemboss #3842). Thêm `gemPublicCssVariablesFrom(css)`.
|
|
80
|
+
- **0.9.7** (06/10/2026): `validateGemFrameworkCss` nhận mọi biến CÔNG KHAI của chính framework, đọc từ stylesheet của nó ở
|
|
81
|
+
cả hai profile: biến khai trên `:root` (`--gem-card-radius`, `--gem-media-radius`, `--gem-ease-soft`, `--gem-grid-row-gap`…)
|
|
82
|
+
và điểm mở chỉ được đọc kèm dự phòng (`--gem-card-background`, `--gem-marquee-duration`…). Trước đó danh sách ghép tay
|
|
83
|
+
bỏ sót 25 biến như vậy, nên section đọc đúng biến framework vẫn nhận `unknown_gem_css_variable` (GemFactory gap #29).
|
|
84
|
+
Biến nội bộ mà framework chỉ khai trên class của nó (`--gem-row-auto-*`, `--gem-stars-*`, `--gem-section-bg`) vẫn bị
|
|
85
|
+
báo, và tên gõ sai cũng vậy.
|
|
86
|
+
- **0.9.6** (06/10/2026): `media-gallery` có chế độ ảnh chính + thumbnail (gallery có `[data-gem-media-main]`): bấm
|
|
87
|
+
thumbnail thì ảnh chính nhận `data-gem-media-src` / `-alt` của nó, bỏ `srcset`, `aria-current` chuyển sang thumbnail
|
|
88
|
+
đó, và phát `gem:media:select` `{ index }` như `behavior.js` của section. Phần tử chỉ khớp theo class mà bọc một hook
|
|
89
|
+
`[data-gem-media-gallery]` không còn bị gắn thành gallery thứ hai (trước đó nó đánh số lại thumbnail bằng cách đếm
|
|
90
|
+
mọi `<img>`, kể cả ảnh chính). Gallery không có ảnh chính chạy như cũ.
|
|
91
|
+
- **0.9.5** (06/10/2026): `sticky-atc` làm đủ việc của `behavior.js` section: giá trên thanh đổi theo biến thể khách
|
|
92
|
+
chọn (`data-gem-variant-price` trên từng `<option>`), và form KHÔNG có `action` (thanh headless) thì chặn submit, phát
|
|
93
|
+
`gem:cart:add` với biến thể đang chọn (không phải biến thể đầu ghi trên nút). Form có `action` (Liquid `/cart/add`)
|
|
94
|
+
vẫn để trình duyệt gửi như cũ. Trước đó plugin chỉ báo `gem:sticky-atc:submit`, nên trang nạp plugin mà không có
|
|
95
|
+
bản của section thì bấm thêm vào giỏ là tải lại trang.
|
|
96
|
+
- **0.9.4** (06/10/2026): nút tạm dừng của `hero-slideshow` đổi chữ theo trạng thái như `behavior.js` của section từng làm:
|
|
97
|
+
khi dừng nút ghi `data-gem-hero-play-label` (mặc định `Play`), khi chạy lại ghi đúng chữ ban đầu của nút (nên nhãn đã
|
|
98
|
+
dịch không bị đè). Nút có phần tử con (icon) giữ nguyên markup, chỉ đổi `aria-pressed`. Đây là chỗ lệch cuối cùng
|
|
99
|
+
giữa plugin và section, để section giao hành vi cho plugin (một chủ, Chris 06/10/2026).
|
|
100
|
+
- **0.9.3** (06/10/2026): CSS Dawn parity chỉ đặt `gap !important` cho lưới thẻ quick-view
|
|
101
|
+
(`.gem-product-card-grid--quick-view-grid`), không cho mọi `.gem-product-card-grid__grid`. Khớp bản chép của gemsite;
|
|
102
|
+
store không đổi gì.
|
|
76
103
|
- **0.9.2** (06/10/2026): `reveal-on-scroll` bỏ qua `data-gem-reveal="none"` và `"default"` (hai giá trị "không áp gì" của
|
|
77
104
|
knob reveal): section hiện ngay, không ẩn chờ cuộn. Trước đó section đặt `none` vẫn bị ẩn như mọi section reveal.
|
|
78
105
|
- **0.9.1** (06/10/2026): plugin `reveal-on-scroll` không còn giấu section khách đang thấy. Section đã nằm trong màn hình
|
package/dist/SOURCE.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "@gemboss/gem-framework",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"gemtheme_sha": "
|
|
3
|
+
"version": "0.9.8",
|
|
4
|
+
"gemtheme_sha": "fea3af50909be36e9fc8968ae407409e324a1c6b",
|
|
5
5
|
"gemtheme_dirty": false,
|
|
6
6
|
"framework_version": "0.1.0"
|
|
7
7
|
}
|
package/dist/gem-runtime.js
CHANGED
|
@@ -382,7 +382,32 @@
|
|
|
382
382
|
const galleries = document.querySelectorAll('[data-gem-media-gallery], .gem-media-gallery, .gem-product-media-gallery');
|
|
383
383
|
let bound = 0;
|
|
384
384
|
galleries.forEach((gallery) => {
|
|
385
|
+
const hooked = !!(gallery.hasAttribute && gallery.hasAttribute('data-gem-media-gallery'));
|
|
386
|
+
if (!hooked && gallery.querySelector && gallery.querySelector('[data-gem-media-gallery]')) return;
|
|
385
387
|
if (!mark(gallery, 'media-gallery')) return;
|
|
388
|
+
const main = gallery.querySelector && gallery.querySelector('[data-gem-media-main]');
|
|
389
|
+
const thumbs = main && gallery.querySelectorAll ? Array.prototype.slice.call(gallery.querySelectorAll('[data-gem-media-item]')) : [];
|
|
390
|
+
if (main && thumbs.length) {
|
|
391
|
+
gallery.dataset.gemMediaState = 'ready';
|
|
392
|
+
const choose = (item, reason) => {
|
|
393
|
+
const src = item.getAttribute('data-gem-media-src');
|
|
394
|
+
const alt = item.getAttribute('data-gem-media-alt');
|
|
395
|
+
if (src) { main.setAttribute('src', src); main.removeAttribute('srcset'); if (alt != null) main.setAttribute('alt', alt); }
|
|
396
|
+
const index = Number(item.getAttribute('data-gem-media-index') || thumbs.indexOf(item));
|
|
397
|
+
thumbs.forEach((thumb) => { const on = thumb === item; thumb.setAttribute('aria-current', String(on)); if (thumb.dataset) thumb.dataset.gemMediaActive = on ? 'true' : 'false'; });
|
|
398
|
+
gallery.dataset.gemMediaActiveIndex = String(index);
|
|
399
|
+
if (gallery.dispatchEvent && typeof window.CustomEvent === 'function') gallery.dispatchEvent(new window.CustomEvent('gem:media:select', { bubbles: true, detail: { index } }));
|
|
400
|
+
api.emit('gem:media:change', { source: 'gem-runtime', index, galleryId: gallery.id || undefined, reason });
|
|
401
|
+
};
|
|
402
|
+
if (canListen(gallery)) gallery.addEventListener('click', (event) => {
|
|
403
|
+
const item = event.target && event.target.closest ? event.target.closest('[data-gem-media-item]') : null;
|
|
404
|
+
if (!item || !gallery.contains(item)) return;
|
|
405
|
+
if (event.preventDefault) event.preventDefault();
|
|
406
|
+
choose(item, 'click');
|
|
407
|
+
});
|
|
408
|
+
bound += 1;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
386
411
|
const items = gallery.querySelectorAll('[data-gem-media-item], img, video, model-viewer');
|
|
387
412
|
const thumbControls = gallery.querySelectorAll('[data-gem-media-thumb]');
|
|
388
413
|
const controls = thumbControls.length ? thumbControls : items;
|
|
@@ -488,7 +513,11 @@
|
|
|
488
513
|
else if (k === 'Home') { if (e.preventDefault) e.preventDefault(); focusDot(0); }
|
|
489
514
|
else if (k === 'End') { if (e.preventDefault) e.preventDefault(); focusDot(dots.length - 1); }
|
|
490
515
|
}); });
|
|
491
|
-
|
|
516
|
+
// The visible label follows the state like the section's own behavior did: the button's own text while playing
|
|
517
|
+
// (kept, so a translated label survives), data-gem-hero-play-label (or 'Play') while paused. Only a text-only button.
|
|
518
|
+
const pauseText = pause && pause.childElementCount === 0 ? pause.textContent : null;
|
|
519
|
+
const playText = (pause && pause.getAttribute && pause.getAttribute('data-gem-hero-play-label')) || 'Play';
|
|
520
|
+
if (canListen(pause)) pause.addEventListener('click', () => { paused = !paused; if (pause.setAttribute) pause.setAttribute('aria-pressed', String(paused)); if (pauseText !== null) pause.textContent = paused ? playText : pauseText; start(); });
|
|
492
521
|
show(active); start(); bound += 1;
|
|
493
522
|
});
|
|
494
523
|
api.emit('gem:hero-slideshow:ready', { source: 'gem-runtime', count: bound });
|
|
@@ -668,8 +697,22 @@
|
|
|
668
697
|
};
|
|
669
698
|
update();
|
|
670
699
|
if (canListen(target)) target.addEventListener('scroll', update, { passive: true });
|
|
700
|
+
const priceEl = bar.querySelector && bar.querySelector('[data-gem-sticky-atc-price]');
|
|
701
|
+
const select = bar.querySelector && bar.querySelector('select');
|
|
702
|
+
const syncPrice = () => { if (!priceEl || !select) return; const opt = select.options && select.selectedIndex > -1 ? select.options[select.selectedIndex] : null; const price = opt && opt.getAttribute && opt.getAttribute('data-gem-variant-price'); if (price) priceEl.textContent = price; };
|
|
703
|
+
if (canListen(select)) select.addEventListener('change', syncPrice);
|
|
704
|
+
syncPrice();
|
|
671
705
|
const form = bar.querySelector && bar.querySelector('form');
|
|
672
|
-
if (canListen(form)) form.addEventListener('submit', () =>
|
|
706
|
+
if (canListen(form)) form.addEventListener('submit', (event) => {
|
|
707
|
+
if (!(form.hasAttribute && form.hasAttribute('action'))) {
|
|
708
|
+
if (event && event.preventDefault) event.preventDefault();
|
|
709
|
+
const choice = form.querySelector && form.querySelector('select');
|
|
710
|
+
const trigger = bar.querySelector && bar.querySelector('[data-gem-variant-id]');
|
|
711
|
+
const variantId = (choice && choice.value) || (trigger && trigger.getAttribute('data-gem-variant-id')) || undefined;
|
|
712
|
+
if (bar.dispatchEvent && typeof window.CustomEvent === 'function') bar.dispatchEvent(new window.CustomEvent('gem:cart:add', { bubbles: true, detail: { variantId, quantity: 1 } }));
|
|
713
|
+
}
|
|
714
|
+
api.emit('gem:sticky-atc:submit', { source: 'gem-runtime', id: bar.id || undefined });
|
|
715
|
+
});
|
|
673
716
|
bound += 1;
|
|
674
717
|
});
|
|
675
718
|
api.emit('gem:sticky-atc:ready', { source: 'gem-runtime', count: bound });
|
package/dist/index.js
CHANGED
|
@@ -4555,7 +4555,38 @@ var mediaGalleryPluginRegistrationLines = [
|
|
|
4555
4555
|
" const galleries = document.querySelectorAll('[data-gem-media-gallery], .gem-media-gallery, .gem-product-media-gallery');",
|
|
4556
4556
|
" let bound = 0;",
|
|
4557
4557
|
" galleries.forEach((gallery) => {",
|
|
4558
|
+
// A wrapper matched only by its class (the thumbnail-stack <section class="gem-product-media-gallery">) around a
|
|
4559
|
+
// [data-gem-media-gallery] hook is not a second gallery: binding both ran every click twice and the outer one
|
|
4560
|
+
// renumbered the thumbnails' data-gem-media-index by counting every <img>, the main image included.
|
|
4561
|
+
" const hooked = !!(gallery.hasAttribute && gallery.hasAttribute('data-gem-media-gallery'));",
|
|
4562
|
+
" if (!hooked && gallery.querySelector && gallery.querySelector('[data-gem-media-gallery]')) return;",
|
|
4558
4563
|
" if (!mark(gallery, 'media-gallery')) return;",
|
|
4564
|
+
// Main image + thumbnails (product-media-gallery.thumbnail-stack.v1): a thumbnail click puts its
|
|
4565
|
+
// data-gem-media-src / -alt on [data-gem-media-main], as the section's behavior.js did. Without this the plugin
|
|
4566
|
+
// only moved aria-current and the big image never changed. The markup's data-gem-media-index is kept.
|
|
4567
|
+
" const main = gallery.querySelector && gallery.querySelector('[data-gem-media-main]');",
|
|
4568
|
+
" const thumbs = main && gallery.querySelectorAll ? Array.prototype.slice.call(gallery.querySelectorAll('[data-gem-media-item]')) : [];",
|
|
4569
|
+
" if (main && thumbs.length) {",
|
|
4570
|
+
" gallery.dataset.gemMediaState = 'ready';",
|
|
4571
|
+
" const choose = (item, reason) => {",
|
|
4572
|
+
" const src = item.getAttribute('data-gem-media-src');",
|
|
4573
|
+
" const alt = item.getAttribute('data-gem-media-alt');",
|
|
4574
|
+
" if (src) { main.setAttribute('src', src); main.removeAttribute('srcset'); if (alt != null) main.setAttribute('alt', alt); }",
|
|
4575
|
+
" const index = Number(item.getAttribute('data-gem-media-index') || thumbs.indexOf(item));",
|
|
4576
|
+
" thumbs.forEach((thumb) => { const on = thumb === item; thumb.setAttribute('aria-current', String(on)); if (thumb.dataset) thumb.dataset.gemMediaActive = on ? 'true' : 'false'; });",
|
|
4577
|
+
" gallery.dataset.gemMediaActiveIndex = String(index);",
|
|
4578
|
+
" if (gallery.dispatchEvent && typeof window.CustomEvent === 'function') gallery.dispatchEvent(new window.CustomEvent('gem:media:select', { bubbles: true, detail: { index } }));",
|
|
4579
|
+
" api.emit('gem:media:change', { source: 'gem-runtime', index, galleryId: gallery.id || undefined, reason });",
|
|
4580
|
+
" };",
|
|
4581
|
+
" if (canListen(gallery)) gallery.addEventListener('click', (event) => {",
|
|
4582
|
+
" const item = event.target && event.target.closest ? event.target.closest('[data-gem-media-item]') : null;",
|
|
4583
|
+
" if (!item || !gallery.contains(item)) return;",
|
|
4584
|
+
" if (event.preventDefault) event.preventDefault();",
|
|
4585
|
+
" choose(item, 'click');",
|
|
4586
|
+
" });",
|
|
4587
|
+
" bound += 1;",
|
|
4588
|
+
" return;",
|
|
4589
|
+
" }",
|
|
4559
4590
|
" const items = gallery.querySelectorAll('[data-gem-media-item], img, video, model-viewer');",
|
|
4560
4591
|
" const thumbControls = gallery.querySelectorAll('[data-gem-media-thumb]');",
|
|
4561
4592
|
" const controls = thumbControls.length ? thumbControls : items;",
|
|
@@ -4667,7 +4698,11 @@ var heroSlideshowPluginRegistrationLines = [
|
|
|
4667
4698
|
" else if (k === 'Home') { if (e.preventDefault) e.preventDefault(); focusDot(0); }",
|
|
4668
4699
|
" else if (k === 'End') { if (e.preventDefault) e.preventDefault(); focusDot(dots.length - 1); }",
|
|
4669
4700
|
" }); });",
|
|
4670
|
-
"
|
|
4701
|
+
" // The visible label follows the state like the section's own behavior did: the button's own text while playing",
|
|
4702
|
+
" // (kept, so a translated label survives), data-gem-hero-play-label (or 'Play') while paused. Only a text-only button.",
|
|
4703
|
+
" const pauseText = pause && pause.childElementCount === 0 ? pause.textContent : null;",
|
|
4704
|
+
" const playText = (pause && pause.getAttribute && pause.getAttribute('data-gem-hero-play-label')) || 'Play';",
|
|
4705
|
+
" if (canListen(pause)) pause.addEventListener('click', () => { paused = !paused; if (pause.setAttribute) pause.setAttribute('aria-pressed', String(paused)); if (pauseText !== null) pause.textContent = paused ? playText : pauseText; start(); });",
|
|
4671
4706
|
" show(active); start(); bound += 1;",
|
|
4672
4707
|
" });",
|
|
4673
4708
|
" api.emit('gem:hero-slideshow:ready', { source: 'gem-runtime', count: bound });",
|
|
@@ -4882,8 +4917,27 @@ var stickyAtcPluginRegistrationLines = [
|
|
|
4882
4917
|
" };",
|
|
4883
4918
|
" update();",
|
|
4884
4919
|
" if (canListen(target)) target.addEventListener('scroll', update, { passive: true });",
|
|
4920
|
+
// The price follows the variant <select> (each <option> carries its own data-gem-variant-price), as the section's
|
|
4921
|
+
// behavior.js did: otherwise the bar shows the first variant's price whatever the shopper picked.
|
|
4922
|
+
" const priceEl = bar.querySelector && bar.querySelector('[data-gem-sticky-atc-price]');",
|
|
4923
|
+
" const select = bar.querySelector && bar.querySelector('select');",
|
|
4924
|
+
" const syncPrice = () => { if (!priceEl || !select) return; const opt = select.options && select.selectedIndex > -1 ? select.options[select.selectedIndex] : null; const price = opt && opt.getAttribute && opt.getAttribute('data-gem-variant-price'); if (price) priceEl.textContent = price; };",
|
|
4925
|
+
" if (canListen(select)) select.addEventListener('change', syncPrice);",
|
|
4926
|
+
" syncPrice();",
|
|
4927
|
+
// A form WITHOUT an action (the headless bar) has nowhere to post: the plugin adds to cart by dispatching
|
|
4928
|
+
// gem:cart:add with the SELECTED variant (the button's data-gem-variant-id is only the first one), which the
|
|
4929
|
+
// storefront's cart bridge consumes. A form WITH an action (a Liquid /cart/add form) is left to the browser.
|
|
4885
4930
|
" const form = bar.querySelector && bar.querySelector('form');",
|
|
4886
|
-
" if (canListen(form)) form.addEventListener('submit', () =>
|
|
4931
|
+
" if (canListen(form)) form.addEventListener('submit', (event) => {",
|
|
4932
|
+
" if (!(form.hasAttribute && form.hasAttribute('action'))) {",
|
|
4933
|
+
" if (event && event.preventDefault) event.preventDefault();",
|
|
4934
|
+
" const choice = form.querySelector && form.querySelector('select');",
|
|
4935
|
+
" const trigger = bar.querySelector && bar.querySelector('[data-gem-variant-id]');",
|
|
4936
|
+
" const variantId = (choice && choice.value) || (trigger && trigger.getAttribute('data-gem-variant-id')) || undefined;",
|
|
4937
|
+
" if (bar.dispatchEvent && typeof window.CustomEvent === 'function') bar.dispatchEvent(new window.CustomEvent('gem:cart:add', { bubbles: true, detail: { variantId, quantity: 1 } }));",
|
|
4938
|
+
" }",
|
|
4939
|
+
" api.emit('gem:sticky-atc:submit', { source: 'gem-runtime', id: bar.id || undefined });",
|
|
4940
|
+
" });",
|
|
4887
4941
|
" bound += 1;",
|
|
4888
4942
|
" });",
|
|
4889
4943
|
" api.emit('gem:sticky-atc:ready', { source: 'gem-runtime', count: bound });",
|
|
@@ -5662,8 +5716,38 @@ function isAllowedUntokenizedValue(property, value) {
|
|
|
5662
5716
|
function hasRawVisualValue(value) {
|
|
5663
5717
|
return /#[0-9a-f]{3,8}\b|rgb\(|hsl\(|\b\d+(?:\.\d+)?(?:px|rem|em)\b/i.test(value);
|
|
5664
5718
|
}
|
|
5719
|
+
var frameworkPublicVariables;
|
|
5720
|
+
function gemFrameworkPublicCssVariables() {
|
|
5721
|
+
if (!frameworkPublicVariables) {
|
|
5722
|
+
const css = [
|
|
5723
|
+
createGemThemeCss(defaultGemTokens, ":root", { profile: "dawn-parity" }),
|
|
5724
|
+
createGemThemeCss(defaultGemTokens, ":root", { profile: "morph" }),
|
|
5725
|
+
createThemeProfileCss("dawn-parity"),
|
|
5726
|
+
createMorphProfileCss(),
|
|
5727
|
+
createPrimitiveCss(),
|
|
5728
|
+
String(createSectionCss()),
|
|
5729
|
+
String(createShellCss()),
|
|
5730
|
+
String(createGemSectionRuntimeContractCss())
|
|
5731
|
+
].join("\n").replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
5732
|
+
frameworkPublicVariables = gemPublicCssVariablesFrom(css);
|
|
5733
|
+
}
|
|
5734
|
+
return frameworkPublicVariables;
|
|
5735
|
+
}
|
|
5736
|
+
var ROOT_SELECTOR = /^\s*(?::root|html|:host)(?:\[[^\]]*\]|:[a-z-]+(?:\([^()]*\))?)*\s*$/;
|
|
5737
|
+
function gemPublicCssVariablesFrom(stylesheet) {
|
|
5738
|
+
const css = stylesheet.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
5739
|
+
const declaredOnRoot = /* @__PURE__ */ new Set();
|
|
5740
|
+
const declaredElsewhere = /* @__PURE__ */ new Set();
|
|
5741
|
+
for (const [, selector, body] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
5742
|
+
const onRoot = selector.split(",").every((part) => ROOT_SELECTOR.test(part));
|
|
5743
|
+
for (const [, name] of body.matchAll(/(--gem-[a-z0-9-]+)\s*:/g)) (onRoot ? declaredOnRoot : declaredElsewhere).add(name);
|
|
5744
|
+
}
|
|
5745
|
+
const mentioned = new Set([...css.matchAll(/--gem-[a-z0-9-]+/g)].map((match) => match[0]));
|
|
5746
|
+
return new Set([...mentioned].filter((name) => declaredOnRoot.has(name) || !declaredElsewhere.has(name)));
|
|
5747
|
+
}
|
|
5665
5748
|
function isKnownGemCssVariable(variableName, options) {
|
|
5666
5749
|
if (canonicalCssVariableNames.has(variableName)) return true;
|
|
5750
|
+
if (gemFrameworkPublicCssVariables().has(variableName)) return true;
|
|
5667
5751
|
const allowedPrefixes = [
|
|
5668
5752
|
"--gem-seasonal-",
|
|
5669
5753
|
// Global style-variant knobs (--gem-style-space/radius/text/weight/stroke): the
|
|
@@ -42,7 +42,11 @@ function install({ document, window, target, mark, canListen, listen: __gemListe
|
|
|
42
42
|
else if (k === 'Home') { if (e.preventDefault) e.preventDefault(); focusDot(0); }
|
|
43
43
|
else if (k === 'End') { if (e.preventDefault) e.preventDefault(); focusDot(dots.length - 1); }
|
|
44
44
|
}); });
|
|
45
|
-
|
|
45
|
+
// The visible label follows the state like the section's own behavior did: the button's own text while playing
|
|
46
|
+
// (kept, so a translated label survives), data-gem-hero-play-label (or 'Play') while paused. Only a text-only button.
|
|
47
|
+
const pauseText = pause && pause.childElementCount === 0 ? pause.textContent : null;
|
|
48
|
+
const playText = (pause && pause.getAttribute && pause.getAttribute('data-gem-hero-play-label')) || 'Play';
|
|
49
|
+
if (canListen(pause)) __gemListen(pause, 'click', () => { paused = !paused; if (pause.setAttribute) pause.setAttribute('aria-pressed', String(paused)); if (pauseText !== null) pause.textContent = paused ? playText : pauseText; start(); });
|
|
46
50
|
show(active); start(); bound += 1;
|
|
47
51
|
});
|
|
48
52
|
api.emit('gem:hero-slideshow:ready', { source: 'gem-runtime', count: bound });
|
|
@@ -10,7 +10,32 @@ function install({ document, window, target, mark, canListen, listen: __gemListe
|
|
|
10
10
|
const galleries = document.querySelectorAll('[data-gem-media-gallery], .gem-media-gallery, .gem-product-media-gallery');
|
|
11
11
|
let bound = 0;
|
|
12
12
|
galleries.forEach((gallery) => {
|
|
13
|
+
const hooked = !!(gallery.hasAttribute && gallery.hasAttribute('data-gem-media-gallery'));
|
|
14
|
+
if (!hooked && gallery.querySelector && gallery.querySelector('[data-gem-media-gallery]')) return;
|
|
13
15
|
if (!mark(gallery, 'media-gallery')) return;
|
|
16
|
+
const main = gallery.querySelector && gallery.querySelector('[data-gem-media-main]');
|
|
17
|
+
const thumbs = main && gallery.querySelectorAll ? Array.prototype.slice.call(gallery.querySelectorAll('[data-gem-media-item]')) : [];
|
|
18
|
+
if (main && thumbs.length) {
|
|
19
|
+
gallery.dataset.gemMediaState = 'ready';
|
|
20
|
+
const choose = (item, reason) => {
|
|
21
|
+
const src = item.getAttribute('data-gem-media-src');
|
|
22
|
+
const alt = item.getAttribute('data-gem-media-alt');
|
|
23
|
+
if (src) { main.setAttribute('src', src); main.removeAttribute('srcset'); if (alt != null) main.setAttribute('alt', alt); }
|
|
24
|
+
const index = Number(item.getAttribute('data-gem-media-index') || thumbs.indexOf(item));
|
|
25
|
+
thumbs.forEach((thumb) => { const on = thumb === item; thumb.setAttribute('aria-current', String(on)); if (thumb.dataset) thumb.dataset.gemMediaActive = on ? 'true' : 'false'; });
|
|
26
|
+
gallery.dataset.gemMediaActiveIndex = String(index);
|
|
27
|
+
if (gallery.dispatchEvent && typeof window.CustomEvent === 'function') gallery.dispatchEvent(new window.CustomEvent('gem:media:select', { bubbles: true, detail: { index } }));
|
|
28
|
+
api.emit('gem:media:change', { source: 'gem-runtime', index, galleryId: gallery.id || undefined, reason });
|
|
29
|
+
};
|
|
30
|
+
if (canListen(gallery)) __gemListen(gallery, 'click', (event) => {
|
|
31
|
+
const item = event.target && event.target.closest ? event.target.closest('[data-gem-media-item]') : null;
|
|
32
|
+
if (!item || !gallery.contains(item)) return;
|
|
33
|
+
if (event.preventDefault) event.preventDefault();
|
|
34
|
+
choose(item, 'click');
|
|
35
|
+
});
|
|
36
|
+
bound += 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
14
39
|
const items = gallery.querySelectorAll('[data-gem-media-item], img, video, model-viewer');
|
|
15
40
|
const thumbControls = gallery.querySelectorAll('[data-gem-media-thumb]');
|
|
16
41
|
const controls = thumbControls.length ? thumbControls : items;
|
|
@@ -27,8 +27,22 @@ function install({ document, window, target, mark, canListen, listen: __gemListe
|
|
|
27
27
|
};
|
|
28
28
|
update();
|
|
29
29
|
if (canListen(target)) __gemListen(target, 'scroll', update, { passive: true });
|
|
30
|
+
const priceEl = bar.querySelector && bar.querySelector('[data-gem-sticky-atc-price]');
|
|
31
|
+
const select = bar.querySelector && bar.querySelector('select');
|
|
32
|
+
const syncPrice = () => { if (!priceEl || !select) return; const opt = select.options && select.selectedIndex > -1 ? select.options[select.selectedIndex] : null; const price = opt && opt.getAttribute && opt.getAttribute('data-gem-variant-price'); if (price) priceEl.textContent = price; };
|
|
33
|
+
if (canListen(select)) __gemListen(select, 'change', syncPrice);
|
|
34
|
+
syncPrice();
|
|
30
35
|
const form = bar.querySelector && bar.querySelector('form');
|
|
31
|
-
if (canListen(form)) __gemListen(form, 'submit', () =>
|
|
36
|
+
if (canListen(form)) __gemListen(form, 'submit', (event) => {
|
|
37
|
+
if (!(form.hasAttribute && form.hasAttribute('action'))) {
|
|
38
|
+
if (event && event.preventDefault) event.preventDefault();
|
|
39
|
+
const choice = form.querySelector && form.querySelector('select');
|
|
40
|
+
const trigger = bar.querySelector && bar.querySelector('[data-gem-variant-id]');
|
|
41
|
+
const variantId = (choice && choice.value) || (trigger && trigger.getAttribute('data-gem-variant-id')) || undefined;
|
|
42
|
+
if (bar.dispatchEvent && typeof window.CustomEvent === 'function') bar.dispatchEvent(new window.CustomEvent('gem:cart:add', { bubbles: true, detail: { variantId, quantity: 1 } }));
|
|
43
|
+
}
|
|
44
|
+
api.emit('gem:sticky-atc:submit', { source: 'gem-runtime', id: bar.id || undefined });
|
|
45
|
+
});
|
|
32
46
|
bound += 1;
|
|
33
47
|
});
|
|
34
48
|
api.emit('gem:sticky-atc:ready', { source: 'gem-runtime', count: bound });
|