@gemboss/gem-framework 0.9.2 → 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 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.2",
4
- "gemtheme_sha": "3fa982d1fd2a1c77df8c2d706abe8d5d4b509508",
3
+ "version": "0.9.8",
4
+ "gemtheme_sha": "fea3af50909be36e9fc8968ae407409e324a1c6b",
5
5
  "gemtheme_dirty": false,
6
6
  "framework_version": "0.1.0"
7
7
  }
@@ -2466,7 +2466,7 @@ main > .gem-section:first-child { padding-top: 0; }
2466
2466
  .gem-product-card-grid--quick-view-grid .gem-product-card-grid__meta { display: flex; }
2467
2467
  .gem-product-card-grid__controls { display: flex; align-items: center; gap: 20px; min-height: 32px; margin: 0 0 24px; }
2468
2468
  .gem-product-card-grid--quick-view-grid .gem-product-card-grid__controls { min-height: 51px; height: 51px; margin: 0 0 16px !important; padding-top: 10px; }
2469
- .gem-product-card-grid__grid { gap: 8px !important; }
2469
+ .gem-product-card-grid--quick-view-grid .gem-product-card-grid__grid { gap: 8px !important; }
2470
2470
  .gem-product-card-grid__controls .gem-collection-filter-toolbar__filters { display: flex; align-items: center; gap: 18px; border: 0; background: transparent; padding: 0; }
2471
2471
  .gem-product-card-grid__controls .gem-collection-filter-toolbar__filters summary { min-width: auto; border: 0; background: transparent; padding: 0; pointer-events: none; }
2472
2472
  .gem-product-card-grid__controls .gem-collection-filter-toolbar__filter, .gem-product-card-grid__controls .gem-collection-filter-toolbar__sort { display: flex; align-items: center; gap: 8px; font-size: 0.75rem; font-weight: 400; }
@@ -2611,7 +2611,7 @@ main > .gem-section:first-child { padding-top: 0; }
2611
2611
  .gem-hero-banner--compact-collection { padding-block: 25px; }
2612
2612
  .gem-hero-banner--compact-collection .gem-section__heading { font-size: 30px; line-height: 39px; }
2613
2613
  .gem-product-card-grid--quick-view-grid > .gem-section__inner, .gem-main-search > .gem-section__inner { width: calc(100% - 30px); }
2614
- .gem-product-card-grid__grid { gap: 4px !important; }
2614
+ .gem-product-card-grid--quick-view-grid .gem-product-card-grid__grid { gap: 4px !important; }
2615
2615
  .gem-product-card__content { padding-top: 8px; gap: 4px; }
2616
2616
  .gem-product-card__title { font-size: 15px; line-height: 24px; }
2617
2617
  .gem-product-card__price, .gem-product-card__price .gem-price { font-size: 16px; line-height: 24px; }
@@ -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
- if (canListen(pause)) pause.addEventListener('click', () => { paused = !paused; if (pause.setAttribute) pause.setAttribute('aria-pressed', String(paused)); start(); });
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', () => api.emit('gem:sticky-atc:submit', { source: 'gem-runtime', id: bar.id || undefined }));
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
@@ -3292,7 +3292,11 @@ function createDawnParityCss() {
3292
3292
  ".gem-product-card-grid--quick-view-grid .gem-product-card-grid__meta { display: flex; }",
3293
3293
  ".gem-product-card-grid__controls { display: flex; align-items: center; gap: 20px; min-height: 32px; margin: 0 0 24px; }",
3294
3294
  ".gem-product-card-grid--quick-view-grid .gem-product-card-grid__controls { min-height: 51px; height: 51px; margin: 0 0 16px !important; padding-top: 10px; }",
3295
- ".gem-product-card-grid__grid { gap: 8px !important; }",
3295
+ // Scoped to the Dawn quick-view grid it was measured on, as gemsite's copy is (app/globals.css). Unscoped,
3296
+ // it forced an 8px / 4px gap with !important on EVERY section reusing the class: GemFactory's tier-specs
3297
+ // stacked its cards 2-4px apart on mobile (library audit 2026-10-05), and this repo's own
3298
+ // product-card-grid/featured renamed its grid to __cards just to get out from under it (review #628).
3299
+ ".gem-product-card-grid--quick-view-grid .gem-product-card-grid__grid { gap: 8px !important; }",
3296
3300
  ".gem-product-card-grid__controls .gem-collection-filter-toolbar__filters { display: flex; align-items: center; gap: 18px; border: 0; background: transparent; padding: 0; }",
3297
3301
  ".gem-product-card-grid__controls .gem-collection-filter-toolbar__filters summary { min-width: auto; border: 0; background: transparent; padding: 0; pointer-events: none; }",
3298
3302
  ".gem-product-card-grid__controls .gem-collection-filter-toolbar__filter, .gem-product-card-grid__controls .gem-collection-filter-toolbar__sort { display: flex; align-items: center; gap: 8px; font-size: 0.75rem; font-weight: 400; }",
@@ -3445,7 +3449,7 @@ function createDawnParityMobileCss() {
3445
3449
  " .gem-hero-banner--compact-collection { padding-block: 25px; }",
3446
3450
  " .gem-hero-banner--compact-collection .gem-section__heading { font-size: 30px; line-height: 39px; }",
3447
3451
  " .gem-product-card-grid--quick-view-grid > .gem-section__inner, .gem-main-search > .gem-section__inner { width: calc(100% - 30px); }",
3448
- " .gem-product-card-grid__grid { gap: 4px !important; }",
3452
+ " .gem-product-card-grid--quick-view-grid .gem-product-card-grid__grid { gap: 4px !important; }",
3449
3453
  " .gem-product-card__content { padding-top: 8px; gap: 4px; }",
3450
3454
  " .gem-product-card__title { font-size: 15px; line-height: 24px; }",
3451
3455
  " .gem-product-card__price, .gem-product-card__price .gem-price { font-size: 16px; line-height: 24px; }",
@@ -4551,7 +4555,38 @@ var mediaGalleryPluginRegistrationLines = [
4551
4555
  " const galleries = document.querySelectorAll('[data-gem-media-gallery], .gem-media-gallery, .gem-product-media-gallery');",
4552
4556
  " let bound = 0;",
4553
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;",
4554
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
+ " }",
4555
4590
  " const items = gallery.querySelectorAll('[data-gem-media-item], img, video, model-viewer');",
4556
4591
  " const thumbControls = gallery.querySelectorAll('[data-gem-media-thumb]');",
4557
4592
  " const controls = thumbControls.length ? thumbControls : items;",
@@ -4663,7 +4698,11 @@ var heroSlideshowPluginRegistrationLines = [
4663
4698
  " else if (k === 'Home') { if (e.preventDefault) e.preventDefault(); focusDot(0); }",
4664
4699
  " else if (k === 'End') { if (e.preventDefault) e.preventDefault(); focusDot(dots.length - 1); }",
4665
4700
  " }); });",
4666
- " if (canListen(pause)) pause.addEventListener('click', () => { paused = !paused; if (pause.setAttribute) pause.setAttribute('aria-pressed', String(paused)); start(); });",
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(); });",
4667
4706
  " show(active); start(); bound += 1;",
4668
4707
  " });",
4669
4708
  " api.emit('gem:hero-slideshow:ready', { source: 'gem-runtime', count: bound });",
@@ -4878,8 +4917,27 @@ var stickyAtcPluginRegistrationLines = [
4878
4917
  " };",
4879
4918
  " update();",
4880
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.
4881
4930
  " const form = bar.querySelector && bar.querySelector('form');",
4882
- " if (canListen(form)) form.addEventListener('submit', () => api.emit('gem:sticky-atc:submit', { source: 'gem-runtime', id: bar.id || undefined }));",
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
+ " });",
4883
4941
  " bound += 1;",
4884
4942
  " });",
4885
4943
  " api.emit('gem:sticky-atc:ready', { source: 'gem-runtime', count: bound });",
@@ -5658,8 +5716,38 @@ function isAllowedUntokenizedValue(property, value) {
5658
5716
  function hasRawVisualValue(value) {
5659
5717
  return /#[0-9a-f]{3,8}\b|rgb\(|hsl\(|\b\d+(?:\.\d+)?(?:px|rem|em)\b/i.test(value);
5660
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
+ }
5661
5748
  function isKnownGemCssVariable(variableName, options) {
5662
5749
  if (canonicalCssVariableNames.has(variableName)) return true;
5750
+ if (gemFrameworkPublicCssVariables().has(variableName)) return true;
5663
5751
  const allowedPrefixes = [
5664
5752
  "--gem-seasonal-",
5665
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
- if (canListen(pause)) __gemListen(pause, 'click', () => { paused = !paused; if (pause.setAttribute) pause.setAttribute('aria-pressed', String(paused)); start(); });
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', () => api.emit('gem:sticky-atc:submit', { source: 'gem-runtime', id: bar.id || undefined }));
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 });