@gemboss/gem-framework 0.18.16 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -1
- package/dist/SOURCE.json +3 -3
- package/dist/gem-framework.css +23 -1
- package/dist/gem-runtime.js +94 -54
- package/dist/index.js +1668 -43
- package/dist/motion/core.d.ts +208 -0
- package/dist/motion/core.js +1 -0
- package/dist/motion/orbit.d.ts +4 -0
- package/dist/motion/orbit.js +1 -0
- package/dist/motion/quick-view.d.ts +4 -0
- package/dist/motion/quick-view.js +1 -0
- package/dist/motion/rail.d.ts +5 -0
- package/dist/motion/rail.js +1 -0
- package/dist/orbit-base.css +39 -0
- package/dist/plugins/before_after.js +1 -1
- package/dist/plugins/bundle.js +1 -1
- package/dist/plugins/carousel.js +1 -1
- package/dist/plugins/cart-drawer.js +1 -1
- package/dist/plugins/context-adapter.js +1 -1
- package/dist/plugins/countdown.js +1 -1
- package/dist/plugins/disclosure.js +1 -1
- package/dist/plugins/hero-slideshow.js +1 -1
- package/dist/plugins/img-in.js +1 -1
- package/dist/plugins/localization.js +1 -1
- package/dist/plugins/media-gallery.js +1 -1
- package/dist/plugins/mobile-menu.js +1 -1
- package/dist/plugins/offer-engine.js +1 -1
- package/dist/plugins/orbit.d.ts +9 -0
- package/dist/plugins/orbit.js +1 -0
- package/dist/plugins/predictive-search.js +1 -1
- package/dist/plugins/promo-popup.js +1 -1
- package/dist/plugins/quantity-input.js +1 -1
- package/dist/plugins/quick-view.js +1 -69
- package/dist/plugins/rail.d.ts +9 -0
- package/dist/plugins/rail.js +1 -0
- package/dist/plugins/recently-viewed.js +1 -1
- package/dist/plugins/reveal-on-scroll.js +1 -1
- package/dist/plugins/reviews.js +1 -1
- package/dist/plugins/runtime-slot-loader.js +1 -1
- package/dist/plugins/seasonal-effects.js +1 -1
- package/dist/plugins/size-guide.d.ts +9 -0
- package/dist/plugins/size-guide.js +38 -0
- package/dist/plugins/sticky-atc.js +1 -1
- package/dist/plugins/tabs.js +1 -1
- package/dist/plugins/variant-selects.js +1 -1
- package/dist/popup-shell.css +15 -0
- package/dist/primitives.css +4 -1
- package/dist/rail-base.css +48 -0
- package/dist/runtime-core.d.ts +2 -0
- package/dist/runtime-core.js +26 -15
- package/dist/section-gate.js +2368 -36
- package/dist/tokens.css +4 -0
- package/package.json +11 -1
package/dist/section-gate.js
CHANGED
|
@@ -186,6 +186,11 @@ function gemButtonGradientEndCss(accent = "var(--gem-color-accent)", accentText
|
|
|
186
186
|
var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
|
|
187
187
|
var GEM_TAP_MIN_CSS_VAR = "--gem-tap-min: 44px;";
|
|
188
188
|
var GEM_FOCUS_OFFSET_CSS_VAR = "--gem-focus-offset: 3px;";
|
|
189
|
+
var GEM_STROKE_CSS_VARS = [
|
|
190
|
+
"--gem-stroke: calc(1px * var(--gem-style-border, 1));",
|
|
191
|
+
"--gem-stroke-thick: calc(2px * var(--gem-style-border, 1));"
|
|
192
|
+
];
|
|
193
|
+
var GEM_UNDERLINE_THICKNESS_CSS_VAR = "--gem-underline-thickness: 2px;";
|
|
189
194
|
var GEM_DERIVED_SPACE_STEPS = { "2xs": 0.125, ms: 0.75, "4xl": 7 };
|
|
190
195
|
var GEM_DERIVED_SPACE_CSS_VARIABLES = Object.keys(GEM_DERIVED_SPACE_STEPS).map((key) => `--gem-space-${slugify(key)}`);
|
|
191
196
|
function gemDerivedSpaceCssVars(spacing) {
|
|
@@ -330,15 +335,15 @@ function gemBandScheme(c, band) {
|
|
|
330
335
|
const away = mixHex(g, hexRelativeLuminance(i) > hexRelativeLuminance(g) ? "#000000" : "#ffffff", 0.08);
|
|
331
336
|
const surface = hexContrastRatio(i, toward) >= MIN_TEXT_CONTRAST ? toward : away;
|
|
332
337
|
const grounds = [g, surface];
|
|
333
|
-
const
|
|
338
|
+
const text2 = aaTextColor(i, grounds);
|
|
334
339
|
const accent = aaTextColor(c.accent, grounds);
|
|
335
340
|
return {
|
|
336
341
|
background: g,
|
|
337
342
|
surface,
|
|
338
|
-
text,
|
|
343
|
+
text: text2,
|
|
339
344
|
muted: aaTextColor(mixHex(i, g, 0.3), grounds),
|
|
340
345
|
accent,
|
|
341
|
-
accentText: aaLabelColor(c.accentText, accent, [g,
|
|
346
|
+
accentText: aaLabelColor(c.accentText, accent, [g, text2]),
|
|
342
347
|
border: mixHex(g, i, 0.22),
|
|
343
348
|
success: aaTextColor(c.success, grounds),
|
|
344
349
|
warning: aaTextColor(c.warning, grounds),
|
|
@@ -563,7 +568,7 @@ function createGemTokenCss(tokens, selector = ":root", options = {}) {
|
|
|
563
568
|
...phone.layout.section ? [`--gem-layout-section-mobile: ${phone.layout.section};`] : [],
|
|
564
569
|
...toCssVars("component", tokens.component),
|
|
565
570
|
...toCssVars("morphology", tokens.morphology),
|
|
566
|
-
// THE GOVERNED TYPE RAMP — --gem-h1..h6 / --gem-body / --gem-text-sm.
|
|
571
|
+
// THE GOVERNED TYPE RAMP — --gem-h1..h6 / --gem-body / --gem-text-sm / --gem-text-xs (26/10/2026).
|
|
567
572
|
// gemcapture maps every measured font-size onto these names via `font_token`
|
|
568
573
|
// (clone_kit._type_token), and the gem-framework contract documents them, but nothing in the
|
|
569
574
|
// live framework defined them: a rebuild that followed the contract styled its headings with
|
|
@@ -578,7 +583,9 @@ function createGemTokenCss(tokens, selector = ":root", options = {}) {
|
|
|
578
583
|
// type ramp and registered in validation.ts. Follows the accent, so presets retint it for free.
|
|
579
584
|
GEM_FOCUS_RING_CSS_VAR,
|
|
580
585
|
GEM_TAP_MIN_CSS_VAR,
|
|
581
|
-
GEM_FOCUS_OFFSET_CSS_VAR
|
|
586
|
+
GEM_FOCUS_OFFSET_CSS_VAR,
|
|
587
|
+
...GEM_STROKE_CSS_VARS,
|
|
588
|
+
GEM_UNDERLINE_THICKNESS_CSS_VAR
|
|
582
589
|
];
|
|
583
590
|
const formBlock = gemFormCssBlock(tokens.form);
|
|
584
591
|
const buttonCase = gemButtonCaseCss(tokens, selector);
|
|
@@ -626,6 +633,13 @@ function gemTypeRampCssVars(typography) {
|
|
|
626
633
|
return [
|
|
627
634
|
`--gem-type-base: ${typeRampValue(typography, "base") ?? "1rem"};`,
|
|
628
635
|
`--gem-type-scale: ${typeRampValue(typography, "scale") ?? "1.22"};`,
|
|
636
|
+
// Chris 26/10/2026: P3 of the GemPages v7 scale (H1–H6, P1–P3 + Custom), one step under text-sm. P1 is body, P2 is
|
|
637
|
+
// text-sm; P3 had no name, so a caption or a fine-print line could only be a px count that no style pack retunes.
|
|
638
|
+
// Same pattern as text-sm, off the base rather than off text-sm: base ÷ scale², 10.75px at 16px and 1.22. Declared
|
|
639
|
+
// FIRST because this list is the ramp's order, small to large, and the seam's `steps` must equal it in order
|
|
640
|
+
// (check-token-steps-match-framework.ts). At the defaults it sits above gem_font_size's 10px floor; that floor bounds
|
|
641
|
+
// the px number only, so a plan with a steeper ratio (1.333: 9px) can take this step lower, as it can text-sm.
|
|
642
|
+
"--gem-text-xs: calc(var(--gem-type-base) / (var(--gem-type-scale) * var(--gem-type-scale)));",
|
|
629
643
|
"--gem-text-sm: calc(var(--gem-type-base) / var(--gem-type-scale));",
|
|
630
644
|
"--gem-body: var(--gem-type-base);",
|
|
631
645
|
"--gem-h6: calc(var(--gem-type-base) * var(--gem-type-scale));",
|
|
@@ -735,6 +749,1330 @@ function gemFrameworkEditionCss() {
|
|
|
735
749
|
return GEM_FRAMEWORK_EDITIONS.flatMap((e) => e.css.map((rule) => rule.replaceAll("&", gemFrameworkEditionScope(e.edition)))).join("\n");
|
|
736
750
|
}
|
|
737
751
|
|
|
752
|
+
// ../../packages/gem-framework/src/motion/core.js
|
|
753
|
+
var clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
|
|
754
|
+
var lerp = (a, b, t) => a + (b - a) * t;
|
|
755
|
+
var mod = (v, n) => (v % n + n) % n;
|
|
756
|
+
function smoothstep(edge0, edge1, v) {
|
|
757
|
+
const t = clamp((v - edge0) / (edge1 - edge0), 0, 1);
|
|
758
|
+
return t * t * (3 - 2 * t);
|
|
759
|
+
}
|
|
760
|
+
var follow = (dt, tau) => tau <= 0 ? 1 : 1 - Math.exp(-dt / tau);
|
|
761
|
+
function spring(s, target, dt, stiffness, damping) {
|
|
762
|
+
const f = dt / 16.67;
|
|
763
|
+
s.v += (-(s.x - target) * stiffness - s.v * damping) * f;
|
|
764
|
+
s.x += s.v * f;
|
|
765
|
+
return s;
|
|
766
|
+
}
|
|
767
|
+
function ringDelta(a, b, n) {
|
|
768
|
+
let d = mod(b - a, n);
|
|
769
|
+
if (d > n / 2) d -= n;
|
|
770
|
+
return d;
|
|
771
|
+
}
|
|
772
|
+
function ringPose(i, n, pos, o) {
|
|
773
|
+
const theta = ringDelta(pos, i, n) * o.spread;
|
|
774
|
+
const hidden = Math.abs(theta) > Math.PI;
|
|
775
|
+
const depth = (1 - Math.cos(theta)) / 2;
|
|
776
|
+
return {
|
|
777
|
+
x: o.rx * Math.sin(theta),
|
|
778
|
+
y: -o.ry * depth,
|
|
779
|
+
depth,
|
|
780
|
+
scale: lerp(1, o.back, depth),
|
|
781
|
+
opacity: hidden ? 0 : lerp(1, o.fade, depth),
|
|
782
|
+
blur: o.blur * depth,
|
|
783
|
+
z: Math.round((1 - depth) * 100),
|
|
784
|
+
lean: Math.sin(theta) * o.lean,
|
|
785
|
+
hidden
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
var throwTarget = (pos, velocity, carry, maxSlots) => Math.round(pos + clamp(velocity * carry, -maxSlots, maxSlots));
|
|
789
|
+
var written = /* @__PURE__ */ new WeakMap();
|
|
790
|
+
function setStyle(el, prop, value) {
|
|
791
|
+
let rec = written.get(el);
|
|
792
|
+
if (!rec) written.set(el, rec = {});
|
|
793
|
+
if (rec[prop] === value) return;
|
|
794
|
+
rec[prop] = value;
|
|
795
|
+
if (value === "") el.style.removeProperty(prop);
|
|
796
|
+
else el.style.setProperty(prop, value);
|
|
797
|
+
}
|
|
798
|
+
var effects = /* @__PURE__ */ new Map();
|
|
799
|
+
function defineEffect(spec) {
|
|
800
|
+
const effect = { hook: `[data-gem-${spec.key}]`, load: "viewport", renderer: "dom", tier: "static", fallback: "markup", settings: {}, ...spec, declared: spec };
|
|
801
|
+
effects.set(effect.key, effect);
|
|
802
|
+
return effect;
|
|
803
|
+
}
|
|
804
|
+
function frameTask(ctx, el, step, options) {
|
|
805
|
+
const task = ctx.scheduler.add(step, options);
|
|
806
|
+
ctx.cleanup(() => task.remove());
|
|
807
|
+
observeVisibility(ctx, el, (shown) => task.visible(shown));
|
|
808
|
+
return task;
|
|
809
|
+
}
|
|
810
|
+
function observeVisibility(ctx, el, cb, margin) {
|
|
811
|
+
const IO = ctx.window.IntersectionObserver;
|
|
812
|
+
if (typeof IO !== "function") return;
|
|
813
|
+
new IO((entries) => cb(entries.some((e) => e.isIntersecting)), { rootMargin: margin || "120px" }).observe(el);
|
|
814
|
+
}
|
|
815
|
+
function observeSize(ctx, el, cb) {
|
|
816
|
+
const RO = ctx.window.ResizeObserver;
|
|
817
|
+
if (typeof RO === "function") new RO(() => cb()).observe(el);
|
|
818
|
+
else ctx.on(ctx.window, "resize", () => cb());
|
|
819
|
+
}
|
|
820
|
+
function horizontalDrag(ctx, el, h) {
|
|
821
|
+
let id = null;
|
|
822
|
+
let x0 = 0;
|
|
823
|
+
let y0 = 0;
|
|
824
|
+
let decided = false;
|
|
825
|
+
let horizontal = false;
|
|
826
|
+
let moved = 0;
|
|
827
|
+
let samples = [];
|
|
828
|
+
const release = (e, fire) => {
|
|
829
|
+
if (e.pointerId !== id) return;
|
|
830
|
+
id = null;
|
|
831
|
+
const first = samples[0];
|
|
832
|
+
const last = samples[samples.length - 1];
|
|
833
|
+
const span = last.t - first.t;
|
|
834
|
+
const vx = span > 0 && e.timeStamp - last.t < 80 ? (last.x - first.x) / span : 0;
|
|
835
|
+
if (fire && horizontal) h.end(vx, moved, e);
|
|
836
|
+
else h.end(0, 0, e);
|
|
837
|
+
};
|
|
838
|
+
ctx.on(el, "pointerdown", (e) => {
|
|
839
|
+
if (e.button !== 0 || id !== null) return;
|
|
840
|
+
id = e.pointerId;
|
|
841
|
+
x0 = e.clientX;
|
|
842
|
+
y0 = e.clientY;
|
|
843
|
+
decided = horizontal = false;
|
|
844
|
+
moved = 0;
|
|
845
|
+
samples = [{ x: e.clientX, t: e.timeStamp }];
|
|
846
|
+
h.start(e);
|
|
847
|
+
});
|
|
848
|
+
ctx.on(el, "pointermove", (e) => {
|
|
849
|
+
if (e.pointerId !== id) return;
|
|
850
|
+
const dx = e.clientX - x0;
|
|
851
|
+
if (!decided && Math.hypot(dx, e.clientY - y0) > 6) {
|
|
852
|
+
decided = true;
|
|
853
|
+
horizontal = Math.abs(dx) > Math.abs(e.clientY - y0);
|
|
854
|
+
if (!horizontal) return release(e, false);
|
|
855
|
+
try {
|
|
856
|
+
el.setPointerCapture(e.pointerId);
|
|
857
|
+
} catch {
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (!horizontal) return;
|
|
861
|
+
moved = Math.max(moved, Math.abs(dx));
|
|
862
|
+
samples.push({ x: e.clientX, t: e.timeStamp });
|
|
863
|
+
while (samples.length > 2 && e.timeStamp - samples[0].t > 90) samples.shift();
|
|
864
|
+
h.move(dx, e);
|
|
865
|
+
});
|
|
866
|
+
ctx.on(el, "pointerup", (e) => release(e, true));
|
|
867
|
+
ctx.on(el, "pointercancel", (e) => release(e, true));
|
|
868
|
+
}
|
|
869
|
+
function wheelSteps(ctx, el, onStep) {
|
|
870
|
+
let acc = 0;
|
|
871
|
+
let lock = 0;
|
|
872
|
+
ctx.on(
|
|
873
|
+
el,
|
|
874
|
+
"wheel",
|
|
875
|
+
(e) => {
|
|
876
|
+
if (Math.abs(e.deltaX) <= Math.abs(e.deltaY) || e.target.closest && e.target.closest("dialog")) return;
|
|
877
|
+
e.preventDefault();
|
|
878
|
+
acc += e.deltaX;
|
|
879
|
+
if (e.timeStamp < lock || Math.abs(acc) < 24) return;
|
|
880
|
+
onStep(Math.sign(acc));
|
|
881
|
+
acc = 0;
|
|
882
|
+
lock = e.timeStamp + 280;
|
|
883
|
+
},
|
|
884
|
+
{ passive: false }
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
function arrowKeys(ctx, el, { step, activate, accept }) {
|
|
888
|
+
ctx.on(el, "keydown", (e) => {
|
|
889
|
+
if (accept && !accept(e)) return;
|
|
890
|
+
if (e.key === "ArrowRight" || e.key === "ArrowLeft") step(e.key === "ArrowRight" ? 1 : -1);
|
|
891
|
+
else if (!activate || e.key !== "Enter" && e.key !== " " || activate(e) === false) return;
|
|
892
|
+
e.preventDefault();
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
function flip(el, from, options) {
|
|
896
|
+
if (!from || !from.width || typeof el.animate !== "function") return null;
|
|
897
|
+
const box = el.getBoundingClientRect();
|
|
898
|
+
if (!box.height) return null;
|
|
899
|
+
const away = { transform: `translate(${from.left + from.width / 2 - box.left - box.width / 2}px, ${from.top + from.height / 2 - box.top - box.height / 2}px) scale(${from.height / box.height})` };
|
|
900
|
+
const home = { transform: "none" };
|
|
901
|
+
return el.animate(options.back ? [home, away] : [away, home], { duration: options.duration, easing: options.easing, fill: "both" });
|
|
902
|
+
}
|
|
903
|
+
function loadImage(doc, image) {
|
|
904
|
+
const img = doc.createElement("img");
|
|
905
|
+
if (image.srcset) img.srcset = image.srcset;
|
|
906
|
+
img.src = image.src;
|
|
907
|
+
return typeof img.decode === "function" ? img.decode().then(() => img, () => img) : Promise.resolve(img);
|
|
908
|
+
}
|
|
909
|
+
function putImage(img, image) {
|
|
910
|
+
if (!img || !image || !image.src || img.getAttribute("src") === image.src) return;
|
|
911
|
+
if (image.srcset) img.setAttribute("srcset", image.srcset);
|
|
912
|
+
else img.removeAttribute("srcset");
|
|
913
|
+
img.setAttribute("src", image.src);
|
|
914
|
+
}
|
|
915
|
+
function swapText(el, text2, still) {
|
|
916
|
+
if (!el || el.textContent === text2) return;
|
|
917
|
+
if (still || typeof el.animate !== "function") {
|
|
918
|
+
el.textContent = text2;
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const out = el.animate([{ opacity: 1 }, { opacity: 0, transform: "translateY(-6px)" }], { duration: 140, fill: "forwards" });
|
|
922
|
+
out.onfinish = () => {
|
|
923
|
+
el.textContent = text2;
|
|
924
|
+
out.cancel();
|
|
925
|
+
el.animate([{ opacity: 0, transform: "translateY(8px)" }, { opacity: 1, transform: "none" }], { duration: 220, easing: "cubic-bezier(.2,.7,.2,1)" });
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
function readProduct(el) {
|
|
929
|
+
try {
|
|
930
|
+
const p = JSON.parse(el.getAttribute("data-gem-product") || "null");
|
|
931
|
+
return p && p.image && p.image.src ? p : null;
|
|
932
|
+
} catch {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
function formatMoney(cents, format) {
|
|
937
|
+
const value = (Number(cents) || 0) / 100;
|
|
938
|
+
const group = (str, sep) => str.replace(/\B(?=(\d{3})+(?!\d))/g, sep);
|
|
939
|
+
const [int, dec] = value.toFixed(2).split(".");
|
|
940
|
+
return (format || "${{amount}}").replace(/\{\{\s*(\w+)\s*\}\}/, (_, key) => {
|
|
941
|
+
if (key === "amount_no_decimals") return group(String(Math.round(value)), ",");
|
|
942
|
+
if (key === "amount_with_comma_separator") return `${group(int, ".")},${dec}`;
|
|
943
|
+
if (key === "amount_no_decimals_with_comma_separator") return group(String(Math.round(value)), ".");
|
|
944
|
+
return `${group(int, ",")}.${dec}`;
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
function priceText(p, el, which) {
|
|
948
|
+
const compare = which === "compare";
|
|
949
|
+
const words = compare ? p.compare_at_text : p.price_text;
|
|
950
|
+
if (words != null) return String(words);
|
|
951
|
+
const cents = compare ? p.compare_at : p.price;
|
|
952
|
+
if (cents == null || cents === "") return "";
|
|
953
|
+
if (typeof cents !== "number") return String(cents);
|
|
954
|
+
const holder = el && el.closest ? el.closest("[data-gem-money-format]") : null;
|
|
955
|
+
return formatMoney(cents, holder ? holder.getAttribute("data-gem-money-format") : void 0);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// ../../packages/gem-framework/src/motion/orbit.js
|
|
959
|
+
var pad = (v) => String(v).padStart(2, "0");
|
|
960
|
+
function mountOrbit(root, ctx) {
|
|
961
|
+
const items = [];
|
|
962
|
+
const products = [];
|
|
963
|
+
const hits = [];
|
|
964
|
+
const imgs = [];
|
|
965
|
+
root.querySelectorAll("[data-gem-orbit-item]").forEach((item) => {
|
|
966
|
+
const product = readProduct(item);
|
|
967
|
+
if (!product) return;
|
|
968
|
+
items.push(item);
|
|
969
|
+
products.push(product);
|
|
970
|
+
hits.push(item.querySelector(".gem-orbit__hit") || item);
|
|
971
|
+
imgs.push(item.querySelector("img"));
|
|
972
|
+
});
|
|
973
|
+
const n = items.length;
|
|
974
|
+
if (!n) return false;
|
|
975
|
+
const part = (name) => root.querySelector(".gem-orbit__" + name);
|
|
976
|
+
const stage = part("stage") || root;
|
|
977
|
+
const track = part("track");
|
|
978
|
+
const style = ctx.renderer.style;
|
|
979
|
+
let s = ctx.settings;
|
|
980
|
+
const start = Math.min(n - 1, s.start);
|
|
981
|
+
let pos = start;
|
|
982
|
+
let target = start;
|
|
983
|
+
let dragFrom = 0;
|
|
984
|
+
let dragging = false;
|
|
985
|
+
let rendered = false;
|
|
986
|
+
let front = -1;
|
|
987
|
+
let ring = null;
|
|
988
|
+
let slotPx = 60;
|
|
989
|
+
const full = () => ctx.tier() === "full";
|
|
990
|
+
const layout = () => {
|
|
991
|
+
s = ctx.reread();
|
|
992
|
+
const w = stage.clientWidth;
|
|
993
|
+
ring = {
|
|
994
|
+
rx: w * s.rx,
|
|
995
|
+
ry: stage.clientHeight * s.tilt,
|
|
996
|
+
back: s.back,
|
|
997
|
+
fade: s.fade,
|
|
998
|
+
blur: full() ? s.blur : 0,
|
|
999
|
+
lean: s.lean,
|
|
1000
|
+
spread: (s.spread || 360 / n) * Math.PI / 180
|
|
1001
|
+
};
|
|
1002
|
+
slotPx = Math.max(60, ring.rx * ring.spread / 1.2);
|
|
1003
|
+
};
|
|
1004
|
+
const setTint = (tint) => {
|
|
1005
|
+
if (!s.tint) return;
|
|
1006
|
+
if (tint) root.style.setProperty("--gem-motion-tint", tint);
|
|
1007
|
+
else root.style.removeProperty("--gem-motion-tint");
|
|
1008
|
+
};
|
|
1009
|
+
const frontIndex = () => mod(Math.round(pos), n);
|
|
1010
|
+
const indexOf = (el) => items.findIndex((item) => item === el || item.contains(el));
|
|
1011
|
+
const updateFront = (initial) => {
|
|
1012
|
+
const i = frontIndex();
|
|
1013
|
+
if (i === front) return;
|
|
1014
|
+
front = i;
|
|
1015
|
+
const p = products[i];
|
|
1016
|
+
items.forEach((el, k) => {
|
|
1017
|
+
if (k === i) el.setAttribute("aria-current", "true");
|
|
1018
|
+
else el.removeAttribute("aria-current");
|
|
1019
|
+
hits[k].tabIndex = k === i ? 0 : -1;
|
|
1020
|
+
});
|
|
1021
|
+
if (!initial) {
|
|
1022
|
+
const still = ctx.tier() === "static";
|
|
1023
|
+
const price = priceText(p, root);
|
|
1024
|
+
swapText(part("name"), p.title || "", still);
|
|
1025
|
+
swapText(part("price"), price, still);
|
|
1026
|
+
const count = part("count");
|
|
1027
|
+
if (count) count.textContent = pad(i + 1) + " / " + pad(n);
|
|
1028
|
+
const bar = part("bar");
|
|
1029
|
+
if (bar) bar.style.transform = "scaleX(" + (i + 1) / n + ")";
|
|
1030
|
+
const live = part("live");
|
|
1031
|
+
const words = { index: i + 1, count: n, title: p.title || "", price };
|
|
1032
|
+
if (live) live.textContent = s.live.replace(/\{(\w+)\}/g, (m, k) => Object.hasOwn(words, k) ? String(words[k]) : m);
|
|
1033
|
+
setTint(p.tint);
|
|
1034
|
+
}
|
|
1035
|
+
ctx.emit("gem:motion:front", { index: i, product: p });
|
|
1036
|
+
};
|
|
1037
|
+
const frame = (dt, time) => {
|
|
1038
|
+
let moving = dragging;
|
|
1039
|
+
if (!dragging) {
|
|
1040
|
+
const d = target - pos;
|
|
1041
|
+
if (ctx.tier() === "static" || Math.abs(d) < 8e-4) pos = target;
|
|
1042
|
+
else {
|
|
1043
|
+
pos += d * follow(dt, s.ease);
|
|
1044
|
+
moving = true;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
const drift = s.drift && full();
|
|
1048
|
+
for (let i = 0; i < n; i++) {
|
|
1049
|
+
const p = ringPose(i, n, pos, ring);
|
|
1050
|
+
const bob = drift ? Math.sin(time / 1100 + i * 1.9) * 5 * (1 - p.depth) : 0;
|
|
1051
|
+
const el = items[i];
|
|
1052
|
+
style(el, "transform", `translate3d(${p.x.toFixed(1)}px, ${(p.y + bob).toFixed(1)}px, 0) scale(${p.scale.toFixed(3)}) rotate(${p.lean.toFixed(2)}deg)`);
|
|
1053
|
+
style(el, "opacity", p.opacity.toFixed(3));
|
|
1054
|
+
style(el, "z-index", String(p.z));
|
|
1055
|
+
const blur = Math.round(p.blur * 2) / 2;
|
|
1056
|
+
style(el, "filter", blur > 0 ? `blur(${blur}px)` : "none");
|
|
1057
|
+
style(el, "visibility", p.hidden ? "hidden" : "visible");
|
|
1058
|
+
}
|
|
1059
|
+
rendered = true;
|
|
1060
|
+
updateFront(false);
|
|
1061
|
+
return moving || drift;
|
|
1062
|
+
};
|
|
1063
|
+
const refresh = () => {
|
|
1064
|
+
layout();
|
|
1065
|
+
if (rendered) task.kick();
|
|
1066
|
+
};
|
|
1067
|
+
const task = frameTask(ctx, root, frame, { onDegrade: refresh });
|
|
1068
|
+
const goTo = (index) => {
|
|
1069
|
+
const from = Math.round(target);
|
|
1070
|
+
target = from + ringDelta(from, mod(index, n), n);
|
|
1071
|
+
task.kick();
|
|
1072
|
+
};
|
|
1073
|
+
hits.forEach((hit, i) => {
|
|
1074
|
+
ctx.on(hit, "click", (event) => {
|
|
1075
|
+
if (i === frontIndex()) return;
|
|
1076
|
+
event.preventDefault();
|
|
1077
|
+
goTo(i);
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
1080
|
+
root.querySelectorAll("[data-gem-orbit-step]").forEach((button) => {
|
|
1081
|
+
ctx.on(button, "click", () => goTo(target + (Number(button.getAttribute("data-gem-orbit-step")) || 0)));
|
|
1082
|
+
});
|
|
1083
|
+
if (track) {
|
|
1084
|
+
ctx.on(track, "click", (event) => {
|
|
1085
|
+
const r = track.getBoundingClientRect();
|
|
1086
|
+
if (r.width) goTo(Math.round((event.clientX - r.left) / r.width * (n - 1)));
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
horizontalDrag(ctx, stage, {
|
|
1090
|
+
start: () => {
|
|
1091
|
+
dragFrom = pos;
|
|
1092
|
+
},
|
|
1093
|
+
move: (dx) => {
|
|
1094
|
+
dragging = true;
|
|
1095
|
+
root.setAttribute("data-gem-state", "dragging");
|
|
1096
|
+
pos = dragFrom - dx / slotPx;
|
|
1097
|
+
task.kick();
|
|
1098
|
+
},
|
|
1099
|
+
end: (vx, moved) => {
|
|
1100
|
+
if (!dragging) return;
|
|
1101
|
+
dragging = false;
|
|
1102
|
+
root.setAttribute("data-gem-state", "ready");
|
|
1103
|
+
target = moved < 4 ? Math.round(pos) : throwTarget(pos, -vx / slotPx, s.carry, Math.min(3, Math.ceil(n / 2)));
|
|
1104
|
+
task.kick();
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
wheelSteps(ctx, root, (d) => goTo(target + d));
|
|
1108
|
+
arrowKeys(ctx, root, {
|
|
1109
|
+
accept: (e) => e.target === root || hits.includes(e.target),
|
|
1110
|
+
step: (d) => goTo(target + d),
|
|
1111
|
+
activate: (e) => e.target === root ? hits[frontIndex()].click() : false
|
|
1112
|
+
});
|
|
1113
|
+
layout();
|
|
1114
|
+
observeSize(ctx, root, refresh);
|
|
1115
|
+
root.gemQuickViewHost = {
|
|
1116
|
+
key: "orbit",
|
|
1117
|
+
products,
|
|
1118
|
+
indexOf,
|
|
1119
|
+
opens: (el) => indexOf(el) === frontIndex(),
|
|
1120
|
+
imageFor: (i) => imgs[i] || null,
|
|
1121
|
+
triggerFor: (i) => hits[i] || null,
|
|
1122
|
+
goTo,
|
|
1123
|
+
setOpen: (open) => root.setAttribute("data-gem-state", open ? "open" : "ready"),
|
|
1124
|
+
setImage: (i, image) => putImage(imgs[i], image),
|
|
1125
|
+
setTint
|
|
1126
|
+
};
|
|
1127
|
+
root.setAttribute("data-gem-quick-view-host", "orbit");
|
|
1128
|
+
root.setAttribute("data-gem-state", "ready");
|
|
1129
|
+
updateFront(true);
|
|
1130
|
+
if (s.drift && full()) task.kick();
|
|
1131
|
+
return () => {
|
|
1132
|
+
delete root.gemQuickViewHost;
|
|
1133
|
+
root.removeAttribute("data-gem-quick-view-host");
|
|
1134
|
+
root.removeAttribute("data-gem-state");
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
var orbitEffect = defineEffect({
|
|
1138
|
+
key: "orbit",
|
|
1139
|
+
hook: "[data-gem-orbit]",
|
|
1140
|
+
load: "viewport",
|
|
1141
|
+
renderer: "dom",
|
|
1142
|
+
tier: "static",
|
|
1143
|
+
fallback: "markup",
|
|
1144
|
+
budget: { gzip: 6144 },
|
|
1145
|
+
settings: {
|
|
1146
|
+
rx: { type: "number", min: 0.1, max: 0.5, step: 0.01, default: 0.36, css: true, label: "Ring width (share of the stage width)" },
|
|
1147
|
+
tilt: { type: "number", min: 0, max: 0.5, step: 0.01, default: 0.3, css: true, label: "Tilt (rise of the back, share of the stage height)" },
|
|
1148
|
+
size: { type: "number", min: 0.2, max: 0.9, step: 0.01, default: 0.62, css: true, label: "Piece size (share of the stage height)" },
|
|
1149
|
+
back: { type: "number", min: 0.1, max: 1, step: 0.01, default: 0.42, css: true, label: "Scale at the back" },
|
|
1150
|
+
fade: { type: "number", min: 0, max: 1, step: 0.01, default: 0.35, css: true, label: "Opacity at the back" },
|
|
1151
|
+
blur: { type: "number", min: 0, max: 8, step: 0.5, default: 2.5, css: true, label: "Blur at the back (px)" },
|
|
1152
|
+
lean: { type: "number", min: 0, max: 20, step: 1, default: 5, css: true, label: "Lean at the sides (degrees)" },
|
|
1153
|
+
spread: { type: "number", min: 0, max: 180, step: 1, default: 0, css: true, label: "Degrees between pieces (0: round the full ring)" },
|
|
1154
|
+
start: { type: "integer", min: 0, default: 0, css: true, label: "Piece in front at first" },
|
|
1155
|
+
ease: { type: "number", min: 40, max: 600, step: 10, default: 140, label: "Settle time (ms)" },
|
|
1156
|
+
carry: { type: "number", min: 0, max: 800, step: 10, default: 240, label: "How far a throw carries (ms)" },
|
|
1157
|
+
tint: { type: "boolean", default: true, label: "Tint the band with the front piece's colour" },
|
|
1158
|
+
drift: { type: "boolean", default: false, label: "Idle float (full tier only)" },
|
|
1159
|
+
live: { type: "text", default: "{index} / {count}: {title}, {price}", label: "What a screen reader hears when the front piece changes" }
|
|
1160
|
+
},
|
|
1161
|
+
mount: mountOrbit
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
// ../../packages/gem-framework/src/motion/rail.js
|
|
1165
|
+
var COMPACT_BELOW = 700;
|
|
1166
|
+
var ROD_TOP = 14;
|
|
1167
|
+
function createTurn(el) {
|
|
1168
|
+
if (!el) return null;
|
|
1169
|
+
const side = el.querySelector(".gem-turn__side");
|
|
1170
|
+
const front = el.querySelector(".gem-turn__front");
|
|
1171
|
+
const sheets = el.querySelectorAll(".gem-turn__sheet");
|
|
1172
|
+
const frames = Math.floor(Number(el.getAttribute("data-gem-turn-frames"))) || 0;
|
|
1173
|
+
const cols = Math.floor(Number(el.getAttribute("data-gem-turn-cols"))) || 0;
|
|
1174
|
+
const mode = sheets.length > 1 && frames > 1 && cols > 0 ? "sprite" : side && front ? "pair" : front ? "flip" : null;
|
|
1175
|
+
if (!mode) return null;
|
|
1176
|
+
const rows = Math.ceil(frames / Math.max(1, cols)) || 1;
|
|
1177
|
+
const place = (sheet, f) => setStyle(sheet, "transform", `translate(${(-(f % cols) * 100 / cols).toFixed(3)}%, ${(-Math.floor(f / cols) * 100 / rows).toFixed(3)}%)`);
|
|
1178
|
+
let last = -1;
|
|
1179
|
+
return {
|
|
1180
|
+
mode,
|
|
1181
|
+
/** what the quick view flies out of */
|
|
1182
|
+
image: mode === "sprite" ? el : front,
|
|
1183
|
+
set(t) {
|
|
1184
|
+
const q = Math.round(t * 1e3) / 1e3;
|
|
1185
|
+
if (q === last) return;
|
|
1186
|
+
last = q;
|
|
1187
|
+
if (mode === "pair") {
|
|
1188
|
+
setStyle(side, "transform", `perspective(900px) rotateY(${(-82 * q).toFixed(1)}deg)`);
|
|
1189
|
+
setStyle(side, "opacity", (1 - smoothstep(0.42, 0.78, q)).toFixed(3));
|
|
1190
|
+
setStyle(front, "opacity", smoothstep(0.45, 0.82, q).toFixed(3));
|
|
1191
|
+
setStyle(front, "transform", `scale(${lerp(0.9, 1, q).toFixed(3)})`);
|
|
1192
|
+
} else if (mode === "sprite") {
|
|
1193
|
+
const f = q * (frames - 1);
|
|
1194
|
+
const a = Math.floor(f);
|
|
1195
|
+
place(sheets[0], a);
|
|
1196
|
+
place(sheets[1], Math.min(frames - 1, a + 1));
|
|
1197
|
+
setStyle(sheets[1], "opacity", (f - a).toFixed(3));
|
|
1198
|
+
} else {
|
|
1199
|
+
setStyle(front, "transform", `perspective(900px) rotateY(${lerp(-74, 0, q).toFixed(1)}deg) scale(${lerp(0.86, 1, q).toFixed(3)})`);
|
|
1200
|
+
}
|
|
1201
|
+
},
|
|
1202
|
+
/** Another colour of the same piece (picked in the quick view). A sprite keeps its sheet. */
|
|
1203
|
+
setImages(image, sideImage) {
|
|
1204
|
+
if (mode === "sprite") return;
|
|
1205
|
+
putImage(front, image);
|
|
1206
|
+
putImage(side, sideImage);
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
function mountRail(root, ctx) {
|
|
1211
|
+
const items = [];
|
|
1212
|
+
const products = [];
|
|
1213
|
+
const hits = [];
|
|
1214
|
+
const bodies = [];
|
|
1215
|
+
const turns = [];
|
|
1216
|
+
root.querySelectorAll("[data-gem-rail-item]").forEach((item) => {
|
|
1217
|
+
const product = readProduct(item);
|
|
1218
|
+
if (!product) return;
|
|
1219
|
+
const hit = item.querySelector(".gem-rail__hit") || item;
|
|
1220
|
+
items.push(item);
|
|
1221
|
+
products.push(product);
|
|
1222
|
+
hits.push(hit);
|
|
1223
|
+
bodies.push(item.querySelector(".gem-rail__body") || hit);
|
|
1224
|
+
turns.push(createTurn(item.querySelector(".gem-turn")));
|
|
1225
|
+
});
|
|
1226
|
+
const n = items.length;
|
|
1227
|
+
if (!n) return false;
|
|
1228
|
+
const stage = root.querySelector(".gem-rail__stage") || root;
|
|
1229
|
+
const track = root.querySelector(".gem-rail__track") || stage;
|
|
1230
|
+
const caption = root.querySelector(".gem-rail__caption");
|
|
1231
|
+
const rod = root.querySelector(".gem-rail__rod path");
|
|
1232
|
+
const style = ctx.renderer.style;
|
|
1233
|
+
const turn = items.map(() => 0);
|
|
1234
|
+
const hangers = items.map(() => ({ x: 0, v: 0 }));
|
|
1235
|
+
let s = ctx.settings;
|
|
1236
|
+
let goal = items.map(() => 0);
|
|
1237
|
+
let focused = -1;
|
|
1238
|
+
let compact = false;
|
|
1239
|
+
let scrolled = false;
|
|
1240
|
+
let rendered = false;
|
|
1241
|
+
let w = 0;
|
|
1242
|
+
let h = 0;
|
|
1243
|
+
let lastX = 0;
|
|
1244
|
+
let lastT = 0;
|
|
1245
|
+
let dpx = 0;
|
|
1246
|
+
const still = () => ctx.tier() === "static";
|
|
1247
|
+
const rodPath = () => `M0 ${ROD_TOP} Q50 ${Math.round(ROD_TOP * 10 + s.sag * 2e3) / 10} 100 ${ROD_TOP}`;
|
|
1248
|
+
if (rod && rod.getAttribute("d") !== rodPath()) rod.setAttribute("d", rodPath());
|
|
1249
|
+
const layout = () => {
|
|
1250
|
+
s = ctx.reread();
|
|
1251
|
+
const was = compact;
|
|
1252
|
+
compact = root.clientWidth < COMPACT_BELOW;
|
|
1253
|
+
w = stage.clientWidth;
|
|
1254
|
+
h = stage.clientHeight;
|
|
1255
|
+
if (compact && !was) items.forEach((el) => style(el, "transform", ""));
|
|
1256
|
+
};
|
|
1257
|
+
const rodY = (x) => {
|
|
1258
|
+
const t = clamp(x / Math.max(1, w), 0, 1);
|
|
1259
|
+
return h * ROD_TOP / 100 + 4 * s.sag * h * t * (1 - t);
|
|
1260
|
+
};
|
|
1261
|
+
const slots = () => {
|
|
1262
|
+
const weights = turn.map((t) => 1 + s.grow * t * t);
|
|
1263
|
+
const unit = w * 0.92 / weights.reduce((a, b) => a + b, 0);
|
|
1264
|
+
let acc = w * 0.04;
|
|
1265
|
+
return weights.map((x) => {
|
|
1266
|
+
const c = acc + x * unit / 2;
|
|
1267
|
+
acc += x * unit;
|
|
1268
|
+
return { c, width: x * unit };
|
|
1269
|
+
});
|
|
1270
|
+
};
|
|
1271
|
+
const nearest = (x) => {
|
|
1272
|
+
let best = -1;
|
|
1273
|
+
let bestD = Infinity;
|
|
1274
|
+
slots().forEach(({ c, width }, i) => {
|
|
1275
|
+
const d = Math.abs(c - x);
|
|
1276
|
+
if (d < bestD && d < width * 1.2) {
|
|
1277
|
+
best = i;
|
|
1278
|
+
bestD = d;
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
return best;
|
|
1282
|
+
};
|
|
1283
|
+
const setTint = (tint) => {
|
|
1284
|
+
if (!s.tint) return;
|
|
1285
|
+
if (tint) root.style.setProperty("--gem-motion-tint", tint);
|
|
1286
|
+
else root.style.removeProperty("--gem-motion-tint");
|
|
1287
|
+
};
|
|
1288
|
+
const setFocus = (i, initial) => {
|
|
1289
|
+
if (i === focused) return;
|
|
1290
|
+
focused = i;
|
|
1291
|
+
goal = goal.map((_, k) => k === i ? 1 : 0);
|
|
1292
|
+
const p = products[i];
|
|
1293
|
+
if (initial) {
|
|
1294
|
+
turn[i] = 1;
|
|
1295
|
+
if (turns[i]) turns[i].set(1);
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
if (caption) caption.textContent = p ? [p.title, priceText(p, root)].filter(Boolean).join(" \xB7 ") : "";
|
|
1299
|
+
if (p) {
|
|
1300
|
+
setTint(p.tint);
|
|
1301
|
+
ctx.emit("gem:motion:front", { index: i, product: p });
|
|
1302
|
+
}
|
|
1303
|
+
task.kick();
|
|
1304
|
+
};
|
|
1305
|
+
const frame = (dt) => {
|
|
1306
|
+
const k = follow(dt, s.ease);
|
|
1307
|
+
let moving = false;
|
|
1308
|
+
if (compact && scrolled) {
|
|
1309
|
+
const mid = track.scrollLeft + track.clientWidth / 2;
|
|
1310
|
+
let best = 0;
|
|
1311
|
+
items.forEach((el, i) => {
|
|
1312
|
+
if (Math.abs(el.offsetLeft + el.offsetWidth / 2 - mid) < Math.abs(items[best].offsetLeft + items[best].offsetWidth / 2 - mid)) best = i;
|
|
1313
|
+
});
|
|
1314
|
+
if (best !== focused) setFocus(best);
|
|
1315
|
+
}
|
|
1316
|
+
for (let i = 0; i < n; i++) {
|
|
1317
|
+
const d = goal[i] - turn[i];
|
|
1318
|
+
turn[i] = still() || Math.abs(d) < 2e-3 ? goal[i] : turn[i] + d * k;
|
|
1319
|
+
if (turn[i] !== goal[i]) moving = true;
|
|
1320
|
+
const hanger = hangers[i];
|
|
1321
|
+
if (still()) hanger.x = hanger.v = 0;
|
|
1322
|
+
else {
|
|
1323
|
+
spring(hanger, 0, dt, 0.02, 0.09);
|
|
1324
|
+
hanger.x = clamp(hanger.x, -9, 9);
|
|
1325
|
+
if (Math.abs(hanger.x) > 0.02 || Math.abs(hanger.v) > 0.02) moving = true;
|
|
1326
|
+
else hanger.x = hanger.v = 0;
|
|
1327
|
+
}
|
|
1328
|
+
if (turns[i]) turns[i].set(turn[i]);
|
|
1329
|
+
style(bodies[i], "transform", `rotate(${hanger.x.toFixed(2)}deg)`);
|
|
1330
|
+
}
|
|
1331
|
+
if (!compact) {
|
|
1332
|
+
slots().forEach(({ c }, i) => {
|
|
1333
|
+
style(items[i], "transform", `translate3d(${c.toFixed(1)}px, ${rodY(c).toFixed(1)}px, 0)`);
|
|
1334
|
+
style(items[i], "z-index", String(Math.round(10 + turn[i] * 10)));
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
rendered = true;
|
|
1338
|
+
return moving;
|
|
1339
|
+
};
|
|
1340
|
+
const refresh = () => {
|
|
1341
|
+
layout();
|
|
1342
|
+
if (rendered) task.kick();
|
|
1343
|
+
};
|
|
1344
|
+
const task = frameTask(ctx, root, frame, { onDegrade: refresh });
|
|
1345
|
+
hits.forEach((hit, i) => ctx.on(hit, "focus", () => setFocus(i)));
|
|
1346
|
+
ctx.on(stage, "click", (event) => {
|
|
1347
|
+
if (compact || hits.some((hit) => hit.contains(event.target))) return;
|
|
1348
|
+
const i = nearest(event.clientX - stage.getBoundingClientRect().left);
|
|
1349
|
+
if (i >= 0) hits[i].click();
|
|
1350
|
+
});
|
|
1351
|
+
ctx.on(stage, "pointermove", (event) => {
|
|
1352
|
+
if (compact || event.pointerType !== "mouse") return;
|
|
1353
|
+
const x = event.clientX - stage.getBoundingClientRect().left;
|
|
1354
|
+
dpx = clamp((x - lastX) / Math.max(1, event.timeStamp - lastT), -3, 3);
|
|
1355
|
+
lastX = x;
|
|
1356
|
+
lastT = event.timeStamp;
|
|
1357
|
+
if (!still() && s.swing) {
|
|
1358
|
+
slots().forEach(({ c, width }, i) => {
|
|
1359
|
+
hangers[i].v += dpx * 0.35 * s.swing * clamp(1 - Math.abs(c - x) / (width * 2.5), 0, 1);
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
setFocus(nearest(x));
|
|
1363
|
+
task.kick();
|
|
1364
|
+
});
|
|
1365
|
+
ctx.on(stage, "pointerleave", () => {
|
|
1366
|
+
if (!compact) setFocus(-1);
|
|
1367
|
+
});
|
|
1368
|
+
ctx.on(track, "keydown", (event) => {
|
|
1369
|
+
if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return;
|
|
1370
|
+
event.preventDefault();
|
|
1371
|
+
hits[clamp(Math.max(0, focused) + (event.key === "ArrowRight" ? 1 : -1), 0, n - 1)].focus();
|
|
1372
|
+
});
|
|
1373
|
+
ctx.on(
|
|
1374
|
+
track,
|
|
1375
|
+
"scroll",
|
|
1376
|
+
() => {
|
|
1377
|
+
if (!compact) return;
|
|
1378
|
+
scrolled = true;
|
|
1379
|
+
task.kick();
|
|
1380
|
+
},
|
|
1381
|
+
{ passive: true }
|
|
1382
|
+
);
|
|
1383
|
+
layout();
|
|
1384
|
+
observeSize(ctx, root, refresh);
|
|
1385
|
+
if (compact) setFocus(0, true);
|
|
1386
|
+
const goTo = (index) => {
|
|
1387
|
+
const i = (index % n + n) % n;
|
|
1388
|
+
if (compact && typeof track.scrollTo === "function") {
|
|
1389
|
+
track.scrollTo({ left: items[i].offsetLeft - (track.clientWidth - items[i].offsetWidth) / 2, behavior: still() ? "auto" : "smooth" });
|
|
1390
|
+
}
|
|
1391
|
+
setFocus(i);
|
|
1392
|
+
};
|
|
1393
|
+
root.gemQuickViewHost = {
|
|
1394
|
+
key: "rail",
|
|
1395
|
+
products,
|
|
1396
|
+
indexOf: (el) => items.findIndex((item) => item === el || item.contains(el)),
|
|
1397
|
+
opens: () => true,
|
|
1398
|
+
imageFor: (i) => (turns[i] ? turns[i].image : items[i] && items[i].querySelector("img")) || null,
|
|
1399
|
+
triggerFor: (i) => hits[i] || null,
|
|
1400
|
+
goTo,
|
|
1401
|
+
setOpen: (open) => root.setAttribute("data-gem-state", open ? "open" : "ready"),
|
|
1402
|
+
setImage: (i, image, side) => turns[i] && turns[i].setImages(image, side),
|
|
1403
|
+
setTint
|
|
1404
|
+
};
|
|
1405
|
+
root.setAttribute("data-gem-quick-view-host", "rail");
|
|
1406
|
+
root.setAttribute("data-gem-state", "ready");
|
|
1407
|
+
return () => {
|
|
1408
|
+
delete root.gemQuickViewHost;
|
|
1409
|
+
root.removeAttribute("data-gem-quick-view-host");
|
|
1410
|
+
root.removeAttribute("data-gem-state");
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
var railEffect = defineEffect({
|
|
1414
|
+
key: "rail",
|
|
1415
|
+
hook: "[data-gem-rail]",
|
|
1416
|
+
load: "viewport",
|
|
1417
|
+
renderer: "dom",
|
|
1418
|
+
tier: "static",
|
|
1419
|
+
fallback: "markup",
|
|
1420
|
+
budget: { gzip: 6144 },
|
|
1421
|
+
settings: {
|
|
1422
|
+
sag: { type: "number", min: 0, max: 0.2, step: 0.01, default: 0.07, css: true, label: "How far the rod dips (share of the stage height)" },
|
|
1423
|
+
height: { type: "number", min: 0.3, max: 0.85, step: 0.01, default: 0.7, css: true, label: "Garment height (share of the stage height)" },
|
|
1424
|
+
grow: { type: "number", min: 0, max: 3, step: 0.1, default: 1.5, label: "How much wider a turned piece gets" },
|
|
1425
|
+
swing: { type: "number", min: 0, max: 2, step: 0.1, default: 1, label: "Hanger swing (0: off)" },
|
|
1426
|
+
ease: { type: "number", min: 40, max: 600, step: 10, default: 120, label: "Turn time (ms)" },
|
|
1427
|
+
tint: { type: "boolean", default: true, label: "Tint the band with the turned piece's colour" }
|
|
1428
|
+
},
|
|
1429
|
+
mount: mountRail
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
// ../../packages/gem-framework/src/motion/quick-view.js
|
|
1433
|
+
var EASE_OUT = "cubic-bezier(.2,.75,.15,1)";
|
|
1434
|
+
var LABELS = {
|
|
1435
|
+
add: "Add to bag",
|
|
1436
|
+
adding: "Adding\u2026",
|
|
1437
|
+
added: "Added",
|
|
1438
|
+
added_status: "added to bag",
|
|
1439
|
+
failed: "Could not add to bag",
|
|
1440
|
+
choose: "Choose an option",
|
|
1441
|
+
sold_out: "Sold out",
|
|
1442
|
+
details: "Full details",
|
|
1443
|
+
previous: "Previous product",
|
|
1444
|
+
next: "Next product",
|
|
1445
|
+
close: "Close",
|
|
1446
|
+
colour: "Colour",
|
|
1447
|
+
size: "Size"
|
|
1448
|
+
};
|
|
1449
|
+
var panelCount = 0;
|
|
1450
|
+
var pad2 = (v) => String(v).padStart(2, "0");
|
|
1451
|
+
var text = (v) => v == null ? "" : String(v);
|
|
1452
|
+
function cardHost(trigger) {
|
|
1453
|
+
const own = trigger.closest("[data-gem-product]");
|
|
1454
|
+
const group = trigger.closest("[data-gem-quick-view-group]");
|
|
1455
|
+
const products = [];
|
|
1456
|
+
const sources = [];
|
|
1457
|
+
let index = -1;
|
|
1458
|
+
for (const el of group ? group.querySelectorAll("[data-gem-product]") : own ? [own] : []) {
|
|
1459
|
+
const product = readProduct(el);
|
|
1460
|
+
if (!product) continue;
|
|
1461
|
+
if (el === own) index = products.length;
|
|
1462
|
+
products.push(product);
|
|
1463
|
+
sources.push(el);
|
|
1464
|
+
}
|
|
1465
|
+
return {
|
|
1466
|
+
index,
|
|
1467
|
+
host: {
|
|
1468
|
+
key: "card",
|
|
1469
|
+
products,
|
|
1470
|
+
imageFor: (i) => sources[i] ? sources[i].querySelector("img") : null,
|
|
1471
|
+
triggerFor: (i) => sources[i] ? sources[i].matches("[data-gem-quick-view-trigger]") ? sources[i] : sources[i].querySelector("[data-gem-quick-view-trigger]") : null,
|
|
1472
|
+
goTo: () => {
|
|
1473
|
+
},
|
|
1474
|
+
setOpen: () => {
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
function sessionOf(ctx) {
|
|
1480
|
+
if (ctx.session.quickView) return ctx.session.quickView;
|
|
1481
|
+
const shared = { panels: /* @__PURE__ */ new Map(), pending: null };
|
|
1482
|
+
ctx.session.quickView = shared;
|
|
1483
|
+
ctx.on(ctx.window, "gem:cart:added", () => shared.pending && shared.pending(true));
|
|
1484
|
+
ctx.on(ctx.window, "gem:cart:add-failed", () => shared.pending && shared.pending(false));
|
|
1485
|
+
ctx.cleanup(() => {
|
|
1486
|
+
shared.panels.forEach((panel) => panel.destroy());
|
|
1487
|
+
shared.panels.clear();
|
|
1488
|
+
});
|
|
1489
|
+
return shared;
|
|
1490
|
+
}
|
|
1491
|
+
function createPanel(parent, ctx, shared) {
|
|
1492
|
+
const doc = parent.ownerDocument;
|
|
1493
|
+
const win = ctx.window;
|
|
1494
|
+
const still = () => ctx.tier() === "static";
|
|
1495
|
+
const make = (tag, className, attrs) => {
|
|
1496
|
+
const node = doc.createElement(tag);
|
|
1497
|
+
node.className = className;
|
|
1498
|
+
for (const [k, v] of Object.entries(attrs || {})) node.setAttribute(k, v);
|
|
1499
|
+
return node;
|
|
1500
|
+
};
|
|
1501
|
+
panelCount += 1;
|
|
1502
|
+
const titleId = "gem-quick-view-title-" + panelCount;
|
|
1503
|
+
const shell = make("div", "gem-popup-shell gem-quick-view__shell", {
|
|
1504
|
+
"data-gem-quick-view-panel": "",
|
|
1505
|
+
"data-gem-popup-desktop": "split-left",
|
|
1506
|
+
"data-gem-popup-mobile": "sheet",
|
|
1507
|
+
"data-gem-popup-media": "top",
|
|
1508
|
+
"data-gem-popup-align": "start"
|
|
1509
|
+
});
|
|
1510
|
+
const dialog = make("dialog", "gem-popup-shell__dialog gem-quick-view__dialog", { "aria-labelledby": titleId });
|
|
1511
|
+
const frame = make("div", "gem-popup-shell__frame gem-quick-view__frame");
|
|
1512
|
+
const surface = make("div", "gem-quick-view__surface", { "aria-hidden": "true" });
|
|
1513
|
+
const close = make("button", "gem-popup-shell__close gem-quick-view__close", { type: "button" });
|
|
1514
|
+
const media = make("div", "gem-popup-shell__media gem-quick-view__media");
|
|
1515
|
+
const img = make("img", "gem-quick-view__image", { alt: "" });
|
|
1516
|
+
const imgNext = make("img", "gem-quick-view__image gem-quick-view__image--next", { alt: "", "aria-hidden": "true" });
|
|
1517
|
+
const body = make("div", "gem-popup-shell__panel gem-quick-view__panel");
|
|
1518
|
+
const bar = make("div", "gem-quick-view__bar");
|
|
1519
|
+
const prev = make("button", "gem-quick-view__icon", { type: "button" });
|
|
1520
|
+
const next = make("button", "gem-quick-view__icon", { type: "button" });
|
|
1521
|
+
const counter = make("span", "gem-quick-view__count");
|
|
1522
|
+
const eyebrow = make("p", "gem-quick-view__eyebrow");
|
|
1523
|
+
const heading = make("h2", "gem-quick-view__title", { id: titleId });
|
|
1524
|
+
const price = make("p", "gem-quick-view__price");
|
|
1525
|
+
const compare = make("s", "gem-quick-view__compare");
|
|
1526
|
+
const swatches = make("div", "gem-quick-view__swatches", { role: "radiogroup" });
|
|
1527
|
+
const sizes = make("div", "gem-quick-view__sizes", { role: "radiogroup" });
|
|
1528
|
+
const add = make("button", "gem-button gem-button--primary gem-quick-view__add", { type: "button" });
|
|
1529
|
+
const status = make("p", "gem-quick-view__status", { role: "status" });
|
|
1530
|
+
const desc = make("p", "gem-quick-view__description");
|
|
1531
|
+
const details = make("a", "gem-quick-view__details");
|
|
1532
|
+
close.textContent = "\xD7";
|
|
1533
|
+
prev.textContent = "\u2039";
|
|
1534
|
+
next.textContent = "\u203A";
|
|
1535
|
+
media.append(img, imgNext);
|
|
1536
|
+
bar.append(prev, counter, next);
|
|
1537
|
+
body.append(bar, eyebrow, heading, price, compare, swatches, sizes, add, status, desc, details);
|
|
1538
|
+
frame.append(close, media, body);
|
|
1539
|
+
dialog.append(surface, frame);
|
|
1540
|
+
shell.append(dialog);
|
|
1541
|
+
parent.append(shell);
|
|
1542
|
+
const chosen = /* @__PURE__ */ new Map();
|
|
1543
|
+
let host = null;
|
|
1544
|
+
let list = [];
|
|
1545
|
+
let index = 0;
|
|
1546
|
+
let color = 0;
|
|
1547
|
+
let size = -1;
|
|
1548
|
+
let source = null;
|
|
1549
|
+
let returnTo = null;
|
|
1550
|
+
let priceAt = null;
|
|
1551
|
+
let labels = LABELS;
|
|
1552
|
+
let closing = false;
|
|
1553
|
+
let resetTimer = 0;
|
|
1554
|
+
const product = () => list[index];
|
|
1555
|
+
const animate = (el, frames, options) => typeof el.animate === "function" ? el.animate(frames, options) : null;
|
|
1556
|
+
const cancelAll = (el) => {
|
|
1557
|
+
if (el.getAnimations) el.getAnimations().forEach((a) => a.cancel());
|
|
1558
|
+
};
|
|
1559
|
+
const hideSource = (hide) => {
|
|
1560
|
+
if (!source) return;
|
|
1561
|
+
if (hide) source.setAttribute("data-gem-quick-view-source", "");
|
|
1562
|
+
else source.removeAttribute("data-gem-quick-view-source");
|
|
1563
|
+
};
|
|
1564
|
+
const moveSource = () => {
|
|
1565
|
+
const target = host.imageFor(index);
|
|
1566
|
+
if (target === source) return;
|
|
1567
|
+
hideSource(false);
|
|
1568
|
+
source = target;
|
|
1569
|
+
if (dialog.open) hideSource(true);
|
|
1570
|
+
};
|
|
1571
|
+
const setTint = (tint) => {
|
|
1572
|
+
if (host.setTint) host.setTint(tint);
|
|
1573
|
+
else if (tint) shell.style.setProperty("--gem-motion-tint", tint);
|
|
1574
|
+
else shell.style.removeProperty("--gem-motion-tint");
|
|
1575
|
+
};
|
|
1576
|
+
const showImage = (image, fade) => {
|
|
1577
|
+
img.alt = image.alt || text(product().title);
|
|
1578
|
+
if (!fade || still() || typeof imgNext.animate !== "function") return putImage(img, image);
|
|
1579
|
+
putImage(imgNext, image);
|
|
1580
|
+
const swap = () => {
|
|
1581
|
+
const settle = () => {
|
|
1582
|
+
putImage(img, image);
|
|
1583
|
+
imgNext.removeAttribute("src");
|
|
1584
|
+
imgNext.removeAttribute("srcset");
|
|
1585
|
+
};
|
|
1586
|
+
const run = imgNext.animate([{ opacity: 0, transform: "scale(.97)" }, { opacity: 1, transform: "none" }], { duration: 280, easing: EASE_OUT });
|
|
1587
|
+
if (run && run.finished) run.finished.then(settle, settle);
|
|
1588
|
+
else settle();
|
|
1589
|
+
};
|
|
1590
|
+
if (typeof imgNext.decode === "function") imgNext.decode().then(swap, swap);
|
|
1591
|
+
else swap();
|
|
1592
|
+
};
|
|
1593
|
+
const variant = () => {
|
|
1594
|
+
const p = product();
|
|
1595
|
+
const c = p.colors && p.colors[color];
|
|
1596
|
+
const z = p.sizes && p.sizes[size];
|
|
1597
|
+
if (!Array.isArray(p.variants)) return null;
|
|
1598
|
+
return p.variants.find((v) => (!c || (v.options || []).includes(c.name)) && (!z || (v.options || []).includes(z.name))) || null;
|
|
1599
|
+
};
|
|
1600
|
+
const variantId = () => {
|
|
1601
|
+
const p = product();
|
|
1602
|
+
const match = variant();
|
|
1603
|
+
if (match) return match.available === false ? null : match.id;
|
|
1604
|
+
const c = p.colors && p.colors[color];
|
|
1605
|
+
const z = p.sizes && p.sizes[size];
|
|
1606
|
+
if (z && z.variant_id != null) return z.variant_id;
|
|
1607
|
+
if (c && c.variant_ids && c.variant_ids.length) return c.variant_ids[0];
|
|
1608
|
+
return p.variant_id != null ? p.variant_id : null;
|
|
1609
|
+
};
|
|
1610
|
+
const priced = () => {
|
|
1611
|
+
const v = variant();
|
|
1612
|
+
return v && (v.price_text != null || v.price != null) ? v : product();
|
|
1613
|
+
};
|
|
1614
|
+
const showPrice = (fade) => {
|
|
1615
|
+
const words = priceText(priced(), priceAt);
|
|
1616
|
+
if (fade) swapText(price, words, still());
|
|
1617
|
+
else price.textContent = words;
|
|
1618
|
+
compare.textContent = priceText(priced(), priceAt, "compare");
|
|
1619
|
+
compare.hidden = !compare.textContent;
|
|
1620
|
+
};
|
|
1621
|
+
const updateAdd = () => {
|
|
1622
|
+
const p = product();
|
|
1623
|
+
const z = p.sizes && p.sizes[size];
|
|
1624
|
+
const soldOut = p.sold_out === true || Boolean(p.sizes && p.sizes.length) && size < 0;
|
|
1625
|
+
add.disabled = soldOut;
|
|
1626
|
+
add.textContent = soldOut ? labels.sold_out : [labels.add, z ? z.name : "", priceText(priced(), priceAt)].filter(Boolean).join(" \xB7 ");
|
|
1627
|
+
};
|
|
1628
|
+
const render = (fade) => {
|
|
1629
|
+
const p = product();
|
|
1630
|
+
const colors = Array.isArray(p.colors) ? p.colors : [];
|
|
1631
|
+
color = Math.min(chosen.get(p.id) || 0, Math.max(0, colors.length - 1));
|
|
1632
|
+
size = (p.sizes || []).findIndex((z) => z.available !== false);
|
|
1633
|
+
const c = colors[color];
|
|
1634
|
+
counter.textContent = pad2(index + 1) + " / " + pad2(list.length);
|
|
1635
|
+
bar.hidden = list.length < 2;
|
|
1636
|
+
const put = (el, value) => {
|
|
1637
|
+
if (fade) swapText(el, value, still());
|
|
1638
|
+
else el.textContent = value;
|
|
1639
|
+
};
|
|
1640
|
+
put(eyebrow, text(p.eyebrow));
|
|
1641
|
+
put(heading, text(p.title));
|
|
1642
|
+
showPrice(fade);
|
|
1643
|
+
eyebrow.hidden = !p.eyebrow;
|
|
1644
|
+
desc.textContent = text(p.description);
|
|
1645
|
+
desc.hidden = !p.description;
|
|
1646
|
+
details.textContent = labels.details;
|
|
1647
|
+
if (p.url) details.setAttribute("href", p.url);
|
|
1648
|
+
details.hidden = !p.url;
|
|
1649
|
+
showImage(c && c.image || p.image, fade);
|
|
1650
|
+
setTint(c && c.tint || p.tint);
|
|
1651
|
+
swatches.replaceChildren(
|
|
1652
|
+
...colors.map((col, i) => {
|
|
1653
|
+
const b = make("button", "gem-quick-view__swatch", { type: "button", role: "radio", "aria-checked": String(i === color), "aria-label": col.name, title: col.name, "data-gem-quick-view-color": String(i) });
|
|
1654
|
+
if (col.hex) b.style.setProperty("--gem-quick-view-swatch", col.hex);
|
|
1655
|
+
return b;
|
|
1656
|
+
})
|
|
1657
|
+
);
|
|
1658
|
+
swatches.hidden = colors.length < 2;
|
|
1659
|
+
sizes.replaceChildren(
|
|
1660
|
+
...(p.sizes || []).map((z, i) => {
|
|
1661
|
+
const b = make("button", "gem-quick-view__size", { type: "button", role: "radio", "aria-checked": String(i === size), "data-gem-quick-view-size": String(i) });
|
|
1662
|
+
b.textContent = z.name;
|
|
1663
|
+
if (z.available === false) {
|
|
1664
|
+
b.disabled = true;
|
|
1665
|
+
b.setAttribute("aria-label", z.name + ", " + labels.sold_out);
|
|
1666
|
+
}
|
|
1667
|
+
return b;
|
|
1668
|
+
})
|
|
1669
|
+
);
|
|
1670
|
+
sizes.hidden = !(p.sizes && p.sizes.length);
|
|
1671
|
+
status.textContent = "";
|
|
1672
|
+
updateAdd();
|
|
1673
|
+
moveSource();
|
|
1674
|
+
for (const k of [index - 1, index + 1]) if (list.length > 1) loadImage(doc, list[(k + list.length) % list.length].image);
|
|
1675
|
+
};
|
|
1676
|
+
const step = (delta) => {
|
|
1677
|
+
if (list.length < 2) return;
|
|
1678
|
+
index = (index + delta + list.length) % list.length;
|
|
1679
|
+
host.goTo(index);
|
|
1680
|
+
render(true);
|
|
1681
|
+
};
|
|
1682
|
+
const finish = (ok, before) => {
|
|
1683
|
+
shared.pending = null;
|
|
1684
|
+
add.disabled = false;
|
|
1685
|
+
add.textContent = ok ? labels.added : before;
|
|
1686
|
+
status.textContent = ok ? text(product().title) + ": " + labels.added_status : labels.failed;
|
|
1687
|
+
if (resetTimer) win.clearTimeout(resetTimer);
|
|
1688
|
+
resetTimer = ok ? win.setTimeout(updateAdd, 1600) : 0;
|
|
1689
|
+
};
|
|
1690
|
+
const addToCart = () => {
|
|
1691
|
+
const id = variantId();
|
|
1692
|
+
if (id == null || id === "") {
|
|
1693
|
+
status.textContent = labels.choose;
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
const before = add.textContent;
|
|
1697
|
+
add.disabled = true;
|
|
1698
|
+
add.textContent = labels.adding;
|
|
1699
|
+
add.dispatchEvent(new win.CustomEvent("gem:cart:add", { bubbles: true, composed: true, detail: { variantId: String(id), quantity: 1 } }));
|
|
1700
|
+
if (win.__gemCartAddBridge) shared.pending = (ok) => finish(ok, before);
|
|
1701
|
+
else finish(true, before);
|
|
1702
|
+
};
|
|
1703
|
+
const pick = (group, i) => {
|
|
1704
|
+
Array.from(group.children).forEach((el, k) => el.setAttribute("aria-checked", String(k === i)));
|
|
1705
|
+
if (group === sizes) size = i;
|
|
1706
|
+
else {
|
|
1707
|
+
const p = product();
|
|
1708
|
+
const c = p.colors && p.colors[i];
|
|
1709
|
+
if (!c) return;
|
|
1710
|
+
color = i;
|
|
1711
|
+
chosen.set(p.id, i);
|
|
1712
|
+
if (c.image) {
|
|
1713
|
+
showImage(c.image, true);
|
|
1714
|
+
if (host.setImage) host.setImage(index, c.image, c.side);
|
|
1715
|
+
}
|
|
1716
|
+
setTint(c.tint || p.tint);
|
|
1717
|
+
}
|
|
1718
|
+
showPrice(false);
|
|
1719
|
+
updateAdd();
|
|
1720
|
+
};
|
|
1721
|
+
const done = (reason) => {
|
|
1722
|
+
dialog.removeAttribute("data-gem-state");
|
|
1723
|
+
cancelAll(img);
|
|
1724
|
+
if (dialog.open) dialog.close();
|
|
1725
|
+
hideSource(false);
|
|
1726
|
+
host.setOpen(false);
|
|
1727
|
+
closing = false;
|
|
1728
|
+
const back = host.triggerFor(index) || returnTo;
|
|
1729
|
+
if (back && typeof back.focus === "function") back.focus();
|
|
1730
|
+
ctx.api.emit("gem:quick-view:close", { source: "gem-runtime", reason, productId: product().id });
|
|
1731
|
+
};
|
|
1732
|
+
const shut = (reason) => {
|
|
1733
|
+
if (!dialog.open || closing) return;
|
|
1734
|
+
closing = true;
|
|
1735
|
+
moveSource();
|
|
1736
|
+
const to = source && source.getBoundingClientRect ? source.getBoundingClientRect() : null;
|
|
1737
|
+
const flight = still() || !to ? null : flip(img, to, { back: true, duration: 440, easing: "cubic-bezier(.4,0,.2,1)" });
|
|
1738
|
+
if (!flight || !flight.finished) return done(reason);
|
|
1739
|
+
dialog.setAttribute("data-gem-state", "closing");
|
|
1740
|
+
for (const el of [body, surface, close]) animate(el, [{ opacity: 1 }, { opacity: 0 }], { duration: 220, fill: "forwards" });
|
|
1741
|
+
host.setOpen(false);
|
|
1742
|
+
const end = () => {
|
|
1743
|
+
[body, surface, close].forEach(cancelAll);
|
|
1744
|
+
done(reason);
|
|
1745
|
+
};
|
|
1746
|
+
flight.finished.then(end, end);
|
|
1747
|
+
};
|
|
1748
|
+
ctx.on(close, "click", () => shut("close-control"));
|
|
1749
|
+
ctx.on(prev, "click", () => step(-1));
|
|
1750
|
+
ctx.on(next, "click", () => step(1));
|
|
1751
|
+
ctx.on(add, "click", addToCart);
|
|
1752
|
+
for (const [group, attr] of [[swatches, "data-gem-quick-view-color"], [sizes, "data-gem-quick-view-size"]]) {
|
|
1753
|
+
ctx.on(group, "click", (event) => {
|
|
1754
|
+
const b = event.target.closest && event.target.closest(`[${attr}]`);
|
|
1755
|
+
if (b && !b.disabled) pick(group, Number(b.getAttribute(attr)));
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
ctx.on(dialog, "cancel", (event) => {
|
|
1759
|
+
event.preventDefault();
|
|
1760
|
+
shut("escape");
|
|
1761
|
+
});
|
|
1762
|
+
ctx.on(dialog, "click", (event) => {
|
|
1763
|
+
if (event.target === dialog) shut("backdrop");
|
|
1764
|
+
});
|
|
1765
|
+
ctx.on(dialog, "keydown", (event) => {
|
|
1766
|
+
if (event.target.closest && event.target.closest(".gem-quick-view__swatches, .gem-quick-view__sizes")) return;
|
|
1767
|
+
if (event.key === "ArrowRight" || event.key === "ArrowLeft") step(event.key === "ArrowRight" ? 1 : -1);
|
|
1768
|
+
});
|
|
1769
|
+
return {
|
|
1770
|
+
shell,
|
|
1771
|
+
open(nextHost, at, trigger, nextLabels) {
|
|
1772
|
+
if (dialog.open) return;
|
|
1773
|
+
cancelAll(img);
|
|
1774
|
+
host = nextHost;
|
|
1775
|
+
list = host.products;
|
|
1776
|
+
index = at;
|
|
1777
|
+
labels = nextLabels;
|
|
1778
|
+
returnTo = trigger;
|
|
1779
|
+
priceAt = trigger;
|
|
1780
|
+
source = null;
|
|
1781
|
+
close.setAttribute("aria-label", labels.close);
|
|
1782
|
+
prev.setAttribute("aria-label", labels.previous);
|
|
1783
|
+
next.setAttribute("aria-label", labels.next);
|
|
1784
|
+
swatches.setAttribute("aria-label", labels.colour);
|
|
1785
|
+
sizes.setAttribute("aria-label", labels.size);
|
|
1786
|
+
render(false);
|
|
1787
|
+
source = host.imageFor(index);
|
|
1788
|
+
const from = source && source.getBoundingClientRect ? source.getBoundingClientRect() : null;
|
|
1789
|
+
dialog.showModal();
|
|
1790
|
+
dialog.setAttribute("data-gem-state", "open");
|
|
1791
|
+
host.setOpen(true);
|
|
1792
|
+
hideSource(true);
|
|
1793
|
+
ctx.api.emit("gem:quick-view:open", { source: "gem-runtime", productId: product().id, productUrl: product().url || null, index, count: list.length, host: host.key });
|
|
1794
|
+
if (still()) return;
|
|
1795
|
+
flip(img, from, { duration: 560, easing: EASE_OUT });
|
|
1796
|
+
animate(surface, [{ opacity: 0, transform: "scale(.97)" }, { opacity: 1, transform: "none" }], { duration: 360, easing: EASE_OUT });
|
|
1797
|
+
Array.from(body.children).forEach(
|
|
1798
|
+
(el, i) => animate(el, [{ opacity: 0, transform: "translateY(12px)" }, { opacity: 1, transform: "none" }], { duration: 380, delay: 120 + i * 40, easing: EASE_OUT, fill: "backwards" })
|
|
1799
|
+
);
|
|
1800
|
+
},
|
|
1801
|
+
/** Unmount: close at once (no flight) and leave the root as the server drew it. */
|
|
1802
|
+
destroy() {
|
|
1803
|
+
if (dialog.open) done("unmount");
|
|
1804
|
+
shell.remove();
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
function mountQuickView(trigger, ctx) {
|
|
1809
|
+
const shared = sessionOf(ctx);
|
|
1810
|
+
ctx.on(trigger, "click", (event) => {
|
|
1811
|
+
if (event.defaultPrevented) return;
|
|
1812
|
+
const hostEl = trigger.closest("[data-gem-quick-view-host]");
|
|
1813
|
+
const host = hostEl && hostEl.gemQuickViewHost;
|
|
1814
|
+
if (host && !host.opens(trigger)) return;
|
|
1815
|
+
const card = host ? null : cardHost(trigger);
|
|
1816
|
+
const index = host ? host.indexOf(trigger) : card.index;
|
|
1817
|
+
if (index < 0 || !(host || card.host).products[index]) return;
|
|
1818
|
+
event.preventDefault();
|
|
1819
|
+
const parent = host && hostEl || trigger.closest("[data-gem-quick-view-group]") || trigger.closest(".gem-section, [data-gem-section-id]") || trigger.parentElement;
|
|
1820
|
+
let panel = shared.panels.get(parent);
|
|
1821
|
+
if (!panel || !panel.shell.isConnected) shared.panels.set(parent, panel = createPanel(parent, ctx, shared));
|
|
1822
|
+
const own = ctx.reread().labels;
|
|
1823
|
+
panel.open(host || card.host, index, trigger, { ...LABELS, ...own && typeof own === "object" ? own : {} });
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
var quickViewEffect = defineEffect({
|
|
1827
|
+
key: "quick-view",
|
|
1828
|
+
hook: "[data-gem-quick-view-trigger]",
|
|
1829
|
+
load: "interaction",
|
|
1830
|
+
renderer: "dom",
|
|
1831
|
+
tier: "static",
|
|
1832
|
+
fallback: "markup",
|
|
1833
|
+
budget: { gzip: 6144 },
|
|
1834
|
+
settings: {
|
|
1835
|
+
labels: {
|
|
1836
|
+
type: "json",
|
|
1837
|
+
default: {},
|
|
1838
|
+
inherit: true,
|
|
1839
|
+
label: "The panel's words, JSON on the trigger or an ancestor: add, adding, added, added_status, failed, choose, sold_out, details, previous, next, close, colour, size"
|
|
1840
|
+
}
|
|
1841
|
+
},
|
|
1842
|
+
mount: mountQuickView
|
|
1843
|
+
});
|
|
1844
|
+
|
|
1845
|
+
// ../../packages/gem-framework/src/motion-engine.ts
|
|
1846
|
+
var GEM_ORBIT_BASE_CSS_LINES = [
|
|
1847
|
+
".gem-orbit { display: block; position: relative; background-color: var(--gem-motion-tint, transparent); }",
|
|
1848
|
+
".gem-orbit:focus-visible { outline: var(--gem-focus-ring); outline-offset: calc(-1 * var(--gem-focus-offset)); }",
|
|
1849
|
+
".gem-orbit__stage { position: relative; height: var(--gem-orbit-stage-height, clamp(340px, 58vh, 620px)); overflow: hidden; container-type: size; }",
|
|
1850
|
+
".gem-orbit__ring { list-style: none; margin: 0; padding: 0; position: absolute; left: 50%; top: 66%; width: 0; height: 0; }",
|
|
1851
|
+
".gem-orbit__item {",
|
|
1852
|
+
" --gem-orbit-k: calc(mod(var(--gem-orbit-i, 0) - var(--gem-orbit-start, 0) + var(--gem-orbit-n, 1) / 2, var(--gem-orbit-n, 1)) - var(--gem-orbit-n, 1) / 2);",
|
|
1853
|
+
" --gem-orbit-a: calc(var(--gem-orbit-k) * var(--gem-orbit-spread, calc(360 / var(--gem-orbit-n, 1))) * 1deg);",
|
|
1854
|
+
" --gem-orbit-d: calc((1 - cos(var(--gem-orbit-a))) / 2);",
|
|
1855
|
+
" position: absolute; left: calc(var(--gem-orbit-size, 0.62) * -50cqh); top: calc(var(--gem-orbit-size, 0.62) * -50cqh);",
|
|
1856
|
+
" width: calc(var(--gem-orbit-size, 0.62) * 100cqh); height: calc(var(--gem-orbit-size, 0.62) * 100cqh);",
|
|
1857
|
+
" transform: translate3d(calc(var(--gem-orbit-rx, 0.36) * 100cqw * sin(var(--gem-orbit-a))), calc(var(--gem-orbit-tilt, 0.3) * -100cqh * var(--gem-orbit-d)), 0)",
|
|
1858
|
+
" scale(calc(1 + (var(--gem-orbit-back, 0.42) - 1) * var(--gem-orbit-d))) rotate(calc(sin(var(--gem-orbit-a)) * var(--gem-orbit-lean, 5) * 1deg));",
|
|
1859
|
+
" opacity: calc(1 + (var(--gem-orbit-fade, 0.35) - 1) * var(--gem-orbit-d));",
|
|
1860
|
+
" z-index: calc((1 - var(--gem-orbit-d)) * 100);",
|
|
1861
|
+
" filter: blur(calc(var(--gem-orbit-blur, 2.5) * var(--gem-orbit-d) * 1px));",
|
|
1862
|
+
"}",
|
|
1863
|
+
".gem-orbit__item::after {",
|
|
1864
|
+
" content: ''; position: absolute; left: 18%; right: 18%; bottom: -2%; height: 7%; z-index: -1; border-radius: var(--gem-radius-pill);",
|
|
1865
|
+
" background: radial-gradient(closest-side, color-mix(in srgb, var(--gem-color-scrim, #000) 18%, transparent), transparent);",
|
|
1866
|
+
"}",
|
|
1867
|
+
// A hit is a <button> or an <a href> to the product (still a link on a page without the engine).
|
|
1868
|
+
".gem-orbit__hit { display: block; width: 100%; height: 100%; padding: 0; border: 0; color: inherit; background: transparent; text-decoration: none; cursor: pointer; }",
|
|
1869
|
+
".gem-orbit__hit:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); border-radius: var(--gem-radius-md); }",
|
|
1870
|
+
".gem-orbit__img { display: block; width: 100%; height: 100%; object-fit: contain; pointer-events: none; }",
|
|
1871
|
+
".gem-orbit__caption { display: grid; justify-items: center; gap: var(--gem-space-2xs); margin: calc(-1 * var(--gem-space-xs)) 0 0; text-align: center; }",
|
|
1872
|
+
".gem-orbit__name { margin: 0; font-family: var(--gem-font-heading); font-size: var(--gem-h4); font-weight: 600; line-height: 1.2; }",
|
|
1873
|
+
".gem-orbit__price { margin: 0; color: var(--gem-color-muted); font-variant-numeric: tabular-nums; }",
|
|
1874
|
+
".gem-orbit__dock { display: flex; align-items: center; justify-content: center; gap: var(--gem-space-sm); padding: var(--gem-space-md) 0 var(--gem-space-xs); font-size: var(--gem-text-sm); font-variant-numeric: tabular-nums; }",
|
|
1875
|
+
".gem-orbit__arrow {",
|
|
1876
|
+
" display: inline-grid; place-items: center; width: var(--gem-tap-min); height: var(--gem-tap-min); padding: 0; cursor: pointer;",
|
|
1877
|
+
" border: var(--gem-stroke, 1px) solid currentColor; border-radius: var(--gem-radius-pill); color: inherit; background: transparent; font: inherit; line-height: 1;",
|
|
1878
|
+
"}",
|
|
1879
|
+
".gem-orbit__arrow:hover { color: var(--gem-color-background); background: var(--gem-color-text); border-color: var(--gem-color-text); }",
|
|
1880
|
+
".gem-orbit__arrow:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); }",
|
|
1881
|
+
".gem-orbit__count { min-width: 7ch; text-align: center; font-variant-numeric: tabular-nums; }",
|
|
1882
|
+
".gem-orbit__live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }",
|
|
1883
|
+
".gem-orbit__track { position: relative; width: min(180px, 30vw); height: var(--gem-stroke-thick, 2px); background: color-mix(in srgb, currentColor 18%, transparent); }",
|
|
1884
|
+
".gem-orbit__track::before { content: ''; position: absolute; inset: calc(-1 * var(--gem-space-sm)) 0; }",
|
|
1885
|
+
".gem-orbit__bar { position: absolute; inset: 0; background: currentColor; transform-origin: left; transform: scaleX(calc((var(--gem-orbit-start, 0) + 1) / var(--gem-orbit-n, 1))); }",
|
|
1886
|
+
// The light and static tiers draw no blur (a phone, reduced motion): the first paint already matches what they draw.
|
|
1887
|
+
"@media (pointer: coarse), (prefers-reduced-motion: reduce) { .gem-orbit__item { filter: none; } }"
|
|
1888
|
+
];
|
|
1889
|
+
var GEM_RAIL_BASE_CSS_LINES = [
|
|
1890
|
+
".gem-rail { display: block; position: relative; container-type: inline-size; background-color: var(--gem-motion-tint, transparent); }",
|
|
1891
|
+
".gem-rail__stage { position: relative; height: var(--gem-rail-stage-height, clamp(380px, 62vh, 640px)); overflow: hidden; container-type: size; }",
|
|
1892
|
+
// The rod is drawn in a 0..100 viewBox with preserveAspectRatio="none": it fits any stage without being redrawn.
|
|
1893
|
+
".gem-rail__rod { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }",
|
|
1894
|
+
".gem-rail__rod path { fill: none; stroke: var(--gem-rail-rod, currentColor); stroke-width: 5; stroke-linecap: round; vector-effect: non-scaling-stroke; opacity: 0.85; }",
|
|
1895
|
+
".gem-rail__track { list-style: none; margin: 0; padding: 0; position: absolute; inset: 0; }",
|
|
1896
|
+
// At rest slot i of n is centred at 4% + 92% · (i + ½) / n of the width and hangs from the rod's curve there.
|
|
1897
|
+
".gem-rail__item {",
|
|
1898
|
+
" --gem-rail-t: calc(0.04 + 0.92 * (var(--gem-rail-i, 0) + 0.5) / var(--gem-rail-n, 1));",
|
|
1899
|
+
" position: absolute; left: 0; top: 0; width: 0; height: 0; z-index: 10;",
|
|
1900
|
+
" transform: translate3d(calc(var(--gem-rail-t) * 100cqw), calc((0.14 + 4 * var(--gem-rail-sag, 0.07) * var(--gem-rail-t) * (1 - var(--gem-rail-t))) * 100cqh), 0);",
|
|
1901
|
+
"}",
|
|
1902
|
+
".gem-rail__hit {",
|
|
1903
|
+
" position: absolute; left: 0; top: -6px; width: calc(var(--gem-rail-height, 0.7) * 78cqh); height: calc(var(--gem-rail-height, 0.7) * 100cqh + 34px);",
|
|
1904
|
+
" padding: 0; border: 0; color: inherit; background: transparent; text-decoration: none; cursor: pointer; transform: translateX(-50%);",
|
|
1905
|
+
"}",
|
|
1906
|
+
".gem-rail__hit:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); border-radius: var(--gem-radius-md); }",
|
|
1907
|
+
".gem-rail__body { position: absolute; inset: 0; display: block; transform-origin: 50% 0; }",
|
|
1908
|
+
".gem-rail__hook {",
|
|
1909
|
+
" position: absolute; left: calc(50% - 8px); top: 0; width: 16px; height: 22px; opacity: 0.8;",
|
|
1910
|
+
" border: var(--gem-stroke-thick, 2px) solid var(--gem-rail-rod, currentColor); border-bottom: 0; border-radius: var(--gem-radius-md) var(--gem-radius-md) 0 0;",
|
|
1911
|
+
"}",
|
|
1912
|
+
".gem-rail__hook::after { content: ''; position: absolute; left: calc(50% - 23px); top: 18px; width: 46px; height: var(--gem-stroke-thick, 2px); background: var(--gem-rail-rod, currentColor); border-radius: var(--gem-radius-sm); }",
|
|
1913
|
+
".gem-rail__tag {",
|
|
1914
|
+
" position: absolute; left: calc(50% + 14px); top: 18px; z-index: 2; padding: var(--gem-space-2xs) var(--gem-space-xs); transform: rotate(4deg);",
|
|
1915
|
+
" font-family: var(--gem-font-mono); font-size: var(--gem-text-xs); font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums;",
|
|
1916
|
+
" color: var(--gem-color-text); background: var(--gem-color-background); border-radius: var(--gem-radius-sm); box-shadow: var(--gem-shadow-sm);",
|
|
1917
|
+
"}",
|
|
1918
|
+
".gem-rail .gem-turn { position: absolute; inset: 26px 0 0; width: auto; height: auto; }",
|
|
1919
|
+
".gem-rail__caption { margin: var(--gem-space-xs) 0 0; min-height: 1.5em; text-align: center; font-variant-numeric: tabular-nums; }",
|
|
1920
|
+
// Under 700 px (the effect's breakpoint, on the same width): a swipe row with snap points; the centred piece turns.
|
|
1921
|
+
"@container (max-width: 699px) {",
|
|
1922
|
+
" .gem-rail__stage { height: auto; overflow: visible; container-type: normal; }",
|
|
1923
|
+
" .gem-rail__rod { display: none; }",
|
|
1924
|
+
" .gem-rail__track { position: static; display: flex; gap: var(--gem-space-2xs); overflow-x: auto; scroll-snap-type: x mandatory; padding: 0 20cqw var(--gem-space-xs); scrollbar-width: none; }",
|
|
1925
|
+
" .gem-rail__track::-webkit-scrollbar { display: none; }",
|
|
1926
|
+
" .gem-rail__item { position: relative; flex: 0 0 60cqw; width: auto; height: calc(72cqw + 34px); transform: none; scroll-snap-align: center; border-top: var(--gem-stroke-thick, 4px) solid var(--gem-rail-rod, currentColor); }",
|
|
1927
|
+
" .gem-rail__hit { left: 50%; width: 100%; height: 100%; }",
|
|
1928
|
+
// The row rests with its first piece centred (the track's padding) and facing the shopper; the plugin settles there.
|
|
1929
|
+
" .gem-rail__item:first-child .gem-turn__side { opacity: 0; transform: perspective(900px) rotateY(-82deg); }",
|
|
1930
|
+
" .gem-rail__item:first-child .gem-turn:has(> .gem-turn__side) > .gem-turn__front { opacity: 1; transform: scale(1); }",
|
|
1931
|
+
" .gem-rail__item:first-child .gem-turn:not(:has(> .gem-turn__side)) > .gem-turn__front { transform: perspective(900px) rotateY(0deg) scale(1); }",
|
|
1932
|
+
" .gem-rail__item:first-child .gem-turn__sheet { transform: translate(calc(-100% / var(--gem-turn-cols, 1) * mod(var(--gem-turn-frames, 1) - 1, var(--gem-turn-cols, 1))), calc(-100% / var(--gem-turn-rows, 1) * round(down, (var(--gem-turn-frames, 1) - 1) / var(--gem-turn-cols, 1)))); }",
|
|
1933
|
+
"}",
|
|
1934
|
+
// The turn rests on its side view: pair (side + front photo), flip (front only), sprite (two copies of a frame sheet).
|
|
1935
|
+
".gem-turn { position: relative; display: block; width: 100%; height: 100%; }",
|
|
1936
|
+
".gem-turn[data-gem-turn-frames] { overflow: hidden; }",
|
|
1937
|
+
".gem-turn__side, .gem-turn__front { position: absolute; inset: 0; display: block; width: 100%; height: 100%; object-fit: contain; object-position: 50% 0; pointer-events: none; }",
|
|
1938
|
+
".gem-turn__side { transform: perspective(900px) rotateY(0deg); backface-visibility: hidden; }",
|
|
1939
|
+
".gem-turn:has(> .gem-turn__side) > .gem-turn__front { opacity: 0; transform: scale(0.9); }",
|
|
1940
|
+
".gem-turn:not(:has(> .gem-turn__side)) > .gem-turn__front { transform: perspective(900px) rotateY(-74deg) scale(0.86); }",
|
|
1941
|
+
".gem-turn__sheet { position: absolute; left: 0; top: 0; display: block; max-width: none; pointer-events: none; width: calc(var(--gem-turn-cols, 1) * 100%); height: calc(var(--gem-turn-rows, 1) * 100%); }",
|
|
1942
|
+
".gem-turn__sheet + .gem-turn__sheet { opacity: 0; }"
|
|
1943
|
+
];
|
|
1944
|
+
var GEM_ORBIT_BASE_CSS = GEM_ORBIT_BASE_CSS_LINES.join("\n") + "\n";
|
|
1945
|
+
var GEM_RAIL_BASE_CSS = GEM_RAIL_BASE_CSS_LINES.join("\n") + "\n";
|
|
1946
|
+
var GEM_ORBIT_RUNTIME_CSS = [
|
|
1947
|
+
".gem-orbit { --gem-orbit-css: 1; outline: none; user-select: none; -webkit-user-select: none; transition: background-color var(--gem-motion-slow, 0.6s) var(--gem-ease-soft, ease); }",
|
|
1948
|
+
".gem-orbit[data-gem-state] .gem-orbit__stage { touch-action: pan-y; cursor: grab; }",
|
|
1949
|
+
".gem-orbit[data-gem-state='dragging'] .gem-orbit__stage { cursor: grabbing; }",
|
|
1950
|
+
".gem-orbit[data-gem-state] .gem-orbit__item { will-change: transform; }",
|
|
1951
|
+
".gem-orbit[data-gem-state] .gem-orbit__hit { transition: opacity var(--gem-motion-base, 0.35s) var(--gem-ease-soft, ease), transform var(--gem-motion-base, 0.35s) var(--gem-ease-soft, ease); }",
|
|
1952
|
+
".gem-orbit[data-gem-state] .gem-orbit__bar { transition: transform var(--gem-motion-slow, 0.4s) var(--gem-ease-soft, ease); }",
|
|
1953
|
+
".gem-orbit[data-gem-state] .gem-orbit__arrow { transition: color var(--gem-motion-fast, 0.2s) ease, background-color var(--gem-motion-fast, 0.2s) ease; }",
|
|
1954
|
+
".gem-orbit[data-gem-state='open'] .gem-orbit__item:not([aria-current='true']) .gem-orbit__hit { opacity: 0; transform: scale(0.92); }",
|
|
1955
|
+
"@media (prefers-reduced-motion: reduce) { .gem-orbit, .gem-orbit[data-gem-state] .gem-orbit__hit, .gem-orbit[data-gem-state] .gem-orbit__bar, .gem-orbit[data-gem-state] .gem-orbit__arrow { transition: none; } }"
|
|
1956
|
+
].join("\n") + "\n";
|
|
1957
|
+
var GEM_RAIL_RUNTIME_CSS = [
|
|
1958
|
+
".gem-rail { --gem-rail-css: 1; user-select: none; -webkit-user-select: none; transition: background-color var(--gem-motion-slow, 0.6s) var(--gem-ease-soft, ease); }",
|
|
1959
|
+
".gem-rail[data-gem-state] .gem-rail__item { will-change: transform; }",
|
|
1960
|
+
// Unturned neighbours' hit boxes overlap on the rail: once bound, a click on the stage goes to the piece whose slot it is in
|
|
1961
|
+
// (motion/rail.js), so the hits take no pointer events; keyboard focus and Enter stay on them. The phone row keeps its own.
|
|
1962
|
+
".gem-rail[data-gem-state] .gem-rail__stage { cursor: pointer; }",
|
|
1963
|
+
".gem-rail[data-gem-state] .gem-rail__hit { pointer-events: none; transition: opacity var(--gem-motion-base, 0.35s) var(--gem-ease-soft, ease); }",
|
|
1964
|
+
"@container (max-width: 699px) { .gem-rail[data-gem-state] .gem-rail__hit { pointer-events: auto; } }",
|
|
1965
|
+
".gem-rail[data-gem-state='open'] .gem-rail__hit { opacity: 0.15; }",
|
|
1966
|
+
"@media (prefers-reduced-motion: reduce) { .gem-rail, .gem-rail[data-gem-state] .gem-rail__hit { transition: none; } }"
|
|
1967
|
+
].join("\n") + "\n";
|
|
1968
|
+
var GEM_QUICK_VIEW_CSS = [
|
|
1969
|
+
"[data-gem-quick-view-trigger] { --gem-quick-view-css: 1; }",
|
|
1970
|
+
".gem-quick-view__shell { display: contents; }",
|
|
1971
|
+
".gem-quick-view__shell .gem-quick-view__dialog { overflow: visible; isolation: isolate; color: var(--gem-color-text); background: transparent; box-shadow: none; user-select: text; -webkit-user-select: text; }",
|
|
1972
|
+
".gem-quick-view__surface {",
|
|
1973
|
+
" position: absolute; inset: 0; z-index: -1; border-radius: inherit; box-shadow: var(--gem-shadow-md);",
|
|
1974
|
+
" background: color-mix(in srgb, var(--gem-motion-tint, var(--gem-color-background)) 45%, var(--gem-color-background));",
|
|
1975
|
+
" transition: background-color var(--gem-motion-slow, 0.5s) var(--gem-ease-soft, ease);",
|
|
1976
|
+
"}",
|
|
1977
|
+
".gem-quick-view__shell .gem-quick-view__media { overflow: visible; background: transparent; min-height: 0; }",
|
|
1978
|
+
".gem-quick-view__image { position: absolute; inset: 6% 4%; width: 92%; height: 88%; object-fit: contain; transform-origin: 50% 50%; will-change: transform; }",
|
|
1979
|
+
".gem-quick-view__image--next { opacity: 0; pointer-events: none; }",
|
|
1980
|
+
".gem-quick-view__image--next:not([src]) { display: none; }",
|
|
1981
|
+
".gem-quick-view__shell .gem-quick-view__panel { justify-content: flex-start; gap: var(--gem-space-sm); }",
|
|
1982
|
+
".gem-quick-view__shell [hidden] { display: none; }",
|
|
1983
|
+
".gem-quick-view__bar { display: flex; align-items: center; gap: var(--gem-space-xs); font-family: var(--gem-font-mono); font-size: var(--gem-text-xs); font-variant-numeric: tabular-nums; }",
|
|
1984
|
+
".gem-quick-view__icon {",
|
|
1985
|
+
" display: inline-grid; place-items: center; width: var(--gem-tap-min); height: var(--gem-tap-min); padding: 0; cursor: pointer;",
|
|
1986
|
+
" border: var(--gem-stroke, 1px) solid color-mix(in srgb, currentColor 25%, transparent); border-radius: var(--gem-radius-pill);",
|
|
1987
|
+
" color: inherit; background: transparent; font: inherit; font-size: var(--gem-h5); line-height: 1;",
|
|
1988
|
+
"}",
|
|
1989
|
+
".gem-quick-view__icon:hover { border-color: currentColor; }",
|
|
1990
|
+
".gem-quick-view__icon:focus-visible, .gem-quick-view__swatch:focus-visible, .gem-quick-view__size:focus-visible, .gem-quick-view__details:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); }",
|
|
1991
|
+
".gem-quick-view__eyebrow { margin: var(--gem-space-md) 0 0; color: var(--gem-color-muted); font-family: var(--gem-font-mono); font-size: var(--gem-text-xs); letter-spacing: 0.12em; line-height: 1.3; text-transform: uppercase; }",
|
|
1992
|
+
".gem-quick-view__title { margin: 0; font-family: var(--gem-font-heading); font-size: var(--gem-h2); line-height: 1.05; }",
|
|
1993
|
+
".gem-quick-view__price { margin: 0; font-size: var(--gem-h5); font-variant-numeric: tabular-nums; }",
|
|
1994
|
+
".gem-quick-view__compare { margin: calc(-1 * var(--gem-space-xs)) 0 0; color: var(--gem-color-muted); font-size: var(--gem-text-sm); }",
|
|
1995
|
+
".gem-quick-view__swatches, .gem-quick-view__sizes { display: flex; flex-wrap: wrap; gap: var(--gem-space-xs); }",
|
|
1996
|
+
".gem-quick-view__swatch { position: relative; width: var(--gem-tap-min); height: var(--gem-tap-min); padding: 0; border: 0; color: inherit; background: transparent; cursor: pointer; }",
|
|
1997
|
+
".gem-quick-view__swatch::before {",
|
|
1998
|
+
" content: ''; position: absolute; inset: 8px; border-radius: var(--gem-radius-pill); background: var(--gem-quick-view-swatch, var(--gem-color-surface));",
|
|
1999
|
+
" box-shadow: inset 0 0 0 var(--gem-stroke, 1px) color-mix(in srgb, var(--gem-color-text) 15%, transparent);",
|
|
2000
|
+
"}",
|
|
2001
|
+
".gem-quick-view__swatch[aria-checked='true']::after { content: ''; position: absolute; inset: 3px; border: var(--gem-stroke-thick, 2px) solid currentColor; border-radius: var(--gem-radius-pill); }",
|
|
2002
|
+
".gem-quick-view__size {",
|
|
2003
|
+
" display: inline-grid; place-items: center; min-width: var(--gem-tap-min); height: var(--gem-tap-min); padding: 0 var(--gem-space-sm); cursor: pointer;",
|
|
2004
|
+
" border: var(--gem-stroke, 1px) solid color-mix(in srgb, currentColor 22%, transparent); border-radius: var(--gem-radius-pill);",
|
|
2005
|
+
" color: inherit; background: transparent; font: inherit; font-size: var(--gem-text-sm);",
|
|
2006
|
+
"}",
|
|
2007
|
+
".gem-quick-view__size[aria-checked='true'] { color: var(--gem-color-background); background: var(--gem-color-text); border-color: var(--gem-color-text); }",
|
|
2008
|
+
".gem-quick-view__size:disabled { opacity: 0.4; text-decoration: line-through; cursor: not-allowed; }",
|
|
2009
|
+
".gem-quick-view__add { width: 100%; margin: var(--gem-space-xs) 0 0; font-variant-numeric: tabular-nums; }",
|
|
2010
|
+
".gem-quick-view__status { margin: 0; min-height: 1.2em; color: var(--gem-color-muted); font-size: var(--gem-text-sm); }",
|
|
2011
|
+
".gem-quick-view__description { margin: 0; max-width: 46ch; font-size: var(--gem-text-sm); line-height: 1.55; }",
|
|
2012
|
+
".gem-quick-view__details { color: inherit; font-size: var(--gem-text-sm); text-underline-offset: 3px; }",
|
|
2013
|
+
// The photo the panel flew out of stays hidden in the page while the panel shows it.
|
|
2014
|
+
"[data-gem-quick-view-source] { visibility: hidden; }",
|
|
2015
|
+
"@keyframes gem-quick-view-fade { from { opacity: 0; } to { opacity: 1; } }",
|
|
2016
|
+
".gem-quick-view__dialog[data-gem-state='open']::backdrop { animation: gem-quick-view-fade var(--gem-motion-base, 0.35s) ease both; }",
|
|
2017
|
+
".gem-quick-view__dialog[data-gem-state='closing']::backdrop { animation: gem-quick-view-fade var(--gem-motion-base, 0.35s) ease reverse both; }",
|
|
2018
|
+
"@media (max-width: 749px) { .gem-quick-view__shell .gem-quick-view__media { height: 34dvh; } }",
|
|
2019
|
+
"@media (prefers-reduced-motion: reduce) { .gem-quick-view__dialog::backdrop { animation: none; } .gem-quick-view__surface { transition: none; } }"
|
|
2020
|
+
].join("\n") + "\n";
|
|
2021
|
+
var GEM_MOTION_CLASSES = [
|
|
2022
|
+
...["", "__stage", "__ring", "__item", "__hit", "__img", "__caption", "__name", "__price", "__dock", "__arrow", "__count", "__track", "__bar", "__live"].map((part) => `gem-orbit${part}`),
|
|
2023
|
+
...["", "__stage", "__rod", "__track", "__item", "__hit", "__body", "__hook", "__tag", "__caption"].map((part) => `gem-rail${part}`),
|
|
2024
|
+
...["", "__side", "__front", "__sheet"].map((part) => `gem-turn${part}`),
|
|
2025
|
+
...[
|
|
2026
|
+
"shell",
|
|
2027
|
+
"dialog",
|
|
2028
|
+
"frame",
|
|
2029
|
+
"surface",
|
|
2030
|
+
"close",
|
|
2031
|
+
"media",
|
|
2032
|
+
"image",
|
|
2033
|
+
"image--next",
|
|
2034
|
+
"panel",
|
|
2035
|
+
"bar",
|
|
2036
|
+
"icon",
|
|
2037
|
+
"count",
|
|
2038
|
+
"eyebrow",
|
|
2039
|
+
"title",
|
|
2040
|
+
"price",
|
|
2041
|
+
"compare",
|
|
2042
|
+
"swatches",
|
|
2043
|
+
"swatch",
|
|
2044
|
+
"sizes",
|
|
2045
|
+
"size",
|
|
2046
|
+
"add",
|
|
2047
|
+
"status",
|
|
2048
|
+
"description",
|
|
2049
|
+
"details"
|
|
2050
|
+
].map((part) => `gem-quick-view__${part}`)
|
|
2051
|
+
];
|
|
2052
|
+
var gemMotionEffectList = [orbitEffect, railEffect, quickViewEffect];
|
|
2053
|
+
var compactCss = (css) => css.replace(/\s+/g, " ").replace(/\s*([{};])\s*/g, "$1").replace(/:\s+/g, ":").replace(/,\s+/g, ",").replace(/;}/g, "}").trim();
|
|
2054
|
+
var effectPluginLines = (effect, name, css, before = [], detail = "count") => [
|
|
2055
|
+
` registerPlugin('${effect.key}', ({ api }) => {`,
|
|
2056
|
+
...before,
|
|
2057
|
+
" const on = (el, type, fn, opt) => { if (canListen(el)) el.addEventListener(type, fn, opt); };",
|
|
2058
|
+
` const count = mountEffects(${name}, { window, document, on, mark, api, css: ${JSON.stringify(compactCss(css))} }, onUnmount);`,
|
|
2059
|
+
` api.emit('gem:${effect.key}:ready', { source: 'gem-runtime', ${detail} });`,
|
|
2060
|
+
" });"
|
|
2061
|
+
];
|
|
2062
|
+
var effectImports = (file, name) => ['import { mountEffects } from "../motion/core.js";', `import { ${name} } from "../motion/${file}.js";`];
|
|
2063
|
+
function gemMotionChunks(legacyQuickView) {
|
|
2064
|
+
return {
|
|
2065
|
+
orbit: { hook: orbitEffect.hook, when: orbitEffect.load, imports: effectImports("orbit", "orbitEffect"), body: effectPluginLines(orbitEffect, "orbitEffect", GEM_ORBIT_RUNTIME_CSS) },
|
|
2066
|
+
rail: { hook: railEffect.hook, when: railEffect.load, imports: effectImports("rail", "railEffect"), body: effectPluginLines(railEffect, "railEffect", GEM_RAIL_RUNTIME_CSS) },
|
|
2067
|
+
"quick-view": {
|
|
2068
|
+
hook: `${quickViewEffect.hook}, ${legacyQuickView.hook}`,
|
|
2069
|
+
when: quickViewEffect.load,
|
|
2070
|
+
imports: effectImports("quick-view", "quickViewEffect"),
|
|
2071
|
+
body: effectPluginLines(quickViewEffect, "quickViewEffect", GEM_QUICK_VIEW_CSS, legacyQuickView.lines, legacyQuickView.readyDetail)
|
|
2072
|
+
}
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
|
|
738
2076
|
// ../../packages/gem-framework/src/manifest.ts
|
|
739
2077
|
var gemFrameworkPrimitiveGroups = [
|
|
740
2078
|
{
|
|
@@ -930,6 +2268,12 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
930
2268
|
"gem-popup-shell__teaser"
|
|
931
2269
|
]
|
|
932
2270
|
},
|
|
2271
|
+
{
|
|
2272
|
+
// The motion engine (motion-engine.ts, 0.19.0): the orbit, rail and turn markup a section draws, and the quick view panel
|
|
2273
|
+
// the quick-view plugin builds. Styled by their own base / runtime stylesheets, never by gem-framework.css.
|
|
2274
|
+
group: "motion",
|
|
2275
|
+
classes: [...GEM_MOTION_CLASSES]
|
|
2276
|
+
},
|
|
933
2277
|
{
|
|
934
2278
|
group: "sections",
|
|
935
2279
|
classes: [
|
|
@@ -1103,6 +2447,24 @@ var GEM_POPUP_SHELL_CSS_LINES = [
|
|
|
1103
2447
|
" .gem-popup-shell[data-gem-popup-media='background']:is([data-gem-popup-desktop='split-left'], [data-gem-popup-desktop='split-right'], [data-gem-popup-desktop='card']) .gem-popup-shell__close { color: var(--gem-color-text); }",
|
|
1104
2448
|
" .gem-popup-shell[data-gem-popup-media='background']:is([data-gem-popup-desktop='split-left'], [data-gem-popup-desktop='split-right'], [data-gem-popup-desktop='card']) .gem-popup-shell__media::after { content: none; }",
|
|
1105
2449
|
"}",
|
|
2450
|
+
// Ngăn bám mép (drawer-end, GemFactory gap #49, 21/10/2026): bảng size mở từ nút cạnh ô chọn size là ngăn cao hết màn ở mép
|
|
2451
|
+
// phải trên desktop (American Tall 640px, Alo 745px), không phải hộp giữa màn. Rộng theo --gem-popup-drawer-width (mặc định
|
|
2452
|
+
// 640px, không quá màn), bám mép CUỐI dòng (margin-inline: auto 0, nên dưới RTL là mép trái) và trượt vào từ đó
|
|
2453
|
+
// (--gem-motion-base / --gem-ease-enter, như ngăn giỏ của framework). Chỉ là bố cục desktop: mobile vẫn takeover / sheet.
|
|
2454
|
+
"@keyframes gem-popup-drawer-in { from { translate: calc(100% * var(--gem-popup-drawer-dir, 1)) 0; } }",
|
|
2455
|
+
".gem-popup-shell__dialog:dir(rtl) { --gem-popup-drawer-dir: -1; }",
|
|
2456
|
+
"@media (min-width: 750px) {",
|
|
2457
|
+
" .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__dialog {",
|
|
2458
|
+
" width: min(var(--gem-popup-drawer-width, 640px), 100vw); max-width: 100vw; height: 100dvh; max-height: 100dvh;",
|
|
2459
|
+
" margin-block: 0; margin-inline: auto 0; border-radius: 0;",
|
|
2460
|
+
" animation: gem-popup-drawer-in var(--gem-motion-base, 180ms) var(--gem-ease-enter, ease-out);",
|
|
2461
|
+
" }",
|
|
2462
|
+
" .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__frame { grid-template-columns: 1fr; align-content: start; min-height: 100%; }",
|
|
2463
|
+
" .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__media { max-height: 240px; }",
|
|
2464
|
+
" .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__panel { justify-content: flex-start; align-items: stretch; text-align: start; }",
|
|
2465
|
+
"}",
|
|
2466
|
+
// SAU rule animation và cùng độ ưu tiên (bài học ngăn giỏ 02/10/2026: đặt trước hay yếu hơn thì ngăn vẫn trượt).
|
|
2467
|
+
"@media (prefers-reduced-motion: reduce) { .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__dialog { animation: none; } }",
|
|
1106
2468
|
"@media (max-width: 749px) {",
|
|
1107
2469
|
// Mobile: takeover is the whole screen; a sheet rises from the bottom to at most 88% of it. The image
|
|
1108
2470
|
// sits under the text (top when media=top), capped at a third of the screen; with no image or the image
|
|
@@ -1138,6 +2500,9 @@ var GEM_POPUP_SHELL_CSS_LINES = [
|
|
|
1138
2500
|
"@media (min-width: 750px) {",
|
|
1139
2501
|
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] { padding: 0; }",
|
|
1140
2502
|
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] .gem-popup-shell__dialog { height: 720px; max-height: none; }",
|
|
2503
|
+
// Ngăn trong canvas: cao như khung xem trước của fullscreen, đứng yên (không trượt mỗi lần canvas vẽ lại).
|
|
2504
|
+
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='drawer-end'] { padding-inline: 0; }",
|
|
2505
|
+
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='drawer-end'] .gem-popup-shell__dialog { height: 720px; max-height: none; animation: none; }",
|
|
1141
2506
|
"}",
|
|
1142
2507
|
"@media (max-width: 749px) {",
|
|
1143
2508
|
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'] { padding: 0; }",
|
|
@@ -1333,7 +2698,7 @@ var GEM_INLINE_MARK_CSS_LINES = [
|
|
|
1333
2698
|
":where(.gem-mark--alert) { color: var(--gem-color-danger, #b42318); }",
|
|
1334
2699
|
":where(.gem-mark--highlight.gem-mark--accent), :where(.gem-mark--highlight.gem-mark--alert) { color: var(--gem-mark-highlight-ink, #1f1d18); }",
|
|
1335
2700
|
".gem-mark-link { color: var(--gem-color-accent); text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }",
|
|
1336
|
-
".gem-mark-link:hover { text-decoration-thickness: 2px; }",
|
|
2701
|
+
".gem-mark-link:hover { text-decoration-thickness: var(--gem-underline-thickness, 2px); }",
|
|
1337
2702
|
// Bút dạ thắng màu link: chữ trên nền vàng giữ mực đậm, dù span nằm trong link hay link nằm trong span (review #740 đo màu nhấn
|
|
1338
2703
|
// trên vàng 4,32:1 ở nền trắng, 2,47:1 trên băng tối, 1,05:1 trên băng màu nhấn).
|
|
1339
2704
|
".gem-mark-link.gem-mark--highlight, .gem-mark--highlight .gem-mark-link { color: var(--gem-mark-highlight-ink, #1f1d18); }"
|
|
@@ -1552,6 +2917,14 @@ function createPrimitiveCss() {
|
|
|
1552
2917
|
" cursor: not-allowed;",
|
|
1553
2918
|
" opacity: 0.55;",
|
|
1554
2919
|
"}",
|
|
2920
|
+
// Button shape of ONE section (GemFactory gap #69, 22/10/2026): matching a source, a section needs square, softly rounded
|
|
2921
|
+
// or pill buttons whatever the preset's button radius is, and writing border-radius on .gem-button takes the button
|
|
2922
|
+
// away from the preset for good (GemFactory's button_token_shadowed). `data-gem-button-shape` on the section root (or on
|
|
2923
|
+
// one button) sets the radius the button already reads; with no attribute the preset decides, as before. Square and
|
|
2924
|
+
// pill are shapes, so they do not scale; soft is a size, so it follows the preset's radius scale (--gem-style-radius).
|
|
2925
|
+
"[data-gem-button-shape='square'] { --gem-radius-button: 0px; }",
|
|
2926
|
+
"[data-gem-button-shape='soft'] { --gem-radius-button: var(--gem-radius-sm); }",
|
|
2927
|
+
"[data-gem-button-shape='pill'] { --gem-radius-button: 999px; }",
|
|
1555
2928
|
".gem-input:disabled, .gem-select:disabled, .gem-textarea:disabled, .gem-quantity-input__button:disabled {",
|
|
1556
2929
|
" cursor: not-allowed;",
|
|
1557
2930
|
" opacity: 0.6;",
|
|
@@ -2929,8 +4302,8 @@ function createPrimitiveCss() {
|
|
|
2929
4302
|
function createGemSectionRuntimeContractCss(selector = "[data-gem-section-library-id]", options = {}) {
|
|
2930
4303
|
const background = options.backgroundVar ?? "var(--gem-color-background)";
|
|
2931
4304
|
const surface = options.surfaceVar ?? "var(--gem-color-surface)";
|
|
2932
|
-
const
|
|
2933
|
-
const mutedText = options.mutedTextVar ?? `color-mix(in srgb, ${
|
|
4305
|
+
const text2 = options.textVar ?? "var(--gem-color-text)";
|
|
4306
|
+
const mutedText = options.mutedTextVar ?? `color-mix(in srgb, ${text2} 72%, transparent)`;
|
|
2934
4307
|
const accent = options.accentVar ?? "var(--gem-color-accent)";
|
|
2935
4308
|
const accentText = options.accentTextVar ?? "var(--gem-color-accent-text, #ffffff)";
|
|
2936
4309
|
const border = options.borderVar ?? "var(--gem-color-border)";
|
|
@@ -2940,7 +4313,7 @@ function createGemSectionRuntimeContractCss(selector = "[data-gem-section-librar
|
|
|
2940
4313
|
`${selector} {`,
|
|
2941
4314
|
" box-sizing: border-box;",
|
|
2942
4315
|
` background: ${background};`,
|
|
2943
|
-
` color: ${
|
|
4316
|
+
` color: ${text2};`,
|
|
2944
4317
|
"}",
|
|
2945
4318
|
`${selector} *,`,
|
|
2946
4319
|
`${selector} *::before,`,
|
|
@@ -2999,7 +4372,7 @@ function createGemSectionRuntimeContractCss(selector = "[data-gem-section-librar
|
|
|
2999
4372
|
" border-radius: var(--gem-radius-pill, 999px);",
|
|
3000
4373
|
" padding: 6px 10px;",
|
|
3001
4374
|
` background: ${surface};`,
|
|
3002
|
-
` color: ${
|
|
4375
|
+
` color: ${text2};`,
|
|
3003
4376
|
" font-size: 0.8125rem;",
|
|
3004
4377
|
" font-weight: 700;",
|
|
3005
4378
|
"}",
|
|
@@ -5427,6 +6800,760 @@ function bundleEngineRuntimeScript() {
|
|
|
5427
6800
|
}
|
|
5428
6801
|
|
|
5429
6802
|
// ../../packages/gem-framework/src/runtime.ts
|
|
6803
|
+
var cartDrawerPluginRegistrationLines = [
|
|
6804
|
+
" registerPlugin('cart-drawer', ({ api }) => {",
|
|
6805
|
+
" const drawers = document.querySelectorAll('[data-gem-cart-drawer], [data-gem-cart-canvas], .gem-cart-drawer');",
|
|
6806
|
+
` const cartLinks = document.querySelectorAll('[data-gem-cart-drawer-trigger], [data-gem-cart-toggle], a[href*="/cart"]');`,
|
|
6807
|
+
` const cartForms = document.querySelectorAll('form[action*="/cart/add"], form[action*="/cart"]');`,
|
|
6808
|
+
" const drawer = drawers[0];",
|
|
6809
|
+
" const setDrawerA11y = (isOpen) => {",
|
|
6810
|
+
" if (!drawer) return;",
|
|
6811
|
+
" drawer.setAttribute && drawer.setAttribute('aria-expanded', isOpen ? 'true' : 'false');",
|
|
6812
|
+
" const panel = drawer.querySelector && drawer.querySelector('[data-gem-cart-canvas-panel], .gem-cart-drawer__panel');",
|
|
6813
|
+
" if (panel && panel.setAttribute) panel.setAttribute('aria-hidden', isOpen ? 'false' : 'true');",
|
|
6814
|
+
" };",
|
|
6815
|
+
" const openDrawer = (reason, href) => {",
|
|
6816
|
+
" if (drawer && drawer.tagName === 'DETAILS') drawer.open = true;",
|
|
6817
|
+
" if (drawer && drawer.dataset) drawer.dataset.gemCartDrawerState = 'open';",
|
|
6818
|
+
" setDrawerA11y(true);",
|
|
6819
|
+
" api.emit('gem:cart:open', { source: 'gem-runtime', reason, href });",
|
|
6820
|
+
" };",
|
|
6821
|
+
" const closeDrawer = (reason) => {",
|
|
6822
|
+
" if (drawer && drawer.tagName === 'DETAILS') drawer.open = false;",
|
|
6823
|
+
" if (drawer && drawer.dataset) drawer.dataset.gemCartDrawerState = 'closed';",
|
|
6824
|
+
" setDrawerA11y(false);",
|
|
6825
|
+
" api.emit('gem:cart:close', { source: 'gem-runtime', reason });",
|
|
6826
|
+
" };",
|
|
6827
|
+
" let bound = 0;",
|
|
6828
|
+
" drawers.forEach((drawer) => {",
|
|
6829
|
+
" if (!mark(drawer, 'cart-drawer')) return;",
|
|
6830
|
+
" drawer.dataset.gemCartDrawerState = drawer.hasAttribute('open') ? 'open' : 'ready';",
|
|
6831
|
+
" setDrawerA11y(drawer.hasAttribute('open'));",
|
|
6832
|
+
" drawer.querySelectorAll('[data-gem-cart-drawer-close], [data-gem-cart-canvas-close]').forEach((control) => { if (!canListen(control)) return; control.addEventListener('click', (event) => {",
|
|
6833
|
+
" event.preventDefault && event.preventDefault();",
|
|
6834
|
+
" closeDrawer('close-control');",
|
|
6835
|
+
" }); });",
|
|
6836
|
+
" if (canListen(drawer)) drawer.addEventListener('toggle', () => { drawer.dataset.gemCartDrawerState = drawer.hasAttribute('open') ? 'open' : 'closed'; });",
|
|
6837
|
+
" bound += 1;",
|
|
6838
|
+
" });",
|
|
6839
|
+
" cartLinks.forEach((link) => {",
|
|
6840
|
+
" if (!mark(link, 'cart-link')) return;",
|
|
6841
|
+
" if (!canListen(link)) return;",
|
|
6842
|
+
" link.addEventListener('click', (event) => {",
|
|
6843
|
+
" if (drawer && (link.hasAttribute('data-gem-cart-drawer-trigger') || link.hasAttribute('data-gem-cart-toggle'))) event.preventDefault && event.preventDefault();",
|
|
6844
|
+
" openDrawer('cart-link', link.href || undefined);",
|
|
6845
|
+
" });",
|
|
6846
|
+
" bound += 1;",
|
|
6847
|
+
" });",
|
|
6848
|
+
" if (canListen(document)) document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && drawer && drawer.hasAttribute('open')) closeDrawer('escape'); });",
|
|
6849
|
+
" cartForms.forEach((form) => {",
|
|
6850
|
+
" if (!mark(form, 'cart-form')) return;",
|
|
6851
|
+
" if (!canListen(form)) return;",
|
|
6852
|
+
" form.addEventListener('submit', () => api.emit('gem:cart:submit', { source: 'gem-runtime', action: form.getAttribute('action') || undefined }));",
|
|
6853
|
+
" bound += 1;",
|
|
6854
|
+
" });",
|
|
6855
|
+
" api.emit('gem:cart:ready', { source: 'gem-runtime', count: bound });",
|
|
6856
|
+
" });"
|
|
6857
|
+
];
|
|
6858
|
+
var mobileMenuPluginRegistrationLines = [
|
|
6859
|
+
" registerPlugin('mobile-menu', ({ api }) => {",
|
|
6860
|
+
" const menus = document.querySelectorAll('[data-gem-mobile-menu], details.gem-shell-header__drawer');",
|
|
6861
|
+
" let bound = 0;",
|
|
6862
|
+
" menus.forEach((menu) => {",
|
|
6863
|
+
" if (!mark(menu, 'mobile-menu')) return;",
|
|
6864
|
+
" const setMenuA11y = () => {",
|
|
6865
|
+
" menu.setAttribute && menu.setAttribute('aria-expanded', menu.open ? 'true' : 'false');",
|
|
6866
|
+
" const panel = menu.querySelector && menu.querySelector('[data-gem-mobile-menu-panel], .gem-shell-header__drawer-panel');",
|
|
6867
|
+
" if (panel && panel.setAttribute) panel.setAttribute('aria-hidden', menu.open ? 'false' : 'true');",
|
|
6868
|
+
" };",
|
|
6869
|
+
" menu.dataset.gemMenuState = menu.open ? 'open' : 'closed';",
|
|
6870
|
+
" setMenuA11y();",
|
|
6871
|
+
" if (!canListen(menu)) return;",
|
|
6872
|
+
" menu.addEventListener('toggle', () => {",
|
|
6873
|
+
" menu.dataset.gemMenuState = menu.open ? 'open' : 'closed';",
|
|
6874
|
+
" setMenuA11y();",
|
|
6875
|
+
" if (menu.open) {",
|
|
6876
|
+
" const panel = menu.querySelector && menu.querySelector('[data-gem-mobile-menu-panel], .gem-shell-header__drawer-panel');",
|
|
6877
|
+
` const firstFocusable = panel && panel.querySelector && panel.querySelector('a[href], button, select, input, textarea, summary, [tabindex]:not([tabindex="-1"])');`,
|
|
6878
|
+
" if (firstFocusable && firstFocusable.focus) firstFocusable.focus();",
|
|
6879
|
+
" }",
|
|
6880
|
+
" api.emit(menu.open ? 'gem:menu:open' : 'gem:menu:close', { source: 'gem-runtime', id: menu.id || undefined });",
|
|
6881
|
+
" });",
|
|
6882
|
+
" menu.addEventListener('keydown', (event) => {",
|
|
6883
|
+
" if (event.key !== 'Escape' || !menu.open) return;",
|
|
6884
|
+
" menu.open = false;",
|
|
6885
|
+
" menu.dataset.gemMenuState = 'closed';",
|
|
6886
|
+
" setMenuA11y();",
|
|
6887
|
+
" const summary = menu.querySelector && menu.querySelector('summary');",
|
|
6888
|
+
" if (summary && summary.focus) summary.focus();",
|
|
6889
|
+
" });",
|
|
6890
|
+
" bound += 1;",
|
|
6891
|
+
" });",
|
|
6892
|
+
" api.emit('gem:menu:ready', { source: 'gem-runtime', count: bound });",
|
|
6893
|
+
" });"
|
|
6894
|
+
];
|
|
6895
|
+
var quantityInputPluginRegistrationLines = [
|
|
6896
|
+
" registerPlugin('quantity-input', ({ api }) => {",
|
|
6897
|
+
" const controls = document.querySelectorAll('[data-gem-quantity-input], .gem-quantity-input');",
|
|
6898
|
+
" let bound = 0;",
|
|
6899
|
+
" const clamp = (value, min, max) => Math.max(min, Math.min(max, value));",
|
|
6900
|
+
" controls.forEach((control) => {",
|
|
6901
|
+
" if (!mark(control, 'quantity-input')) return;",
|
|
6902
|
+
` const input = control.querySelector && control.querySelector('input[type="number"]');`,
|
|
6903
|
+
" if (!input) return;",
|
|
6904
|
+
" const setValue = (nextValue, reason) => {",
|
|
6905
|
+
" const min = Number.isFinite(Number(input.min)) ? Number(input.min) : 0;",
|
|
6906
|
+
" const max = Number.isFinite(Number(input.max)) && input.max !== '' ? Number(input.max) : Number.MAX_SAFE_INTEGER;",
|
|
6907
|
+
" const value = clamp(Number(nextValue) || min, min, max);",
|
|
6908
|
+
" input.value = String(value);",
|
|
6909
|
+
" api.emit('gem:quantity:change', { source: 'gem-runtime', value, reason, name: input.name || undefined });",
|
|
6910
|
+
" };",
|
|
6911
|
+
" if (!canListen(control) || !canListen(input)) return;",
|
|
6912
|
+
" control.addEventListener('click', (event) => {",
|
|
6913
|
+
" const button = event.target && event.target.closest ? event.target.closest('[data-gem-quantity-change]') : null;",
|
|
6914
|
+
" if (!button) return;",
|
|
6915
|
+
" event.preventDefault && event.preventDefault();",
|
|
6916
|
+
" const delta = Number(button.getAttribute('data-gem-quantity-change') || '0');",
|
|
6917
|
+
" setValue(Number(input.value || 0) + delta, delta > 0 ? 'increase' : 'decrease');",
|
|
6918
|
+
" });",
|
|
6919
|
+
" input.addEventListener('change', () => setValue(input.value, 'input'));",
|
|
6920
|
+
" bound += 1;",
|
|
6921
|
+
" });",
|
|
6922
|
+
" api.emit('gem:quantity:ready', { source: 'gem-runtime', count: bound });",
|
|
6923
|
+
" });"
|
|
6924
|
+
];
|
|
6925
|
+
var variantSelectsPluginRegistrationLines = [
|
|
6926
|
+
" registerPlugin('variant-selects', ({ api }) => {",
|
|
6927
|
+
` const selects = document.querySelectorAll('[data-gem-variant-select], select[name="id"]');`,
|
|
6928
|
+
" let bound = 0;",
|
|
6929
|
+
" selects.forEach((select) => {",
|
|
6930
|
+
" if (!mark(select, 'variant-selects')) return;",
|
|
6931
|
+
" let hasInitialized = false;",
|
|
6932
|
+
" const update = () => {",
|
|
6933
|
+
" const option = select.options && select.options[select.selectedIndex];",
|
|
6934
|
+
" const available = !option || option.getAttribute('data-gem-variant-available') !== 'false';",
|
|
6935
|
+
" const price = option && option.getAttribute('data-gem-variant-price');",
|
|
6936
|
+
" const mediaId = option && option.getAttribute('data-gem-variant-media-id');",
|
|
6937
|
+
" const form = select.closest && select.closest('form');",
|
|
6938
|
+
" 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;",
|
|
6939
|
+
` const submit = form && form.querySelector && form.querySelector('[type="submit"]');`,
|
|
6940
|
+
" const status = form && form.querySelector && form.querySelector('[data-gem-variant-status]');",
|
|
6941
|
+
" const priceOutput = scope && scope.querySelector && scope.querySelector('[data-gem-variant-price-output]');",
|
|
6942
|
+
" const availabilityOutput = scope && scope.querySelector && scope.querySelector('[data-gem-variant-availability]');",
|
|
6943
|
+
" if (form && form.dataset) form.dataset.gemSelectedVariantId = String(select.value || '');",
|
|
6944
|
+
" if (form && form.dataset && price) form.dataset.gemSelectedVariantPrice = price;",
|
|
6945
|
+
" if (form && form.dataset && mediaId) form.dataset.gemSelectedMediaId = mediaId;",
|
|
6946
|
+
" if (submit) submit.disabled = !available;",
|
|
6947
|
+
" if (submit) submit.setAttribute('aria-disabled', available ? 'false' : 'true');",
|
|
6948
|
+
" if (status) status.textContent = available ? 'Selected variant available' : 'Selected variant unavailable';",
|
|
6949
|
+
" if (hasInitialized && price && priceOutput) priceOutput.textContent = price;",
|
|
6950
|
+
" if (availabilityOutput) {",
|
|
6951
|
+
" availabilityOutput.textContent = available ? 'In stock' : 'Sold out';",
|
|
6952
|
+
" availabilityOutput.classList && availabilityOutput.classList.toggle('gem-product-main__availability--in-stock', available);",
|
|
6953
|
+
" availabilityOutput.classList && availabilityOutput.classList.toggle('gem-product-main__availability--sold-out', !available);",
|
|
6954
|
+
" }",
|
|
6955
|
+
" if (mediaId) {",
|
|
6956
|
+
` const escapedMediaId = String(mediaId).replace(/["\\\\]/g, '\\\\$&');`,
|
|
6957
|
+
" 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');",
|
|
6958
|
+
` const thumb = gallery && gallery.querySelector && gallery.querySelector('[data-gem-media-id="' + escapedMediaId + '"]');`,
|
|
6959
|
+
" const mediaIndex = thumb && thumb.getAttribute && thumb.getAttribute('data-gem-media-index');",
|
|
6960
|
+
" if (thumb && typeof thumb.click === 'function') thumb.click();",
|
|
6961
|
+
" if (gallery && mediaIndex !== null && mediaIndex !== undefined) {",
|
|
6962
|
+
" gallery.dataset && (gallery.dataset.gemMediaActiveIndex = String(mediaIndex));",
|
|
6963
|
+
" gallery.querySelectorAll && gallery.querySelectorAll('[data-gem-media-thumb]').forEach((control) => {",
|
|
6964
|
+
" const isActive = control.getAttribute && control.getAttribute('data-gem-media-index') === String(mediaIndex);",
|
|
6965
|
+
" if (isActive) control.setAttribute('aria-current', 'true'); else control.removeAttribute('aria-current');",
|
|
6966
|
+
" if (control.dataset) control.dataset.gemMediaActive = isActive ? 'true' : 'false';",
|
|
6967
|
+
" });",
|
|
6968
|
+
" }",
|
|
6969
|
+
" }",
|
|
6970
|
+
" api.emit('gem:variant:change', { source: 'gem-runtime', variantId: select.value || undefined, available, price, mediaId });",
|
|
6971
|
+
" hasInitialized = true;",
|
|
6972
|
+
" };",
|
|
6973
|
+
" if (!canListen(select)) return;",
|
|
6974
|
+
" select.addEventListener('change', update);",
|
|
6975
|
+
" update();",
|
|
6976
|
+
" bound += 1;",
|
|
6977
|
+
" });",
|
|
6978
|
+
" api.emit('gem:variant:ready', { source: 'gem-runtime', count: bound });",
|
|
6979
|
+
" });"
|
|
6980
|
+
];
|
|
6981
|
+
var predictiveSearchPluginRegistrationLines = [
|
|
6982
|
+
" registerPlugin('predictive-search', ({ api }) => {",
|
|
6983
|
+
` const forms = document.querySelectorAll('[data-gem-predictive-search], [data-gem-search-canvas], form[action*="/search"], form[role="search"]');`,
|
|
6984
|
+
" const toggles = document.querySelectorAll('[data-gem-search-toggle]');",
|
|
6985
|
+
" const canvas = document.querySelector('[data-gem-search-canvas]');",
|
|
6986
|
+
" let bound = 0;",
|
|
6987
|
+
" const setCanvasState = (state, reason = 'api') => {",
|
|
6988
|
+
" if (!canvas || !canvas.dataset) return false;",
|
|
6989
|
+
" canvas.dataset.gemSearchCanvasState = state;",
|
|
6990
|
+
" canvas.setAttribute && canvas.setAttribute('aria-hidden', state === 'open' ? 'false' : 'true');",
|
|
6991
|
+
" if (state === 'open') {",
|
|
6992
|
+
` const input = canvas.querySelector && canvas.querySelector('input[type="search"], input[name="q"]');`,
|
|
6993
|
+
" if (input && input.focus) input.focus();",
|
|
6994
|
+
" }",
|
|
6995
|
+
" api.emit(state === 'open' ? 'gem:search:open' : 'gem:search:close', { source: 'gem-runtime', reason });",
|
|
6996
|
+
" return true;",
|
|
6997
|
+
" };",
|
|
6998
|
+
" forms.forEach((form) => {",
|
|
6999
|
+
" if (!mark(form, 'predictive-search')) return;",
|
|
7000
|
+
` const input = form.querySelector && form.querySelector('input[type="search"], input[name="q"]');`,
|
|
7001
|
+
" const results = form.querySelector && form.querySelector('[data-gem-predictive-search-results]');",
|
|
7002
|
+
" const setState = (state, value = '') => {",
|
|
7003
|
+
" if (form.dataset) form.dataset.gemPredictiveSearchState = state;",
|
|
7004
|
+
" if (results) {",
|
|
7005
|
+
" results.hidden = state === 'closed';",
|
|
7006
|
+
" if (results.dataset) results.dataset.gemPredictiveSearchState = state;",
|
|
7007
|
+
" results.setAttribute && results.setAttribute('aria-hidden', state === 'closed' ? 'true' : 'false');",
|
|
7008
|
+
" if (state === 'typing') results.textContent = value ? 'Search suggestions for ' + value : 'Start typing to search';",
|
|
7009
|
+
" if (state === 'empty') results.textContent = 'Start typing to search';",
|
|
7010
|
+
" }",
|
|
7011
|
+
" };",
|
|
7012
|
+
" if (results) {",
|
|
7013
|
+
" results.hidden = true;",
|
|
7014
|
+
" results.setAttribute && results.setAttribute('role', 'status');",
|
|
7015
|
+
" results.setAttribute && results.setAttribute('aria-live', 'polite');",
|
|
7016
|
+
" }",
|
|
7017
|
+
" setState('closed');",
|
|
7018
|
+
" if (canListen(input)) input.addEventListener('focus', () => { setState(input.value ? 'typing' : 'empty', input.value || ''); api.emit('gem:search:open', { source: 'gem-runtime', value: input.value || '' }); });",
|
|
7019
|
+
" 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' }); } };",
|
|
7020
|
+
" if (canListen(input)) input.addEventListener('input', () => { setState(input.value ? 'typing' : 'empty', input.value || ''); api.emit('gem:search:input', { source: 'gem-runtime', value: input.value || '' }); });",
|
|
7021
|
+
" if (canListen(input)) input.addEventListener('keydown', closeOnEscape);",
|
|
7022
|
+
" if (canListen(form)) form.addEventListener('keydown', closeOnEscape);",
|
|
7023
|
+
" if (canListen(form)) form.addEventListener('submit', () => { setState('submit', input && input.value || ''); api.emit('gem:search:submit', { source: 'gem-runtime', value: input && input.value || '' }); });",
|
|
7024
|
+
" bound += 1;",
|
|
7025
|
+
" });",
|
|
7026
|
+
" if (canvas) {",
|
|
7027
|
+
" canvas.dataset && (canvas.dataset.gemSearchCanvasState = 'closed');",
|
|
7028
|
+
" canvas.setAttribute && canvas.setAttribute('aria-hidden', 'true');",
|
|
7029
|
+
" if (mark(canvas, 'search-canvas')) bound += 1;",
|
|
7030
|
+
" if (canListen(canvas)) canvas.addEventListener('keydown', (event) => { if (event.key === 'Escape') setCanvasState('closed', 'escape'); });",
|
|
7031
|
+
" }",
|
|
7032
|
+
" toggles.forEach((toggle) => {",
|
|
7033
|
+
" if (!mark(toggle, 'search-toggle')) return;",
|
|
7034
|
+
" if (!canListen(toggle)) return;",
|
|
7035
|
+
" toggle.addEventListener('click', (event) => {",
|
|
7036
|
+
" if (canvas) event.preventDefault && event.preventDefault();",
|
|
7037
|
+
" setCanvasState('open', 'toggle');",
|
|
7038
|
+
" });",
|
|
7039
|
+
" bound += 1;",
|
|
7040
|
+
" });",
|
|
7041
|
+
" api.emit('gem:search:ready', { source: 'gem-runtime', count: bound });",
|
|
7042
|
+
" });"
|
|
7043
|
+
];
|
|
7044
|
+
var localizationPluginRegistrationLines = [
|
|
7045
|
+
" registerPlugin('localization', ({ api }) => {",
|
|
7046
|
+
` 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');`,
|
|
7047
|
+
" let bound = 0;",
|
|
7048
|
+
" forms.forEach((form) => {",
|
|
7049
|
+
" if (!mark(form, 'localization')) return;",
|
|
7050
|
+
" form.dataset && (form.dataset.gemLocalizationState = 'ready');",
|
|
7051
|
+
` form.querySelectorAll && form.querySelectorAll('select[name="country_code"], select[name="locale_code"]').forEach((select) => {`,
|
|
7052
|
+
" if (!canListen(select)) return;",
|
|
7053
|
+
" select.addEventListener('change', () => {",
|
|
7054
|
+
" form.dataset && (form.dataset.gemLocalizationState = 'changed');",
|
|
7055
|
+
" api.emit('gem:localization:change', { source: 'gem-runtime', name: select.name || undefined, value: select.value || undefined });",
|
|
7056
|
+
" });",
|
|
7057
|
+
" });",
|
|
7058
|
+
" canListen(form) && form.addEventListener('submit', () => {",
|
|
7059
|
+
" form.dataset && (form.dataset.gemLocalizationState = 'submit');",
|
|
7060
|
+
" api.emit('gem:localization:submit', { source: 'gem-runtime' });",
|
|
7061
|
+
" });",
|
|
7062
|
+
" bound += 1;",
|
|
7063
|
+
" });",
|
|
7064
|
+
" api.emit('gem:localization:ready', { source: 'gem-runtime', count: bound });",
|
|
7065
|
+
" });"
|
|
7066
|
+
];
|
|
7067
|
+
var disclosurePluginRegistrationLines = [
|
|
7068
|
+
" registerPlugin('disclosure', ({ api }) => {",
|
|
7069
|
+
" const items = document.querySelectorAll('details[data-gem-disclosure], .gem-accordion details, .gem-tabs details, .gem-footer-shell details, [data-gem-footer-shell] details');",
|
|
7070
|
+
" let bound = 0;",
|
|
7071
|
+
" items.forEach((item) => {",
|
|
7072
|
+
" if (!mark(item, 'disclosure')) return;",
|
|
7073
|
+
" item.dataset.gemDisclosureState = item.open ? 'open' : 'closed';",
|
|
7074
|
+
" if (!canListen(item)) return;",
|
|
7075
|
+
" item.addEventListener('toggle', () => {",
|
|
7076
|
+
" item.dataset.gemDisclosureState = item.open ? 'open' : 'closed';",
|
|
7077
|
+
" api.emit(item.open ? 'gem:disclosure:open' : 'gem:disclosure:close', { source: 'gem-runtime', id: item.id || undefined });",
|
|
7078
|
+
" });",
|
|
7079
|
+
" item.addEventListener('keydown', (event) => {",
|
|
7080
|
+
" if (event.key !== 'Escape' || !item.open) return;",
|
|
7081
|
+
" event.preventDefault && event.preventDefault();",
|
|
7082
|
+
" item.open = false;",
|
|
7083
|
+
" item.dataset.gemDisclosureState = 'closed';",
|
|
7084
|
+
" const summary = item.querySelector && item.querySelector('summary');",
|
|
7085
|
+
" if (summary && summary.focus) summary.focus();",
|
|
7086
|
+
" api.emit('gem:disclosure:close', { source: 'gem-runtime', id: item.id || undefined, reason: 'escape' });",
|
|
7087
|
+
" });",
|
|
7088
|
+
" bound += 1;",
|
|
7089
|
+
" });",
|
|
7090
|
+
" api.emit('gem:disclosure:ready', { source: 'gem-runtime', count: bound });",
|
|
7091
|
+
" });"
|
|
7092
|
+
];
|
|
7093
|
+
var tabsPluginRegistrationLines = [
|
|
7094
|
+
" registerPlugin('tabs', ({ api }) => {",
|
|
7095
|
+
// Ported from the Liquid theme's initTabs (gem-framework.js) so BOTH render paths honour the same
|
|
7096
|
+
// contract: [data-gem-tabs] root, [data-gem-tab=key] controls, [data-gem-panel=key] panels. The
|
|
7097
|
+
// headless runtime had no tabs plugin at all, while gemcapture's guidance told rebuilds to fake
|
|
7098
|
+
// tabs with <details data-gem-disclosure> — which is an accordion, not tabs, and binds nothing on
|
|
7099
|
+
// the Liquid side. Now the guidance names one contract and both runtimes implement it.
|
|
7100
|
+
" const roots = document.querySelectorAll('[data-gem-tabs]');",
|
|
7101
|
+
" let bound = 0;",
|
|
7102
|
+
" roots.forEach((root) => {",
|
|
7103
|
+
" if (!mark(root, 'tabs')) return;",
|
|
7104
|
+
" const tabs = Array.prototype.slice.call(root.querySelectorAll('[data-gem-tab]'));",
|
|
7105
|
+
" const panels = Array.prototype.slice.call(root.querySelectorAll('[data-gem-panel]'));",
|
|
7106
|
+
" if (!tabs.length || !panels.length) return;",
|
|
7107
|
+
" const select = (key, userInitiated) => {",
|
|
7108
|
+
" tabs.forEach((t) => t.setAttribute('aria-selected', String(t.getAttribute('data-gem-tab') === key)));",
|
|
7109
|
+
" let active = null;",
|
|
7110
|
+
" panels.forEach((p) => {",
|
|
7111
|
+
" if (p.getAttribute('data-gem-panel') === key) { p.removeAttribute('hidden'); active = p; }",
|
|
7112
|
+
" else p.setAttribute('hidden', '');",
|
|
7113
|
+
" });",
|
|
7114
|
+
// Same smart-scroll guard as the Liquid implementation: switching a tab must never jump the page,
|
|
7115
|
+
// and block:'nearest' is a no-op when the panel is already on screen.
|
|
7116
|
+
" if (userInitiated && active && active.getBoundingClientRect) {",
|
|
7117
|
+
" const r = active.getBoundingClientRect();",
|
|
7118
|
+
" const vh = window.innerHeight || document.documentElement.clientHeight;",
|
|
7119
|
+
" if (!(r.top < vh * 0.85 && r.bottom > vh * 0.15) && active.scrollIntoView) {",
|
|
7120
|
+
" active.scrollIntoView({ behavior: 'smooth', block: 'nearest' });",
|
|
7121
|
+
" }",
|
|
7122
|
+
" }",
|
|
7123
|
+
" api.emit('gem:tabs:change', { source: 'gem-runtime', key: key });",
|
|
7124
|
+
" };",
|
|
7125
|
+
" tabs.forEach((t) => {",
|
|
7126
|
+
" if (!canListen(t)) return;",
|
|
7127
|
+
" t.addEventListener('click', () => select(t.getAttribute('data-gem-tab'), true));",
|
|
7128
|
+
" });",
|
|
7129
|
+
// Seed from aria-selected so a captured `state: \"active\"` tab opens on the correct panel.
|
|
7130
|
+
" const initial = tabs.filter((t) => t.getAttribute('aria-selected') === 'true')[0] || tabs[0];",
|
|
7131
|
+
" select(initial.getAttribute('data-gem-tab'), false);",
|
|
7132
|
+
" bound += 1;",
|
|
7133
|
+
" });",
|
|
7134
|
+
" api.emit('gem:tabs:ready', { source: 'gem-runtime', count: bound });",
|
|
7135
|
+
" });"
|
|
7136
|
+
];
|
|
7137
|
+
var mediaGalleryPluginRegistrationLines = [
|
|
7138
|
+
" registerPlugin('media-gallery', ({ api }) => {",
|
|
7139
|
+
" const galleries = document.querySelectorAll('[data-gem-media-gallery], .gem-media-gallery, .gem-product-media-gallery');",
|
|
7140
|
+
" let bound = 0;",
|
|
7141
|
+
" galleries.forEach((gallery) => {",
|
|
7142
|
+
// A wrapper matched only by its class (the thumbnail-stack <section class="gem-product-media-gallery">) around a
|
|
7143
|
+
// [data-gem-media-gallery] hook is not a second gallery: binding both ran every click twice and the outer one
|
|
7144
|
+
// renumbered the thumbnails' data-gem-media-index by counting every <img>, the main image included.
|
|
7145
|
+
" const hooked = !!(gallery.hasAttribute && gallery.hasAttribute('data-gem-media-gallery'));",
|
|
7146
|
+
" if (!hooked && gallery.querySelector && gallery.querySelector('[data-gem-media-gallery]')) return;",
|
|
7147
|
+
" if (!mark(gallery, 'media-gallery')) return;",
|
|
7148
|
+
// Main image + thumbnails (product-media-gallery.thumbnail-stack.v1): a thumbnail click puts its
|
|
7149
|
+
// data-gem-media-src / -alt on [data-gem-media-main], as the section's behavior.js did. Without this the plugin
|
|
7150
|
+
// only moved aria-current and the big image never changed. The markup's data-gem-media-index is kept.
|
|
7151
|
+
" const main = gallery.querySelector && gallery.querySelector('[data-gem-media-main]');",
|
|
7152
|
+
" const thumbs = main && gallery.querySelectorAll ? Array.prototype.slice.call(gallery.querySelectorAll('[data-gem-media-item]')) : [];",
|
|
7153
|
+
" if (main && thumbs.length) {",
|
|
7154
|
+
" gallery.dataset.gemMediaState = 'ready';",
|
|
7155
|
+
" const choose = (item, reason) => {",
|
|
7156
|
+
" const src = item.getAttribute('data-gem-media-src');",
|
|
7157
|
+
" const alt = item.getAttribute('data-gem-media-alt');",
|
|
7158
|
+
" if (src) { main.setAttribute('src', src); main.removeAttribute('srcset'); if (alt != null) main.setAttribute('alt', alt); }",
|
|
7159
|
+
" const index = Number(item.getAttribute('data-gem-media-index') || thumbs.indexOf(item));",
|
|
7160
|
+
" thumbs.forEach((thumb) => { const on = thumb === item; thumb.setAttribute('aria-current', String(on)); if (thumb.dataset) thumb.dataset.gemMediaActive = on ? 'true' : 'false'; });",
|
|
7161
|
+
" gallery.dataset.gemMediaActiveIndex = String(index);",
|
|
7162
|
+
" if (gallery.dispatchEvent && typeof window.CustomEvent === 'function') gallery.dispatchEvent(new window.CustomEvent('gem:media:select', { bubbles: true, detail: { index } }));",
|
|
7163
|
+
" api.emit('gem:media:change', { source: 'gem-runtime', index, galleryId: gallery.id || undefined, reason });",
|
|
7164
|
+
" };",
|
|
7165
|
+
" if (canListen(gallery)) gallery.addEventListener('click', (event) => {",
|
|
7166
|
+
" const item = event.target && event.target.closest ? event.target.closest('[data-gem-media-item]') : null;",
|
|
7167
|
+
" if (!item || !gallery.contains(item)) return;",
|
|
7168
|
+
" if (event.preventDefault) event.preventDefault();",
|
|
7169
|
+
" choose(item, 'click');",
|
|
7170
|
+
" });",
|
|
7171
|
+
" bound += 1;",
|
|
7172
|
+
" return;",
|
|
7173
|
+
" }",
|
|
7174
|
+
" const items = gallery.querySelectorAll('[data-gem-media-item], img, video, model-viewer');",
|
|
7175
|
+
" const thumbControls = gallery.querySelectorAll('[data-gem-media-thumb]');",
|
|
7176
|
+
" const controls = thumbControls.length ? thumbControls : items;",
|
|
7177
|
+
" gallery.dataset.gemMediaState = 'ready';",
|
|
7178
|
+
" items.forEach((item, index) => {",
|
|
7179
|
+
" if (!item.dataset) return;",
|
|
7180
|
+
" item.dataset.gemMediaIndex = String(index);",
|
|
7181
|
+
" });",
|
|
7182
|
+
" const activate = (target, reason = 'click') => {",
|
|
7183
|
+
" if (!target) return;",
|
|
7184
|
+
" const index = target.dataset && target.dataset.gemMediaIndex ? Number(target.dataset.gemMediaIndex) : undefined;",
|
|
7185
|
+
" if (!Number.isFinite(index)) return;",
|
|
7186
|
+
" controls.forEach((control) => {",
|
|
7187
|
+
" const isActive = control.dataset && Number(control.dataset.gemMediaIndex) === index;",
|
|
7188
|
+
" if (isActive) control.setAttribute('aria-current', 'true'); else control.removeAttribute('aria-current');",
|
|
7189
|
+
" if (control.dataset) control.dataset.gemMediaActive = isActive ? 'true' : 'false';",
|
|
7190
|
+
" });",
|
|
7191
|
+
" gallery.dataset.gemMediaActiveIndex = String(index);",
|
|
7192
|
+
" api.emit('gem:media:change', { source: 'gem-runtime', index, galleryId: gallery.id || undefined, reason });",
|
|
7193
|
+
" };",
|
|
7194
|
+
` const firstActive = gallery.querySelector('[aria-current="true"], [data-gem-media-thumb]');`,
|
|
7195
|
+
" if (firstActive) activate(firstActive, 'init');",
|
|
7196
|
+
" if (canListen(gallery)) gallery.addEventListener('click', (event) => {",
|
|
7197
|
+
" const target = event.target && event.target.closest ? event.target.closest('[data-gem-media-item], img, video, model-viewer') : null;",
|
|
7198
|
+
" if (!target) return;",
|
|
7199
|
+
" activate(target, 'click');",
|
|
7200
|
+
" });",
|
|
7201
|
+
" controls.forEach((control) => { if (!canListen(control)) return; control.addEventListener('keydown', (event) => {",
|
|
7202
|
+
" if (!['Enter', ' '].includes(event.key)) return;",
|
|
7203
|
+
" event.preventDefault && event.preventDefault();",
|
|
7204
|
+
" activate(control, 'keyboard');",
|
|
7205
|
+
" }); });",
|
|
7206
|
+
" bound += 1;",
|
|
7207
|
+
" });",
|
|
7208
|
+
" api.emit('gem:media:ready', { source: 'gem-runtime', count: bound });",
|
|
7209
|
+
" });"
|
|
7210
|
+
];
|
|
7211
|
+
var carouselPluginRegistrationLines = [
|
|
7212
|
+
" registerPlugin('carousel', ({ api }) => {",
|
|
7213
|
+
" const carousels = document.querySelectorAll('[data-gem-carousel], .gem-carousel');",
|
|
7214
|
+
" let bound = 0;",
|
|
7215
|
+
" carousels.forEach((carousel) => {",
|
|
7216
|
+
" if (!mark(carousel, 'carousel')) return;",
|
|
7217
|
+
" const viewport = carousel.querySelector && (carousel.querySelector('[data-gem-carousel-viewport]') || carousel.querySelector('.gem-carousel__viewport'));",
|
|
7218
|
+
" const slides = carousel.querySelectorAll('[data-gem-carousel-slide], .gem-carousel__slide');",
|
|
7219
|
+
" const previous = carousel.querySelector && carousel.querySelector('[data-gem-carousel-prev]');",
|
|
7220
|
+
" const next = carousel.querySelector && carousel.querySelector('[data-gem-carousel-next]');",
|
|
7221
|
+
" const slideCount = slides.length || 0;",
|
|
7222
|
+
" const currentIndex = () => {",
|
|
7223
|
+
" if (!viewport || !slideCount) return 0;",
|
|
7224
|
+
" const width = Number(viewport.clientWidth || viewport.offsetWidth || 1);",
|
|
7225
|
+
" return Math.max(0, Math.min(slideCount - 1, Math.round(Number(viewport.scrollLeft || 0) / width)));",
|
|
7226
|
+
" };",
|
|
7227
|
+
" const setIndex = (index, reason = 'api') => {",
|
|
7228
|
+
" const nextIndex = Math.max(0, Math.min(slideCount - 1, Number.isFinite(index) ? index : 0));",
|
|
7229
|
+
" carousel.dataset.gemCarouselIndex = String(nextIndex);",
|
|
7230
|
+
" carousel.dataset.gemCarouselCount = String(slideCount);",
|
|
7231
|
+
" if (viewport && viewport.scrollTo) viewport.scrollTo({ left: Number(viewport.clientWidth || viewport.offsetWidth || 0) * nextIndex, behavior: 'smooth' });",
|
|
7232
|
+
" api.emit('gem:carousel:change', { source: 'gem-runtime', id: carousel.id || undefined, index: nextIndex, count: slideCount, reason });",
|
|
7233
|
+
" };",
|
|
7234
|
+
" slides.forEach((slide, index) => { if (slide.dataset) slide.dataset.gemCarouselIndex = String(index); });",
|
|
7235
|
+
" carousel.dataset.gemCarouselState = slideCount > 1 ? 'ready' : 'single';",
|
|
7236
|
+
" carousel.dataset.gemCarouselCount = String(slideCount);",
|
|
7237
|
+
" carousel.dataset.gemCarouselIndex = String(currentIndex());",
|
|
7238
|
+
" if (canListen(previous)) previous.addEventListener('click', (event) => { event.preventDefault && event.preventDefault(); setIndex(currentIndex() - 1, 'previous'); });",
|
|
7239
|
+
" if (canListen(next)) next.addEventListener('click', (event) => { event.preventDefault && event.preventDefault(); setIndex(currentIndex() + 1, 'next'); });",
|
|
7240
|
+
" if (canListen(viewport)) viewport.addEventListener('scroll', () => { carousel.dataset.gemCarouselIndex = String(currentIndex()); }, { passive: true });",
|
|
7241
|
+
" bound += 1;",
|
|
7242
|
+
" });",
|
|
7243
|
+
" api.emit('gem:carousel:ready', { source: 'gem-runtime', count: bound });",
|
|
7244
|
+
" });"
|
|
7245
|
+
];
|
|
7246
|
+
var heroSlideshowPluginRegistrationLines = [
|
|
7247
|
+
" registerPlugin('hero-slideshow', ({ api }) => {",
|
|
7248
|
+
" const roots = document.querySelectorAll('[data-gem-hero-slideshow]');",
|
|
7249
|
+
" let bound = 0;",
|
|
7250
|
+
" roots.forEach((root) => {",
|
|
7251
|
+
" if (!mark(root, 'hero-slideshow')) return;",
|
|
7252
|
+
" const slides = Array.prototype.slice.call(root.querySelectorAll('[data-gem-hero-slide]'));",
|
|
7253
|
+
" if (slides.length < 2) return;",
|
|
7254
|
+
" const dots = Array.prototype.slice.call(root.querySelectorAll('[data-gem-hero-dot]'));",
|
|
7255
|
+
" const previous = root.querySelector && root.querySelector('[data-gem-hero-prev]');",
|
|
7256
|
+
" const next = root.querySelector && root.querySelector('[data-gem-hero-next]');",
|
|
7257
|
+
" const pause = root.querySelector && root.querySelector('[data-gem-hero-pause]');",
|
|
7258
|
+
" const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;",
|
|
7259
|
+
" const autoplay = !!(root.dataset && root.dataset.autoplay === 'true') && !reduce;",
|
|
7260
|
+
" const interval = Math.max(Number((root.dataset && root.dataset.autoplayInterval) || 6000), 4000);",
|
|
7261
|
+
" let active = 0;",
|
|
7262
|
+
" for (let i = 0; i < slides.length; i += 1) { if ((slides[i].className || '').indexOf('gem-hero-slideshow__slide--active') > -1) { active = i; break; } }",
|
|
7263
|
+
" let timer = null; let paused = false;",
|
|
7264
|
+
" const show = (index) => {",
|
|
7265
|
+
" active = (index + slides.length) % slides.length;",
|
|
7266
|
+
" slides.forEach((s, i) => { const on = i === active; if (s.classList) s.classList.toggle('gem-hero-slideshow__slide--active', on); s.hidden = !on; });",
|
|
7267
|
+
// `tabindex` đi cùng `aria-selected` vì các chấm này khai `role=\"tab\"`: một tablist chỉ được
|
|
7268
|
+
// chiếm MỘT điểm dừng Tab, và mũi tên mới là thứ chạy giữa các tab (xử lý ngay dưới).
|
|
7269
|
+
" 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'); } });",
|
|
7270
|
+
" api.emit('gem:hero-slideshow:change', { source: 'gem-runtime', id: root.id || undefined, index: active, count: slides.length });",
|
|
7271
|
+
" };",
|
|
7272
|
+
" const stop = () => { if (timer) { window.clearInterval(timer); timer = null; } };",
|
|
7273
|
+
" const start = () => { stop(); if (autoplay && !paused) timer = window.setInterval(() => { show(active + 1); }, interval); };",
|
|
7274
|
+
" if (canListen(previous)) previous.addEventListener('click', (e) => { if (e.preventDefault) e.preventDefault(); show(active - 1); start(); });",
|
|
7275
|
+
" if (canListen(next)) next.addEventListener('click', (e) => { if (e.preventDefault) e.preventDefault(); show(active + 1); start(); });",
|
|
7276
|
+
" dots.forEach((d, i) => { if (canListen(d)) d.addEventListener('click', () => { show(Number((d.dataset && d.dataset.slideIndex) || i)); start(); }); });",
|
|
7277
|
+
" 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(); };",
|
|
7278
|
+
" dots.forEach((d, i) => { if (canListen(d)) d.addEventListener('keydown', (e) => {",
|
|
7279
|
+
" const k = e.key;",
|
|
7280
|
+
" if (k === 'ArrowRight' || k === 'ArrowDown') { if (e.preventDefault) e.preventDefault(); focusDot(i + 1); }",
|
|
7281
|
+
" else if (k === 'ArrowLeft' || k === 'ArrowUp') { if (e.preventDefault) e.preventDefault(); focusDot(i - 1); }",
|
|
7282
|
+
" else if (k === 'Home') { if (e.preventDefault) e.preventDefault(); focusDot(0); }",
|
|
7283
|
+
" else if (k === 'End') { if (e.preventDefault) e.preventDefault(); focusDot(dots.length - 1); }",
|
|
7284
|
+
" }); });",
|
|
7285
|
+
" // The visible label follows the state like the section's own behavior did: the button's own text while playing",
|
|
7286
|
+
" // (kept, so a translated label survives), data-gem-hero-play-label (or 'Play') while paused. Only a text-only button.",
|
|
7287
|
+
" const pauseText = pause && pause.childElementCount === 0 ? pause.textContent : null;",
|
|
7288
|
+
" const playText = (pause && pause.getAttribute && pause.getAttribute('data-gem-hero-play-label')) || 'Play';",
|
|
7289
|
+
" 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(); });",
|
|
7290
|
+
" show(active); start(); bound += 1;",
|
|
7291
|
+
" });",
|
|
7292
|
+
" api.emit('gem:hero-slideshow:ready', { source: 'gem-runtime', count: bound });",
|
|
7293
|
+
" });"
|
|
7294
|
+
];
|
|
7295
|
+
var legacyQuickViewLines = [
|
|
7296
|
+
` const triggers = document.querySelectorAll('[data-gem-quick-view], [data-gem-action="quick-view"]');`,
|
|
7297
|
+
` const surface = document.querySelector('[data-gem-quick-view-surface], .gem-quick-view, .gem-modal[data-gem-component="quick-view"]');`,
|
|
7298
|
+
" let bound = 0;",
|
|
7299
|
+
" const close = (reason = 'close-control') => {",
|
|
7300
|
+
" if (surface) {",
|
|
7301
|
+
" surface.dataset.gemQuickViewState = 'closed';",
|
|
7302
|
+
" surface.removeAttribute && surface.removeAttribute('open');",
|
|
7303
|
+
" surface.setAttribute && surface.setAttribute('aria-hidden', 'true');",
|
|
7304
|
+
" }",
|
|
7305
|
+
" api.emit('gem:quick-view:close', { source: 'gem-runtime', reason });",
|
|
7306
|
+
" };",
|
|
7307
|
+
" const openSurface = (detail) => {",
|
|
7308
|
+
" if (surface) {",
|
|
7309
|
+
" surface.dataset.gemQuickViewState = 'open';",
|
|
7310
|
+
" surface.dataset.gemProductHandle = detail.productHandle || '';",
|
|
7311
|
+
" surface.dataset.gemProductUrl = detail.productUrl || '';",
|
|
7312
|
+
" surface.setAttribute && surface.setAttribute('open', '');",
|
|
7313
|
+
" surface.setAttribute && surface.setAttribute('aria-hidden', 'false');",
|
|
7314
|
+
" surface.setAttribute && surface.setAttribute('role', 'dialog');",
|
|
7315
|
+
" surface.setAttribute && surface.setAttribute('aria-modal', 'true');",
|
|
7316
|
+
" const content = surface.querySelector && surface.querySelector('[data-gem-quick-view-content]');",
|
|
7317
|
+
" if (content && content.textContent !== undefined) content.textContent = 'Quick view for ' + (detail.productHandle || 'product');",
|
|
7318
|
+
" else if (surface.textContent !== undefined) surface.textContent = 'Quick view for ' + (detail.productHandle || 'product');",
|
|
7319
|
+
" const closeButton = surface.querySelector && surface.querySelector('[data-gem-quick-view-close]');",
|
|
7320
|
+
" if (closeButton && closeButton.focus) closeButton.focus();",
|
|
7321
|
+
" }",
|
|
7322
|
+
" api.emit('gem:quick-view:open', { source: 'gem-runtime', ...detail, hasSurface: Boolean(surface) });",
|
|
7323
|
+
" };",
|
|
7324
|
+
" triggers.forEach((trigger) => {",
|
|
7325
|
+
" if (!mark(trigger, 'quick-view')) return;",
|
|
7326
|
+
" if (!canListen(trigger)) return;",
|
|
7327
|
+
" trigger.addEventListener('click', (event) => {",
|
|
7328
|
+
" const productUrl = trigger.getAttribute && (trigger.getAttribute('data-gem-product-url') || trigger.getAttribute('href'));",
|
|
7329
|
+
" const productHandle = trigger.getAttribute && trigger.getAttribute('data-gem-product-handle');",
|
|
7330
|
+
" if (!productUrl && !productHandle) return;",
|
|
7331
|
+
" event.preventDefault && event.preventDefault();",
|
|
7332
|
+
" openSurface({ productUrl, productHandle });",
|
|
7333
|
+
" });",
|
|
7334
|
+
" bound += 1;",
|
|
7335
|
+
" });",
|
|
7336
|
+
" if (surface && mark(surface, 'quick-view-surface')) {",
|
|
7337
|
+
" surface.dataset.gemQuickViewState = 'closed';",
|
|
7338
|
+
" surface.setAttribute && surface.setAttribute('aria-hidden', 'true');",
|
|
7339
|
+
" if (canListen(surface)) surface.addEventListener('click', (event) => {",
|
|
7340
|
+
" const target = event.target;",
|
|
7341
|
+
" if (target && target.closest && target.closest('[data-gem-quick-view-close]')) close('close-control');",
|
|
7342
|
+
" });",
|
|
7343
|
+
" if (canListen(surface)) surface.addEventListener('keydown', (event) => { if (event.key === 'Escape') close('escape'); });",
|
|
7344
|
+
" }",
|
|
7345
|
+
// A mount on one late trigger (gem-runtime.js binds a section added after boot on its own) finds no surface: it keeps the
|
|
7346
|
+
// page's api.quickView.
|
|
7347
|
+
" if (surface || !api.quickView) api.quickView = { open: (detail = {}) => openSurface({ ...detail }), close };"
|
|
7348
|
+
];
|
|
7349
|
+
var runtimeChunks = gemMotionChunks({
|
|
7350
|
+
hook: '[data-gem-quick-view], [data-gem-action="quick-view"]',
|
|
7351
|
+
lines: legacyQuickViewLines,
|
|
7352
|
+
readyDetail: "count: bound + count, hasSurface: Boolean(surface)"
|
|
7353
|
+
});
|
|
7354
|
+
var promoPopupPluginRegistrationLines = [
|
|
7355
|
+
" registerPlugin('promo-popup', ({ api }) => {",
|
|
7356
|
+
// Only the explicit hook opts in. Every promo-popup section carries the archetype class .gem-promo-popup, and
|
|
7357
|
+
// binding by class took over variants that run their own behavior.js (GemFactory gap #16, 03/10/2026).
|
|
7358
|
+
// A section that has the hook AND its own behavior.js (promo-popup.email-capture.v1) renders the plugin's bound
|
|
7359
|
+
// mark, data-gem-bound-promo-popup, so mark() below skips it (gem-framework 0.8.3).
|
|
7360
|
+
" const popups = document.querySelectorAll('[data-gem-promo-popup]');",
|
|
7361
|
+
" let bound = 0;",
|
|
7362
|
+
" const shown = [];",
|
|
7363
|
+
" const storagePrefix = 'gem:promo-popup-dismissed:';",
|
|
7364
|
+
" const now = () => Date.now ? Date.now() : new Date().getTime();",
|
|
7365
|
+
// ONE storage key, two meanings in the wild (gem-framework 0.8.0). gemsite's port stores WHEN the shopper
|
|
7366
|
+
// dismissed; runtimes up to 0.7 stored when the dismissal ENDS. Read both: a value in the future can only be an
|
|
7367
|
+
// end, so it holds until then (and is rewritten once as a dismissal time); a value in the past is a dismissal time and
|
|
7368
|
+
// the popup's own window applies. Written from now on: the dismissal time (gemsite's meaning), which also lets a
|
|
7369
|
+
// seller who shortens the window shorten it for past dismissals. An end written by 0.7 that has already passed
|
|
7370
|
+
// reads as a dismissal time: that popup stays away one more window at most, never less than the shopper asked.
|
|
7371
|
+
" const readDismissal = (key) => {",
|
|
7372
|
+
" try { const value = Number(target.localStorage && target.localStorage.getItem(storagePrefix + key) || '0'); return Number.isFinite(value) && value > 0 ? value : 0; }",
|
|
7373
|
+
" catch { return 0; }",
|
|
7374
|
+
" };",
|
|
7375
|
+
" const writeDismissedAt = (key, timestamp) => {",
|
|
7376
|
+
" try { if (target.localStorage) target.localStorage.setItem(storagePrefix + key, String(timestamp)); }",
|
|
7377
|
+
" catch { /* storage can be unavailable in private contexts */ }",
|
|
7378
|
+
" };",
|
|
7379
|
+
" const isDismissed = (key, windowMs) => {",
|
|
7380
|
+
" const stored = readDismissal(key);",
|
|
7381
|
+
" if (!stored) return false;",
|
|
7382
|
+
" const at = now();",
|
|
7383
|
+
// Converted ONCE: only when the end minus this window is already past (it is then the dismissal time). An end
|
|
7384
|
+
// further out than one window (the 0.7 window was longer) is left as it is and holds until it passes.
|
|
7385
|
+
" if (stored > at) { if (stored - windowMs <= at) writeDismissedAt(key, stored - windowMs); return true; }",
|
|
7386
|
+
" return at - stored < windowMs;",
|
|
7387
|
+
" };",
|
|
7388
|
+
" popups.forEach((popup, index) => {",
|
|
7389
|
+
" if (!mark(popup, 'promo-popup')) return;",
|
|
7390
|
+
" const key = popup.id || popup.getAttribute && popup.getAttribute('data-gem-section-library-id') || String(index);",
|
|
7391
|
+
" const delaySeconds = Number(popup.getAttribute && (popup.getAttribute('data-gem-promo-delay') || '0'));",
|
|
7392
|
+
" const frequencyDays = Number(popup.getAttribute && (popup.getAttribute('data-gem-promo-frequency-days') || '7'));",
|
|
7393
|
+
" const windowMs = (Number.isFinite(frequencyDays) ? Math.max(1, frequencyDays) : 7) * 86400000;",
|
|
7394
|
+
" const dialog = popup.querySelector && popup.querySelector('[data-gem-promo-popup-dialog]');",
|
|
7395
|
+
" const overlay = popup.querySelector && popup.querySelector('[data-gem-promo-popup-overlay]');",
|
|
7396
|
+
" const show = (reason) => {",
|
|
7397
|
+
" popup.hidden = false;",
|
|
7398
|
+
" if (dialog) dialog.hidden = false;",
|
|
7399
|
+
" if (overlay) overlay.hidden = false;",
|
|
7400
|
+
" popup.dataset.gemPromoPopupState = 'open';",
|
|
7401
|
+
" api.emit('gem:promo-popup:open', { source: 'gem-runtime', id: popup.id || undefined, reason });",
|
|
7402
|
+
" };",
|
|
7403
|
+
" const open = (reason = 'timer') => {",
|
|
7404
|
+
" if (isDismissed(key, windowMs)) { popup.dataset.gemPromoPopupState = 'dismissed'; return false; }",
|
|
7405
|
+
" show(reason);",
|
|
7406
|
+
" return true;",
|
|
7407
|
+
" };",
|
|
7408
|
+
" const close = (reason = 'dismiss') => {",
|
|
7409
|
+
" writeDismissedAt(key, now());",
|
|
7410
|
+
" if (dialog) dialog.hidden = true;",
|
|
7411
|
+
" if (overlay) overlay.hidden = true;",
|
|
7412
|
+
" popup.dataset.gemPromoPopupState = 'closed';",
|
|
7413
|
+
" api.emit('gem:promo-popup:close', { source: 'gem-runtime', id: popup.id || undefined, reason });",
|
|
7414
|
+
" };",
|
|
7415
|
+
" popup.querySelectorAll && popup.querySelectorAll('[data-gem-promo-popup-dismiss]').forEach((button) => {",
|
|
7416
|
+
" if (!canListen(button)) return;",
|
|
7417
|
+
" button.addEventListener('click', (event) => { event.preventDefault && event.preventDefault(); close('dismiss'); });",
|
|
7418
|
+
" });",
|
|
7419
|
+
" if (canListen(overlay)) overlay.addEventListener('click', () => close('overlay'));",
|
|
7420
|
+
" popup.dataset.gemPromoPopupState = 'ready';",
|
|
7421
|
+
" const delay = Math.max(0, Number.isFinite(delaySeconds) ? delaySeconds : 0) * 1000;",
|
|
7422
|
+
// WHEN it opens, gemsite's contract (components/behaviors/promoPopup.js): data-gem-promo-trigger = "delay"
|
|
7423
|
+
// (default, and any unknown value) | "exit_intent" (the pointer leaves through the top 10px of the window; a
|
|
7424
|
+
// touch screen has no pointer to leave with, so it falls back to the delay) | "scroll" (the shopper has scrolled
|
|
7425
|
+
// data-gem-promo-scroll-percent of the page, 10 to 90, default 50; a page that still cannot scroll at the delay
|
|
7426
|
+
// opens then). It opens once per page view, whichever fires first. data-gem-promo-armed says which one is live.
|
|
7427
|
+
// The delay keeps the runtime's default of 0 seconds (gemsite's port defaults to 6).
|
|
7428
|
+
" const trigger = String(popup.getAttribute && popup.getAttribute('data-gem-promo-trigger') || 'delay').trim();",
|
|
7429
|
+
" const rawPercent = String(popup.getAttribute && popup.getAttribute('data-gem-promo-scroll-percent') || '').trim();",
|
|
7430
|
+
" const parsedPercent = rawPercent ? Number(rawPercent) : NaN;",
|
|
7431
|
+
" const scrollPercent = Number.isFinite(parsedPercent) ? Math.min(90, Math.max(10, parsedPercent)) : 50;",
|
|
7432
|
+
" const canHover = () => { try { return Boolean(target.matchMedia && target.matchMedia('(hover: hover) and (pointer: fine)').matches); } catch { return false; } };",
|
|
7433
|
+
" const armed = trigger === 'exit_intent' && canHover() ? 'exit_intent' : trigger === 'scroll' ? 'scroll' : 'delay';",
|
|
7434
|
+
" if (popup.setAttribute) popup.setAttribute('data-gem-promo-armed', armed);",
|
|
7435
|
+
" let opened = false;",
|
|
7436
|
+
" const disarm = [];",
|
|
7437
|
+
" const openOnce = (reason) => { if (opened) return; opened = true; disarm.splice(0).forEach((off) => off()); open(reason); };",
|
|
7438
|
+
// An explicit open (api.promoPopup.open) counts as THE open of this page view: it disarms the pending trigger, so the
|
|
7439
|
+
// delay or exit intent does not show the popup a second time (code review of #675), and it does nothing on a popup
|
|
7440
|
+
// that is already open.
|
|
7441
|
+
" shown.push((reason) => { opened = true; disarm.splice(0).forEach((off) => off()); if (popup.dataset.gemPromoPopupState !== 'open') show(reason); });",
|
|
7442
|
+
" if (armed === 'exit_intent') {",
|
|
7443
|
+
" const onMouseOut = (event) => { if (event.relatedTarget || event.clientY >= 10) return; openOnce('exit_intent'); };",
|
|
7444
|
+
" if (canListen(document)) {",
|
|
7445
|
+
" document.addEventListener('mouseout', onMouseOut);",
|
|
7446
|
+
" disarm.push(() => document.removeEventListener && document.removeEventListener('mouseout', onMouseOut));",
|
|
7447
|
+
" }",
|
|
7448
|
+
" } else if (armed === 'scroll') {",
|
|
7449
|
+
" const room = () => Number(document.documentElement && document.documentElement.scrollHeight || 0) - Number(target.innerHeight || 0);",
|
|
7450
|
+
" const check = () => { const r = room(); if (r > 0 && (Number(target.scrollY || target.pageYOffset || 0) / r) * 100 >= scrollPercent) openOnce('scroll'); };",
|
|
7451
|
+
" let ticking = false;",
|
|
7452
|
+
" const onScroll = () => {",
|
|
7453
|
+
" if (ticking) return;",
|
|
7454
|
+
" ticking = true;",
|
|
7455
|
+
" const frame = target.requestAnimationFrame ? (fn) => target.requestAnimationFrame(fn) : (fn) => target.setTimeout && target.setTimeout(fn, 16);",
|
|
7456
|
+
" frame(() => { ticking = false; check(); });",
|
|
7457
|
+
" };",
|
|
7458
|
+
" if (canListen(target)) {",
|
|
7459
|
+
" target.addEventListener('scroll', onScroll, { passive: true });",
|
|
7460
|
+
" target.addEventListener('resize', check);",
|
|
7461
|
+
" target.addEventListener('load', check);",
|
|
7462
|
+
" disarm.push(() => { if (!target.removeEventListener) return; target.removeEventListener('scroll', onScroll); target.removeEventListener('resize', check); target.removeEventListener('load', check); });",
|
|
7463
|
+
" }",
|
|
7464
|
+
" if (target.setTimeout) {",
|
|
7465
|
+
" const fallback = target.setTimeout(() => { if (room() <= 0) openOnce('timer'); }, delay);",
|
|
7466
|
+
" disarm.push(() => target.clearTimeout && target.clearTimeout(fallback));",
|
|
7467
|
+
" }",
|
|
7468
|
+
" check();",
|
|
7469
|
+
" } else {",
|
|
7470
|
+
" target.setTimeout ? target.setTimeout(() => openOnce('timer'), delay) : openOnce('timer');",
|
|
7471
|
+
" }",
|
|
7472
|
+
" bound += 1;",
|
|
7473
|
+
" });",
|
|
7474
|
+
// The popups THIS plugin bound, never every [data-gem-promo-popup]: one a section's own behavior.js drives carries the
|
|
7475
|
+
// bound mark already (email-capture renders data-gem-bound-promo-popup="section") and is not the plugin's to open. An
|
|
7476
|
+
// explicit open (a teaser, a test) shows the dialog itself, not only the root, and ignores the dismissal window,
|
|
7477
|
+
// which is about opening on its own. Before 0.8.3 it unhid the root only, so the dialog stayed hidden.
|
|
7478
|
+
" api.promoPopup = { open: () => shown.forEach((show) => show('api')) };",
|
|
7479
|
+
" api.emit('gem:promo-popup:ready', { source: 'gem-runtime', count: bound });",
|
|
7480
|
+
" });"
|
|
7481
|
+
];
|
|
7482
|
+
var stickyAtcPluginRegistrationLines = [
|
|
7483
|
+
" registerPlugin('sticky-atc', ({ api }) => {",
|
|
7484
|
+
" const bars = document.querySelectorAll('[data-gem-sticky-atc], .gem-sticky-atc');",
|
|
7485
|
+
" let bound = 0;",
|
|
7486
|
+
" const setState = (bar, state) => {",
|
|
7487
|
+
" if (!bar || !bar.dataset) return;",
|
|
7488
|
+
" if (bar.dataset.gemStickyAtcState === state) return;",
|
|
7489
|
+
" bar.dataset.gemStickyAtcState = state;",
|
|
7490
|
+
" api.emit(state === 'visible' ? 'gem:sticky-atc:show' : 'gem:sticky-atc:hide', { source: 'gem-runtime', id: bar.id || undefined });",
|
|
7491
|
+
" };",
|
|
7492
|
+
" bars.forEach((bar) => {",
|
|
7493
|
+
" if (!mark(bar, 'sticky-atc')) return;",
|
|
7494
|
+
// Two attribute names (gem-framework 0.8.0), each with its own default. data-gem-sticky-threshold is the
|
|
7495
|
+
// runtime's (default 240). data-gem-sticky-atc-threshold is gemsite's (StickyAtcCompactBar.tsx): when the bar
|
|
7496
|
+
// carries it, gemsite's rule decides: that number, else the --gem-sticky-atc-threshold CSS variable, else 360.
|
|
7497
|
+
// A bar carrying both follows gemsite's attribute.
|
|
7498
|
+
" const cssThreshold = () => { try { return target.getComputedStyle ? Number(target.getComputedStyle(bar).getPropertyValue('--gem-sticky-atc-threshold')) : 0; } catch { return 0; } };",
|
|
7499
|
+
" const threshold = bar.hasAttribute && bar.hasAttribute('data-gem-sticky-atc-threshold')",
|
|
7500
|
+
" ? (Number(bar.getAttribute('data-gem-sticky-atc-threshold')) || cssThreshold() || 360)",
|
|
7501
|
+
" : Number(bar.getAttribute && (bar.getAttribute('data-gem-sticky-threshold') || '240'));",
|
|
7502
|
+
" const update = () => {",
|
|
7503
|
+
" const scrollY = Number(target.scrollY || target.pageYOffset || 0);",
|
|
7504
|
+
" setState(bar, scrollY >= threshold ? 'visible' : 'hidden');",
|
|
7505
|
+
" };",
|
|
7506
|
+
" update();",
|
|
7507
|
+
" if (canListen(target)) target.addEventListener('scroll', update, { passive: true });",
|
|
7508
|
+
// The price follows the variant <select> (each <option> carries its own data-gem-variant-price), as the section's
|
|
7509
|
+
// behavior.js did: otherwise the bar shows the first variant's price whatever the shopper picked.
|
|
7510
|
+
" const priceEl = bar.querySelector && bar.querySelector('[data-gem-sticky-atc-price]');",
|
|
7511
|
+
" const select = bar.querySelector && bar.querySelector('select');",
|
|
7512
|
+
" 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; };",
|
|
7513
|
+
" if (canListen(select)) select.addEventListener('change', syncPrice);",
|
|
7514
|
+
" syncPrice();",
|
|
7515
|
+
// A form WITHOUT an action (the headless bar) has nowhere to post: the plugin adds to cart by dispatching
|
|
7516
|
+
// gem:cart:add with the SELECTED variant (the button's data-gem-variant-id is only the first one), which the
|
|
7517
|
+
// storefront's cart bridge consumes. A form WITH an action (a Liquid /cart/add form) is left to the browser.
|
|
7518
|
+
" const form = bar.querySelector && bar.querySelector('form');",
|
|
7519
|
+
" if (canListen(form)) form.addEventListener('submit', (event) => {",
|
|
7520
|
+
" if (!(form.hasAttribute && form.hasAttribute('action'))) {",
|
|
7521
|
+
" if (event && event.preventDefault) event.preventDefault();",
|
|
7522
|
+
" const choice = form.querySelector && form.querySelector('select');",
|
|
7523
|
+
" const trigger = bar.querySelector && bar.querySelector('[data-gem-variant-id]');",
|
|
7524
|
+
" const variantId = (choice && choice.value) || (trigger && trigger.getAttribute('data-gem-variant-id')) || undefined;",
|
|
7525
|
+
" if (bar.dispatchEvent && typeof window.CustomEvent === 'function') bar.dispatchEvent(new window.CustomEvent('gem:cart:add', { bubbles: true, detail: { variantId, quantity: 1 } }));",
|
|
7526
|
+
" }",
|
|
7527
|
+
" api.emit('gem:sticky-atc:submit', { source: 'gem-runtime', id: bar.id || undefined });",
|
|
7528
|
+
" });",
|
|
7529
|
+
" bound += 1;",
|
|
7530
|
+
" });",
|
|
7531
|
+
" api.emit('gem:sticky-atc:ready', { source: 'gem-runtime', count: bound });",
|
|
7532
|
+
" });"
|
|
7533
|
+
];
|
|
7534
|
+
var beforeAfterPluginRegistrationLines = [
|
|
7535
|
+
" registerPlugin('before_after', ({ api }) => {",
|
|
7536
|
+
" const frames = document.querySelectorAll('[data-gem-before-after-frame], .gem-before-after__frame');",
|
|
7537
|
+
" let bound = 0;",
|
|
7538
|
+
" const clamp = (value) => Math.max(0, Math.min(100, Number.isFinite(value) ? value : 50));",
|
|
7539
|
+
" frames.forEach((frame) => {",
|
|
7540
|
+
" if (!mark(frame, 'before_after')) return;",
|
|
7541
|
+
` const range = frame.querySelector && frame.querySelector('[data-gem-before-after-range], input[type="range"]');`,
|
|
7542
|
+
" const setPosition = (value, reason = 'init') => {",
|
|
7543
|
+
" const position = clamp(Number(value));",
|
|
7544
|
+
" if (frame.style && frame.style.setProperty) frame.style.setProperty('--gem-before-after-position', position + '%');",
|
|
7545
|
+
" frame.dataset.gemBeforeAfterPosition = String(position);",
|
|
7546
|
+
" if (range && range.value !== undefined) range.value = String(position);",
|
|
7547
|
+
" api.emit('gem:before-after:change', { source: 'gem-runtime', position, reason, id: frame.id || undefined });",
|
|
7548
|
+
" };",
|
|
7549
|
+
" const initial = range && range.value !== undefined ? range.value : frame.getAttribute && frame.getAttribute('data-gem-before-after-position');",
|
|
7550
|
+
" setPosition(initial || 50);",
|
|
7551
|
+
" if (canListen(range)) range.addEventListener('input', () => setPosition(range.value, 'input'));",
|
|
7552
|
+
" bound += 1;",
|
|
7553
|
+
" });",
|
|
7554
|
+
" api.emit('gem:before-after:ready', { source: 'gem-runtime', count: bound });",
|
|
7555
|
+
" });"
|
|
7556
|
+
];
|
|
5430
7557
|
var COUNTDOWN_END_RUNTIME_LINES = [
|
|
5431
7558
|
" const COUNTDOWN_ISO = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d+)?)?(Z|[+-]\\d{2}:?\\d{2})?)?$/i;",
|
|
5432
7559
|
" const countdownEnd = (value) => {",
|
|
@@ -5612,6 +7739,141 @@ var countdownPluginRegistrationLines = [
|
|
|
5612
7739
|
" api.emit('gem:countdown:ready', { source: 'gem-runtime', count: bound });",
|
|
5613
7740
|
" });"
|
|
5614
7741
|
];
|
|
7742
|
+
var revealOnScrollPluginRegistrationLines = [
|
|
7743
|
+
" registerPlugin('reveal-on-scroll', ({ api }) => {",
|
|
7744
|
+
" const nodes = document.querySelectorAll('[data-gem-reveal], .gem-reveal');",
|
|
7745
|
+
" let bound = 0;",
|
|
7746
|
+
" const reveal = (node) => { node.dataset.gemRevealState = 'in'; };",
|
|
7747
|
+
" // Reduced motion and no IntersectionObserver land on the same answer: show everything now (from gemsite revealOnScroll.js).",
|
|
7748
|
+
" const reduce = typeof target.matchMedia === 'function' && target.matchMedia('(prefers-reduced-motion: reduce)').matches;",
|
|
7749
|
+
" if (reduce || !target.IntersectionObserver) { nodes.forEach(reveal); api.emit('gem:reveal:ready', { source: 'gem-runtime', count: 0 }); return; }",
|
|
7750
|
+
" // Above the fold the browser has already painted the section: writing 'ready' would blink it out and back in.",
|
|
7751
|
+
' // `none` and `default` are the knob\'s "apply nothing" values; the attribute is still written, so skip them here.',
|
|
7752
|
+
" const off = (node) => { const v = node.getAttribute ? node.getAttribute('data-gem-reveal') : null; return v === 'none' || v === 'default'; };",
|
|
7753
|
+
" const inViewport = (node) => { if (!node.getBoundingClientRect) return true; const r = node.getBoundingClientRect(); const h = target.innerHeight || (document.documentElement && document.documentElement.clientHeight) || 0; return r.top < h && r.bottom > 0; };",
|
|
7754
|
+
" const observer = new target.IntersectionObserver((entries, obs) => {",
|
|
7755
|
+
" entries.forEach((entry) => {",
|
|
7756
|
+
" if (!entry.isIntersecting) return;",
|
|
7757
|
+
" reveal(entry.target);",
|
|
7758
|
+
" obs.unobserve(entry.target);",
|
|
7759
|
+
" api.emit('gem:reveal:in', { source: 'gem-runtime', id: entry.target.id || undefined });",
|
|
7760
|
+
" });",
|
|
7761
|
+
" }, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });",
|
|
7762
|
+
" nodes.forEach((node) => {",
|
|
7763
|
+
" if (!mark(node, 'reveal-on-scroll')) return;",
|
|
7764
|
+
" if (off(node) || inViewport(node)) { reveal(node); bound += 1; return; }",
|
|
7765
|
+
" if (!node.dataset.gemRevealState) node.dataset.gemRevealState = 'ready';",
|
|
7766
|
+
" observer.observe(node);",
|
|
7767
|
+
" bound += 1;",
|
|
7768
|
+
" });",
|
|
7769
|
+
" api.emit('gem:reveal:ready', { source: 'gem-runtime', count: bound });",
|
|
7770
|
+
" });"
|
|
7771
|
+
];
|
|
7772
|
+
var imgInPluginRegistrationLines = [
|
|
7773
|
+
" registerPlugin('img-in', ({ api }) => {",
|
|
7774
|
+
" const STATE = 'data-gem-img-in-state';",
|
|
7775
|
+
" const BOUND = 'data-gem-bound-img-in';",
|
|
7776
|
+
" const reduce = typeof target.matchMedia === 'function' && target.matchMedia('(prefers-reduced-motion: reduce)').matches;",
|
|
7777
|
+
" const style = (img) => (typeof target.getComputedStyle === 'function' ? target.getComputedStyle(img) : null);",
|
|
7778
|
+
" const owned = (s) => Boolean(s && ((s.opacity && s.opacity !== '1') || (s.filter && s.filter !== 'none') || (s.animationName && s.animationName !== 'none')));",
|
|
7779
|
+
" const overridden = (s) => Boolean(s && ((s.opacity && s.opacity !== '0.6') || (s.filter && s.filter.indexOf('blur') === -1)));",
|
|
7780
|
+
" const settle = (event) => {",
|
|
7781
|
+
" const img = event.target;",
|
|
7782
|
+
" if (!img || img.tagName !== 'IMG' || !img.hasAttribute(BOUND)) return;",
|
|
7783
|
+
" const state = img.getAttribute(STATE);",
|
|
7784
|
+
" if (event.type === 'load' || event.type === 'error') {",
|
|
7785
|
+
" if (state !== 'loading') return;",
|
|
7786
|
+
" if (event.type === 'load' && !reduce && !overridden(style(img))) img.setAttribute(STATE, 'in');",
|
|
7787
|
+
" else img.removeAttribute(STATE);",
|
|
7788
|
+
" return;",
|
|
7789
|
+
" }",
|
|
7790
|
+
" if (state === 'in' && event.animationName === 'gem-img-in') img.removeAttribute(STATE);",
|
|
7791
|
+
" };",
|
|
7792
|
+
" ['load', 'error', 'animationend', 'animationcancel'].forEach((type) => document.addEventListener(type, settle, true));",
|
|
7793
|
+
" const candidates = [];",
|
|
7794
|
+
" document.querySelectorAll('img').forEach((img) => {",
|
|
7795
|
+
" if (!img.closest('[data-gem-img-in]')) return;",
|
|
7796
|
+
" if (img.complete) { if (!img.hasAttribute(BOUND)) img.removeAttribute(STATE); return; }",
|
|
7797
|
+
" if (mark(img, 'img-in')) candidates.push(img);",
|
|
7798
|
+
" });",
|
|
7799
|
+
" const own = candidates.map((img) => owned(style(img)));",
|
|
7800
|
+
" let bound = 0;",
|
|
7801
|
+
" candidates.forEach((img, i) => { if (own[i]) return; img.setAttribute(STATE, 'loading'); bound += 1; });",
|
|
7802
|
+
" api.emit('gem:img-in:ready', { source: 'gem-runtime', count: bound });",
|
|
7803
|
+
" });"
|
|
7804
|
+
];
|
|
7805
|
+
var recentlyViewedPluginRegistrationLines = [
|
|
7806
|
+
" registerPlugin('recently-viewed', ({ api }) => {",
|
|
7807
|
+
" const storageKey = 'gem:recently-viewed-products';",
|
|
7808
|
+
" const readItems = () => {",
|
|
7809
|
+
" try { return JSON.parse(target.localStorage && target.localStorage.getItem(storageKey) || '[]'); }",
|
|
7810
|
+
" catch { return []; }",
|
|
7811
|
+
" };",
|
|
7812
|
+
" const writeItems = (items) => {",
|
|
7813
|
+
" try { if (target.localStorage) target.localStorage.setItem(storageKey, JSON.stringify(items.slice(0, 12))); }",
|
|
7814
|
+
" catch { /* storage can be unavailable in private contexts */ }",
|
|
7815
|
+
" };",
|
|
7816
|
+
` const escapeHtml = (value) => String(value || '').replace(/[&<>'"]/g, (char) => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char]));`,
|
|
7817
|
+
" const renderItem = (item) => {",
|
|
7818
|
+
" const url = item && item.url ? String(item.url) : '#';",
|
|
7819
|
+
" const label = item && (item.title || item.handle || item.url) ? String(item.title || item.handle || item.url) : 'Recently viewed product';",
|
|
7820
|
+
` return '<a class="gem-card gem-recently-viewed__item" href="' + escapeHtml(url) + '"><strong>' + escapeHtml(label) + '</strong></a>';`,
|
|
7821
|
+
" };",
|
|
7822
|
+
" const current = document.querySelector('[data-gem-current-product], [data-gem-product-handle]');",
|
|
7823
|
+
" if (current) {",
|
|
7824
|
+
" const handle = current.getAttribute && current.getAttribute('data-gem-product-handle');",
|
|
7825
|
+
" const url = current.getAttribute && (current.getAttribute('data-gem-product-url') || current.getAttribute('href'));",
|
|
7826
|
+
" const title = current.getAttribute && current.getAttribute('data-gem-product-title');",
|
|
7827
|
+
" if (handle || url) {",
|
|
7828
|
+
" const nextItem = { handle, url, title, recordedAt: new Date().toISOString() };",
|
|
7829
|
+
" const items = [nextItem, ...readItems().filter((item) => item && item.handle !== handle && item.url !== url)];",
|
|
7830
|
+
" writeItems(items);",
|
|
7831
|
+
" api.emit('gem:recently-viewed:record', { source: 'gem-runtime', handle, url });",
|
|
7832
|
+
" }",
|
|
7833
|
+
" }",
|
|
7834
|
+
" const rails = document.querySelectorAll('[data-gem-recently-viewed], .gem-recently-viewed');",
|
|
7835
|
+
" let bound = 0;",
|
|
7836
|
+
" rails.forEach((rail) => {",
|
|
7837
|
+
" if (!mark(rail, 'recently-viewed')) return;",
|
|
7838
|
+
" const limit = Number(rail.getAttribute && (rail.getAttribute('data-gem-recently-viewed-limit') || '4'));",
|
|
7839
|
+
" const items = readItems().slice(0, Number.isFinite(limit) ? limit : 4);",
|
|
7840
|
+
" const list = rail.querySelector && (rail.querySelector('[data-gem-recently-viewed-list]') || rail);",
|
|
7841
|
+
" const empty = rail.querySelector && rail.querySelector('[data-gem-recently-viewed-empty]');",
|
|
7842
|
+
" rail.dataset.gemRecentlyViewedCount = String(items.length);",
|
|
7843
|
+
" if (items.length > 0) {",
|
|
7844
|
+
" rail.dataset.gemRecentlyViewedState = 'ready';",
|
|
7845
|
+
" if (list) {",
|
|
7846
|
+
" list.hidden = false;",
|
|
7847
|
+
" if (list.dataset) list.dataset.gemRecentlyViewedCount = String(items.length);",
|
|
7848
|
+
" if ('innerHTML' in list) list.innerHTML = items.map(renderItem).join('');",
|
|
7849
|
+
" else if ('textContent' in list) list.textContent = items.map((item) => item && (item.title || item.handle || item.url) || 'Recently viewed product').join(', ');",
|
|
7850
|
+
" }",
|
|
7851
|
+
" if (empty) empty.hidden = true;",
|
|
7852
|
+
" } else {",
|
|
7853
|
+
" rail.dataset.gemRecentlyViewedState = 'empty';",
|
|
7854
|
+
" if (empty) empty.hidden = false;",
|
|
7855
|
+
" }",
|
|
7856
|
+
" bound += 1;",
|
|
7857
|
+
" });",
|
|
7858
|
+
" api.recentlyViewed = { list: readItems };",
|
|
7859
|
+
" api.emit('gem:recently-viewed:ready', { source: 'gem-runtime', count: bound });",
|
|
7860
|
+
" });"
|
|
7861
|
+
];
|
|
7862
|
+
var runtimeSlotLoaderPluginRegistrationLines = [
|
|
7863
|
+
" registerPlugin('runtime-slot-loader', ({ api }) => {",
|
|
7864
|
+
" const slots = document.querySelectorAll('[data-gem-runtime-slot], [data-gem-footer-block-builder], .gem-navigation-shell__layout');",
|
|
7865
|
+
" let bound = 0;",
|
|
7866
|
+
" slots.forEach((slot, index) => {",
|
|
7867
|
+
" if (!mark(slot, 'runtime-slot-loader')) return;",
|
|
7868
|
+
" if (slot.dataset) {",
|
|
7869
|
+
" slot.dataset.gemRuntimeSlotState = 'ready';",
|
|
7870
|
+
" if (!slot.dataset.gemRuntimeSlotIndex) slot.dataset.gemRuntimeSlotIndex = String(index);",
|
|
7871
|
+
" }",
|
|
7872
|
+
" bound += 1;",
|
|
7873
|
+
" });",
|
|
7874
|
+
" api.emit('gem:slot:ready', { source: 'gem-runtime', count: bound });",
|
|
7875
|
+
" });"
|
|
7876
|
+
];
|
|
5615
7877
|
var offerEnginePluginRegistrationLines = [
|
|
5616
7878
|
" registerPlugin('offer-engine', ({ api }) => {",
|
|
5617
7879
|
" " + offerEngineRuntimeScript(),
|
|
@@ -5628,6 +7890,58 @@ var bundlePluginRegistrationLines = [
|
|
|
5628
7890
|
" " + bundleEngineRuntimeScript(),
|
|
5629
7891
|
" });"
|
|
5630
7892
|
];
|
|
7893
|
+
var sizeGuidePluginRegistrationLines = [
|
|
7894
|
+
" registerPlugin('size-guide', ({ api }) => {",
|
|
7895
|
+
" const page = document.documentElement && typeof document.documentElement.querySelectorAll === 'function' ? document.documentElement : document;",
|
|
7896
|
+
" const guides = Array.prototype.slice.call(page.querySelectorAll('[data-gem-size-guide]'));",
|
|
7897
|
+
" const opens = (target) => guides.some((guide) => !target || guide.getAttribute('data-gem-size-guide') === target || guide.id === target);",
|
|
7898
|
+
" let bound = 0;",
|
|
7899
|
+
" document.querySelectorAll('[data-gem-size-guide-trigger]').forEach((trigger) => {",
|
|
7900
|
+
" const target = trigger.getAttribute('data-gem-size-guide-trigger') || '';",
|
|
7901
|
+
" if (!opens(target)) { trigger.hidden = true; return; }",
|
|
7902
|
+
" if (!mark(trigger, 'size-guide')) return;",
|
|
7903
|
+
" trigger.hidden = false;",
|
|
7904
|
+
" if (!canListen(trigger)) return;",
|
|
7905
|
+
" trigger.addEventListener('click', (event) => {",
|
|
7906
|
+
" event.preventDefault && event.preventDefault();",
|
|
7907
|
+
" const holder = trigger.closest ? trigger.closest('[data-product-id]') : null;",
|
|
7908
|
+
" const detail = { source: 'gem-runtime', sectionId: target || null, productId: holder ? holder.getAttribute('data-product-id') : null };",
|
|
7909
|
+
" if (typeof window.CustomEvent === 'function') window.dispatchEvent(new window.CustomEvent('gem:size-guide:open', { detail }));",
|
|
7910
|
+
" api.emit('gem:size-guide:open', detail);",
|
|
7911
|
+
" });",
|
|
7912
|
+
" bound += 1;",
|
|
7913
|
+
" });",
|
|
7914
|
+
" api.emit('gem:size-guide:ready', { source: 'gem-runtime', count: bound, guides: guides.length });",
|
|
7915
|
+
" });"
|
|
7916
|
+
];
|
|
7917
|
+
var runtimePluginSourceLines = [
|
|
7918
|
+
["cart-drawer", cartDrawerPluginRegistrationLines],
|
|
7919
|
+
["mobile-menu", mobileMenuPluginRegistrationLines],
|
|
7920
|
+
["quantity-input", quantityInputPluginRegistrationLines],
|
|
7921
|
+
["variant-selects", variantSelectsPluginRegistrationLines],
|
|
7922
|
+
["predictive-search", predictiveSearchPluginRegistrationLines],
|
|
7923
|
+
["localization", localizationPluginRegistrationLines],
|
|
7924
|
+
["disclosure", disclosurePluginRegistrationLines],
|
|
7925
|
+
["tabs", tabsPluginRegistrationLines],
|
|
7926
|
+
["media-gallery", mediaGalleryPluginRegistrationLines],
|
|
7927
|
+
["carousel", carouselPluginRegistrationLines],
|
|
7928
|
+
["hero-slideshow", heroSlideshowPluginRegistrationLines],
|
|
7929
|
+
["quick-view", runtimeChunks["quick-view"].body],
|
|
7930
|
+
["promo-popup", promoPopupPluginRegistrationLines],
|
|
7931
|
+
["sticky-atc", stickyAtcPluginRegistrationLines],
|
|
7932
|
+
["before_after", beforeAfterPluginRegistrationLines],
|
|
7933
|
+
["countdown", countdownPluginRegistrationLines],
|
|
7934
|
+
["reveal-on-scroll", revealOnScrollPluginRegistrationLines],
|
|
7935
|
+
["img-in", imgInPluginRegistrationLines],
|
|
7936
|
+
["recently-viewed", recentlyViewedPluginRegistrationLines],
|
|
7937
|
+
["runtime-slot-loader", runtimeSlotLoaderPluginRegistrationLines],
|
|
7938
|
+
["offer-engine", offerEnginePluginRegistrationLines],
|
|
7939
|
+
["reviews", reviewsPluginRegistrationLines],
|
|
7940
|
+
["bundle", bundlePluginRegistrationLines],
|
|
7941
|
+
["size-guide", sizeGuidePluginRegistrationLines],
|
|
7942
|
+
["orbit", runtimeChunks.orbit.body],
|
|
7943
|
+
["rail", runtimeChunks.rail.body]
|
|
7944
|
+
];
|
|
5631
7945
|
|
|
5632
7946
|
// ../../packages/gem-framework/src/validation.ts
|
|
5633
7947
|
var validationRulesByCode = new Map(gemFrameworkValidationRules.map((rule) => [rule.code, rule]));
|
|
@@ -5639,7 +7953,7 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
|
5639
7953
|
const namespace = group === "typography" ? "font" : group === "spacing" ? "space" : group;
|
|
5640
7954
|
return keys.map((key) => `--gem-${namespace}-${slugify(key)}`);
|
|
5641
7955
|
}),
|
|
5642
|
-
// The governed type ramp (--gem-h1…h6, --gem-body, --gem-text-sm, --gem-type-base/-scale). The
|
|
7956
|
+
// The governed type ramp (--gem-h1…h6, --gem-body, --gem-text-sm, --gem-text-xs, --gem-type-base/-scale). The
|
|
5643
7957
|
// framework stylesheet declares it on every page, but it is not a token GROUP, so it was missing here
|
|
5644
7958
|
// and a section sizing its heading with `var(--gem-h2)` got `unknown_gem_css_variable`. Read from the
|
|
5645
7959
|
// function that emits it, so the ramp and this list cannot drift.
|
|
@@ -5658,7 +7972,12 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
|
5658
7972
|
// The row primitive's variables (primitives.ts GEM_ROW_CSS). Sections SET these on their grid.
|
|
5659
7973
|
...GEM_ROW_CSS_VARIABLES,
|
|
5660
7974
|
// The form set's variables (form-set.ts): set from a plan's `tokens.form`, read by fields under [data-gem-form-set].
|
|
5661
|
-
...GEM_FORM_CSS_VARIABLES
|
|
7975
|
+
...GEM_FORM_CSS_VARIABLES,
|
|
7976
|
+
// The motion engine's (motion-engine.ts, 0.19.0), all of them: a section copies the base CSS VERBATIM into its own CSS,
|
|
7977
|
+
// internals (--gem-orbit-a, --gem-rail-t) included, and sets --gem-orbit-i / -n, the settings and --gem-motion-tint.
|
|
7978
|
+
...new Set([GEM_ORBIT_BASE_CSS, GEM_RAIL_BASE_CSS, GEM_ORBIT_RUNTIME_CSS, GEM_RAIL_RUNTIME_CSS, GEM_QUICK_VIEW_CSS].join("\n").match(/--gem-[a-z0-9-]+/g) ?? []),
|
|
7979
|
+
// …and every setting an effect reads from a CSS variable (`css: true`): a section sets it in its stylesheet, per breakpoint.
|
|
7980
|
+
...gemMotionEffectList.flatMap((effect) => Object.entries(effect.settings).filter(([, setting]) => setting.css).map(([name]) => `--gem-${effect.key}-${name}`))
|
|
5662
7981
|
]);
|
|
5663
7982
|
var TOKENIZED_PROPERTIES = [
|
|
5664
7983
|
"background",
|
|
@@ -5766,14 +8085,22 @@ function validateGemFrameworkMarkup(markup, artifactPath, options = {}) {
|
|
|
5766
8085
|
}
|
|
5767
8086
|
return result(issues);
|
|
5768
8087
|
}
|
|
8088
|
+
var NAMESPACE_URL = String.raw`https?:\/\/www\.w3\.org\/[\w.\/#:~%+-]*`;
|
|
8089
|
+
var SHARE_NETWORK = "facebook|twitter|x|pinterest|whatsapp|threads";
|
|
8090
|
+
var AS_A_LINK = String.raw`(?<=(?:<a\s[^<>]{0,400}?(?<![\w-])href=\{?|[{,]\s{0,40}["']?(?:${SHARE_NETWORK})["']?\s{0,8}:\s{0,8})["'\x60])`;
|
|
8091
|
+
var SHARE_INTENT = String.raw`https?:\/\/(?:(?:www\.)?facebook\.com\/sharer\/sharer\.php|(?:twitter|x)\.com\/intent\/(?:tweet|post)|(?:www\.)?pinterest\.com\/pin\/create\/button\/|wa\.me\/\d*|(?:www\.)?threads\.(?:net|com)\/intent\/post)\?`;
|
|
8092
|
+
var NEVER_FETCHED_URL = new RegExp(`${NAMESPACE_URL}|${AS_A_LINK}${SHARE_INTENT}`, "gi");
|
|
8093
|
+
var JSON_LD = /<script\b[^>]*type=["']application\/ld\+json["'][^>]*>[\s\S]*?<\/script>/gi;
|
|
8094
|
+
function namesExternalDependency(withoutJsonLd) {
|
|
8095
|
+
return /https?:\/\/|\/\/cdn\./i.test(withoutJsonLd.replace(NEVER_FETCHED_URL, ""));
|
|
8096
|
+
}
|
|
5769
8097
|
function validateSharedSafety(content, artifactPath) {
|
|
5770
8098
|
const issues = [];
|
|
5771
|
-
const
|
|
5772
|
-
|
|
5773
|
-
if (/<script\b/i.test(contentWithoutJsonLd)) {
|
|
8099
|
+
const withoutJsonLd = content.replace(JSON_LD, "");
|
|
8100
|
+
if (/<script\b/i.test(withoutJsonLd)) {
|
|
5774
8101
|
issues.push(ruleIssue("inline_script", "Gem Framework artifacts must not include inline script tags.", artifactPath));
|
|
5775
8102
|
}
|
|
5776
|
-
if (
|
|
8103
|
+
if (namesExternalDependency(withoutJsonLd)) {
|
|
5777
8104
|
issues.push(ruleIssue("external_dependency", "Gem Framework artifacts must not include external runtime or CDN dependencies.", artifactPath));
|
|
5778
8105
|
}
|
|
5779
8106
|
return issues;
|
|
@@ -5802,6 +8129,9 @@ function gemFrameworkPublicCssVariables() {
|
|
|
5802
8129
|
String(createSectionCss()),
|
|
5803
8130
|
String(createShellCss()),
|
|
5804
8131
|
String(createGemSectionRuntimeContractCss()),
|
|
8132
|
+
// popup-shell.css (its own file since 0.8.3, so not in createPrimitiveCss): a popup section sets
|
|
8133
|
+
// --gem-popup-drawer-width (0.18.17, drawer-end) and must not be told the framework's own variable is unknown.
|
|
8134
|
+
GEM_POPUP_SHELL_CSS,
|
|
5805
8135
|
// The button case rule only exists when a token sets it; a section may still read its variables (a CTA rule
|
|
5806
8136
|
// beats the zero-specificity rule, so opting in means `text-transform: var(--gem-font-button-transform, none)`).
|
|
5807
8137
|
gemButtonCaseCss({ typography: { buttonTransform: "none", buttonLetterSpacing: "normal" } })
|
|
@@ -5812,11 +8142,13 @@ function gemFrameworkPublicCssVariables() {
|
|
|
5812
8142
|
}
|
|
5813
8143
|
var ROOT_SELECTOR = /^\s*(?::root|html|:host)(?:\[[^\]]*\]|:[a-z-]+(?:\([^()]*\))?)*\s*$/;
|
|
5814
8144
|
var EDITION_ROOT_SELECTOR = /^\s*:where\(\s*\[data-gem-edition=(["'])\d+\1\](?:\s*,\s*\[data-gem-edition=(["'])\d+\2\])*\s*\)\s*$/;
|
|
8145
|
+
var KNOB_SCOPE_SELECTOR = /^\s*\[data-gem-[a-z0-9-]+(?:=(["'])[^"']*\1)?\]\s*$/;
|
|
5815
8146
|
function gemPublicCssVariablesFrom(stylesheet) {
|
|
5816
8147
|
const css = stylesheet.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
5817
8148
|
const declaredOnRoot = /* @__PURE__ */ new Set();
|
|
5818
8149
|
const declaredElsewhere = /* @__PURE__ */ new Set();
|
|
5819
8150
|
for (const [, selector, body] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
8151
|
+
if (selector.split(",").every((part) => KNOB_SCOPE_SELECTOR.test(part))) continue;
|
|
5820
8152
|
const onRoot = EDITION_ROOT_SELECTOR.test(selector) || selector.split(",").every((part) => ROOT_SELECTOR.test(part));
|
|
5821
8153
|
for (const [, name] of body.matchAll(/(--gem-[a-z0-9-]+)\s*:/g)) (onRoot ? declaredOnRoot : declaredElsewhere).add(name);
|
|
5822
8154
|
}
|
|
@@ -6137,9 +8469,9 @@ var LABEL_LEAD = /^(?:(?:grand\s+|sub\s*)?total|subtotal|price|unless\s+noted|t
|
|
|
6137
8469
|
var QUESTION_END = /[??❓❔][\s\p{Extended_Pictographic}\uFE0F]*$/u;
|
|
6138
8470
|
var MENTIONS_SHIPPING = /ship|deliver|vận chuyển|giao hàng/iu;
|
|
6139
8471
|
var SHIPPING_ZERO_ENTRIES = Object.entries(SHIPPING_ZERO_SENTENCES);
|
|
6140
|
-
function sentencesOf(
|
|
8472
|
+
function sentencesOf(text2) {
|
|
6141
8473
|
const out = [];
|
|
6142
|
-
for (const line of
|
|
8474
|
+
for (const line of text2.split(/\n+|\s[|•·]\s|\s(?=[✓✔☑]\s)/u)) {
|
|
6143
8475
|
let cur = "";
|
|
6144
8476
|
for (const piece of line.split(/(?<=[.!?;])\s+/u)) {
|
|
6145
8477
|
cur = cur ? `${cur} ${piece}` : piece;
|
|
@@ -6154,8 +8486,8 @@ function sentencesOf(text) {
|
|
|
6154
8486
|
}
|
|
6155
8487
|
function shippingZeroPromiseIn(raw) {
|
|
6156
8488
|
if (!MENTIONS_SHIPPING.test(raw)) return null;
|
|
6157
|
-
const
|
|
6158
|
-
for (const sentence of sentencesOf(
|
|
8489
|
+
const text2 = raw.normalize("NFC").replace(/\\n/g, "\n").replace(/\\(['"’])/g, "$1");
|
|
8490
|
+
for (const sentence of sentencesOf(text2)) {
|
|
6159
8491
|
if (QUESTION_END.test(sentence)) continue;
|
|
6160
8492
|
const whole = sentence.replace(DECOR_START, "").replace(DECOR_END, "").trim();
|
|
6161
8493
|
const clauses = LABEL_LEAD.test(whole) ? [whole] : [whole, ...whole.split(/,\s+|\s+[—–]\s+/u)].map((c) => c.replace(DECOR_START, "").replace(DECOR_END, "").trim());
|
|
@@ -6209,26 +8541,26 @@ function demoValues(src) {
|
|
|
6209
8541
|
return out;
|
|
6210
8542
|
}
|
|
6211
8543
|
var PRICE_ANY = new RegExp(`[$\xA3\u20AC]\\s?\\d|\\b\\d[\\d.,]*\\s?(?:USD|EUR|GBP)\\b|${PROMISE_VI_PARTS.price}`, "iu");
|
|
6212
|
-
function isQuestion(
|
|
6213
|
-
const t =
|
|
8544
|
+
function isQuestion(text2) {
|
|
8545
|
+
const t = text2.trim();
|
|
6214
8546
|
if (!/\?$/.test(t) || /[.!?]\s+\S/.test(t)) return false;
|
|
6215
8547
|
return /^(?:can|do|does|did|how|what|when|where|why|who|which|is|are|will|would|should|could|may)\b/i.test(t) || /(?:không|chưa|nào|sao|gì|đâu|bao lâu|bao nhiêu|mấy|được không|à|hả|nhỉ)\s*\?$/iu.test(t) || /^(?:làm sao|tại sao|vì sao|khi nào|bao lâu|có)(?![\p{L}])/iu.test(t);
|
|
6216
8548
|
}
|
|
6217
8549
|
function claimIn(raw) {
|
|
6218
|
-
const
|
|
6219
|
-
if (CLAIM_TEXT.test(
|
|
6220
|
-
if (INVENTED_PERSON.test(`"${
|
|
6221
|
-
if (isQuestion(
|
|
6222
|
-
if (PRICE_K_VI.test(
|
|
6223
|
-
if (PROMISE.test(
|
|
8550
|
+
const text2 = raw.normalize("NFC");
|
|
8551
|
+
if (CLAIM_TEXT.test(text2) || CLAIM_TEXT_VI.test(text2)) return "hardcoded claim";
|
|
8552
|
+
if (INVENTED_PERSON.test(`"${text2}"`) || INVENTED_PERSON_VI.test(text2)) return "invented person";
|
|
8553
|
+
if (isQuestion(text2)) return PRICE_ANY.test(text2) || PRICE_K_VI.test(text2) ? "promise the merchant never made" : null;
|
|
8554
|
+
if (PRICE_K_VI.test(text2) && /^\s*\d[\d.,]*\s?k\s*\+?\s*$/.test(text2)) return "hardcoded figure";
|
|
8555
|
+
if (PROMISE.test(text2) || PROMISE_VI.test(text2) || shippingZeroPromiseIn(text2) || PRICE_K_VI.test(text2)) return "promise the merchant never made";
|
|
6224
8556
|
return null;
|
|
6225
8557
|
}
|
|
6226
8558
|
function demoInvents(value) {
|
|
6227
8559
|
const v = value.trim();
|
|
6228
8560
|
if (/^-?\d/.test(v)) return "number";
|
|
6229
8561
|
if (/^[{[]/.test(v) && /(?:^|[:[,])\s*-?\d/.test(v.slice(1))) return "number";
|
|
6230
|
-
for (const
|
|
6231
|
-
const kind = claimIn(
|
|
8562
|
+
for (const text2 of stringLiteralsIn(v)) {
|
|
8563
|
+
const kind = claimIn(text2);
|
|
6232
8564
|
if (kind) return kind;
|
|
6233
8565
|
}
|
|
6234
8566
|
return null;
|
|
@@ -6281,12 +8613,12 @@ function fabricatedProofInSource(raw) {
|
|
|
6281
8613
|
caught.add(`${at}\0key\0${key}`);
|
|
6282
8614
|
out.push({ line: at, why: `plan literal ${key}: ${num}`, text: line.trim() });
|
|
6283
8615
|
}
|
|
6284
|
-
for (const
|
|
6285
|
-
if (
|
|
6286
|
-
const kind = claimIn(
|
|
8616
|
+
for (const text2 of stringLiteralsIn(line)) {
|
|
8617
|
+
if (text2.includes("${")) continue;
|
|
8618
|
+
const kind = claimIn(text2);
|
|
6287
8619
|
if (!kind) continue;
|
|
6288
|
-
caught.add(`${at}\0str\0${
|
|
6289
|
-
out.push({ line: at, why: `${kind} ${JSON.stringify(
|
|
8620
|
+
caught.add(`${at}\0str\0${text2}`);
|
|
8621
|
+
out.push({ line: at, why: `${kind} ${JSON.stringify(text2)}`, text: line.trim() });
|
|
6290
8622
|
}
|
|
6291
8623
|
});
|
|
6292
8624
|
const code = src.replace(/(["'])((?:\\.|(?!\1)[^\\\n])*)\1/g, (_m, q, body) => q + body.replace(/[^\n]/g, " ") + q);
|
|
@@ -6295,7 +8627,7 @@ function fabricatedProofInSource(raw) {
|
|
|
6295
8627
|
out.push({ line: at, why: `verified by default (${m[0].replace(/\s+/g, " ").trim()}); badge only on verified === true`, text: (lines[at - 1] ?? "").trim() });
|
|
6296
8628
|
}
|
|
6297
8629
|
for (const r of demoReads) {
|
|
6298
|
-
const onDemoLine = caught.has(`${r.entry.line}\0key\0${r.key}`) || stringLiteralsIn(r.entry.value).some((
|
|
8630
|
+
const onDemoLine = caught.has(`${r.entry.line}\0key\0${r.key}`) || stringLiteralsIn(r.entry.value).some((text2) => caught.has(`${r.entry.line}\0str\0${text2}`));
|
|
6299
8631
|
if (!onDemoLine) out.push({ line: r.at, why: r.why, text: r.text });
|
|
6300
8632
|
}
|
|
6301
8633
|
return out.sort((a, b) => a.line - b.line);
|