@gemboss/gem-framework 0.18.16 → 0.20.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 +109 -1
- package/dist/SOURCE.json +3 -3
- package/dist/gem-framework.css +23 -1
- package/dist/gem-runtime.js +97 -54
- package/dist/index.js +2511 -44
- 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/delivery-estimate.d.ts +9 -0
- package/dist/plugins/delivery-estimate.js +1 -0
- 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/personalize.d.ts +9 -0
- package/dist/plugins/personalize.js +1 -0
- package/dist/plugins/predictive-search.js +1 -1
- package/dist/plugins/print-preview.d.ts +9 -0
- package/dist/plugins/print-preview.js +1 -0
- 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/pod/core.d.ts +26 -0
- package/dist/pod/core.js +1 -0
- package/dist/pod/delivery-estimate.d.ts +28 -0
- package/dist/pod/delivery-estimate.js +1 -0
- package/dist/pod/personalize.d.ts +25 -0
- package/dist/pod/personalize.js +1 -0
- package/dist/pod/print-preview.d.ts +8 -0
- package/dist/pod/print-preview.js +1 -0
- package/dist/pod-buybox.css +55 -0
- 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 +2522 -36
- package/dist/tokens.css +4 -0
- package/package.json +20 -1
package/dist/index.js
CHANGED
|
@@ -121,11 +121,11 @@ function color(value) {
|
|
|
121
121
|
function gemSectionDividerStyle(props, bandBackground) {
|
|
122
122
|
if (!gemSectionDividers(props).length) return {};
|
|
123
123
|
const value = props?.divider_flip;
|
|
124
|
-
const
|
|
124
|
+
const flip2 = value === true || value === "true" || value === 1 || value === "1";
|
|
125
125
|
return {
|
|
126
126
|
"--gem-element-divider-height": `${height(props?.divider_height) ?? 48}px`,
|
|
127
127
|
"--gem-element-divider-color": color(props?.divider_color) ?? color(bandBackground) ?? "var(--gem-section-bg, var(--gem-color-background))",
|
|
128
|
-
"--gem-element-divider-scale-x":
|
|
128
|
+
"--gem-element-divider-scale-x": flip2 ? "-1" : "1"
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
function gemSectionDividerResponsiveStyle(props) {
|
|
@@ -435,10 +435,15 @@ function gemButtonGradientEndCss(accent = "var(--gem-color-accent)", accentText
|
|
|
435
435
|
}
|
|
436
436
|
|
|
437
437
|
// ../../packages/gem-framework/src/tokens.ts
|
|
438
|
-
var gemFrameworkVersion = "0.
|
|
438
|
+
var gemFrameworkVersion = "0.20.0";
|
|
439
439
|
var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
|
|
440
440
|
var GEM_TAP_MIN_CSS_VAR = "--gem-tap-min: 44px;";
|
|
441
441
|
var GEM_FOCUS_OFFSET_CSS_VAR = "--gem-focus-offset: 3px;";
|
|
442
|
+
var GEM_STROKE_CSS_VARS = [
|
|
443
|
+
"--gem-stroke: calc(1px * var(--gem-style-border, 1));",
|
|
444
|
+
"--gem-stroke-thick: calc(2px * var(--gem-style-border, 1));"
|
|
445
|
+
];
|
|
446
|
+
var GEM_UNDERLINE_THICKNESS_CSS_VAR = "--gem-underline-thickness: 2px;";
|
|
442
447
|
var GEM_DERIVED_SPACE_STEPS = { "2xs": 0.125, ms: 0.75, "4xl": 7 };
|
|
443
448
|
var GEM_DERIVED_SPACE_CSS_VARIABLES = Object.keys(GEM_DERIVED_SPACE_STEPS).map((key) => `--gem-space-${slugify(key)}`);
|
|
444
449
|
function gemDerivedSpaceCssVars(spacing) {
|
|
@@ -583,15 +588,15 @@ function gemBandScheme(c, band) {
|
|
|
583
588
|
const away = mixHex(g, hexRelativeLuminance(i) > hexRelativeLuminance(g) ? "#000000" : "#ffffff", 0.08);
|
|
584
589
|
const surface = hexContrastRatio(i, toward) >= MIN_TEXT_CONTRAST ? toward : away;
|
|
585
590
|
const grounds = [g, surface];
|
|
586
|
-
const
|
|
591
|
+
const text2 = aaTextColor(i, grounds);
|
|
587
592
|
const accent = aaTextColor(c.accent, grounds);
|
|
588
593
|
return {
|
|
589
594
|
background: g,
|
|
590
595
|
surface,
|
|
591
|
-
text,
|
|
596
|
+
text: text2,
|
|
592
597
|
muted: aaTextColor(mixHex(i, g, 0.3), grounds),
|
|
593
598
|
accent,
|
|
594
|
-
accentText: aaLabelColor(c.accentText, accent, [g,
|
|
599
|
+
accentText: aaLabelColor(c.accentText, accent, [g, text2]),
|
|
595
600
|
border: mixHex(g, i, 0.22),
|
|
596
601
|
success: aaTextColor(c.success, grounds),
|
|
597
602
|
warning: aaTextColor(c.warning, grounds),
|
|
@@ -859,7 +864,7 @@ function createGemTokenCss(tokens, selector = ":root", options = {}) {
|
|
|
859
864
|
...phone.layout.section ? [`--gem-layout-section-mobile: ${phone.layout.section};`] : [],
|
|
860
865
|
...toCssVars("component", tokens.component),
|
|
861
866
|
...toCssVars("morphology", tokens.morphology),
|
|
862
|
-
// THE GOVERNED TYPE RAMP — --gem-h1..h6 / --gem-body / --gem-text-sm.
|
|
867
|
+
// THE GOVERNED TYPE RAMP — --gem-h1..h6 / --gem-body / --gem-text-sm / --gem-text-xs (26/10/2026).
|
|
863
868
|
// gemcapture maps every measured font-size onto these names via `font_token`
|
|
864
869
|
// (clone_kit._type_token), and the gem-framework contract documents them, but nothing in the
|
|
865
870
|
// live framework defined them: a rebuild that followed the contract styled its headings with
|
|
@@ -874,7 +879,9 @@ function createGemTokenCss(tokens, selector = ":root", options = {}) {
|
|
|
874
879
|
// type ramp and registered in validation.ts. Follows the accent, so presets retint it for free.
|
|
875
880
|
GEM_FOCUS_RING_CSS_VAR,
|
|
876
881
|
GEM_TAP_MIN_CSS_VAR,
|
|
877
|
-
GEM_FOCUS_OFFSET_CSS_VAR
|
|
882
|
+
GEM_FOCUS_OFFSET_CSS_VAR,
|
|
883
|
+
...GEM_STROKE_CSS_VARS,
|
|
884
|
+
GEM_UNDERLINE_THICKNESS_CSS_VAR
|
|
878
885
|
];
|
|
879
886
|
const formBlock = gemFormCssBlock(tokens.form);
|
|
880
887
|
const buttonCase = gemButtonCaseCss(tokens, selector);
|
|
@@ -922,6 +929,13 @@ function gemTypeRampCssVars(typography) {
|
|
|
922
929
|
return [
|
|
923
930
|
`--gem-type-base: ${typeRampValue(typography, "base") ?? "1rem"};`,
|
|
924
931
|
`--gem-type-scale: ${typeRampValue(typography, "scale") ?? "1.22"};`,
|
|
932
|
+
// 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
|
|
933
|
+
// 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.
|
|
934
|
+
// Same pattern as text-sm, off the base rather than off text-sm: base ÷ scale², 10.75px at 16px and 1.22. Declared
|
|
935
|
+
// FIRST because this list is the ramp's order, small to large, and the seam's `steps` must equal it in order
|
|
936
|
+
// (check-token-steps-match-framework.ts). At the defaults it sits above gem_font_size's 10px floor; that floor bounds
|
|
937
|
+
// the px number only, so a plan with a steeper ratio (1.333: 9px) can take this step lower, as it can text-sm.
|
|
938
|
+
"--gem-text-xs: calc(var(--gem-type-base) / (var(--gem-type-scale) * var(--gem-type-scale)));",
|
|
925
939
|
"--gem-text-sm: calc(var(--gem-type-base) / var(--gem-type-scale));",
|
|
926
940
|
"--gem-body: var(--gem-type-base);",
|
|
927
941
|
"--gem-h6: calc(var(--gem-type-base) * var(--gem-type-scale));",
|
|
@@ -1050,6 +1064,2179 @@ function gemFrameworkEditionCss() {
|
|
|
1050
1064
|
return GEM_FRAMEWORK_EDITIONS.flatMap((e) => e.css.map((rule) => rule.replaceAll("&", gemFrameworkEditionScope(e.edition)))).join("\n");
|
|
1051
1065
|
}
|
|
1052
1066
|
|
|
1067
|
+
// ../../packages/gem-framework/src/motion/core.js
|
|
1068
|
+
var clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
|
|
1069
|
+
var lerp = (a, b, t) => a + (b - a) * t;
|
|
1070
|
+
var mod = (v, n) => (v % n + n) % n;
|
|
1071
|
+
function smoothstep(edge0, edge1, v) {
|
|
1072
|
+
const t = clamp((v - edge0) / (edge1 - edge0), 0, 1);
|
|
1073
|
+
return t * t * (3 - 2 * t);
|
|
1074
|
+
}
|
|
1075
|
+
var follow = (dt, tau) => tau <= 0 ? 1 : 1 - Math.exp(-dt / tau);
|
|
1076
|
+
function spring(s, target2, dt, stiffness, damping) {
|
|
1077
|
+
const f = dt / 16.67;
|
|
1078
|
+
s.v += (-(s.x - target2) * stiffness - s.v * damping) * f;
|
|
1079
|
+
s.x += s.v * f;
|
|
1080
|
+
return s;
|
|
1081
|
+
}
|
|
1082
|
+
function ringDelta(a, b, n) {
|
|
1083
|
+
let d = mod(b - a, n);
|
|
1084
|
+
if (d > n / 2) d -= n;
|
|
1085
|
+
return d;
|
|
1086
|
+
}
|
|
1087
|
+
function ringPose(i, n, pos, o) {
|
|
1088
|
+
const theta = ringDelta(pos, i, n) * o.spread;
|
|
1089
|
+
const hidden = Math.abs(theta) > Math.PI;
|
|
1090
|
+
const depth = (1 - Math.cos(theta)) / 2;
|
|
1091
|
+
return {
|
|
1092
|
+
x: o.rx * Math.sin(theta),
|
|
1093
|
+
y: -o.ry * depth,
|
|
1094
|
+
depth,
|
|
1095
|
+
scale: lerp(1, o.back, depth),
|
|
1096
|
+
opacity: hidden ? 0 : lerp(1, o.fade, depth),
|
|
1097
|
+
blur: o.blur * depth,
|
|
1098
|
+
z: Math.round((1 - depth) * 100),
|
|
1099
|
+
lean: Math.sin(theta) * o.lean,
|
|
1100
|
+
hidden
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
var throwTarget = (pos, velocity, carry, maxSlots) => Math.round(pos + clamp(velocity * carry, -maxSlots, maxSlots));
|
|
1104
|
+
var scrollProgress = (rect, height2) => clamp((height2 - rect.top) / (height2 + rect.height), 0, 1);
|
|
1105
|
+
var TIERS = ["static", "light", "full"];
|
|
1106
|
+
var query = (win, q) => Boolean(win && typeof win.matchMedia === "function" && win.matchMedia(q).matches);
|
|
1107
|
+
var reducedMotion = (win) => query(win, "(prefers-reduced-motion: reduce)");
|
|
1108
|
+
function capabilityTier(win) {
|
|
1109
|
+
if (reducedMotion(win)) return "static";
|
|
1110
|
+
const nav = win && win.navigator || {};
|
|
1111
|
+
const lean = nav.connection && nav.connection.saveData || nav.deviceMemory && nav.deviceMemory <= 2 || query(win, "(pointer: coarse)");
|
|
1112
|
+
return lean ? "light" : "full";
|
|
1113
|
+
}
|
|
1114
|
+
var written = /* @__PURE__ */ new WeakMap();
|
|
1115
|
+
function setStyle(el, prop, value) {
|
|
1116
|
+
let rec = written.get(el);
|
|
1117
|
+
if (!rec) written.set(el, rec = {});
|
|
1118
|
+
if (rec[prop] === value) return;
|
|
1119
|
+
rec[prop] = value;
|
|
1120
|
+
if (value === "") el.style.removeProperty(prop);
|
|
1121
|
+
else el.style.setProperty(prop, value);
|
|
1122
|
+
}
|
|
1123
|
+
var effects = /* @__PURE__ */ new Map();
|
|
1124
|
+
var SETTING_TYPES = ["number", "integer", "boolean", "enum", "text", "json"];
|
|
1125
|
+
function defineEffect(spec) {
|
|
1126
|
+
const effect = { hook: `[data-gem-${spec.key}]`, load: "viewport", renderer: "dom", tier: "static", fallback: "markup", settings: {}, ...spec, declared: spec };
|
|
1127
|
+
effects.set(effect.key, effect);
|
|
1128
|
+
return effect;
|
|
1129
|
+
}
|
|
1130
|
+
function checkEffect(effect) {
|
|
1131
|
+
const spec = effect.declared || effect;
|
|
1132
|
+
const key = String(spec.key);
|
|
1133
|
+
const problems = [];
|
|
1134
|
+
if (!/^[a-z][a-z0-9-]*$/.test(key)) problems.push(`key "${key}" is not kebab-case`);
|
|
1135
|
+
if (typeof spec.mount !== "function") problems.push(`${key} has no mount()`);
|
|
1136
|
+
if (spec.tier && !TIERS.includes(spec.tier)) problems.push(`${key}: tier must be one of ${TIERS.join(", ")}`);
|
|
1137
|
+
if ((spec.tier && spec.tier !== "static" || spec.renderer && spec.renderer !== "dom") && !["markup", "poster"].includes(spec.fallback)) {
|
|
1138
|
+
problems.push(`${key} is heavy and must declare a static fallback (markup or poster)`);
|
|
1139
|
+
}
|
|
1140
|
+
for (const [name, s] of Object.entries(spec.settings || {})) {
|
|
1141
|
+
if (!SETTING_TYPES.includes(s.type) || !("default" in s) || !s.label) problems.push(`${key}.${name} needs a type, a default and a label`);
|
|
1142
|
+
else if ((s.type === "number" || s.type === "integer") && (s.min != null && s.default < s.min || s.max != null && s.default > s.max)) problems.push(`${key}.${name}: default out of range`);
|
|
1143
|
+
else if (s.type === "enum" && !(s.options || []).includes(s.default)) problems.push(`${key}.${name}: default is not an option`);
|
|
1144
|
+
}
|
|
1145
|
+
return problems;
|
|
1146
|
+
}
|
|
1147
|
+
function frameTask(ctx, el, step, options) {
|
|
1148
|
+
const task = ctx.scheduler.add(step, options);
|
|
1149
|
+
ctx.cleanup(() => task.remove());
|
|
1150
|
+
observeVisibility(ctx, el, (shown) => task.visible(shown));
|
|
1151
|
+
return task;
|
|
1152
|
+
}
|
|
1153
|
+
function observeVisibility(ctx, el, cb, margin) {
|
|
1154
|
+
const IO = ctx.window.IntersectionObserver;
|
|
1155
|
+
if (typeof IO !== "function") return;
|
|
1156
|
+
new IO((entries) => cb(entries.some((e) => e.isIntersecting)), { rootMargin: margin || "120px" }).observe(el);
|
|
1157
|
+
}
|
|
1158
|
+
function observeSize(ctx, el, cb) {
|
|
1159
|
+
const RO = ctx.window.ResizeObserver;
|
|
1160
|
+
if (typeof RO === "function") new RO(() => cb()).observe(el);
|
|
1161
|
+
else ctx.on(ctx.window, "resize", () => cb());
|
|
1162
|
+
}
|
|
1163
|
+
function horizontalDrag(ctx, el, h) {
|
|
1164
|
+
let id = null;
|
|
1165
|
+
let x0 = 0;
|
|
1166
|
+
let y0 = 0;
|
|
1167
|
+
let decided = false;
|
|
1168
|
+
let horizontal = false;
|
|
1169
|
+
let moved = 0;
|
|
1170
|
+
let samples = [];
|
|
1171
|
+
const release = (e, fire) => {
|
|
1172
|
+
if (e.pointerId !== id) return;
|
|
1173
|
+
id = null;
|
|
1174
|
+
const first = samples[0];
|
|
1175
|
+
const last = samples[samples.length - 1];
|
|
1176
|
+
const span = last.t - first.t;
|
|
1177
|
+
const vx = span > 0 && e.timeStamp - last.t < 80 ? (last.x - first.x) / span : 0;
|
|
1178
|
+
if (fire && horizontal) h.end(vx, moved, e);
|
|
1179
|
+
else h.end(0, 0, e);
|
|
1180
|
+
};
|
|
1181
|
+
ctx.on(el, "pointerdown", (e) => {
|
|
1182
|
+
if (e.button !== 0 || id !== null) return;
|
|
1183
|
+
id = e.pointerId;
|
|
1184
|
+
x0 = e.clientX;
|
|
1185
|
+
y0 = e.clientY;
|
|
1186
|
+
decided = horizontal = false;
|
|
1187
|
+
moved = 0;
|
|
1188
|
+
samples = [{ x: e.clientX, t: e.timeStamp }];
|
|
1189
|
+
h.start(e);
|
|
1190
|
+
});
|
|
1191
|
+
ctx.on(el, "pointermove", (e) => {
|
|
1192
|
+
if (e.pointerId !== id) return;
|
|
1193
|
+
const dx = e.clientX - x0;
|
|
1194
|
+
if (!decided && Math.hypot(dx, e.clientY - y0) > 6) {
|
|
1195
|
+
decided = true;
|
|
1196
|
+
horizontal = Math.abs(dx) > Math.abs(e.clientY - y0);
|
|
1197
|
+
if (!horizontal) return release(e, false);
|
|
1198
|
+
try {
|
|
1199
|
+
el.setPointerCapture(e.pointerId);
|
|
1200
|
+
} catch {
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (!horizontal) return;
|
|
1204
|
+
moved = Math.max(moved, Math.abs(dx));
|
|
1205
|
+
samples.push({ x: e.clientX, t: e.timeStamp });
|
|
1206
|
+
while (samples.length > 2 && e.timeStamp - samples[0].t > 90) samples.shift();
|
|
1207
|
+
h.move(dx, e);
|
|
1208
|
+
});
|
|
1209
|
+
ctx.on(el, "pointerup", (e) => release(e, true));
|
|
1210
|
+
ctx.on(el, "pointercancel", (e) => release(e, true));
|
|
1211
|
+
}
|
|
1212
|
+
function wheelSteps(ctx, el, onStep) {
|
|
1213
|
+
let acc = 0;
|
|
1214
|
+
let lock = 0;
|
|
1215
|
+
ctx.on(
|
|
1216
|
+
el,
|
|
1217
|
+
"wheel",
|
|
1218
|
+
(e) => {
|
|
1219
|
+
if (Math.abs(e.deltaX) <= Math.abs(e.deltaY) || e.target.closest && e.target.closest("dialog")) return;
|
|
1220
|
+
e.preventDefault();
|
|
1221
|
+
acc += e.deltaX;
|
|
1222
|
+
if (e.timeStamp < lock || Math.abs(acc) < 24) return;
|
|
1223
|
+
onStep(Math.sign(acc));
|
|
1224
|
+
acc = 0;
|
|
1225
|
+
lock = e.timeStamp + 280;
|
|
1226
|
+
},
|
|
1227
|
+
{ passive: false }
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
function arrowKeys(ctx, el, { step, activate, accept }) {
|
|
1231
|
+
ctx.on(el, "keydown", (e) => {
|
|
1232
|
+
if (accept && !accept(e)) return;
|
|
1233
|
+
if (e.key === "ArrowRight" || e.key === "ArrowLeft") step(e.key === "ArrowRight" ? 1 : -1);
|
|
1234
|
+
else if (!activate || e.key !== "Enter" && e.key !== " " || activate(e) === false) return;
|
|
1235
|
+
e.preventDefault();
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
function flip(el, from, options) {
|
|
1239
|
+
if (!from || !from.width || typeof el.animate !== "function") return null;
|
|
1240
|
+
const box = el.getBoundingClientRect();
|
|
1241
|
+
if (!box.height) return null;
|
|
1242
|
+
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})` };
|
|
1243
|
+
const home = { transform: "none" };
|
|
1244
|
+
return el.animate(options.back ? [home, away] : [away, home], { duration: options.duration, easing: options.easing, fill: "both" });
|
|
1245
|
+
}
|
|
1246
|
+
function loadImage(doc, image) {
|
|
1247
|
+
const img = doc.createElement("img");
|
|
1248
|
+
if (image.srcset) img.srcset = image.srcset;
|
|
1249
|
+
img.src = image.src;
|
|
1250
|
+
return typeof img.decode === "function" ? img.decode().then(() => img, () => img) : Promise.resolve(img);
|
|
1251
|
+
}
|
|
1252
|
+
function putImage(img, image) {
|
|
1253
|
+
if (!img || !image || !image.src || img.getAttribute("src") === image.src) return;
|
|
1254
|
+
if (image.srcset) img.setAttribute("srcset", image.srcset);
|
|
1255
|
+
else img.removeAttribute("srcset");
|
|
1256
|
+
img.setAttribute("src", image.src);
|
|
1257
|
+
}
|
|
1258
|
+
function swapText(el, text2, still) {
|
|
1259
|
+
if (!el || el.textContent === text2) return;
|
|
1260
|
+
if (still || typeof el.animate !== "function") {
|
|
1261
|
+
el.textContent = text2;
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
const out = el.animate([{ opacity: 1 }, { opacity: 0, transform: "translateY(-6px)" }], { duration: 140, fill: "forwards" });
|
|
1265
|
+
out.onfinish = () => {
|
|
1266
|
+
el.textContent = text2;
|
|
1267
|
+
out.cancel();
|
|
1268
|
+
el.animate([{ opacity: 0, transform: "translateY(8px)" }, { opacity: 1, transform: "none" }], { duration: 220, easing: "cubic-bezier(.2,.7,.2,1)" });
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
function readProduct(el) {
|
|
1272
|
+
try {
|
|
1273
|
+
const p = JSON.parse(el.getAttribute("data-gem-product") || "null");
|
|
1274
|
+
return p && p.image && p.image.src ? p : null;
|
|
1275
|
+
} catch {
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
function formatMoney(cents, format) {
|
|
1280
|
+
const value = (Number(cents) || 0) / 100;
|
|
1281
|
+
const group = (str, sep) => str.replace(/\B(?=(\d{3})+(?!\d))/g, sep);
|
|
1282
|
+
const [int, dec] = value.toFixed(2).split(".");
|
|
1283
|
+
return (format || "${{amount}}").replace(/\{\{\s*(\w+)\s*\}\}/, (_, key) => {
|
|
1284
|
+
if (key === "amount_no_decimals") return group(String(Math.round(value)), ",");
|
|
1285
|
+
if (key === "amount_with_comma_separator") return `${group(int, ".")},${dec}`;
|
|
1286
|
+
if (key === "amount_no_decimals_with_comma_separator") return group(String(Math.round(value)), ".");
|
|
1287
|
+
return `${group(int, ",")}.${dec}`;
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
function priceText(p, el, which) {
|
|
1291
|
+
const compare = which === "compare";
|
|
1292
|
+
const words = compare ? p.compare_at_text : p.price_text;
|
|
1293
|
+
if (words != null) return String(words);
|
|
1294
|
+
const cents = compare ? p.compare_at : p.price;
|
|
1295
|
+
if (cents == null || cents === "") return "";
|
|
1296
|
+
if (typeof cents !== "number") return String(cents);
|
|
1297
|
+
const holder = el && el.closest ? el.closest("[data-gem-money-format]") : null;
|
|
1298
|
+
return formatMoney(cents, holder ? holder.getAttribute("data-gem-money-format") : void 0);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// ../../packages/gem-framework/src/motion/orbit.js
|
|
1302
|
+
var pad = (v) => String(v).padStart(2, "0");
|
|
1303
|
+
function mountOrbit(root, ctx) {
|
|
1304
|
+
const items = [];
|
|
1305
|
+
const products = [];
|
|
1306
|
+
const hits = [];
|
|
1307
|
+
const imgs = [];
|
|
1308
|
+
root.querySelectorAll("[data-gem-orbit-item]").forEach((item) => {
|
|
1309
|
+
const product = readProduct(item);
|
|
1310
|
+
if (!product) return;
|
|
1311
|
+
items.push(item);
|
|
1312
|
+
products.push(product);
|
|
1313
|
+
hits.push(item.querySelector(".gem-orbit__hit") || item);
|
|
1314
|
+
imgs.push(item.querySelector("img"));
|
|
1315
|
+
});
|
|
1316
|
+
const n = items.length;
|
|
1317
|
+
if (!n) return false;
|
|
1318
|
+
const part = (name) => root.querySelector(".gem-orbit__" + name);
|
|
1319
|
+
const stage = part("stage") || root;
|
|
1320
|
+
const track = part("track");
|
|
1321
|
+
const style = ctx.renderer.style;
|
|
1322
|
+
let s = ctx.settings;
|
|
1323
|
+
const start = Math.min(n - 1, s.start);
|
|
1324
|
+
let pos = start;
|
|
1325
|
+
let target2 = start;
|
|
1326
|
+
let dragFrom = 0;
|
|
1327
|
+
let dragging = false;
|
|
1328
|
+
let rendered = false;
|
|
1329
|
+
let front = -1;
|
|
1330
|
+
let ring = null;
|
|
1331
|
+
let slotPx = 60;
|
|
1332
|
+
const full = () => ctx.tier() === "full";
|
|
1333
|
+
const layout = () => {
|
|
1334
|
+
s = ctx.reread();
|
|
1335
|
+
const w = stage.clientWidth;
|
|
1336
|
+
ring = {
|
|
1337
|
+
rx: w * s.rx,
|
|
1338
|
+
ry: stage.clientHeight * s.tilt,
|
|
1339
|
+
back: s.back,
|
|
1340
|
+
fade: s.fade,
|
|
1341
|
+
blur: full() ? s.blur : 0,
|
|
1342
|
+
lean: s.lean,
|
|
1343
|
+
spread: (s.spread || 360 / n) * Math.PI / 180
|
|
1344
|
+
};
|
|
1345
|
+
slotPx = Math.max(60, ring.rx * ring.spread / 1.2);
|
|
1346
|
+
};
|
|
1347
|
+
const setTint = (tint) => {
|
|
1348
|
+
if (!s.tint) return;
|
|
1349
|
+
if (tint) root.style.setProperty("--gem-motion-tint", tint);
|
|
1350
|
+
else root.style.removeProperty("--gem-motion-tint");
|
|
1351
|
+
};
|
|
1352
|
+
const frontIndex = () => mod(Math.round(pos), n);
|
|
1353
|
+
const indexOf = (el) => items.findIndex((item) => item === el || item.contains(el));
|
|
1354
|
+
const updateFront = (initial) => {
|
|
1355
|
+
const i = frontIndex();
|
|
1356
|
+
if (i === front) return;
|
|
1357
|
+
front = i;
|
|
1358
|
+
const p = products[i];
|
|
1359
|
+
items.forEach((el, k) => {
|
|
1360
|
+
if (k === i) el.setAttribute("aria-current", "true");
|
|
1361
|
+
else el.removeAttribute("aria-current");
|
|
1362
|
+
hits[k].tabIndex = k === i ? 0 : -1;
|
|
1363
|
+
});
|
|
1364
|
+
if (!initial) {
|
|
1365
|
+
const still = ctx.tier() === "static";
|
|
1366
|
+
const price = priceText(p, root);
|
|
1367
|
+
swapText(part("name"), p.title || "", still);
|
|
1368
|
+
swapText(part("price"), price, still);
|
|
1369
|
+
const count = part("count");
|
|
1370
|
+
if (count) count.textContent = pad(i + 1) + " / " + pad(n);
|
|
1371
|
+
const bar = part("bar");
|
|
1372
|
+
if (bar) bar.style.transform = "scaleX(" + (i + 1) / n + ")";
|
|
1373
|
+
const live = part("live");
|
|
1374
|
+
const words = { index: i + 1, count: n, title: p.title || "", price };
|
|
1375
|
+
if (live) live.textContent = s.live.replace(/\{(\w+)\}/g, (m, k) => Object.hasOwn(words, k) ? String(words[k]) : m);
|
|
1376
|
+
setTint(p.tint);
|
|
1377
|
+
}
|
|
1378
|
+
ctx.emit("gem:motion:front", { index: i, product: p });
|
|
1379
|
+
};
|
|
1380
|
+
const frame = (dt, time) => {
|
|
1381
|
+
let moving = dragging;
|
|
1382
|
+
if (!dragging) {
|
|
1383
|
+
const d = target2 - pos;
|
|
1384
|
+
if (ctx.tier() === "static" || Math.abs(d) < 8e-4) pos = target2;
|
|
1385
|
+
else {
|
|
1386
|
+
pos += d * follow(dt, s.ease);
|
|
1387
|
+
moving = true;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const drift = s.drift && full();
|
|
1391
|
+
for (let i = 0; i < n; i++) {
|
|
1392
|
+
const p = ringPose(i, n, pos, ring);
|
|
1393
|
+
const bob = drift ? Math.sin(time / 1100 + i * 1.9) * 5 * (1 - p.depth) : 0;
|
|
1394
|
+
const el = items[i];
|
|
1395
|
+
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)`);
|
|
1396
|
+
style(el, "opacity", p.opacity.toFixed(3));
|
|
1397
|
+
style(el, "z-index", String(p.z));
|
|
1398
|
+
const blur = Math.round(p.blur * 2) / 2;
|
|
1399
|
+
style(el, "filter", blur > 0 ? `blur(${blur}px)` : "none");
|
|
1400
|
+
style(el, "visibility", p.hidden ? "hidden" : "visible");
|
|
1401
|
+
}
|
|
1402
|
+
rendered = true;
|
|
1403
|
+
updateFront(false);
|
|
1404
|
+
return moving || drift;
|
|
1405
|
+
};
|
|
1406
|
+
const refresh = () => {
|
|
1407
|
+
layout();
|
|
1408
|
+
if (rendered) task.kick();
|
|
1409
|
+
};
|
|
1410
|
+
const task = frameTask(ctx, root, frame, { onDegrade: refresh });
|
|
1411
|
+
const goTo = (index) => {
|
|
1412
|
+
const from = Math.round(target2);
|
|
1413
|
+
target2 = from + ringDelta(from, mod(index, n), n);
|
|
1414
|
+
task.kick();
|
|
1415
|
+
};
|
|
1416
|
+
hits.forEach((hit, i) => {
|
|
1417
|
+
ctx.on(hit, "click", (event) => {
|
|
1418
|
+
if (i === frontIndex()) return;
|
|
1419
|
+
event.preventDefault();
|
|
1420
|
+
goTo(i);
|
|
1421
|
+
});
|
|
1422
|
+
});
|
|
1423
|
+
root.querySelectorAll("[data-gem-orbit-step]").forEach((button) => {
|
|
1424
|
+
ctx.on(button, "click", () => goTo(target2 + (Number(button.getAttribute("data-gem-orbit-step")) || 0)));
|
|
1425
|
+
});
|
|
1426
|
+
if (track) {
|
|
1427
|
+
ctx.on(track, "click", (event) => {
|
|
1428
|
+
const r = track.getBoundingClientRect();
|
|
1429
|
+
if (r.width) goTo(Math.round((event.clientX - r.left) / r.width * (n - 1)));
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
horizontalDrag(ctx, stage, {
|
|
1433
|
+
start: () => {
|
|
1434
|
+
dragFrom = pos;
|
|
1435
|
+
},
|
|
1436
|
+
move: (dx) => {
|
|
1437
|
+
dragging = true;
|
|
1438
|
+
root.setAttribute("data-gem-state", "dragging");
|
|
1439
|
+
pos = dragFrom - dx / slotPx;
|
|
1440
|
+
task.kick();
|
|
1441
|
+
},
|
|
1442
|
+
end: (vx, moved) => {
|
|
1443
|
+
if (!dragging) return;
|
|
1444
|
+
dragging = false;
|
|
1445
|
+
root.setAttribute("data-gem-state", "ready");
|
|
1446
|
+
target2 = moved < 4 ? Math.round(pos) : throwTarget(pos, -vx / slotPx, s.carry, Math.min(3, Math.ceil(n / 2)));
|
|
1447
|
+
task.kick();
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
wheelSteps(ctx, root, (d) => goTo(target2 + d));
|
|
1451
|
+
arrowKeys(ctx, root, {
|
|
1452
|
+
accept: (e) => e.target === root || hits.includes(e.target),
|
|
1453
|
+
step: (d) => goTo(target2 + d),
|
|
1454
|
+
activate: (e) => e.target === root ? hits[frontIndex()].click() : false
|
|
1455
|
+
});
|
|
1456
|
+
layout();
|
|
1457
|
+
observeSize(ctx, root, refresh);
|
|
1458
|
+
root.gemQuickViewHost = {
|
|
1459
|
+
key: "orbit",
|
|
1460
|
+
products,
|
|
1461
|
+
indexOf,
|
|
1462
|
+
opens: (el) => indexOf(el) === frontIndex(),
|
|
1463
|
+
imageFor: (i) => imgs[i] || null,
|
|
1464
|
+
triggerFor: (i) => hits[i] || null,
|
|
1465
|
+
goTo,
|
|
1466
|
+
setOpen: (open) => root.setAttribute("data-gem-state", open ? "open" : "ready"),
|
|
1467
|
+
setImage: (i, image) => putImage(imgs[i], image),
|
|
1468
|
+
setTint
|
|
1469
|
+
};
|
|
1470
|
+
root.setAttribute("data-gem-quick-view-host", "orbit");
|
|
1471
|
+
root.setAttribute("data-gem-state", "ready");
|
|
1472
|
+
updateFront(true);
|
|
1473
|
+
if (s.drift && full()) task.kick();
|
|
1474
|
+
return () => {
|
|
1475
|
+
delete root.gemQuickViewHost;
|
|
1476
|
+
root.removeAttribute("data-gem-quick-view-host");
|
|
1477
|
+
root.removeAttribute("data-gem-state");
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
var orbitEffect = defineEffect({
|
|
1481
|
+
key: "orbit",
|
|
1482
|
+
hook: "[data-gem-orbit]",
|
|
1483
|
+
load: "viewport",
|
|
1484
|
+
renderer: "dom",
|
|
1485
|
+
tier: "static",
|
|
1486
|
+
fallback: "markup",
|
|
1487
|
+
budget: { gzip: 6144 },
|
|
1488
|
+
settings: {
|
|
1489
|
+
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)" },
|
|
1490
|
+
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)" },
|
|
1491
|
+
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)" },
|
|
1492
|
+
back: { type: "number", min: 0.1, max: 1, step: 0.01, default: 0.42, css: true, label: "Scale at the back" },
|
|
1493
|
+
fade: { type: "number", min: 0, max: 1, step: 0.01, default: 0.35, css: true, label: "Opacity at the back" },
|
|
1494
|
+
blur: { type: "number", min: 0, max: 8, step: 0.5, default: 2.5, css: true, label: "Blur at the back (px)" },
|
|
1495
|
+
lean: { type: "number", min: 0, max: 20, step: 1, default: 5, css: true, label: "Lean at the sides (degrees)" },
|
|
1496
|
+
spread: { type: "number", min: 0, max: 180, step: 1, default: 0, css: true, label: "Degrees between pieces (0: round the full ring)" },
|
|
1497
|
+
start: { type: "integer", min: 0, default: 0, css: true, label: "Piece in front at first" },
|
|
1498
|
+
ease: { type: "number", min: 40, max: 600, step: 10, default: 140, label: "Settle time (ms)" },
|
|
1499
|
+
carry: { type: "number", min: 0, max: 800, step: 10, default: 240, label: "How far a throw carries (ms)" },
|
|
1500
|
+
tint: { type: "boolean", default: true, label: "Tint the band with the front piece's colour" },
|
|
1501
|
+
drift: { type: "boolean", default: false, label: "Idle float (full tier only)" },
|
|
1502
|
+
live: { type: "text", default: "{index} / {count}: {title}, {price}", label: "What a screen reader hears when the front piece changes" }
|
|
1503
|
+
},
|
|
1504
|
+
mount: mountOrbit
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
// ../../packages/gem-framework/src/motion/rail.js
|
|
1508
|
+
var COMPACT_BELOW = 700;
|
|
1509
|
+
var ROD_TOP = 14;
|
|
1510
|
+
function createTurn(el) {
|
|
1511
|
+
if (!el) return null;
|
|
1512
|
+
const side = el.querySelector(".gem-turn__side");
|
|
1513
|
+
const front = el.querySelector(".gem-turn__front");
|
|
1514
|
+
const sheets = el.querySelectorAll(".gem-turn__sheet");
|
|
1515
|
+
const frames = Math.floor(Number(el.getAttribute("data-gem-turn-frames"))) || 0;
|
|
1516
|
+
const cols = Math.floor(Number(el.getAttribute("data-gem-turn-cols"))) || 0;
|
|
1517
|
+
const mode = sheets.length > 1 && frames > 1 && cols > 0 ? "sprite" : side && front ? "pair" : front ? "flip" : null;
|
|
1518
|
+
if (!mode) return null;
|
|
1519
|
+
const rows = Math.ceil(frames / Math.max(1, cols)) || 1;
|
|
1520
|
+
const place = (sheet, f) => setStyle(sheet, "transform", `translate(${(-(f % cols) * 100 / cols).toFixed(3)}%, ${(-Math.floor(f / cols) * 100 / rows).toFixed(3)}%)`);
|
|
1521
|
+
let last = -1;
|
|
1522
|
+
return {
|
|
1523
|
+
mode,
|
|
1524
|
+
/** what the quick view flies out of */
|
|
1525
|
+
image: mode === "sprite" ? el : front,
|
|
1526
|
+
set(t) {
|
|
1527
|
+
const q = Math.round(t * 1e3) / 1e3;
|
|
1528
|
+
if (q === last) return;
|
|
1529
|
+
last = q;
|
|
1530
|
+
if (mode === "pair") {
|
|
1531
|
+
setStyle(side, "transform", `perspective(900px) rotateY(${(-82 * q).toFixed(1)}deg)`);
|
|
1532
|
+
setStyle(side, "opacity", (1 - smoothstep(0.42, 0.78, q)).toFixed(3));
|
|
1533
|
+
setStyle(front, "opacity", smoothstep(0.45, 0.82, q).toFixed(3));
|
|
1534
|
+
setStyle(front, "transform", `scale(${lerp(0.9, 1, q).toFixed(3)})`);
|
|
1535
|
+
} else if (mode === "sprite") {
|
|
1536
|
+
const f = q * (frames - 1);
|
|
1537
|
+
const a = Math.floor(f);
|
|
1538
|
+
place(sheets[0], a);
|
|
1539
|
+
place(sheets[1], Math.min(frames - 1, a + 1));
|
|
1540
|
+
setStyle(sheets[1], "opacity", (f - a).toFixed(3));
|
|
1541
|
+
} else {
|
|
1542
|
+
setStyle(front, "transform", `perspective(900px) rotateY(${lerp(-74, 0, q).toFixed(1)}deg) scale(${lerp(0.86, 1, q).toFixed(3)})`);
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
/** Another colour of the same piece (picked in the quick view). A sprite keeps its sheet. */
|
|
1546
|
+
setImages(image, sideImage) {
|
|
1547
|
+
if (mode === "sprite") return;
|
|
1548
|
+
putImage(front, image);
|
|
1549
|
+
putImage(side, sideImage);
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
function mountRail(root, ctx) {
|
|
1554
|
+
const items = [];
|
|
1555
|
+
const products = [];
|
|
1556
|
+
const hits = [];
|
|
1557
|
+
const bodies = [];
|
|
1558
|
+
const turns = [];
|
|
1559
|
+
root.querySelectorAll("[data-gem-rail-item]").forEach((item) => {
|
|
1560
|
+
const product = readProduct(item);
|
|
1561
|
+
if (!product) return;
|
|
1562
|
+
const hit = item.querySelector(".gem-rail__hit") || item;
|
|
1563
|
+
items.push(item);
|
|
1564
|
+
products.push(product);
|
|
1565
|
+
hits.push(hit);
|
|
1566
|
+
bodies.push(item.querySelector(".gem-rail__body") || hit);
|
|
1567
|
+
turns.push(createTurn(item.querySelector(".gem-turn")));
|
|
1568
|
+
});
|
|
1569
|
+
const n = items.length;
|
|
1570
|
+
if (!n) return false;
|
|
1571
|
+
const stage = root.querySelector(".gem-rail__stage") || root;
|
|
1572
|
+
const track = root.querySelector(".gem-rail__track") || stage;
|
|
1573
|
+
const caption = root.querySelector(".gem-rail__caption");
|
|
1574
|
+
const rod = root.querySelector(".gem-rail__rod path");
|
|
1575
|
+
const style = ctx.renderer.style;
|
|
1576
|
+
const turn = items.map(() => 0);
|
|
1577
|
+
const hangers = items.map(() => ({ x: 0, v: 0 }));
|
|
1578
|
+
let s = ctx.settings;
|
|
1579
|
+
let goal = items.map(() => 0);
|
|
1580
|
+
let focused = -1;
|
|
1581
|
+
let compact = false;
|
|
1582
|
+
let scrolled = false;
|
|
1583
|
+
let rendered = false;
|
|
1584
|
+
let w = 0;
|
|
1585
|
+
let h = 0;
|
|
1586
|
+
let lastX = 0;
|
|
1587
|
+
let lastT = 0;
|
|
1588
|
+
let dpx = 0;
|
|
1589
|
+
const still = () => ctx.tier() === "static";
|
|
1590
|
+
const rodPath = () => `M0 ${ROD_TOP} Q50 ${Math.round(ROD_TOP * 10 + s.sag * 2e3) / 10} 100 ${ROD_TOP}`;
|
|
1591
|
+
if (rod && rod.getAttribute("d") !== rodPath()) rod.setAttribute("d", rodPath());
|
|
1592
|
+
const layout = () => {
|
|
1593
|
+
s = ctx.reread();
|
|
1594
|
+
const was = compact;
|
|
1595
|
+
compact = root.clientWidth < COMPACT_BELOW;
|
|
1596
|
+
w = stage.clientWidth;
|
|
1597
|
+
h = stage.clientHeight;
|
|
1598
|
+
if (compact && !was) items.forEach((el) => style(el, "transform", ""));
|
|
1599
|
+
};
|
|
1600
|
+
const rodY = (x) => {
|
|
1601
|
+
const t = clamp(x / Math.max(1, w), 0, 1);
|
|
1602
|
+
return h * ROD_TOP / 100 + 4 * s.sag * h * t * (1 - t);
|
|
1603
|
+
};
|
|
1604
|
+
const slots = () => {
|
|
1605
|
+
const weights = turn.map((t) => 1 + s.grow * t * t);
|
|
1606
|
+
const unit = w * 0.92 / weights.reduce((a, b) => a + b, 0);
|
|
1607
|
+
let acc = w * 0.04;
|
|
1608
|
+
return weights.map((x) => {
|
|
1609
|
+
const c = acc + x * unit / 2;
|
|
1610
|
+
acc += x * unit;
|
|
1611
|
+
return { c, width: x * unit };
|
|
1612
|
+
});
|
|
1613
|
+
};
|
|
1614
|
+
const nearest = (x) => {
|
|
1615
|
+
let best = -1;
|
|
1616
|
+
let bestD = Infinity;
|
|
1617
|
+
slots().forEach(({ c, width }, i) => {
|
|
1618
|
+
const d = Math.abs(c - x);
|
|
1619
|
+
if (d < bestD && d < width * 1.2) {
|
|
1620
|
+
best = i;
|
|
1621
|
+
bestD = d;
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
return best;
|
|
1625
|
+
};
|
|
1626
|
+
const setTint = (tint) => {
|
|
1627
|
+
if (!s.tint) return;
|
|
1628
|
+
if (tint) root.style.setProperty("--gem-motion-tint", tint);
|
|
1629
|
+
else root.style.removeProperty("--gem-motion-tint");
|
|
1630
|
+
};
|
|
1631
|
+
const setFocus = (i, initial) => {
|
|
1632
|
+
if (i === focused) return;
|
|
1633
|
+
focused = i;
|
|
1634
|
+
goal = goal.map((_, k) => k === i ? 1 : 0);
|
|
1635
|
+
const p = products[i];
|
|
1636
|
+
if (initial) {
|
|
1637
|
+
turn[i] = 1;
|
|
1638
|
+
if (turns[i]) turns[i].set(1);
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
if (caption) caption.textContent = p ? [p.title, priceText(p, root)].filter(Boolean).join(" \xB7 ") : "";
|
|
1642
|
+
if (p) {
|
|
1643
|
+
setTint(p.tint);
|
|
1644
|
+
ctx.emit("gem:motion:front", { index: i, product: p });
|
|
1645
|
+
}
|
|
1646
|
+
task.kick();
|
|
1647
|
+
};
|
|
1648
|
+
const frame = (dt) => {
|
|
1649
|
+
const k = follow(dt, s.ease);
|
|
1650
|
+
let moving = false;
|
|
1651
|
+
if (compact && scrolled) {
|
|
1652
|
+
const mid = track.scrollLeft + track.clientWidth / 2;
|
|
1653
|
+
let best = 0;
|
|
1654
|
+
items.forEach((el, i) => {
|
|
1655
|
+
if (Math.abs(el.offsetLeft + el.offsetWidth / 2 - mid) < Math.abs(items[best].offsetLeft + items[best].offsetWidth / 2 - mid)) best = i;
|
|
1656
|
+
});
|
|
1657
|
+
if (best !== focused) setFocus(best);
|
|
1658
|
+
}
|
|
1659
|
+
for (let i = 0; i < n; i++) {
|
|
1660
|
+
const d = goal[i] - turn[i];
|
|
1661
|
+
turn[i] = still() || Math.abs(d) < 2e-3 ? goal[i] : turn[i] + d * k;
|
|
1662
|
+
if (turn[i] !== goal[i]) moving = true;
|
|
1663
|
+
const hanger = hangers[i];
|
|
1664
|
+
if (still()) hanger.x = hanger.v = 0;
|
|
1665
|
+
else {
|
|
1666
|
+
spring(hanger, 0, dt, 0.02, 0.09);
|
|
1667
|
+
hanger.x = clamp(hanger.x, -9, 9);
|
|
1668
|
+
if (Math.abs(hanger.x) > 0.02 || Math.abs(hanger.v) > 0.02) moving = true;
|
|
1669
|
+
else hanger.x = hanger.v = 0;
|
|
1670
|
+
}
|
|
1671
|
+
if (turns[i]) turns[i].set(turn[i]);
|
|
1672
|
+
style(bodies[i], "transform", `rotate(${hanger.x.toFixed(2)}deg)`);
|
|
1673
|
+
}
|
|
1674
|
+
if (!compact) {
|
|
1675
|
+
slots().forEach(({ c }, i) => {
|
|
1676
|
+
style(items[i], "transform", `translate3d(${c.toFixed(1)}px, ${rodY(c).toFixed(1)}px, 0)`);
|
|
1677
|
+
style(items[i], "z-index", String(Math.round(10 + turn[i] * 10)));
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
rendered = true;
|
|
1681
|
+
return moving;
|
|
1682
|
+
};
|
|
1683
|
+
const refresh = () => {
|
|
1684
|
+
layout();
|
|
1685
|
+
if (rendered) task.kick();
|
|
1686
|
+
};
|
|
1687
|
+
const task = frameTask(ctx, root, frame, { onDegrade: refresh });
|
|
1688
|
+
hits.forEach((hit, i) => ctx.on(hit, "focus", () => setFocus(i)));
|
|
1689
|
+
ctx.on(stage, "click", (event) => {
|
|
1690
|
+
if (compact || hits.some((hit) => hit.contains(event.target))) return;
|
|
1691
|
+
const i = nearest(event.clientX - stage.getBoundingClientRect().left);
|
|
1692
|
+
if (i >= 0) hits[i].click();
|
|
1693
|
+
});
|
|
1694
|
+
ctx.on(stage, "pointermove", (event) => {
|
|
1695
|
+
if (compact || event.pointerType !== "mouse") return;
|
|
1696
|
+
const x = event.clientX - stage.getBoundingClientRect().left;
|
|
1697
|
+
dpx = clamp((x - lastX) / Math.max(1, event.timeStamp - lastT), -3, 3);
|
|
1698
|
+
lastX = x;
|
|
1699
|
+
lastT = event.timeStamp;
|
|
1700
|
+
if (!still() && s.swing) {
|
|
1701
|
+
slots().forEach(({ c, width }, i) => {
|
|
1702
|
+
hangers[i].v += dpx * 0.35 * s.swing * clamp(1 - Math.abs(c - x) / (width * 2.5), 0, 1);
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
setFocus(nearest(x));
|
|
1706
|
+
task.kick();
|
|
1707
|
+
});
|
|
1708
|
+
ctx.on(stage, "pointerleave", () => {
|
|
1709
|
+
if (!compact) setFocus(-1);
|
|
1710
|
+
});
|
|
1711
|
+
ctx.on(track, "keydown", (event) => {
|
|
1712
|
+
if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return;
|
|
1713
|
+
event.preventDefault();
|
|
1714
|
+
hits[clamp(Math.max(0, focused) + (event.key === "ArrowRight" ? 1 : -1), 0, n - 1)].focus();
|
|
1715
|
+
});
|
|
1716
|
+
ctx.on(
|
|
1717
|
+
track,
|
|
1718
|
+
"scroll",
|
|
1719
|
+
() => {
|
|
1720
|
+
if (!compact) return;
|
|
1721
|
+
scrolled = true;
|
|
1722
|
+
task.kick();
|
|
1723
|
+
},
|
|
1724
|
+
{ passive: true }
|
|
1725
|
+
);
|
|
1726
|
+
layout();
|
|
1727
|
+
observeSize(ctx, root, refresh);
|
|
1728
|
+
if (compact) setFocus(0, true);
|
|
1729
|
+
const goTo = (index) => {
|
|
1730
|
+
const i = (index % n + n) % n;
|
|
1731
|
+
if (compact && typeof track.scrollTo === "function") {
|
|
1732
|
+
track.scrollTo({ left: items[i].offsetLeft - (track.clientWidth - items[i].offsetWidth) / 2, behavior: still() ? "auto" : "smooth" });
|
|
1733
|
+
}
|
|
1734
|
+
setFocus(i);
|
|
1735
|
+
};
|
|
1736
|
+
root.gemQuickViewHost = {
|
|
1737
|
+
key: "rail",
|
|
1738
|
+
products,
|
|
1739
|
+
indexOf: (el) => items.findIndex((item) => item === el || item.contains(el)),
|
|
1740
|
+
opens: () => true,
|
|
1741
|
+
imageFor: (i) => (turns[i] ? turns[i].image : items[i] && items[i].querySelector("img")) || null,
|
|
1742
|
+
triggerFor: (i) => hits[i] || null,
|
|
1743
|
+
goTo,
|
|
1744
|
+
setOpen: (open) => root.setAttribute("data-gem-state", open ? "open" : "ready"),
|
|
1745
|
+
setImage: (i, image, side) => turns[i] && turns[i].setImages(image, side),
|
|
1746
|
+
setTint
|
|
1747
|
+
};
|
|
1748
|
+
root.setAttribute("data-gem-quick-view-host", "rail");
|
|
1749
|
+
root.setAttribute("data-gem-state", "ready");
|
|
1750
|
+
return () => {
|
|
1751
|
+
delete root.gemQuickViewHost;
|
|
1752
|
+
root.removeAttribute("data-gem-quick-view-host");
|
|
1753
|
+
root.removeAttribute("data-gem-state");
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
var railEffect = defineEffect({
|
|
1757
|
+
key: "rail",
|
|
1758
|
+
hook: "[data-gem-rail]",
|
|
1759
|
+
load: "viewport",
|
|
1760
|
+
renderer: "dom",
|
|
1761
|
+
tier: "static",
|
|
1762
|
+
fallback: "markup",
|
|
1763
|
+
budget: { gzip: 6144 },
|
|
1764
|
+
settings: {
|
|
1765
|
+
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)" },
|
|
1766
|
+
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)" },
|
|
1767
|
+
grow: { type: "number", min: 0, max: 3, step: 0.1, default: 1.5, label: "How much wider a turned piece gets" },
|
|
1768
|
+
swing: { type: "number", min: 0, max: 2, step: 0.1, default: 1, label: "Hanger swing (0: off)" },
|
|
1769
|
+
ease: { type: "number", min: 40, max: 600, step: 10, default: 120, label: "Turn time (ms)" },
|
|
1770
|
+
tint: { type: "boolean", default: true, label: "Tint the band with the turned piece's colour" }
|
|
1771
|
+
},
|
|
1772
|
+
mount: mountRail
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
// ../../packages/gem-framework/src/motion/quick-view.js
|
|
1776
|
+
var EASE_OUT = "cubic-bezier(.2,.75,.15,1)";
|
|
1777
|
+
var LABELS = {
|
|
1778
|
+
add: "Add to bag",
|
|
1779
|
+
adding: "Adding\u2026",
|
|
1780
|
+
added: "Added",
|
|
1781
|
+
added_status: "added to bag",
|
|
1782
|
+
failed: "Could not add to bag",
|
|
1783
|
+
choose: "Choose an option",
|
|
1784
|
+
sold_out: "Sold out",
|
|
1785
|
+
details: "Full details",
|
|
1786
|
+
previous: "Previous product",
|
|
1787
|
+
next: "Next product",
|
|
1788
|
+
close: "Close",
|
|
1789
|
+
colour: "Colour",
|
|
1790
|
+
size: "Size"
|
|
1791
|
+
};
|
|
1792
|
+
var panelCount = 0;
|
|
1793
|
+
var pad2 = (v) => String(v).padStart(2, "0");
|
|
1794
|
+
var text = (v) => v == null ? "" : String(v);
|
|
1795
|
+
function cardHost(trigger) {
|
|
1796
|
+
const own = trigger.closest("[data-gem-product]");
|
|
1797
|
+
const group = trigger.closest("[data-gem-quick-view-group]");
|
|
1798
|
+
const products = [];
|
|
1799
|
+
const sources = [];
|
|
1800
|
+
let index = -1;
|
|
1801
|
+
for (const el of group ? group.querySelectorAll("[data-gem-product]") : own ? [own] : []) {
|
|
1802
|
+
const product = readProduct(el);
|
|
1803
|
+
if (!product) continue;
|
|
1804
|
+
if (el === own) index = products.length;
|
|
1805
|
+
products.push(product);
|
|
1806
|
+
sources.push(el);
|
|
1807
|
+
}
|
|
1808
|
+
return {
|
|
1809
|
+
index,
|
|
1810
|
+
host: {
|
|
1811
|
+
key: "card",
|
|
1812
|
+
products,
|
|
1813
|
+
imageFor: (i) => sources[i] ? sources[i].querySelector("img") : null,
|
|
1814
|
+
triggerFor: (i) => sources[i] ? sources[i].matches("[data-gem-quick-view-trigger]") ? sources[i] : sources[i].querySelector("[data-gem-quick-view-trigger]") : null,
|
|
1815
|
+
goTo: () => {
|
|
1816
|
+
},
|
|
1817
|
+
setOpen: () => {
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
function sessionOf(ctx) {
|
|
1823
|
+
if (ctx.session.quickView) return ctx.session.quickView;
|
|
1824
|
+
const shared = { panels: /* @__PURE__ */ new Map(), pending: null };
|
|
1825
|
+
ctx.session.quickView = shared;
|
|
1826
|
+
ctx.on(ctx.window, "gem:cart:added", () => shared.pending && shared.pending(true));
|
|
1827
|
+
ctx.on(ctx.window, "gem:cart:add-failed", () => shared.pending && shared.pending(false));
|
|
1828
|
+
ctx.cleanup(() => {
|
|
1829
|
+
shared.panels.forEach((panel) => panel.destroy());
|
|
1830
|
+
shared.panels.clear();
|
|
1831
|
+
});
|
|
1832
|
+
return shared;
|
|
1833
|
+
}
|
|
1834
|
+
function createPanel(parent, ctx, shared) {
|
|
1835
|
+
const doc = parent.ownerDocument;
|
|
1836
|
+
const win = ctx.window;
|
|
1837
|
+
const still = () => ctx.tier() === "static";
|
|
1838
|
+
const make = (tag, className, attrs) => {
|
|
1839
|
+
const node = doc.createElement(tag);
|
|
1840
|
+
node.className = className;
|
|
1841
|
+
for (const [k, v] of Object.entries(attrs || {})) node.setAttribute(k, v);
|
|
1842
|
+
return node;
|
|
1843
|
+
};
|
|
1844
|
+
panelCount += 1;
|
|
1845
|
+
const titleId = "gem-quick-view-title-" + panelCount;
|
|
1846
|
+
const shell = make("div", "gem-popup-shell gem-quick-view__shell", {
|
|
1847
|
+
"data-gem-quick-view-panel": "",
|
|
1848
|
+
"data-gem-popup-desktop": "split-left",
|
|
1849
|
+
"data-gem-popup-mobile": "sheet",
|
|
1850
|
+
"data-gem-popup-media": "top",
|
|
1851
|
+
"data-gem-popup-align": "start"
|
|
1852
|
+
});
|
|
1853
|
+
const dialog = make("dialog", "gem-popup-shell__dialog gem-quick-view__dialog", { "aria-labelledby": titleId });
|
|
1854
|
+
const frame = make("div", "gem-popup-shell__frame gem-quick-view__frame");
|
|
1855
|
+
const surface = make("div", "gem-quick-view__surface", { "aria-hidden": "true" });
|
|
1856
|
+
const close = make("button", "gem-popup-shell__close gem-quick-view__close", { type: "button" });
|
|
1857
|
+
const media = make("div", "gem-popup-shell__media gem-quick-view__media");
|
|
1858
|
+
const img = make("img", "gem-quick-view__image", { alt: "" });
|
|
1859
|
+
const imgNext = make("img", "gem-quick-view__image gem-quick-view__image--next", { alt: "", "aria-hidden": "true" });
|
|
1860
|
+
const body = make("div", "gem-popup-shell__panel gem-quick-view__panel");
|
|
1861
|
+
const bar = make("div", "gem-quick-view__bar");
|
|
1862
|
+
const prev = make("button", "gem-quick-view__icon", { type: "button" });
|
|
1863
|
+
const next = make("button", "gem-quick-view__icon", { type: "button" });
|
|
1864
|
+
const counter = make("span", "gem-quick-view__count");
|
|
1865
|
+
const eyebrow = make("p", "gem-quick-view__eyebrow");
|
|
1866
|
+
const heading = make("h2", "gem-quick-view__title", { id: titleId });
|
|
1867
|
+
const price = make("p", "gem-quick-view__price");
|
|
1868
|
+
const compare = make("s", "gem-quick-view__compare");
|
|
1869
|
+
const swatches = make("div", "gem-quick-view__swatches", { role: "radiogroup" });
|
|
1870
|
+
const sizes = make("div", "gem-quick-view__sizes", { role: "radiogroup" });
|
|
1871
|
+
const add = make("button", "gem-button gem-button--primary gem-quick-view__add", { type: "button" });
|
|
1872
|
+
const status = make("p", "gem-quick-view__status", { role: "status" });
|
|
1873
|
+
const desc = make("p", "gem-quick-view__description");
|
|
1874
|
+
const details = make("a", "gem-quick-view__details");
|
|
1875
|
+
close.textContent = "\xD7";
|
|
1876
|
+
prev.textContent = "\u2039";
|
|
1877
|
+
next.textContent = "\u203A";
|
|
1878
|
+
media.append(img, imgNext);
|
|
1879
|
+
bar.append(prev, counter, next);
|
|
1880
|
+
body.append(bar, eyebrow, heading, price, compare, swatches, sizes, add, status, desc, details);
|
|
1881
|
+
frame.append(close, media, body);
|
|
1882
|
+
dialog.append(surface, frame);
|
|
1883
|
+
shell.append(dialog);
|
|
1884
|
+
parent.append(shell);
|
|
1885
|
+
const chosen = /* @__PURE__ */ new Map();
|
|
1886
|
+
let host = null;
|
|
1887
|
+
let list = [];
|
|
1888
|
+
let index = 0;
|
|
1889
|
+
let color2 = 0;
|
|
1890
|
+
let size = -1;
|
|
1891
|
+
let source = null;
|
|
1892
|
+
let returnTo = null;
|
|
1893
|
+
let priceAt = null;
|
|
1894
|
+
let labels = LABELS;
|
|
1895
|
+
let closing = false;
|
|
1896
|
+
let resetTimer = 0;
|
|
1897
|
+
const product = () => list[index];
|
|
1898
|
+
const animate = (el, frames, options) => typeof el.animate === "function" ? el.animate(frames, options) : null;
|
|
1899
|
+
const cancelAll = (el) => {
|
|
1900
|
+
if (el.getAnimations) el.getAnimations().forEach((a) => a.cancel());
|
|
1901
|
+
};
|
|
1902
|
+
const hideSource = (hide) => {
|
|
1903
|
+
if (!source) return;
|
|
1904
|
+
if (hide) source.setAttribute("data-gem-quick-view-source", "");
|
|
1905
|
+
else source.removeAttribute("data-gem-quick-view-source");
|
|
1906
|
+
};
|
|
1907
|
+
const moveSource = () => {
|
|
1908
|
+
const target2 = host.imageFor(index);
|
|
1909
|
+
if (target2 === source) return;
|
|
1910
|
+
hideSource(false);
|
|
1911
|
+
source = target2;
|
|
1912
|
+
if (dialog.open) hideSource(true);
|
|
1913
|
+
};
|
|
1914
|
+
const setTint = (tint) => {
|
|
1915
|
+
if (host.setTint) host.setTint(tint);
|
|
1916
|
+
else if (tint) shell.style.setProperty("--gem-motion-tint", tint);
|
|
1917
|
+
else shell.style.removeProperty("--gem-motion-tint");
|
|
1918
|
+
};
|
|
1919
|
+
const showImage = (image, fade) => {
|
|
1920
|
+
img.alt = image.alt || text(product().title);
|
|
1921
|
+
if (!fade || still() || typeof imgNext.animate !== "function") return putImage(img, image);
|
|
1922
|
+
putImage(imgNext, image);
|
|
1923
|
+
const swap = () => {
|
|
1924
|
+
const settle = () => {
|
|
1925
|
+
putImage(img, image);
|
|
1926
|
+
imgNext.removeAttribute("src");
|
|
1927
|
+
imgNext.removeAttribute("srcset");
|
|
1928
|
+
};
|
|
1929
|
+
const run = imgNext.animate([{ opacity: 0, transform: "scale(.97)" }, { opacity: 1, transform: "none" }], { duration: 280, easing: EASE_OUT });
|
|
1930
|
+
if (run && run.finished) run.finished.then(settle, settle);
|
|
1931
|
+
else settle();
|
|
1932
|
+
};
|
|
1933
|
+
if (typeof imgNext.decode === "function") imgNext.decode().then(swap, swap);
|
|
1934
|
+
else swap();
|
|
1935
|
+
};
|
|
1936
|
+
const variant = () => {
|
|
1937
|
+
const p = product();
|
|
1938
|
+
const c = p.colors && p.colors[color2];
|
|
1939
|
+
const z = p.sizes && p.sizes[size];
|
|
1940
|
+
if (!Array.isArray(p.variants)) return null;
|
|
1941
|
+
return p.variants.find((v) => (!c || (v.options || []).includes(c.name)) && (!z || (v.options || []).includes(z.name))) || null;
|
|
1942
|
+
};
|
|
1943
|
+
const variantId = () => {
|
|
1944
|
+
const p = product();
|
|
1945
|
+
const match = variant();
|
|
1946
|
+
if (match) return match.available === false ? null : match.id;
|
|
1947
|
+
const c = p.colors && p.colors[color2];
|
|
1948
|
+
const z = p.sizes && p.sizes[size];
|
|
1949
|
+
if (z && z.variant_id != null) return z.variant_id;
|
|
1950
|
+
if (c && c.variant_ids && c.variant_ids.length) return c.variant_ids[0];
|
|
1951
|
+
return p.variant_id != null ? p.variant_id : null;
|
|
1952
|
+
};
|
|
1953
|
+
const priced = () => {
|
|
1954
|
+
const v = variant();
|
|
1955
|
+
return v && (v.price_text != null || v.price != null) ? v : product();
|
|
1956
|
+
};
|
|
1957
|
+
const showPrice = (fade) => {
|
|
1958
|
+
const words = priceText(priced(), priceAt);
|
|
1959
|
+
if (fade) swapText(price, words, still());
|
|
1960
|
+
else price.textContent = words;
|
|
1961
|
+
compare.textContent = priceText(priced(), priceAt, "compare");
|
|
1962
|
+
compare.hidden = !compare.textContent;
|
|
1963
|
+
};
|
|
1964
|
+
const updateAdd = () => {
|
|
1965
|
+
const p = product();
|
|
1966
|
+
const z = p.sizes && p.sizes[size];
|
|
1967
|
+
const soldOut = p.sold_out === true || Boolean(p.sizes && p.sizes.length) && size < 0;
|
|
1968
|
+
add.disabled = soldOut;
|
|
1969
|
+
add.textContent = soldOut ? labels.sold_out : [labels.add, z ? z.name : "", priceText(priced(), priceAt)].filter(Boolean).join(" \xB7 ");
|
|
1970
|
+
};
|
|
1971
|
+
const render = (fade) => {
|
|
1972
|
+
const p = product();
|
|
1973
|
+
const colors = Array.isArray(p.colors) ? p.colors : [];
|
|
1974
|
+
color2 = Math.min(chosen.get(p.id) || 0, Math.max(0, colors.length - 1));
|
|
1975
|
+
size = (p.sizes || []).findIndex((z) => z.available !== false);
|
|
1976
|
+
const c = colors[color2];
|
|
1977
|
+
counter.textContent = pad2(index + 1) + " / " + pad2(list.length);
|
|
1978
|
+
bar.hidden = list.length < 2;
|
|
1979
|
+
const put = (el, value) => {
|
|
1980
|
+
if (fade) swapText(el, value, still());
|
|
1981
|
+
else el.textContent = value;
|
|
1982
|
+
};
|
|
1983
|
+
put(eyebrow, text(p.eyebrow));
|
|
1984
|
+
put(heading, text(p.title));
|
|
1985
|
+
showPrice(fade);
|
|
1986
|
+
eyebrow.hidden = !p.eyebrow;
|
|
1987
|
+
desc.textContent = text(p.description);
|
|
1988
|
+
desc.hidden = !p.description;
|
|
1989
|
+
details.textContent = labels.details;
|
|
1990
|
+
if (p.url) details.setAttribute("href", p.url);
|
|
1991
|
+
details.hidden = !p.url;
|
|
1992
|
+
showImage(c && c.image || p.image, fade);
|
|
1993
|
+
setTint(c && c.tint || p.tint);
|
|
1994
|
+
swatches.replaceChildren(
|
|
1995
|
+
...colors.map((col, i) => {
|
|
1996
|
+
const b = make("button", "gem-quick-view__swatch", { type: "button", role: "radio", "aria-checked": String(i === color2), "aria-label": col.name, title: col.name, "data-gem-quick-view-color": String(i) });
|
|
1997
|
+
if (col.hex) b.style.setProperty("--gem-quick-view-swatch", col.hex);
|
|
1998
|
+
return b;
|
|
1999
|
+
})
|
|
2000
|
+
);
|
|
2001
|
+
swatches.hidden = colors.length < 2;
|
|
2002
|
+
sizes.replaceChildren(
|
|
2003
|
+
...(p.sizes || []).map((z, i) => {
|
|
2004
|
+
const b = make("button", "gem-quick-view__size", { type: "button", role: "radio", "aria-checked": String(i === size), "data-gem-quick-view-size": String(i) });
|
|
2005
|
+
b.textContent = z.name;
|
|
2006
|
+
if (z.available === false) {
|
|
2007
|
+
b.disabled = true;
|
|
2008
|
+
b.setAttribute("aria-label", z.name + ", " + labels.sold_out);
|
|
2009
|
+
}
|
|
2010
|
+
return b;
|
|
2011
|
+
})
|
|
2012
|
+
);
|
|
2013
|
+
sizes.hidden = !(p.sizes && p.sizes.length);
|
|
2014
|
+
status.textContent = "";
|
|
2015
|
+
updateAdd();
|
|
2016
|
+
moveSource();
|
|
2017
|
+
for (const k of [index - 1, index + 1]) if (list.length > 1) loadImage(doc, list[(k + list.length) % list.length].image);
|
|
2018
|
+
};
|
|
2019
|
+
const step = (delta) => {
|
|
2020
|
+
if (list.length < 2) return;
|
|
2021
|
+
index = (index + delta + list.length) % list.length;
|
|
2022
|
+
host.goTo(index);
|
|
2023
|
+
render(true);
|
|
2024
|
+
};
|
|
2025
|
+
const finish = (ok, before) => {
|
|
2026
|
+
shared.pending = null;
|
|
2027
|
+
add.disabled = false;
|
|
2028
|
+
add.textContent = ok ? labels.added : before;
|
|
2029
|
+
status.textContent = ok ? text(product().title) + ": " + labels.added_status : labels.failed;
|
|
2030
|
+
if (resetTimer) win.clearTimeout(resetTimer);
|
|
2031
|
+
resetTimer = ok ? win.setTimeout(updateAdd, 1600) : 0;
|
|
2032
|
+
};
|
|
2033
|
+
const addToCart = () => {
|
|
2034
|
+
const id = variantId();
|
|
2035
|
+
if (id == null || id === "") {
|
|
2036
|
+
status.textContent = labels.choose;
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
const before = add.textContent;
|
|
2040
|
+
add.disabled = true;
|
|
2041
|
+
add.textContent = labels.adding;
|
|
2042
|
+
add.dispatchEvent(new win.CustomEvent("gem:cart:add", { bubbles: true, composed: true, detail: { variantId: String(id), quantity: 1 } }));
|
|
2043
|
+
if (win.__gemCartAddBridge) shared.pending = (ok) => finish(ok, before);
|
|
2044
|
+
else finish(true, before);
|
|
2045
|
+
};
|
|
2046
|
+
const pick = (group, i) => {
|
|
2047
|
+
Array.from(group.children).forEach((el, k) => el.setAttribute("aria-checked", String(k === i)));
|
|
2048
|
+
if (group === sizes) size = i;
|
|
2049
|
+
else {
|
|
2050
|
+
const p = product();
|
|
2051
|
+
const c = p.colors && p.colors[i];
|
|
2052
|
+
if (!c) return;
|
|
2053
|
+
color2 = i;
|
|
2054
|
+
chosen.set(p.id, i);
|
|
2055
|
+
if (c.image) {
|
|
2056
|
+
showImage(c.image, true);
|
|
2057
|
+
if (host.setImage) host.setImage(index, c.image, c.side);
|
|
2058
|
+
}
|
|
2059
|
+
setTint(c.tint || p.tint);
|
|
2060
|
+
}
|
|
2061
|
+
showPrice(false);
|
|
2062
|
+
updateAdd();
|
|
2063
|
+
};
|
|
2064
|
+
const done = (reason) => {
|
|
2065
|
+
dialog.removeAttribute("data-gem-state");
|
|
2066
|
+
cancelAll(img);
|
|
2067
|
+
if (dialog.open) dialog.close();
|
|
2068
|
+
hideSource(false);
|
|
2069
|
+
host.setOpen(false);
|
|
2070
|
+
closing = false;
|
|
2071
|
+
const back = host.triggerFor(index) || returnTo;
|
|
2072
|
+
if (back && typeof back.focus === "function") back.focus();
|
|
2073
|
+
ctx.api.emit("gem:quick-view:close", { source: "gem-runtime", reason, productId: product().id });
|
|
2074
|
+
};
|
|
2075
|
+
const shut = (reason) => {
|
|
2076
|
+
if (!dialog.open || closing) return;
|
|
2077
|
+
closing = true;
|
|
2078
|
+
moveSource();
|
|
2079
|
+
const to = source && source.getBoundingClientRect ? source.getBoundingClientRect() : null;
|
|
2080
|
+
const flight = still() || !to ? null : flip(img, to, { back: true, duration: 440, easing: "cubic-bezier(.4,0,.2,1)" });
|
|
2081
|
+
if (!flight || !flight.finished) return done(reason);
|
|
2082
|
+
dialog.setAttribute("data-gem-state", "closing");
|
|
2083
|
+
for (const el of [body, surface, close]) animate(el, [{ opacity: 1 }, { opacity: 0 }], { duration: 220, fill: "forwards" });
|
|
2084
|
+
host.setOpen(false);
|
|
2085
|
+
const end = () => {
|
|
2086
|
+
[body, surface, close].forEach(cancelAll);
|
|
2087
|
+
done(reason);
|
|
2088
|
+
};
|
|
2089
|
+
flight.finished.then(end, end);
|
|
2090
|
+
};
|
|
2091
|
+
ctx.on(close, "click", () => shut("close-control"));
|
|
2092
|
+
ctx.on(prev, "click", () => step(-1));
|
|
2093
|
+
ctx.on(next, "click", () => step(1));
|
|
2094
|
+
ctx.on(add, "click", addToCart);
|
|
2095
|
+
for (const [group, attr5] of [[swatches, "data-gem-quick-view-color"], [sizes, "data-gem-quick-view-size"]]) {
|
|
2096
|
+
ctx.on(group, "click", (event) => {
|
|
2097
|
+
const b = event.target.closest && event.target.closest(`[${attr5}]`);
|
|
2098
|
+
if (b && !b.disabled) pick(group, Number(b.getAttribute(attr5)));
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
ctx.on(dialog, "cancel", (event) => {
|
|
2102
|
+
event.preventDefault();
|
|
2103
|
+
shut("escape");
|
|
2104
|
+
});
|
|
2105
|
+
ctx.on(dialog, "click", (event) => {
|
|
2106
|
+
if (event.target === dialog) shut("backdrop");
|
|
2107
|
+
});
|
|
2108
|
+
ctx.on(dialog, "keydown", (event) => {
|
|
2109
|
+
if (event.target.closest && event.target.closest(".gem-quick-view__swatches, .gem-quick-view__sizes")) return;
|
|
2110
|
+
if (event.key === "ArrowRight" || event.key === "ArrowLeft") step(event.key === "ArrowRight" ? 1 : -1);
|
|
2111
|
+
});
|
|
2112
|
+
return {
|
|
2113
|
+
shell,
|
|
2114
|
+
open(nextHost, at, trigger, nextLabels) {
|
|
2115
|
+
if (dialog.open) return;
|
|
2116
|
+
cancelAll(img);
|
|
2117
|
+
host = nextHost;
|
|
2118
|
+
list = host.products;
|
|
2119
|
+
index = at;
|
|
2120
|
+
labels = nextLabels;
|
|
2121
|
+
returnTo = trigger;
|
|
2122
|
+
priceAt = trigger;
|
|
2123
|
+
source = null;
|
|
2124
|
+
close.setAttribute("aria-label", labels.close);
|
|
2125
|
+
prev.setAttribute("aria-label", labels.previous);
|
|
2126
|
+
next.setAttribute("aria-label", labels.next);
|
|
2127
|
+
swatches.setAttribute("aria-label", labels.colour);
|
|
2128
|
+
sizes.setAttribute("aria-label", labels.size);
|
|
2129
|
+
render(false);
|
|
2130
|
+
source = host.imageFor(index);
|
|
2131
|
+
const from = source && source.getBoundingClientRect ? source.getBoundingClientRect() : null;
|
|
2132
|
+
dialog.showModal();
|
|
2133
|
+
dialog.setAttribute("data-gem-state", "open");
|
|
2134
|
+
host.setOpen(true);
|
|
2135
|
+
hideSource(true);
|
|
2136
|
+
ctx.api.emit("gem:quick-view:open", { source: "gem-runtime", productId: product().id, productUrl: product().url || null, index, count: list.length, host: host.key });
|
|
2137
|
+
if (still()) return;
|
|
2138
|
+
flip(img, from, { duration: 560, easing: EASE_OUT });
|
|
2139
|
+
animate(surface, [{ opacity: 0, transform: "scale(.97)" }, { opacity: 1, transform: "none" }], { duration: 360, easing: EASE_OUT });
|
|
2140
|
+
Array.from(body.children).forEach(
|
|
2141
|
+
(el, i) => animate(el, [{ opacity: 0, transform: "translateY(12px)" }, { opacity: 1, transform: "none" }], { duration: 380, delay: 120 + i * 40, easing: EASE_OUT, fill: "backwards" })
|
|
2142
|
+
);
|
|
2143
|
+
},
|
|
2144
|
+
/** Unmount: close at once (no flight) and leave the root as the server drew it. */
|
|
2145
|
+
destroy() {
|
|
2146
|
+
if (dialog.open) done("unmount");
|
|
2147
|
+
shell.remove();
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
function mountQuickView(trigger, ctx) {
|
|
2152
|
+
const shared = sessionOf(ctx);
|
|
2153
|
+
ctx.on(trigger, "click", (event) => {
|
|
2154
|
+
if (event.defaultPrevented) return;
|
|
2155
|
+
const hostEl = trigger.closest("[data-gem-quick-view-host]");
|
|
2156
|
+
const host = hostEl && hostEl.gemQuickViewHost;
|
|
2157
|
+
if (host && !host.opens(trigger)) return;
|
|
2158
|
+
const card = host ? null : cardHost(trigger);
|
|
2159
|
+
const index = host ? host.indexOf(trigger) : card.index;
|
|
2160
|
+
if (index < 0 || !(host || card.host).products[index]) return;
|
|
2161
|
+
event.preventDefault();
|
|
2162
|
+
const parent = host && hostEl || trigger.closest("[data-gem-quick-view-group]") || trigger.closest(".gem-section, [data-gem-section-id]") || trigger.parentElement;
|
|
2163
|
+
let panel = shared.panels.get(parent);
|
|
2164
|
+
if (!panel || !panel.shell.isConnected) shared.panels.set(parent, panel = createPanel(parent, ctx, shared));
|
|
2165
|
+
const own = ctx.reread().labels;
|
|
2166
|
+
panel.open(host || card.host, index, trigger, { ...LABELS, ...own && typeof own === "object" ? own : {} });
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
var quickViewEffect = defineEffect({
|
|
2170
|
+
key: "quick-view",
|
|
2171
|
+
hook: "[data-gem-quick-view-trigger]",
|
|
2172
|
+
load: "interaction",
|
|
2173
|
+
renderer: "dom",
|
|
2174
|
+
tier: "static",
|
|
2175
|
+
fallback: "markup",
|
|
2176
|
+
budget: { gzip: 6144 },
|
|
2177
|
+
settings: {
|
|
2178
|
+
labels: {
|
|
2179
|
+
type: "json",
|
|
2180
|
+
default: {},
|
|
2181
|
+
inherit: true,
|
|
2182
|
+
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"
|
|
2183
|
+
}
|
|
2184
|
+
},
|
|
2185
|
+
mount: mountQuickView
|
|
2186
|
+
});
|
|
2187
|
+
|
|
2188
|
+
// ../../packages/gem-framework/src/motion-engine.ts
|
|
2189
|
+
var GEM_ORBIT_BASE_CSS_LINES = [
|
|
2190
|
+
".gem-orbit { display: block; position: relative; background-color: var(--gem-motion-tint, transparent); }",
|
|
2191
|
+
".gem-orbit:focus-visible { outline: var(--gem-focus-ring); outline-offset: calc(-1 * var(--gem-focus-offset)); }",
|
|
2192
|
+
".gem-orbit__stage { position: relative; height: var(--gem-orbit-stage-height, clamp(340px, 58vh, 620px)); overflow: hidden; container-type: size; }",
|
|
2193
|
+
".gem-orbit__ring { list-style: none; margin: 0; padding: 0; position: absolute; left: 50%; top: 66%; width: 0; height: 0; }",
|
|
2194
|
+
".gem-orbit__item {",
|
|
2195
|
+
" --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);",
|
|
2196
|
+
" --gem-orbit-a: calc(var(--gem-orbit-k) * var(--gem-orbit-spread, calc(360 / var(--gem-orbit-n, 1))) * 1deg);",
|
|
2197
|
+
" --gem-orbit-d: calc((1 - cos(var(--gem-orbit-a))) / 2);",
|
|
2198
|
+
" position: absolute; left: calc(var(--gem-orbit-size, 0.62) * -50cqh); top: calc(var(--gem-orbit-size, 0.62) * -50cqh);",
|
|
2199
|
+
" width: calc(var(--gem-orbit-size, 0.62) * 100cqh); height: calc(var(--gem-orbit-size, 0.62) * 100cqh);",
|
|
2200
|
+
" 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)",
|
|
2201
|
+
" 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));",
|
|
2202
|
+
" opacity: calc(1 + (var(--gem-orbit-fade, 0.35) - 1) * var(--gem-orbit-d));",
|
|
2203
|
+
" z-index: calc((1 - var(--gem-orbit-d)) * 100);",
|
|
2204
|
+
" filter: blur(calc(var(--gem-orbit-blur, 2.5) * var(--gem-orbit-d) * 1px));",
|
|
2205
|
+
"}",
|
|
2206
|
+
".gem-orbit__item::after {",
|
|
2207
|
+
" content: ''; position: absolute; left: 18%; right: 18%; bottom: -2%; height: 7%; z-index: -1; border-radius: var(--gem-radius-pill);",
|
|
2208
|
+
" background: radial-gradient(closest-side, color-mix(in srgb, var(--gem-color-scrim, #000) 18%, transparent), transparent);",
|
|
2209
|
+
"}",
|
|
2210
|
+
// A hit is a <button> or an <a href> to the product (still a link on a page without the engine).
|
|
2211
|
+
".gem-orbit__hit { display: block; width: 100%; height: 100%; padding: 0; border: 0; color: inherit; background: transparent; text-decoration: none; cursor: pointer; }",
|
|
2212
|
+
".gem-orbit__hit:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); border-radius: var(--gem-radius-md); }",
|
|
2213
|
+
".gem-orbit__img { display: block; width: 100%; height: 100%; object-fit: contain; pointer-events: none; }",
|
|
2214
|
+
".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; }",
|
|
2215
|
+
".gem-orbit__name { margin: 0; font-family: var(--gem-font-heading); font-size: var(--gem-h4); font-weight: 600; line-height: 1.2; }",
|
|
2216
|
+
".gem-orbit__price { margin: 0; color: var(--gem-color-muted); font-variant-numeric: tabular-nums; }",
|
|
2217
|
+
".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; }",
|
|
2218
|
+
".gem-orbit__arrow {",
|
|
2219
|
+
" display: inline-grid; place-items: center; width: var(--gem-tap-min); height: var(--gem-tap-min); padding: 0; cursor: pointer;",
|
|
2220
|
+
" border: var(--gem-stroke, 1px) solid currentColor; border-radius: var(--gem-radius-pill); color: inherit; background: transparent; font: inherit; line-height: 1;",
|
|
2221
|
+
"}",
|
|
2222
|
+
".gem-orbit__arrow:hover { color: var(--gem-color-background); background: var(--gem-color-text); border-color: var(--gem-color-text); }",
|
|
2223
|
+
".gem-orbit__arrow:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); }",
|
|
2224
|
+
".gem-orbit__count { min-width: 7ch; text-align: center; font-variant-numeric: tabular-nums; }",
|
|
2225
|
+
".gem-orbit__live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }",
|
|
2226
|
+
".gem-orbit__track { position: relative; width: min(180px, 30vw); height: var(--gem-stroke-thick, 2px); background: color-mix(in srgb, currentColor 18%, transparent); }",
|
|
2227
|
+
".gem-orbit__track::before { content: ''; position: absolute; inset: calc(-1 * var(--gem-space-sm)) 0; }",
|
|
2228
|
+
".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))); }",
|
|
2229
|
+
// The light and static tiers draw no blur (a phone, reduced motion): the first paint already matches what they draw.
|
|
2230
|
+
"@media (pointer: coarse), (prefers-reduced-motion: reduce) { .gem-orbit__item { filter: none; } }"
|
|
2231
|
+
];
|
|
2232
|
+
var GEM_RAIL_BASE_CSS_LINES = [
|
|
2233
|
+
".gem-rail { display: block; position: relative; container-type: inline-size; background-color: var(--gem-motion-tint, transparent); }",
|
|
2234
|
+
".gem-rail__stage { position: relative; height: var(--gem-rail-stage-height, clamp(380px, 62vh, 640px)); overflow: hidden; container-type: size; }",
|
|
2235
|
+
// The rod is drawn in a 0..100 viewBox with preserveAspectRatio="none": it fits any stage without being redrawn.
|
|
2236
|
+
".gem-rail__rod { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }",
|
|
2237
|
+
".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; }",
|
|
2238
|
+
".gem-rail__track { list-style: none; margin: 0; padding: 0; position: absolute; inset: 0; }",
|
|
2239
|
+
// At rest slot i of n is centred at 4% + 92% · (i + ½) / n of the width and hangs from the rod's curve there.
|
|
2240
|
+
".gem-rail__item {",
|
|
2241
|
+
" --gem-rail-t: calc(0.04 + 0.92 * (var(--gem-rail-i, 0) + 0.5) / var(--gem-rail-n, 1));",
|
|
2242
|
+
" position: absolute; left: 0; top: 0; width: 0; height: 0; z-index: 10;",
|
|
2243
|
+
" 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);",
|
|
2244
|
+
"}",
|
|
2245
|
+
".gem-rail__hit {",
|
|
2246
|
+
" 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);",
|
|
2247
|
+
" padding: 0; border: 0; color: inherit; background: transparent; text-decoration: none; cursor: pointer; transform: translateX(-50%);",
|
|
2248
|
+
"}",
|
|
2249
|
+
".gem-rail__hit:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); border-radius: var(--gem-radius-md); }",
|
|
2250
|
+
".gem-rail__body { position: absolute; inset: 0; display: block; transform-origin: 50% 0; }",
|
|
2251
|
+
".gem-rail__hook {",
|
|
2252
|
+
" position: absolute; left: calc(50% - 8px); top: 0; width: 16px; height: 22px; opacity: 0.8;",
|
|
2253
|
+
" 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;",
|
|
2254
|
+
"}",
|
|
2255
|
+
".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); }",
|
|
2256
|
+
".gem-rail__tag {",
|
|
2257
|
+
" position: absolute; left: calc(50% + 14px); top: 18px; z-index: 2; padding: var(--gem-space-2xs) var(--gem-space-xs); transform: rotate(4deg);",
|
|
2258
|
+
" font-family: var(--gem-font-mono); font-size: var(--gem-text-xs); font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums;",
|
|
2259
|
+
" color: var(--gem-color-text); background: var(--gem-color-background); border-radius: var(--gem-radius-sm); box-shadow: var(--gem-shadow-sm);",
|
|
2260
|
+
"}",
|
|
2261
|
+
".gem-rail .gem-turn { position: absolute; inset: 26px 0 0; width: auto; height: auto; }",
|
|
2262
|
+
".gem-rail__caption { margin: var(--gem-space-xs) 0 0; min-height: 1.5em; text-align: center; font-variant-numeric: tabular-nums; }",
|
|
2263
|
+
// Under 700 px (the effect's breakpoint, on the same width): a swipe row with snap points; the centred piece turns.
|
|
2264
|
+
"@container (max-width: 699px) {",
|
|
2265
|
+
" .gem-rail__stage { height: auto; overflow: visible; container-type: normal; }",
|
|
2266
|
+
" .gem-rail__rod { display: none; }",
|
|
2267
|
+
" .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; }",
|
|
2268
|
+
" .gem-rail__track::-webkit-scrollbar { display: none; }",
|
|
2269
|
+
" .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); }",
|
|
2270
|
+
" .gem-rail__hit { left: 50%; width: 100%; height: 100%; }",
|
|
2271
|
+
// The row rests with its first piece centred (the track's padding) and facing the shopper; the plugin settles there.
|
|
2272
|
+
" .gem-rail__item:first-child .gem-turn__side { opacity: 0; transform: perspective(900px) rotateY(-82deg); }",
|
|
2273
|
+
" .gem-rail__item:first-child .gem-turn:has(> .gem-turn__side) > .gem-turn__front { opacity: 1; transform: scale(1); }",
|
|
2274
|
+
" .gem-rail__item:first-child .gem-turn:not(:has(> .gem-turn__side)) > .gem-turn__front { transform: perspective(900px) rotateY(0deg) scale(1); }",
|
|
2275
|
+
" .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)))); }",
|
|
2276
|
+
"}",
|
|
2277
|
+
// The turn rests on its side view: pair (side + front photo), flip (front only), sprite (two copies of a frame sheet).
|
|
2278
|
+
".gem-turn { position: relative; display: block; width: 100%; height: 100%; }",
|
|
2279
|
+
".gem-turn[data-gem-turn-frames] { overflow: hidden; }",
|
|
2280
|
+
".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; }",
|
|
2281
|
+
".gem-turn__side { transform: perspective(900px) rotateY(0deg); backface-visibility: hidden; }",
|
|
2282
|
+
".gem-turn:has(> .gem-turn__side) > .gem-turn__front { opacity: 0; transform: scale(0.9); }",
|
|
2283
|
+
".gem-turn:not(:has(> .gem-turn__side)) > .gem-turn__front { transform: perspective(900px) rotateY(-74deg) scale(0.86); }",
|
|
2284
|
+
".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%); }",
|
|
2285
|
+
".gem-turn__sheet + .gem-turn__sheet { opacity: 0; }"
|
|
2286
|
+
];
|
|
2287
|
+
function scopeMotionCss(css, scope) {
|
|
2288
|
+
const roots = /^\.gem-(?:orbit|rail)(?![\w-])/;
|
|
2289
|
+
const prefix = (list) => list.split(/,(?![^(]*\))/).map((one) => one.trim()).flatMap((one) => roots.test(one) ? [`${scope} ${one}`, `${scope}${one}`] : [`${scope} ${one}`]).join(", ");
|
|
2290
|
+
let out = "";
|
|
2291
|
+
let prelude = "";
|
|
2292
|
+
const stack = [];
|
|
2293
|
+
for (const ch of css) {
|
|
2294
|
+
if (ch === "{") {
|
|
2295
|
+
const head = prelude.trim();
|
|
2296
|
+
const atRule = head.startsWith("@");
|
|
2297
|
+
out += prelude.slice(0, prelude.length - prelude.trimStart().length) + (atRule ? head : prefix(head)) + " {";
|
|
2298
|
+
stack.push(atRule);
|
|
2299
|
+
prelude = "";
|
|
2300
|
+
} else if (ch === "}") {
|
|
2301
|
+
out += prelude + "}";
|
|
2302
|
+
prelude = "";
|
|
2303
|
+
stack.pop();
|
|
2304
|
+
} else if (stack.length && stack[stack.length - 1] === false) {
|
|
2305
|
+
out += ch;
|
|
2306
|
+
} else {
|
|
2307
|
+
prelude += ch;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
return out + prelude;
|
|
2311
|
+
}
|
|
2312
|
+
var GEM_ORBIT_BASE_CSS = GEM_ORBIT_BASE_CSS_LINES.join("\n") + "\n";
|
|
2313
|
+
var GEM_RAIL_BASE_CSS = GEM_RAIL_BASE_CSS_LINES.join("\n") + "\n";
|
|
2314
|
+
var gemOrbitBaseCss = (scope) => scopeMotionCss(GEM_ORBIT_BASE_CSS, scope);
|
|
2315
|
+
var gemRailBaseCss = (scope) => scopeMotionCss(GEM_RAIL_BASE_CSS, scope);
|
|
2316
|
+
var GEM_ORBIT_RUNTIME_CSS = [
|
|
2317
|
+
".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); }",
|
|
2318
|
+
".gem-orbit[data-gem-state] .gem-orbit__stage { touch-action: pan-y; cursor: grab; }",
|
|
2319
|
+
".gem-orbit[data-gem-state='dragging'] .gem-orbit__stage { cursor: grabbing; }",
|
|
2320
|
+
".gem-orbit[data-gem-state] .gem-orbit__item { will-change: transform; }",
|
|
2321
|
+
".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); }",
|
|
2322
|
+
".gem-orbit[data-gem-state] .gem-orbit__bar { transition: transform var(--gem-motion-slow, 0.4s) var(--gem-ease-soft, ease); }",
|
|
2323
|
+
".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; }",
|
|
2324
|
+
".gem-orbit[data-gem-state='open'] .gem-orbit__item:not([aria-current='true']) .gem-orbit__hit { opacity: 0; transform: scale(0.92); }",
|
|
2325
|
+
"@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; } }"
|
|
2326
|
+
].join("\n") + "\n";
|
|
2327
|
+
var GEM_RAIL_RUNTIME_CSS = [
|
|
2328
|
+
".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); }",
|
|
2329
|
+
".gem-rail[data-gem-state] .gem-rail__item { will-change: transform; }",
|
|
2330
|
+
// Unturned neighbours' hit boxes overlap on the rail: once bound, a click on the stage goes to the piece whose slot it is in
|
|
2331
|
+
// (motion/rail.js), so the hits take no pointer events; keyboard focus and Enter stay on them. The phone row keeps its own.
|
|
2332
|
+
".gem-rail[data-gem-state] .gem-rail__stage { cursor: pointer; }",
|
|
2333
|
+
".gem-rail[data-gem-state] .gem-rail__hit { pointer-events: none; transition: opacity var(--gem-motion-base, 0.35s) var(--gem-ease-soft, ease); }",
|
|
2334
|
+
"@container (max-width: 699px) { .gem-rail[data-gem-state] .gem-rail__hit { pointer-events: auto; } }",
|
|
2335
|
+
".gem-rail[data-gem-state='open'] .gem-rail__hit { opacity: 0.15; }",
|
|
2336
|
+
"@media (prefers-reduced-motion: reduce) { .gem-rail, .gem-rail[data-gem-state] .gem-rail__hit { transition: none; } }"
|
|
2337
|
+
].join("\n") + "\n";
|
|
2338
|
+
var GEM_QUICK_VIEW_CSS = [
|
|
2339
|
+
"[data-gem-quick-view-trigger] { --gem-quick-view-css: 1; }",
|
|
2340
|
+
".gem-quick-view__shell { display: contents; }",
|
|
2341
|
+
".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; }",
|
|
2342
|
+
".gem-quick-view__surface {",
|
|
2343
|
+
" position: absolute; inset: 0; z-index: -1; border-radius: inherit; box-shadow: var(--gem-shadow-md);",
|
|
2344
|
+
" background: color-mix(in srgb, var(--gem-motion-tint, var(--gem-color-background)) 45%, var(--gem-color-background));",
|
|
2345
|
+
" transition: background-color var(--gem-motion-slow, 0.5s) var(--gem-ease-soft, ease);",
|
|
2346
|
+
"}",
|
|
2347
|
+
".gem-quick-view__shell .gem-quick-view__media { overflow: visible; background: transparent; min-height: 0; }",
|
|
2348
|
+
".gem-quick-view__image { position: absolute; inset: 6% 4%; width: 92%; height: 88%; object-fit: contain; transform-origin: 50% 50%; will-change: transform; }",
|
|
2349
|
+
".gem-quick-view__image--next { opacity: 0; pointer-events: none; }",
|
|
2350
|
+
".gem-quick-view__image--next:not([src]) { display: none; }",
|
|
2351
|
+
".gem-quick-view__shell .gem-quick-view__panel { justify-content: flex-start; gap: var(--gem-space-sm); }",
|
|
2352
|
+
".gem-quick-view__shell [hidden] { display: none; }",
|
|
2353
|
+
".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; }",
|
|
2354
|
+
".gem-quick-view__icon {",
|
|
2355
|
+
" display: inline-grid; place-items: center; width: var(--gem-tap-min); height: var(--gem-tap-min); padding: 0; cursor: pointer;",
|
|
2356
|
+
" border: var(--gem-stroke, 1px) solid color-mix(in srgb, currentColor 25%, transparent); border-radius: var(--gem-radius-pill);",
|
|
2357
|
+
" color: inherit; background: transparent; font: inherit; font-size: var(--gem-h5); line-height: 1;",
|
|
2358
|
+
"}",
|
|
2359
|
+
".gem-quick-view__icon:hover { border-color: currentColor; }",
|
|
2360
|
+
".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); }",
|
|
2361
|
+
".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; }",
|
|
2362
|
+
".gem-quick-view__title { margin: 0; font-family: var(--gem-font-heading); font-size: var(--gem-h2); line-height: 1.05; }",
|
|
2363
|
+
".gem-quick-view__price { margin: 0; font-size: var(--gem-h5); font-variant-numeric: tabular-nums; }",
|
|
2364
|
+
".gem-quick-view__compare { margin: calc(-1 * var(--gem-space-xs)) 0 0; color: var(--gem-color-muted); font-size: var(--gem-text-sm); }",
|
|
2365
|
+
".gem-quick-view__swatches, .gem-quick-view__sizes { display: flex; flex-wrap: wrap; gap: var(--gem-space-xs); }",
|
|
2366
|
+
".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; }",
|
|
2367
|
+
".gem-quick-view__swatch::before {",
|
|
2368
|
+
" content: ''; position: absolute; inset: 8px; border-radius: var(--gem-radius-pill); background: var(--gem-quick-view-swatch, var(--gem-color-surface));",
|
|
2369
|
+
" box-shadow: inset 0 0 0 var(--gem-stroke, 1px) color-mix(in srgb, var(--gem-color-text) 15%, transparent);",
|
|
2370
|
+
"}",
|
|
2371
|
+
".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); }",
|
|
2372
|
+
".gem-quick-view__size {",
|
|
2373
|
+
" 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;",
|
|
2374
|
+
" border: var(--gem-stroke, 1px) solid color-mix(in srgb, currentColor 22%, transparent); border-radius: var(--gem-radius-pill);",
|
|
2375
|
+
" color: inherit; background: transparent; font: inherit; font-size: var(--gem-text-sm);",
|
|
2376
|
+
"}",
|
|
2377
|
+
".gem-quick-view__size[aria-checked='true'] { color: var(--gem-color-background); background: var(--gem-color-text); border-color: var(--gem-color-text); }",
|
|
2378
|
+
".gem-quick-view__size:disabled { opacity: 0.4; text-decoration: line-through; cursor: not-allowed; }",
|
|
2379
|
+
".gem-quick-view__add { width: 100%; margin: var(--gem-space-xs) 0 0; font-variant-numeric: tabular-nums; }",
|
|
2380
|
+
".gem-quick-view__status { margin: 0; min-height: 1.2em; color: var(--gem-color-muted); font-size: var(--gem-text-sm); }",
|
|
2381
|
+
".gem-quick-view__description { margin: 0; max-width: 46ch; font-size: var(--gem-text-sm); line-height: 1.55; }",
|
|
2382
|
+
".gem-quick-view__details { color: inherit; font-size: var(--gem-text-sm); text-underline-offset: 3px; }",
|
|
2383
|
+
// The photo the panel flew out of stays hidden in the page while the panel shows it.
|
|
2384
|
+
"[data-gem-quick-view-source] { visibility: hidden; }",
|
|
2385
|
+
"@keyframes gem-quick-view-fade { from { opacity: 0; } to { opacity: 1; } }",
|
|
2386
|
+
".gem-quick-view__dialog[data-gem-state='open']::backdrop { animation: gem-quick-view-fade var(--gem-motion-base, 0.35s) ease both; }",
|
|
2387
|
+
".gem-quick-view__dialog[data-gem-state='closing']::backdrop { animation: gem-quick-view-fade var(--gem-motion-base, 0.35s) ease reverse both; }",
|
|
2388
|
+
"@media (max-width: 749px) { .gem-quick-view__shell .gem-quick-view__media { height: 34dvh; } }",
|
|
2389
|
+
"@media (prefers-reduced-motion: reduce) { .gem-quick-view__dialog::backdrop { animation: none; } .gem-quick-view__surface { transition: none; } }"
|
|
2390
|
+
].join("\n") + "\n";
|
|
2391
|
+
var GEM_MOTION_CLASSES = [
|
|
2392
|
+
...["", "__stage", "__ring", "__item", "__hit", "__img", "__caption", "__name", "__price", "__dock", "__arrow", "__count", "__track", "__bar", "__live"].map((part) => `gem-orbit${part}`),
|
|
2393
|
+
...["", "__stage", "__rod", "__track", "__item", "__hit", "__body", "__hook", "__tag", "__caption"].map((part) => `gem-rail${part}`),
|
|
2394
|
+
...["", "__side", "__front", "__sheet"].map((part) => `gem-turn${part}`),
|
|
2395
|
+
...[
|
|
2396
|
+
"shell",
|
|
2397
|
+
"dialog",
|
|
2398
|
+
"frame",
|
|
2399
|
+
"surface",
|
|
2400
|
+
"close",
|
|
2401
|
+
"media",
|
|
2402
|
+
"image",
|
|
2403
|
+
"image--next",
|
|
2404
|
+
"panel",
|
|
2405
|
+
"bar",
|
|
2406
|
+
"icon",
|
|
2407
|
+
"count",
|
|
2408
|
+
"eyebrow",
|
|
2409
|
+
"title",
|
|
2410
|
+
"price",
|
|
2411
|
+
"compare",
|
|
2412
|
+
"swatches",
|
|
2413
|
+
"swatch",
|
|
2414
|
+
"sizes",
|
|
2415
|
+
"size",
|
|
2416
|
+
"add",
|
|
2417
|
+
"status",
|
|
2418
|
+
"description",
|
|
2419
|
+
"details"
|
|
2420
|
+
].map((part) => `gem-quick-view__${part}`)
|
|
2421
|
+
];
|
|
2422
|
+
var GEM_MOTION_RUNTIME_FILES = ["motion/core.js", "motion/orbit.js", "motion/rail.js", "motion/quick-view.js"];
|
|
2423
|
+
var gemMotionEffectList = [orbitEffect, railEffect, quickViewEffect];
|
|
2424
|
+
function gemMotionEffectManifest() {
|
|
2425
|
+
return gemMotionEffectList.map(({ key, hook, load, renderer, tier, fallback, budget, settings }) => ({
|
|
2426
|
+
key,
|
|
2427
|
+
hook,
|
|
2428
|
+
load,
|
|
2429
|
+
renderer,
|
|
2430
|
+
tier,
|
|
2431
|
+
fallback,
|
|
2432
|
+
budget,
|
|
2433
|
+
settings: Object.fromEntries(Object.entries(settings).map(([name, setting]) => [name, { ...setting, attribute: `data-gem-${key}-${name}`, ...setting.css ? { variable: `--gem-${key}-${name}` } : {} }]))
|
|
2434
|
+
}));
|
|
2435
|
+
}
|
|
2436
|
+
var compactCss = (css) => css.replace(/\s+/g, " ").replace(/\s*([{};])\s*/g, "$1").replace(/:\s+/g, ":").replace(/,\s+/g, ",").replace(/;}/g, "}").trim();
|
|
2437
|
+
var effectPluginLines = (effect, name, css, before = [], detail = "count") => [
|
|
2438
|
+
` registerPlugin('${effect.key}', ({ api }) => {`,
|
|
2439
|
+
...before,
|
|
2440
|
+
" const on = (el, type, fn, opt) => { if (canListen(el)) el.addEventListener(type, fn, opt); };",
|
|
2441
|
+
` const count = mountEffects(${name}, { window, document, on, mark, api, css: ${JSON.stringify(compactCss(css))} }, onUnmount);`,
|
|
2442
|
+
` api.emit('gem:${effect.key}:ready', { source: 'gem-runtime', ${detail} });`,
|
|
2443
|
+
" });"
|
|
2444
|
+
];
|
|
2445
|
+
var effectImports = (file, name) => ['import { mountEffects } from "../motion/core.js";', `import { ${name} } from "../motion/${file}.js";`];
|
|
2446
|
+
function gemMotionChunks(legacyQuickView) {
|
|
2447
|
+
return {
|
|
2448
|
+
orbit: { hook: orbitEffect.hook, when: orbitEffect.load, imports: effectImports("orbit", "orbitEffect"), body: effectPluginLines(orbitEffect, "orbitEffect", GEM_ORBIT_RUNTIME_CSS) },
|
|
2449
|
+
rail: { hook: railEffect.hook, when: railEffect.load, imports: effectImports("rail", "railEffect"), body: effectPluginLines(railEffect, "railEffect", GEM_RAIL_RUNTIME_CSS) },
|
|
2450
|
+
"quick-view": {
|
|
2451
|
+
hook: `${quickViewEffect.hook}, ${legacyQuickView.hook}`,
|
|
2452
|
+
when: quickViewEffect.load,
|
|
2453
|
+
imports: effectImports("quick-view", "quickViewEffect"),
|
|
2454
|
+
body: effectPluginLines(quickViewEffect, "quickViewEffect", GEM_QUICK_VIEW_CSS, legacyQuickView.lines, legacyQuickView.readyDetail)
|
|
2455
|
+
}
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// ../../packages/gem-framework/src/pod/core.js
|
|
2460
|
+
var FIELD = "[data-gem-personalize]";
|
|
2461
|
+
var SCOPES = ["[data-gem-personalize-scope]", '[data-gem-element="product"]', 'form[action*="/cart/add"]', "[data-gem-section-id]"];
|
|
2462
|
+
function scopeOf(el) {
|
|
2463
|
+
for (const selector of SCOPES) {
|
|
2464
|
+
const found = el && el.closest ? el.closest(selector) : null;
|
|
2465
|
+
if (found) return found;
|
|
2466
|
+
}
|
|
2467
|
+
return el && el.ownerDocument ? el.ownerDocument.documentElement : null;
|
|
2468
|
+
}
|
|
2469
|
+
var uploads = /* @__PURE__ */ new WeakMap();
|
|
2470
|
+
var attr = (el, name) => el && el.getAttribute ? el.getAttribute(name) : null;
|
|
2471
|
+
var trim = (value) => typeof value === "string" ? value.trim() : "";
|
|
2472
|
+
function fieldName(field) {
|
|
2473
|
+
const own = trim(attr(field, "data-gem-personalize-name"));
|
|
2474
|
+
if (own) return own;
|
|
2475
|
+
const control = field.querySelector && field.querySelector("input, select, textarea");
|
|
2476
|
+
return control ? trim(control.getAttribute("name")) : "";
|
|
2477
|
+
}
|
|
2478
|
+
function fieldType(field) {
|
|
2479
|
+
const own = trim(attr(field, "data-gem-personalize-type"));
|
|
2480
|
+
if (own) return own;
|
|
2481
|
+
const control = field.querySelector && field.querySelector("input, select, textarea");
|
|
2482
|
+
if (!control) return "text";
|
|
2483
|
+
if (control.tagName === "SELECT") return "select";
|
|
2484
|
+
if (control.tagName === "TEXTAREA") return "textarea";
|
|
2485
|
+
return control.type === "file" ? "upload" : control.type === "checkbox" ? "checkbox" : control.type === "radio" ? "swatch" : "text";
|
|
2486
|
+
}
|
|
2487
|
+
var controlsOf = (field) => Array.prototype.slice.call(field.querySelectorAll ? field.querySelectorAll("input, select, textarea") : []).filter((c) => c.type !== "hidden");
|
|
2488
|
+
var HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
2489
|
+
var hexColor = (value) => HEX.test(trim(value)) ? trim(value) : "";
|
|
2490
|
+
function readField(field) {
|
|
2491
|
+
const name = fieldName(field);
|
|
2492
|
+
const type = fieldType(field);
|
|
2493
|
+
const out = { name, type, value: "", color: "", preview: "", key: "" };
|
|
2494
|
+
const controls = controlsOf(field);
|
|
2495
|
+
if (type === "upload") {
|
|
2496
|
+
const state = uploads.get(field);
|
|
2497
|
+
if (state) Object.assign(out, { value: state.url || "", preview: state.preview || state.url || "", key: state.key || "" });
|
|
2498
|
+
return out;
|
|
2499
|
+
}
|
|
2500
|
+
if (type === "checkbox") {
|
|
2501
|
+
const box2 = controls.find((c) => c.type === "checkbox" && c.checked);
|
|
2502
|
+
if (box2) out.value = box2.value && box2.value !== "on" ? box2.value : "Yes";
|
|
2503
|
+
return out;
|
|
2504
|
+
}
|
|
2505
|
+
if (type === "select") {
|
|
2506
|
+
const select = controls.find((c) => c.tagName === "SELECT");
|
|
2507
|
+
const option = select && select.selectedIndex > -1 ? select.options[select.selectedIndex] : null;
|
|
2508
|
+
out.value = trim(select ? select.value : "");
|
|
2509
|
+
out.color = hexColor(attr(option, "data-gem-personalize-color"));
|
|
2510
|
+
return out;
|
|
2511
|
+
}
|
|
2512
|
+
const radios = controls.filter((c) => c.type === "radio");
|
|
2513
|
+
if (radios.length) {
|
|
2514
|
+
const picked = radios.find((c) => c.checked);
|
|
2515
|
+
if (picked) {
|
|
2516
|
+
out.value = trim(picked.value);
|
|
2517
|
+
const label = picked.closest ? picked.closest("label") : null;
|
|
2518
|
+
out.color = hexColor(attr(picked, "data-gem-personalize-color") || attr(label, "data-gem-personalize-color"));
|
|
2519
|
+
}
|
|
2520
|
+
return out;
|
|
2521
|
+
}
|
|
2522
|
+
const box = controls[0];
|
|
2523
|
+
out.value = trim(box ? box.value : "");
|
|
2524
|
+
return out;
|
|
2525
|
+
}
|
|
2526
|
+
function readScope(scope) {
|
|
2527
|
+
const values = {};
|
|
2528
|
+
const fields = scope && scope.querySelectorAll ? scope.querySelectorAll(FIELD) : [];
|
|
2529
|
+
Array.prototype.forEach.call(fields, (field) => {
|
|
2530
|
+
const read = readField(field);
|
|
2531
|
+
if (read.name) values[read.name] = field.hidden ? { ...read, value: "", color: "", preview: "" } : read;
|
|
2532
|
+
});
|
|
2533
|
+
return values;
|
|
2534
|
+
}
|
|
2535
|
+
var adopted = /* @__PURE__ */ new WeakMap();
|
|
2536
|
+
function adoptCss(el, key, sentinel, css) {
|
|
2537
|
+
const doc = el.ownerDocument;
|
|
2538
|
+
const view = doc && doc.defaultView;
|
|
2539
|
+
if (!css || !view) return false;
|
|
2540
|
+
let keys = adopted.get(doc);
|
|
2541
|
+
if (!keys) adopted.set(doc, keys = /* @__PURE__ */ new Set());
|
|
2542
|
+
if (keys.has(key)) return false;
|
|
2543
|
+
keys.add(key);
|
|
2544
|
+
try {
|
|
2545
|
+
if (view.getComputedStyle(el).getPropertyValue(sentinel).trim()) return false;
|
|
2546
|
+
const sheet = new view.CSSStyleSheet();
|
|
2547
|
+
sheet.replaceSync(css);
|
|
2548
|
+
doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, sheet];
|
|
2549
|
+
return true;
|
|
2550
|
+
} catch {
|
|
2551
|
+
return false;
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
// ../../packages/gem-framework/src/pod/personalize.js
|
|
2556
|
+
var PERSONALIZE_MESSAGES = {
|
|
2557
|
+
required: "Fill this in to add to cart.",
|
|
2558
|
+
type: "This file type is not accepted.",
|
|
2559
|
+
size: "This file is over {mb} MB.",
|
|
2560
|
+
failed: "The upload failed. Try again.",
|
|
2561
|
+
uploading: "Wait for the upload to finish.",
|
|
2562
|
+
unavailable: "Photo upload is not available."
|
|
2563
|
+
};
|
|
2564
|
+
var WHEN = /^\s*(!)?\s*([^!=|]+?)\s*(?:(!=|==|=)\s*(.+?))?\s*$/;
|
|
2565
|
+
function parsePersonalizeWhen(text2) {
|
|
2566
|
+
if (typeof text2 !== "string" || !text2.trim()) return null;
|
|
2567
|
+
const m = WHEN.exec(text2);
|
|
2568
|
+
if (!m) return null;
|
|
2569
|
+
const [, not, name, op, rhs] = m;
|
|
2570
|
+
if (!op) return { name: name.trim(), op: not ? "unset" : "set", values: [] };
|
|
2571
|
+
const values = rhs.split("|").map((v) => v.trim().toLowerCase()).filter(Boolean);
|
|
2572
|
+
if (not || !values.length) return null;
|
|
2573
|
+
return { name: name.trim(), op: op === "!=" ? "not" : "is", values };
|
|
2574
|
+
}
|
|
2575
|
+
function personalizeWhenHolds(cond, values) {
|
|
2576
|
+
const key = Object.keys(values).find((k) => k.toLowerCase() === cond.name.toLowerCase());
|
|
2577
|
+
const value = key ? String(values[key].value || "").trim().toLowerCase() : "";
|
|
2578
|
+
if (cond.op === "set") return value !== "";
|
|
2579
|
+
if (cond.op === "unset") return value === "";
|
|
2580
|
+
return cond.op === "is" ? cond.values.includes(value) : !cond.values.includes(value);
|
|
2581
|
+
}
|
|
2582
|
+
function fileAccepted(file, accept) {
|
|
2583
|
+
const tokens = String(accept || "").split(",").map((t) => t.trim().toLowerCase()).filter(Boolean);
|
|
2584
|
+
if (!tokens.length) return true;
|
|
2585
|
+
const type = String(file.type || "").toLowerCase();
|
|
2586
|
+
const name = String(file.name || "").toLowerCase();
|
|
2587
|
+
return tokens.some((t) => t.startsWith(".") ? name.endsWith(t) : t.endsWith("/*") ? type.startsWith(t.slice(0, -1)) : type === t);
|
|
2588
|
+
}
|
|
2589
|
+
var attr2 = (el, name) => el && el.getAttribute ? el.getAttribute(name) : null;
|
|
2590
|
+
var tail = (id) => String(id).split("/").pop();
|
|
2591
|
+
var LISTS = '[data-gem-element="product-related"], [data-gem-element="product-list"]';
|
|
2592
|
+
function mountPersonalize(env, onUnmount) {
|
|
2593
|
+
const { window: win, document: doc, on, mark, api } = env;
|
|
2594
|
+
const owned = /* @__PURE__ */ new Set();
|
|
2595
|
+
const objectUrls = /* @__PURE__ */ new Set();
|
|
2596
|
+
const aborts = /* @__PURE__ */ new Set();
|
|
2597
|
+
let styled = false;
|
|
2598
|
+
let alive = true;
|
|
2599
|
+
const scopeFields = (scope) => [...owned].filter((f) => f.isConnected && scopeOf(f) === scope);
|
|
2600
|
+
const isRequired = (field) => attr2(field, "data-gem-personalize-required") === "true" || controlsOf(field).some((c) => c.required);
|
|
2601
|
+
const message = (field, reason, fill = {}) => {
|
|
2602
|
+
const own = attr2(field, `data-gem-personalize-message-${reason}`) || attr2(scopeOf(field), `data-gem-personalize-message-${reason}`);
|
|
2603
|
+
return (own || PERSONALIZE_MESSAGES[reason] || "").replace(/\{(\w+)\}/g, (_, k) => k in fill ? String(fill[k]) : "");
|
|
2604
|
+
};
|
|
2605
|
+
const errorBox = (field) => {
|
|
2606
|
+
let box = field.querySelector("[data-gem-personalize-error]");
|
|
2607
|
+
if (!box) {
|
|
2608
|
+
box = doc.createElement("p");
|
|
2609
|
+
box.className = "gem-el-product-personalization__error";
|
|
2610
|
+
box.setAttribute("data-gem-personalize-error", "");
|
|
2611
|
+
box.setAttribute("role", "alert");
|
|
2612
|
+
field.appendChild(box);
|
|
2613
|
+
}
|
|
2614
|
+
if (!box.id) box.id = `gem-personalize-error-${Math.random().toString(36).slice(2, 9)}`;
|
|
2615
|
+
return box;
|
|
2616
|
+
};
|
|
2617
|
+
const showProblem = (field, reason, fill) => {
|
|
2618
|
+
const box = errorBox(field);
|
|
2619
|
+
box.textContent = message(field, reason, fill);
|
|
2620
|
+
box.hidden = false;
|
|
2621
|
+
field.setAttribute("data-gem-personalize-invalid", reason);
|
|
2622
|
+
controlsOf(field).forEach((c) => {
|
|
2623
|
+
c.setAttribute("aria-invalid", "true");
|
|
2624
|
+
const ids = (attr2(c, "aria-describedby") || "").split(/\s+/).filter(Boolean);
|
|
2625
|
+
if (!ids.includes(box.id)) c.setAttribute("aria-describedby", [...ids, box.id].join(" "));
|
|
2626
|
+
});
|
|
2627
|
+
};
|
|
2628
|
+
const clearProblem = (field) => {
|
|
2629
|
+
if (!field.hasAttribute("data-gem-personalize-invalid")) return;
|
|
2630
|
+
field.removeAttribute("data-gem-personalize-invalid");
|
|
2631
|
+
const box = field.querySelector("[data-gem-personalize-error]");
|
|
2632
|
+
if (box) {
|
|
2633
|
+
box.textContent = "";
|
|
2634
|
+
box.hidden = true;
|
|
2635
|
+
}
|
|
2636
|
+
controlsOf(field).forEach((c) => c.removeAttribute("aria-invalid"));
|
|
2637
|
+
};
|
|
2638
|
+
const emitChange = (field) => {
|
|
2639
|
+
const { name, value, type, color: color2, preview } = readField(field);
|
|
2640
|
+
field.dispatchEvent(new win.CustomEvent("gem:personalize:change", { bubbles: true, detail: { name, value, type, color: color2, preview } }));
|
|
2641
|
+
};
|
|
2642
|
+
const count = (field) => {
|
|
2643
|
+
const out = field.querySelector("[data-gem-personalize-count]");
|
|
2644
|
+
const box = controlsOf(field).find((c) => c.tagName === "TEXTAREA" || c.type === "text" || c.type === "");
|
|
2645
|
+
const max = Number(attr2(field, "data-gem-personalize-max")) || (box && box.maxLength > 0 ? box.maxLength : 0);
|
|
2646
|
+
if (!out || !box || !max) return;
|
|
2647
|
+
out.textContent = `${box.value.length}/${max}`;
|
|
2648
|
+
if (box.value.length >= max) out.setAttribute("data-gem-personalize-full", "");
|
|
2649
|
+
else out.removeAttribute("data-gem-personalize-full");
|
|
2650
|
+
};
|
|
2651
|
+
const applyConditions = (scope) => {
|
|
2652
|
+
const fields = scopeFields(scope).filter((f) => attr2(f, "data-gem-personalize-when"));
|
|
2653
|
+
for (let pass = 0; pass < 4; pass += 1) {
|
|
2654
|
+
const values = readScope(scope);
|
|
2655
|
+
let changed = false;
|
|
2656
|
+
for (const field of fields) {
|
|
2657
|
+
if (attr2(field, "data-gem-personalize-state") === "unavailable") continue;
|
|
2658
|
+
const cond = parsePersonalizeWhen(attr2(field, "data-gem-personalize-when"));
|
|
2659
|
+
if (!cond) {
|
|
2660
|
+
field.setAttribute("data-gem-personalize-when-invalid", "");
|
|
2661
|
+
continue;
|
|
2662
|
+
}
|
|
2663
|
+
const show = personalizeWhenHolds(cond, values);
|
|
2664
|
+
if (field.hidden === !show) continue;
|
|
2665
|
+
field.hidden = !show;
|
|
2666
|
+
controlsOf(field).forEach((c) => {
|
|
2667
|
+
c.disabled = !show;
|
|
2668
|
+
});
|
|
2669
|
+
if (!show) clearProblem(field);
|
|
2670
|
+
changed = true;
|
|
2671
|
+
}
|
|
2672
|
+
if (!changed) break;
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
const claim = (field) => {
|
|
2676
|
+
if (!mark(field, "personalize")) return false;
|
|
2677
|
+
owned.add(field);
|
|
2678
|
+
if (!styled && env.css) {
|
|
2679
|
+
adoptCss(field, "personalize", "--gem-personalize-css", env.css);
|
|
2680
|
+
styled = true;
|
|
2681
|
+
}
|
|
2682
|
+
const max = Number(attr2(field, "data-gem-personalize-max"));
|
|
2683
|
+
controlsOf(field).forEach((c) => {
|
|
2684
|
+
if (max > 0 && (c.tagName === "TEXTAREA" || c.type === "text") && !(c.maxLength > 0)) c.maxLength = max;
|
|
2685
|
+
});
|
|
2686
|
+
count(field);
|
|
2687
|
+
if (fieldType(field) === "upload" && !endpointOf(field)) unavailable(field);
|
|
2688
|
+
return true;
|
|
2689
|
+
};
|
|
2690
|
+
const claimAll = () => {
|
|
2691
|
+
const fresh = Array.prototype.filter.call(doc.querySelectorAll(FIELD), claim);
|
|
2692
|
+
new Set(fresh.map(scopeOf)).forEach(applyConditions);
|
|
2693
|
+
return fresh.length;
|
|
2694
|
+
};
|
|
2695
|
+
const forget = (field) => {
|
|
2696
|
+
const state = uploads.get(field);
|
|
2697
|
+
if (state && state.preview && objectUrls.delete(state.preview) && win.URL && win.URL.revokeObjectURL) win.URL.revokeObjectURL(state.preview);
|
|
2698
|
+
uploads.delete(field);
|
|
2699
|
+
field.removeAttribute("data-gem-personalize-state");
|
|
2700
|
+
const thumb = field.querySelector("[data-gem-personalize-thumb]");
|
|
2701
|
+
if (thumb) {
|
|
2702
|
+
thumb.removeAttribute("src");
|
|
2703
|
+
thumb.hidden = true;
|
|
2704
|
+
}
|
|
2705
|
+
};
|
|
2706
|
+
const endpointOf = (field) => {
|
|
2707
|
+
const gem = win.GemTheme || {};
|
|
2708
|
+
const url = attr2(field, "data-gem-personalize-endpoint") || attr2(scopeOf(field), "data-gem-personalize-endpoint") || api && api.uploadEndpoint || gem.uploadEndpoint;
|
|
2709
|
+
return typeof url === "string" && url.trim() ? url.trim() : "";
|
|
2710
|
+
};
|
|
2711
|
+
const unavailable = (field) => {
|
|
2712
|
+
forget(field);
|
|
2713
|
+
clearProblem(field);
|
|
2714
|
+
field.setAttribute("data-gem-personalize-state", "unavailable");
|
|
2715
|
+
field.hidden = true;
|
|
2716
|
+
controlsOf(field).forEach((c) => {
|
|
2717
|
+
c.disabled = true;
|
|
2718
|
+
});
|
|
2719
|
+
emitChange(field);
|
|
2720
|
+
};
|
|
2721
|
+
const json = (res) => res && typeof res.json === "function" ? res.json().catch(() => ({})) : Promise.resolve({});
|
|
2722
|
+
const failure = (reason, extra = {}) => Object.assign(new Error(reason), { reason, ...extra });
|
|
2723
|
+
const send = (endpoint, field, file, mb, signal) => {
|
|
2724
|
+
const fetch = (url, init) => win.fetch(url, { ...init, ...signal ? { signal } : {} });
|
|
2725
|
+
return fetch(endpoint, { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json", accept: "application/json" }, body: JSON.stringify({ type: file.type, size: file.size, max_mb: mb }) }).then((res) => json(res).then((answer) => {
|
|
2726
|
+
if (res.status === 404) throw failure("unavailable");
|
|
2727
|
+
if (res.status === 415) throw failure("type");
|
|
2728
|
+
if (res.status === 413) throw failure("size", { mb: Number(answer.max_mb) || mb });
|
|
2729
|
+
if (!res.ok) throw failure("failed");
|
|
2730
|
+
const put = answer && answer.upload;
|
|
2731
|
+
if (put && typeof put.url === "string") {
|
|
2732
|
+
const headers = put.headers && typeof put.headers === "object" ? put.headers : {};
|
|
2733
|
+
return fetch(put.url, { method: typeof put.method === "string" ? put.method : "PUT", headers, body: file, credentials: "omit" }).then((done) => {
|
|
2734
|
+
if (!done || !done.ok) throw failure("failed");
|
|
2735
|
+
return answer;
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
const body = new win.FormData();
|
|
2739
|
+
body.append("file", file, file.name);
|
|
2740
|
+
body.append("name", fieldName(field));
|
|
2741
|
+
return fetch(endpoint, { method: "POST", body, credentials: "same-origin" }).then((res2) => {
|
|
2742
|
+
if (!res2 || !res2.ok) throw failure(res2 && res2.status === 404 ? "unavailable" : "failed");
|
|
2743
|
+
return json(res2);
|
|
2744
|
+
});
|
|
2745
|
+
}));
|
|
2746
|
+
};
|
|
2747
|
+
const upload = (field, input) => {
|
|
2748
|
+
forget(field);
|
|
2749
|
+
clearProblem(field);
|
|
2750
|
+
const file = input.files && input.files[0];
|
|
2751
|
+
if (!file) {
|
|
2752
|
+
emitChange(field);
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
const accept = attr2(field, "data-gem-personalize-accept") || attr2(input, "accept") || "image/*";
|
|
2756
|
+
const mb = Number(attr2(field, "data-gem-personalize-max-mb")) || 10;
|
|
2757
|
+
const endpoint = endpointOf(field);
|
|
2758
|
+
const refuse = (reason, fill = { mb }) => {
|
|
2759
|
+
input.value = "";
|
|
2760
|
+
showProblem(field, reason, fill);
|
|
2761
|
+
emitChange(field);
|
|
2762
|
+
};
|
|
2763
|
+
if (!fileAccepted(file, accept)) return refuse("type");
|
|
2764
|
+
if (file.size > mb * 1048576) return refuse("size");
|
|
2765
|
+
if (!endpoint || typeof win.fetch !== "function") return unavailable(field);
|
|
2766
|
+
const preview = win.URL && win.URL.createObjectURL ? win.URL.createObjectURL(file) : "";
|
|
2767
|
+
if (preview) objectUrls.add(preview);
|
|
2768
|
+
const state = { uploading: true, preview, url: "", key: "" };
|
|
2769
|
+
uploads.set(field, state);
|
|
2770
|
+
field.setAttribute("data-gem-personalize-state", "uploading");
|
|
2771
|
+
const thumb = field.querySelector("[data-gem-personalize-thumb]");
|
|
2772
|
+
if (thumb && preview) {
|
|
2773
|
+
thumb.src = preview;
|
|
2774
|
+
thumb.hidden = false;
|
|
2775
|
+
}
|
|
2776
|
+
emitChange(field);
|
|
2777
|
+
const abort = win.AbortController ? new win.AbortController() : null;
|
|
2778
|
+
if (abort) aborts.add(abort);
|
|
2779
|
+
send(endpoint, field, file, mb, abort && abort.signal).then((answer) => {
|
|
2780
|
+
const url = answer && typeof answer.url === "string" && /^https?:\/\//i.test(answer.url) ? answer.url : "";
|
|
2781
|
+
if (!url) throw failure("failed");
|
|
2782
|
+
if (uploads.get(field) !== state) return;
|
|
2783
|
+
Object.assign(state, { uploading: false, url, key: typeof answer.key === "string" ? answer.key : "" });
|
|
2784
|
+
field.setAttribute("data-gem-personalize-state", "done");
|
|
2785
|
+
clearProblem(field);
|
|
2786
|
+
emitChange(field);
|
|
2787
|
+
}).catch((error) => {
|
|
2788
|
+
if (uploads.get(field) !== state || abort && abort.signal.aborted) return;
|
|
2789
|
+
const reason = error && error.reason;
|
|
2790
|
+
if (reason === "unavailable") return unavailable(field);
|
|
2791
|
+
forget(field);
|
|
2792
|
+
refuse(reason === "type" || reason === "size" ? reason : "failed", { mb: error && error.mb || mb });
|
|
2793
|
+
}).finally(() => {
|
|
2794
|
+
if (abort) aborts.delete(abort);
|
|
2795
|
+
});
|
|
2796
|
+
};
|
|
2797
|
+
const onEdit = (event) => {
|
|
2798
|
+
const field = event.target && event.target.closest ? event.target.closest(FIELD) : null;
|
|
2799
|
+
if (!field || !owned.has(field)) return;
|
|
2800
|
+
const type = fieldType(field);
|
|
2801
|
+
const typing = type === "text" || type === "textarea";
|
|
2802
|
+
if (event.type === "input" ? !typing : typing) return;
|
|
2803
|
+
if (type === "upload") {
|
|
2804
|
+
if (event.target.type === "file") upload(field, event.target);
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2807
|
+
count(field);
|
|
2808
|
+
if (readField(field).value) clearProblem(field);
|
|
2809
|
+
applyConditions(scopeOf(field));
|
|
2810
|
+
emitChange(field);
|
|
2811
|
+
};
|
|
2812
|
+
const problems = (scope) => scopeFields(scope).filter((f) => !f.hidden).flatMap((field) => {
|
|
2813
|
+
const state = uploads.get(field);
|
|
2814
|
+
if (state && state.uploading) return [[field, "uploading"]];
|
|
2815
|
+
return isRequired(field) && !readField(field).value ? [[field, "required"]] : [];
|
|
2816
|
+
});
|
|
2817
|
+
const allowed = (scope) => {
|
|
2818
|
+
applyConditions(scope);
|
|
2819
|
+
const found = problems(scope);
|
|
2820
|
+
scopeFields(scope).forEach((f) => {
|
|
2821
|
+
if (!found.some(([p]) => p === f)) clearProblem(f);
|
|
2822
|
+
});
|
|
2823
|
+
if (!found.length) return true;
|
|
2824
|
+
found.forEach(([field, reason]) => showProblem(field, reason));
|
|
2825
|
+
const [first] = found[0];
|
|
2826
|
+
const control = controlsOf(first)[0];
|
|
2827
|
+
if (control && control.focus) control.focus({ preventScroll: true });
|
|
2828
|
+
if (first.scrollIntoView) first.scrollIntoView({ block: "center" });
|
|
2829
|
+
scope.dispatchEvent(new win.CustomEvent("gem:personalize:blocked", { bubbles: true, detail: { names: found.map(([f]) => fieldName(f)), reasons: found.map(([, r]) => r) } }));
|
|
2830
|
+
return false;
|
|
2831
|
+
};
|
|
2832
|
+
const properties = (scope) => scopeFields(scope).filter((f) => !f.hidden).map(readField).filter((r) => r.name && r.value).flatMap((r) => [
|
|
2833
|
+
{ key: r.name, value: r.value },
|
|
2834
|
+
...r.type === "upload" && r.key ? [{ key: `_${r.name}_upload`, value: r.key }] : []
|
|
2835
|
+
]);
|
|
2836
|
+
const merge = (list, add) => [...Array.isArray(list) ? list.filter((p) => !add.some((q) => p && q.key === p.key)) : [], ...add];
|
|
2837
|
+
const variantOf = (scope) => [scope, ...scope.querySelectorAll("[data-gem-variant-id]")].filter((el) => el === scope || scopeOf(el) === scope && !el.closest(LISTS)).map((el) => attr2(el, "data-gem-variant-id")).filter(Boolean).map(tail);
|
|
2838
|
+
const scopeOfAdd = (event) => {
|
|
2839
|
+
const t = event.target;
|
|
2840
|
+
if (t && t.nodeType === 1) {
|
|
2841
|
+
const scope = scopeOf(t);
|
|
2842
|
+
if (scopeFields(scope).length) return scope;
|
|
2843
|
+
}
|
|
2844
|
+
const detail = event.detail || {};
|
|
2845
|
+
const ids = [detail.variantId, ...Array.isArray(detail.lines) ? detail.lines.map((l) => l && l.variantId) : []].filter(Boolean).map(tail);
|
|
2846
|
+
if (!ids.length) return null;
|
|
2847
|
+
const scopes = [...new Set([...owned].filter((f) => f.isConnected).map(scopeOf))];
|
|
2848
|
+
return scopes.find((scope) => variantOf(scope).some((id) => ids.includes(id))) || null;
|
|
2849
|
+
};
|
|
2850
|
+
const onAdd = (event) => {
|
|
2851
|
+
const scope = scopeOfAdd(event);
|
|
2852
|
+
if (!scope) return;
|
|
2853
|
+
const detail = event.detail && typeof event.detail === "object" ? event.detail : null;
|
|
2854
|
+
if (!allowed(scope)) {
|
|
2855
|
+
event.stopImmediatePropagation();
|
|
2856
|
+
if (event.cancelable) event.preventDefault();
|
|
2857
|
+
win.setTimeout(() => win.dispatchEvent(new win.CustomEvent("gem:cart:add-failed", { detail: { variantId: detail && detail.variantId, reason: "personalization" } })), 0);
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2860
|
+
const add = properties(scope);
|
|
2861
|
+
if (!detail || !add.length) return;
|
|
2862
|
+
if (event.type === "gem:cart:add") {
|
|
2863
|
+
detail.properties = merge(detail.properties, add);
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
const lines = Array.isArray(detail.lines) ? detail.lines.filter((l) => l && typeof l === "object") : [];
|
|
2867
|
+
const ids = variantOf(scope);
|
|
2868
|
+
const mine = lines.filter((l) => ids.includes(tail(l.variantId)));
|
|
2869
|
+
(mine.length ? mine : lines).forEach((line) => {
|
|
2870
|
+
line.properties = merge(line.properties, add);
|
|
2871
|
+
});
|
|
2872
|
+
};
|
|
2873
|
+
const scopeOfForm = (form) => [...new Set([...owned].filter((f) => f.isConnected).map(scopeOf))].filter((s) => s.contains(form)).sort((a, b) => a.contains(b) ? 1 : -1)[0] || null;
|
|
2874
|
+
const onSubmit = (event) => {
|
|
2875
|
+
const form = event.target;
|
|
2876
|
+
if (!form || form.tagName !== "FORM") return;
|
|
2877
|
+
const scope = scopeOfForm(form);
|
|
2878
|
+
if (!scope) return;
|
|
2879
|
+
if (!allowed(scope)) {
|
|
2880
|
+
event.preventDefault();
|
|
2881
|
+
event.stopImmediatePropagation();
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
if (!form.hasAttribute("action")) return;
|
|
2885
|
+
form.querySelectorAll("[data-gem-personalize-hidden]").forEach((el) => el.remove());
|
|
2886
|
+
properties(scope).forEach(({ key, value }) => {
|
|
2887
|
+
const input = doc.createElement("input");
|
|
2888
|
+
Object.assign(input, { type: "hidden", name: `properties[${key}]`, value });
|
|
2889
|
+
input.setAttribute("data-gem-personalize-hidden", "");
|
|
2890
|
+
form.appendChild(input);
|
|
2891
|
+
});
|
|
2892
|
+
};
|
|
2893
|
+
const bound = claimAll();
|
|
2894
|
+
on(doc, "input", onEdit);
|
|
2895
|
+
on(doc, "change", onEdit);
|
|
2896
|
+
on(doc, "submit", onSubmit, true);
|
|
2897
|
+
on(win, "gem:cart:add", onAdd, true);
|
|
2898
|
+
on(win, "gem:cart:add-many", onAdd, true);
|
|
2899
|
+
if (typeof win.MutationObserver === "function") {
|
|
2900
|
+
let queued = false;
|
|
2901
|
+
const watch = new win.MutationObserver((records) => {
|
|
2902
|
+
if (queued || !records.some((r) => Array.prototype.some.call(r.addedNodes, (n) => n.nodeType === 1 && (n.matches(FIELD) || n.querySelector(FIELD))))) return;
|
|
2903
|
+
queued = true;
|
|
2904
|
+
Promise.resolve().then(() => {
|
|
2905
|
+
queued = false;
|
|
2906
|
+
if (alive) claimAll();
|
|
2907
|
+
});
|
|
2908
|
+
});
|
|
2909
|
+
watch.observe(doc.documentElement, { childList: true, subtree: true });
|
|
2910
|
+
onUnmount(() => watch.disconnect());
|
|
2911
|
+
}
|
|
2912
|
+
onUnmount(() => {
|
|
2913
|
+
alive = false;
|
|
2914
|
+
aborts.forEach((a) => a.abort());
|
|
2915
|
+
owned.forEach((field) => {
|
|
2916
|
+
if (field.hidden && (attr2(field, "data-gem-personalize-when") || attr2(field, "data-gem-personalize-state") === "unavailable")) {
|
|
2917
|
+
field.hidden = false;
|
|
2918
|
+
controlsOf(field).forEach((c) => {
|
|
2919
|
+
c.disabled = false;
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
forget(field);
|
|
2923
|
+
clearProblem(field);
|
|
2924
|
+
});
|
|
2925
|
+
owned.clear();
|
|
2926
|
+
});
|
|
2927
|
+
return bound;
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
// ../../packages/gem-framework/src/pod/print-preview.js
|
|
2931
|
+
var HOOK = "[data-gem-print-preview]";
|
|
2932
|
+
var attr3 = (el, name) => el && el.getAttribute ? el.getAttribute(name) || "" : "";
|
|
2933
|
+
function printPreviewContent(root, values) {
|
|
2934
|
+
const pick = (name) => {
|
|
2935
|
+
const key = name && Object.keys(values).find((k) => k.toLowerCase() === name.trim().toLowerCase());
|
|
2936
|
+
return key ? values[key] : null;
|
|
2937
|
+
};
|
|
2938
|
+
const text2 = pick(attr3(root, "data-gem-print-preview-text"));
|
|
2939
|
+
const photo = pick(attr3(root, "data-gem-print-preview-photo"));
|
|
2940
|
+
const spec = attr3(root, "data-gem-print-preview-color").trim();
|
|
2941
|
+
const from = /^from:/i.test(spec) ? pick(spec.slice(5)) : null;
|
|
2942
|
+
const photoUrl = photo ? photo.preview || photo.value : "";
|
|
2943
|
+
return {
|
|
2944
|
+
text: text2 ? String(text2.value || "") : "",
|
|
2945
|
+
photo: /^(?:https?:|blob:|data:image\/)/i.test(photoUrl) ? photoUrl : "",
|
|
2946
|
+
color: from ? from.color || hexColor(from.value) : hexColor(spec)
|
|
2947
|
+
};
|
|
2948
|
+
}
|
|
2949
|
+
function mountPrintPreview(env, onUnmount) {
|
|
2950
|
+
const { window: win, document: doc, on, mark } = env;
|
|
2951
|
+
let count = 0;
|
|
2952
|
+
doc.querySelectorAll(HOOK).forEach((root) => {
|
|
2953
|
+
if (!mark(root, "print-preview")) return;
|
|
2954
|
+
count += 1;
|
|
2955
|
+
if (env.css) adoptCss(root, "print-preview", "--gem-print-preview-css", env.css);
|
|
2956
|
+
const area = root.querySelector("[data-gem-print-preview-area]");
|
|
2957
|
+
if (!area) return;
|
|
2958
|
+
const scope = scopeOf(root);
|
|
2959
|
+
let textEl = null;
|
|
2960
|
+
let photoEl = null;
|
|
2961
|
+
const fit = () => {
|
|
2962
|
+
if (!textEl) return;
|
|
2963
|
+
textEl.style.transform = "";
|
|
2964
|
+
const room = area.clientWidth;
|
|
2965
|
+
const need = textEl.scrollWidth;
|
|
2966
|
+
if (room > 0 && need > room) textEl.style.transform = `scale(${Math.max(0.3, room / need).toFixed(3)})`;
|
|
2967
|
+
};
|
|
2968
|
+
const draw = () => {
|
|
2969
|
+
const { text: text2, photo, color: color2 } = printPreviewContent(root, readScope(scope));
|
|
2970
|
+
if (photo) {
|
|
2971
|
+
if (!photoEl) {
|
|
2972
|
+
photoEl = doc.createElement("img");
|
|
2973
|
+
photoEl.className = "gem-el-product-print-preview__photo";
|
|
2974
|
+
photoEl.alt = "";
|
|
2975
|
+
area.prepend(photoEl);
|
|
2976
|
+
}
|
|
2977
|
+
if (photoEl.getAttribute("src") !== photo) photoEl.src = photo;
|
|
2978
|
+
} else if (photoEl) {
|
|
2979
|
+
photoEl.remove();
|
|
2980
|
+
photoEl = null;
|
|
2981
|
+
}
|
|
2982
|
+
if (text2) {
|
|
2983
|
+
if (!textEl) {
|
|
2984
|
+
textEl = doc.createElement("p");
|
|
2985
|
+
textEl.className = "gem-el-product-print-preview__text";
|
|
2986
|
+
area.append(textEl);
|
|
2987
|
+
}
|
|
2988
|
+
textEl.textContent = text2;
|
|
2989
|
+
fit();
|
|
2990
|
+
} else if (textEl) {
|
|
2991
|
+
textEl.remove();
|
|
2992
|
+
textEl = null;
|
|
2993
|
+
}
|
|
2994
|
+
if (color2) root.style.setProperty("--gem-print-preview-color", color2);
|
|
2995
|
+
else root.style.removeProperty("--gem-print-preview-color");
|
|
2996
|
+
if (text2 || photo) root.setAttribute("data-gem-print-preview-state", "drawn");
|
|
2997
|
+
else root.removeAttribute("data-gem-print-preview-state");
|
|
2998
|
+
};
|
|
2999
|
+
on(scope, "gem:personalize:change", draw);
|
|
3000
|
+
if (typeof win.ResizeObserver === "function") new win.ResizeObserver(fit).observe(area);
|
|
3001
|
+
draw();
|
|
3002
|
+
onUnmount(() => {
|
|
3003
|
+
if (photoEl) photoEl.remove();
|
|
3004
|
+
if (textEl) textEl.remove();
|
|
3005
|
+
root.removeAttribute("data-gem-print-preview-state");
|
|
3006
|
+
root.style.removeProperty("--gem-print-preview-color");
|
|
3007
|
+
});
|
|
3008
|
+
});
|
|
3009
|
+
return count;
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
// ../../packages/gem-framework/src/pod/delivery-estimate.js
|
|
3013
|
+
var DAY = 864e5;
|
|
3014
|
+
function parseDayRange(value) {
|
|
3015
|
+
if (typeof value === "number") return Number.isFinite(value) && value >= 0 ? [Math.floor(value), Math.floor(value)] : null;
|
|
3016
|
+
const m = /^\s*(\d{1,3})\s*(?:[-–—]\s*(\d{1,3}))?\s*$/.exec(String(value == null ? "" : value));
|
|
3017
|
+
if (!m) return null;
|
|
3018
|
+
const min = Number(m[1]);
|
|
3019
|
+
const max = m[2] === void 0 ? min : Number(m[2]);
|
|
3020
|
+
return max >= min ? [min, max] : null;
|
|
3021
|
+
}
|
|
3022
|
+
var localDay = (now) => Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
|
|
3023
|
+
var weekend = (day) => {
|
|
3024
|
+
const d = new Date(day).getUTCDay();
|
|
3025
|
+
return d === 0 || d === 6;
|
|
3026
|
+
};
|
|
3027
|
+
var iso = (day) => new Date(day).toISOString().slice(0, 10);
|
|
3028
|
+
function addDays(day, n, business) {
|
|
3029
|
+
let at = day;
|
|
3030
|
+
let left = n;
|
|
3031
|
+
while (left > 0) {
|
|
3032
|
+
at += DAY;
|
|
3033
|
+
if (!business || !weekend(at)) left -= 1;
|
|
3034
|
+
}
|
|
3035
|
+
return at;
|
|
3036
|
+
}
|
|
3037
|
+
function storeHour(now, timeZone) {
|
|
3038
|
+
if (timeZone) {
|
|
3039
|
+
try {
|
|
3040
|
+
const part = new Intl.DateTimeFormat("en-US", { timeZone, hour: "numeric", hourCycle: "h23" }).formatToParts(now).find((p) => p.type === "hour");
|
|
3041
|
+
if (part) return Number(part.value) % 24;
|
|
3042
|
+
} catch {
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
return now.getHours();
|
|
3046
|
+
}
|
|
3047
|
+
function deliveryEstimate(now, spec) {
|
|
3048
|
+
const production = parseDayRange(spec.production);
|
|
3049
|
+
const shipping = parseDayRange(spec.shipping);
|
|
3050
|
+
if (!production || !shipping || !(now instanceof Date) || Number.isNaN(now.getTime())) return null;
|
|
3051
|
+
const business = spec.businessDays !== false;
|
|
3052
|
+
const today = localDay(now);
|
|
3053
|
+
let start = today;
|
|
3054
|
+
const cutoff = Number(spec.cutoffHour);
|
|
3055
|
+
if (spec.cutoffHour !== void 0 && spec.cutoffHour !== null && spec.cutoffHour !== "" && Number.isInteger(cutoff) && cutoff >= 0 && cutoff <= 23 && storeHour(now, spec.timeZone) >= cutoff) start += DAY;
|
|
3056
|
+
while (business && weekend(start)) start += DAY;
|
|
3057
|
+
const ships = [addDays(start, production[0], business), addDays(start, production[1], business)];
|
|
3058
|
+
const arrives = [addDays(ships[0], shipping[0], business), addDays(ships[1], shipping[1], business)];
|
|
3059
|
+
return { ordered: iso(today), start: iso(start), ships: ships.map(iso), arrives: arrives.map(iso) };
|
|
3060
|
+
}
|
|
3061
|
+
function formatDeliveryRange(from, to, locale) {
|
|
3062
|
+
const at = (d) => /* @__PURE__ */ new Date(`${d}T00:00:00Z`);
|
|
3063
|
+
let fmt;
|
|
3064
|
+
try {
|
|
3065
|
+
fmt = new Intl.DateTimeFormat(locale || void 0, { month: "short", day: "numeric", timeZone: "UTC" });
|
|
3066
|
+
} catch {
|
|
3067
|
+
fmt = new Intl.DateTimeFormat(void 0, { month: "short", day: "numeric", timeZone: "UTC" });
|
|
3068
|
+
}
|
|
3069
|
+
if (!to || to === from) return fmt.format(at(from));
|
|
3070
|
+
return typeof fmt.formatRange === "function" ? fmt.formatRange(at(from), at(to)) : `${fmt.format(at(from))} \u2013 ${fmt.format(at(to))}`;
|
|
3071
|
+
}
|
|
3072
|
+
var attr4 = (el, name) => (el.getAttribute(name) || "").trim();
|
|
3073
|
+
function mountDeliveryEstimate(env) {
|
|
3074
|
+
const { document: doc, mark } = env;
|
|
3075
|
+
let count = 0;
|
|
3076
|
+
doc.querySelectorAll("[data-gem-delivery-estimate]").forEach((root) => {
|
|
3077
|
+
if (!mark(root, "delivery-estimate")) return;
|
|
3078
|
+
const estimate = deliveryEstimate(/* @__PURE__ */ new Date(), {
|
|
3079
|
+
production: attr4(root, "data-gem-delivery-production"),
|
|
3080
|
+
shipping: attr4(root, "data-gem-delivery-shipping"),
|
|
3081
|
+
businessDays: attr4(root, "data-gem-delivery-business-days") !== "false",
|
|
3082
|
+
cutoffHour: attr4(root, "data-gem-delivery-cutoff-hour"),
|
|
3083
|
+
timeZone: attr4(root, "data-gem-delivery-timezone")
|
|
3084
|
+
});
|
|
3085
|
+
if (!estimate) return;
|
|
3086
|
+
const locale = attr4(root, "data-gem-delivery-locale") || doc.documentElement && doc.documentElement.lang || void 0;
|
|
3087
|
+
const ranges = { ordered: [estimate.ordered, estimate.ordered], ships: estimate.ships, arrives: estimate.arrives };
|
|
3088
|
+
root.querySelectorAll("[data-gem-delivery-date]").forEach((out) => {
|
|
3089
|
+
const range = ranges[attr4(out, "data-gem-delivery-date")];
|
|
3090
|
+
if (!range) return;
|
|
3091
|
+
out.textContent = formatDeliveryRange(range[0], range[1], locale);
|
|
3092
|
+
out.setAttribute("data-gem-delivery-from", range[0]);
|
|
3093
|
+
out.setAttribute("data-gem-delivery-to", range[1]);
|
|
3094
|
+
});
|
|
3095
|
+
root.setAttribute("data-gem-delivery-state", "local");
|
|
3096
|
+
count += 1;
|
|
3097
|
+
});
|
|
3098
|
+
return count;
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
// ../../packages/gem-framework/src/pod-engine.ts
|
|
3102
|
+
var P = ".gem-el-product-personalization";
|
|
3103
|
+
var V = ".gem-el-product-print-preview";
|
|
3104
|
+
var D = ".gem-el-product-delivery-estimate";
|
|
3105
|
+
var S = ".gem-el-product-design-switcher";
|
|
3106
|
+
var GEM_POD_BUYBOX_CSS_LINES = [
|
|
3107
|
+
// product-personalization: one field per element. The wrapper is the hook; a field whose condition fails is [hidden].
|
|
3108
|
+
`${P} { display: grid; gap: var(--gem-space-2xs); min-width: 0; }`,
|
|
3109
|
+
`${P}[hidden] { display: none; }`,
|
|
3110
|
+
`${P}__label { display: grid; gap: var(--gem-space-2xs); }`,
|
|
3111
|
+
`${P}__title { font-size: var(--gem-text-sm); font-weight: 600; }`,
|
|
3112
|
+
`${P}__required { color: var(--gem-color-danger); }`,
|
|
3113
|
+
`${P}__input { box-sizing: border-box; width: 100%; min-height: var(--gem-tap-min); padding: var(--gem-space-xs) var(--gem-space-sm); font: inherit; color: var(--gem-color-text); background: var(--gem-color-background); border: var(--gem-stroke, 1px) solid var(--gem-color-border); border-radius: var(--gem-radius-md); }`,
|
|
3114
|
+
`textarea${P}__input { min-height: calc(var(--gem-tap-min) * 2); resize: vertical; }`,
|
|
3115
|
+
`${P}__meta { display: flex; gap: var(--gem-space-xs); align-items: baseline; }`,
|
|
3116
|
+
`${P}__help { margin: 0; color: var(--gem-color-muted); font-size: var(--gem-text-xs); }`,
|
|
3117
|
+
`${P}__count { margin-left: auto; color: var(--gem-color-muted); font-size: var(--gem-text-xs); font-variant-numeric: tabular-nums; }`,
|
|
3118
|
+
// swatch and image-choice: radios inside labels; the radio stays focusable, its picture shows the state.
|
|
3119
|
+
`${P}__options { display: flex; flex-wrap: wrap; gap: var(--gem-space-xs); margin: 0; padding: 0; border: 0; min-width: 0; }`,
|
|
3120
|
+
`${P}__option { position: relative; display: inline-grid; justify-items: center; gap: var(--gem-space-2xs); font-size: var(--gem-text-xs); cursor: pointer; }`,
|
|
3121
|
+
`${P}__option input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }`,
|
|
3122
|
+
`${P}__swatch { display: block; width: var(--gem-tap-min); height: var(--gem-tap-min); border-radius: var(--gem-radius-pill); background: var(--gem-personalize-swatch, var(--gem-color-surface)); border: var(--gem-stroke, 1px) solid var(--gem-color-border); }`,
|
|
3123
|
+
`${P}__choice-image { display: block; width: calc(var(--gem-tap-min) * 1.75); aspect-ratio: 1; object-fit: cover; border-radius: var(--gem-radius-md); border: var(--gem-stroke, 1px) solid var(--gem-color-border); }`,
|
|
3124
|
+
`${P}__option input:checked ~ ${P}__swatch, ${P}__option input:checked ~ ${P}__choice-image { box-shadow: 0 0 0 var(--gem-stroke-thick, 2px) var(--gem-color-background), 0 0 0 calc(var(--gem-stroke-thick, 2px) * 2) var(--gem-color-text); }`,
|
|
3125
|
+
`${P}__option input:focus-visible ~ ${P}__swatch, ${P}__option input:focus-visible ~ ${P}__choice-image { outline: var(--gem-focus-ring); outline-offset: calc(var(--gem-focus-offset) + var(--gem-stroke-thick, 2px) * 2); }`,
|
|
3126
|
+
`${P}__checkbox { display: inline-flex; align-items: center; gap: var(--gem-space-xs); min-height: var(--gem-tap-min); cursor: pointer; }`,
|
|
3127
|
+
// upload: a dashed drop box around a visually hidden (still focusable) file input, a thumbnail once a photo is picked.
|
|
3128
|
+
`${P}__upload { position: relative; display: grid; justify-items: center; gap: var(--gem-space-2xs); padding: var(--gem-space-md); text-align: center; background: var(--gem-color-surface); border: var(--gem-stroke, 1px) dashed var(--gem-color-border); border-radius: var(--gem-radius-md); cursor: pointer; }`,
|
|
3129
|
+
`${P}__upload:focus-within { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); }`,
|
|
3130
|
+
`${P}__file { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }`,
|
|
3131
|
+
`${P}__thumb { display: block; width: calc(var(--gem-tap-min) * 2); aspect-ratio: 1; object-fit: cover; border-radius: var(--gem-radius-sm); }`,
|
|
3132
|
+
`${P}__thumb[hidden], ${P}__thumb:not([src]) { display: none; }`,
|
|
3133
|
+
`${P}__privacy { margin: 0; color: var(--gem-color-muted); font-size: var(--gem-text-xs); }`,
|
|
3134
|
+
`${P}__error { margin: 0; color: var(--gem-color-danger); font-size: var(--gem-text-xs); }`,
|
|
3135
|
+
`${P}__error:empty, ${P}__error[hidden] { display: none; }`,
|
|
3136
|
+
// product-print-preview: the product's current image, and the print area on it (percent of the image, from the element's
|
|
3137
|
+
// area_* props or the gemboss.print_area metafield). Empty until the shopper types or uploads.
|
|
3138
|
+
`${V} { position: relative; display: block; overflow: hidden; border-radius: var(--gem-media-radius, var(--gem-radius-md)); }`,
|
|
3139
|
+
`${V}__media { display: block; width: 100%; height: auto; }`,
|
|
3140
|
+
`${V}__area { position: absolute; left: calc(var(--gem-print-preview-x, 20) * 1%); top: calc(var(--gem-print-preview-y, 35) * 1%); width: calc(var(--gem-print-preview-w, 60) * 1%); height: calc(var(--gem-print-preview-h, 20) * 1%); display: grid; place-items: center; overflow: hidden; pointer-events: none; container-type: size; }`,
|
|
3141
|
+
`${V}[data-gem-print-preview-shape="circle"] ${V}__area { border-radius: 50%; }`,
|
|
3142
|
+
`${V}[data-gem-print-preview-font="body"] { --gem-print-preview-font: var(--gem-font-body); }`,
|
|
3143
|
+
`${V}[data-gem-print-preview-font="accent"] { --gem-print-preview-font: var(--gem-font-accent, var(--gem-font-heading)); }`,
|
|
3144
|
+
// product-delivery-estimate: three steps on a line (timeline), or one sentence (line). Dates are the server's for its today
|
|
3145
|
+
// until the plugin rewrites them in the shopper's.
|
|
3146
|
+
`${D} { display: grid; gap: var(--gem-space-xs); font-size: var(--gem-text-sm); }`,
|
|
3147
|
+
`${D}__steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gem-space-sm); margin: 0; padding: 0; list-style: none; }`,
|
|
3148
|
+
`${D}__step { position: relative; display: grid; justify-items: center; gap: var(--gem-space-2xs); text-align: center; }`,
|
|
3149
|
+
`${D}__step::before { content: ""; width: 0.75em; height: 0.75em; border-radius: var(--gem-radius-pill); background: var(--gem-color-accent); }`,
|
|
3150
|
+
`${D}__step + ${D}__step::after { content: ""; position: absolute; top: 0.375em; right: calc(50% + 0.75em); width: calc(100% - 1.5em + var(--gem-space-sm)); height: var(--gem-stroke, 1px); background: var(--gem-color-border); }`,
|
|
3151
|
+
`${D}__label { color: var(--gem-color-muted); font-size: var(--gem-text-xs); }`,
|
|
3152
|
+
`${D}__date { font-weight: 600; font-variant-numeric: tabular-nums; }`,
|
|
3153
|
+
`${D}__note { margin: 0; color: var(--gem-color-muted); font-size: var(--gem-text-xs); }`,
|
|
3154
|
+
`${D}[data-gem-delivery-style="line"] ${D}__steps { display: flex; flex-wrap: wrap; gap: var(--gem-space-2xs) var(--gem-space-sm); }`,
|
|
3155
|
+
`${D}[data-gem-delivery-style="line"] ${D}__step { display: inline-flex; gap: var(--gem-space-2xs); text-align: start; }`,
|
|
3156
|
+
`${D}[data-gem-delivery-style="line"] ${D}__step::before, ${D}[data-gem-delivery-style="line"] ${D}__step::after { content: none; }`,
|
|
3157
|
+
// product-design-switcher: one link per product that shares the design (pills, or cards with the sibling's photo).
|
|
3158
|
+
`${S} { display: grid; gap: var(--gem-space-xs); }`,
|
|
3159
|
+
`${S}__list { display: flex; flex-wrap: wrap; gap: var(--gem-space-xs); margin: 0; padding: 0; list-style: none; }`,
|
|
3160
|
+
`${S}__item { display: inline-flex; align-items: center; gap: var(--gem-space-2xs); min-height: var(--gem-tap-min); padding: var(--gem-space-2xs) var(--gem-space-sm); font-size: var(--gem-text-sm); color: var(--gem-color-text); text-decoration: none; background: var(--gem-color-background); border: var(--gem-stroke, 1px) solid var(--gem-color-border); border-radius: var(--gem-radius-pill); }`,
|
|
3161
|
+
`${S}__item:hover { border-color: var(--gem-color-text); }`,
|
|
3162
|
+
`${S}__item:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); }`,
|
|
3163
|
+
`${S}__item[aria-current="page"] { border-color: var(--gem-color-text); box-shadow: inset 0 0 0 var(--gem-stroke, 1px) var(--gem-color-text); }`,
|
|
3164
|
+
`${S}__price { color: var(--gem-color-muted); font-variant-numeric: tabular-nums; }`,
|
|
3165
|
+
`${S}__image { display: none; }`,
|
|
3166
|
+
`${S}[data-gem-design-switcher-style="cards"] ${S}__list { display: grid; grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr)); }`,
|
|
3167
|
+
`${S}[data-gem-design-switcher-style="cards"] ${S}__item { flex-direction: column; align-items: stretch; padding: var(--gem-space-xs); border-radius: var(--gem-radius-md); }`,
|
|
3168
|
+
`${S}[data-gem-design-switcher-style="cards"] ${S}__image { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: var(--gem-radius-sm); }`,
|
|
3169
|
+
`${S}[data-gem-design-switcher-style="cards"] ${S}__label { font-weight: 600; }`
|
|
3170
|
+
];
|
|
3171
|
+
var GEM_PERSONALIZE_RUNTIME_CSS_LINES = [
|
|
3172
|
+
`${P}[data-gem-personalize-invalid] ${P}__input, ${P}[data-gem-personalize-invalid] ${P}__upload, ${P}[data-gem-personalize-invalid] ${P}__swatch, ${P}[data-gem-personalize-invalid] ${P}__choice-image { border-color: var(--gem-color-danger); }`,
|
|
3173
|
+
`${P}[data-gem-personalize-state="uploading"] ${P}__upload { cursor: progress; opacity: 0.7; }`,
|
|
3174
|
+
`${P}__count[data-gem-personalize-full] { color: var(--gem-color-text); font-weight: 600; }`
|
|
3175
|
+
];
|
|
3176
|
+
var GEM_PRINT_PREVIEW_RUNTIME_CSS_LINES = [
|
|
3177
|
+
// The words: the theme font the element names, the colour it follows, as many lines as it allows, sized by the area's
|
|
3178
|
+
// height (container units); a word wider than the area is scaled down by the plugin with a transform, never re-laid out.
|
|
3179
|
+
`${V}__text { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: var(--gem-print-preview-lines, 2); max-width: 100%; margin: 0; overflow: hidden; overflow-wrap: anywhere; font-family: var(--gem-print-preview-font, var(--gem-font-heading)); font-size: calc(82cqh / var(--gem-print-preview-lines, 2)); line-height: 1.1; color: var(--gem-print-preview-color, var(--gem-color-text)); text-align: center; transform-origin: center; }`,
|
|
3180
|
+
`${V}[data-gem-print-preview-align="left"] ${V}__area { place-items: center start; }`,
|
|
3181
|
+
`${V}[data-gem-print-preview-align="left"] ${V}__text { text-align: start; transform-origin: left center; }`,
|
|
3182
|
+
`${V}[data-gem-print-preview-align="right"] ${V}__area { place-items: center end; }`,
|
|
3183
|
+
`${V}[data-gem-print-preview-align="right"] ${V}__text { text-align: end; transform-origin: right center; }`,
|
|
3184
|
+
`${V}__photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }`
|
|
3185
|
+
];
|
|
3186
|
+
var GEM_POD_BUYBOX_CSS = GEM_POD_BUYBOX_CSS_LINES.join("\n") + "\n";
|
|
3187
|
+
var GEM_PERSONALIZE_RUNTIME_CSS = GEM_PERSONALIZE_RUNTIME_CSS_LINES.join("\n") + "\n";
|
|
3188
|
+
var GEM_PRINT_PREVIEW_RUNTIME_CSS = GEM_PRINT_PREVIEW_RUNTIME_CSS_LINES.join("\n") + "\n";
|
|
3189
|
+
var GEM_POD_CLASSES = [
|
|
3190
|
+
...["", "__label", "__title", "__required", "__input", "__meta", "__help", "__count", "__options", "__option", "__swatch", "__choice-image", "__checkbox", "__upload", "__file", "__thumb", "__privacy", "__error"].map((part) => `gem-el-product-personalization${part}`),
|
|
3191
|
+
...["", "__media", "__area", "__text", "__photo"].map((part) => `gem-el-product-print-preview${part}`),
|
|
3192
|
+
...["", "__steps", "__step", "__label", "__date", "__note"].map((part) => `gem-el-product-delivery-estimate${part}`),
|
|
3193
|
+
...["", "__list", "__item", "__label", "__price", "__image"].map((part) => `gem-el-product-design-switcher${part}`)
|
|
3194
|
+
];
|
|
3195
|
+
var GEM_POD_CSS_VARIABLES = [
|
|
3196
|
+
"--gem-personalize-css",
|
|
3197
|
+
"--gem-personalize-swatch",
|
|
3198
|
+
"--gem-print-preview-css",
|
|
3199
|
+
"--gem-print-preview-x",
|
|
3200
|
+
"--gem-print-preview-y",
|
|
3201
|
+
"--gem-print-preview-w",
|
|
3202
|
+
"--gem-print-preview-h",
|
|
3203
|
+
"--gem-print-preview-lines",
|
|
3204
|
+
"--gem-print-preview-color",
|
|
3205
|
+
"--gem-print-preview-font",
|
|
3206
|
+
// No accent font token in gem-framework yet: a skin may set it, the preview falls back to the heading font.
|
|
3207
|
+
"--gem-font-accent"
|
|
3208
|
+
];
|
|
3209
|
+
var GEM_POD_RUNTIME_FILES = ["pod/core.js", "pod/personalize.js", "pod/print-preview.js", "pod/delivery-estimate.js"];
|
|
3210
|
+
var GEM_POD_PLUGINS = [
|
|
3211
|
+
{ key: "personalize", hook: "[data-gem-personalize]", when: "viewport", budget: { gzip: 6144 } },
|
|
3212
|
+
{ key: "print-preview", hook: "[data-gem-print-preview]", when: "interaction", budget: { gzip: 6144 } },
|
|
3213
|
+
{ key: "delivery-estimate", hook: "[data-gem-delivery-estimate]", when: "viewport", budget: { gzip: 6144 } }
|
|
3214
|
+
];
|
|
3215
|
+
var MOUNTS = {
|
|
3216
|
+
personalize: { name: "mountPersonalize", file: "personalize", css: GEM_PERSONALIZE_RUNTIME_CSS },
|
|
3217
|
+
"print-preview": { name: "mountPrintPreview", file: "print-preview", css: GEM_PRINT_PREVIEW_RUNTIME_CSS },
|
|
3218
|
+
"delivery-estimate": { name: "mountDeliveryEstimate", file: "delivery-estimate", css: "" }
|
|
3219
|
+
};
|
|
3220
|
+
function gemPodChunks() {
|
|
3221
|
+
const chunks = {};
|
|
3222
|
+
for (const { key, hook, when } of GEM_POD_PLUGINS) {
|
|
3223
|
+
const { name, file, css } = MOUNTS[key];
|
|
3224
|
+
chunks[key] = {
|
|
3225
|
+
hook,
|
|
3226
|
+
when,
|
|
3227
|
+
imports: [`import { ${name} } from "../pod/${file}.js";`],
|
|
3228
|
+
body: [
|
|
3229
|
+
` registerPlugin('${key}', ({ api }) => {`,
|
|
3230
|
+
" const on = (el, type, fn, opt) => { if (canListen(el)) el.addEventListener(type, fn, opt); };",
|
|
3231
|
+
` const count = ${name}({ window, document, on, mark, api${css ? `, css: ${JSON.stringify(compactCss(css))}` : ""} }, onUnmount);`,
|
|
3232
|
+
` api.emit('gem:${key}:ready', { source: 'gem-runtime', count });`,
|
|
3233
|
+
" });"
|
|
3234
|
+
]
|
|
3235
|
+
};
|
|
3236
|
+
}
|
|
3237
|
+
return chunks;
|
|
3238
|
+
}
|
|
3239
|
+
|
|
1053
3240
|
// ../../packages/gem-framework/src/manifest.ts
|
|
1054
3241
|
var gemFrameworkPrimitiveGroups = [
|
|
1055
3242
|
{
|
|
@@ -1245,6 +3432,19 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
1245
3432
|
"gem-popup-shell__teaser"
|
|
1246
3433
|
]
|
|
1247
3434
|
},
|
|
3435
|
+
{
|
|
3436
|
+
// The motion engine (motion-engine.ts, 0.19.0): the orbit, rail and turn markup a section draws, and the quick view panel
|
|
3437
|
+
// the quick-view plugin builds. Styled by their own base / runtime stylesheets, never by gem-framework.css.
|
|
3438
|
+
group: "motion",
|
|
3439
|
+
classes: [...GEM_MOTION_CLASSES]
|
|
3440
|
+
},
|
|
3441
|
+
{
|
|
3442
|
+
// The Boss Theme POD buy box (pod-engine.ts, 0.20.0): the four elements gemsite renders (personalization v2, print
|
|
3443
|
+
// preview, delivery estimate, design switcher) and what their plugins draw. Styled by dist/pod-buybox.css and the
|
|
3444
|
+
// personalize / print-preview chunks' own sheets, never by gem-framework.css.
|
|
3445
|
+
group: "pod",
|
|
3446
|
+
classes: [...GEM_POD_CLASSES]
|
|
3447
|
+
},
|
|
1248
3448
|
{
|
|
1249
3449
|
group: "sections",
|
|
1250
3450
|
classes: [
|
|
@@ -1399,9 +3599,11 @@ var gemFrameworkRuntimePlugins = [
|
|
|
1399
3599
|
events: ["gem:hero-slideshow:ready", "gem:hero-slideshow:change"]
|
|
1400
3600
|
},
|
|
1401
3601
|
{
|
|
3602
|
+
// 0.19.0: a lazy chunk with the motion engine's quick view; idle schedules only gem-runtime.js's stub, which imports
|
|
3603
|
+
// the chunk on the first interaction with a trigger (or at idle), never on a page without one.
|
|
1402
3604
|
key: "quick-view",
|
|
1403
|
-
load_strategy: "
|
|
1404
|
-
events: ["gem:quick-view:open", "gem:quick-view:close"]
|
|
3605
|
+
load_strategy: "idle",
|
|
3606
|
+
events: ["gem:quick-view:ready", "gem:quick-view:open", "gem:quick-view:close", "gem:cart:add"]
|
|
1405
3607
|
},
|
|
1406
3608
|
{
|
|
1407
3609
|
key: "promo-popup",
|
|
@@ -1467,6 +3669,43 @@ var gemFrameworkRuntimePlugins = [
|
|
|
1467
3669
|
key: "bundle",
|
|
1468
3670
|
load_strategy: "idle",
|
|
1469
3671
|
events: ["gem:bundle:ready", "gem:bundle:change", "gem:bundle:complete"]
|
|
3672
|
+
},
|
|
3673
|
+
{
|
|
3674
|
+
// idle, not interaction: the button it shows has to be there before anyone interacts.
|
|
3675
|
+
key: "size-guide",
|
|
3676
|
+
load_strategy: "idle",
|
|
3677
|
+
events: ["gem:size-guide:ready", "gem:size-guide:open"]
|
|
3678
|
+
},
|
|
3679
|
+
// The motion engine's effects (0.19.0): lazy chunks. idle schedules gem-runtime.js's stub, which imports the chunk when a
|
|
3680
|
+
// hook comes near the viewport and never on a page without one.
|
|
3681
|
+
{
|
|
3682
|
+
key: "orbit",
|
|
3683
|
+
load_strategy: "idle",
|
|
3684
|
+
events: ["gem:orbit:ready", "gem:motion:front", "gem:motion:fallback"]
|
|
3685
|
+
},
|
|
3686
|
+
{
|
|
3687
|
+
key: "rail",
|
|
3688
|
+
load_strategy: "idle",
|
|
3689
|
+
events: ["gem:rail:ready", "gem:motion:front", "gem:motion:fallback"]
|
|
3690
|
+
},
|
|
3691
|
+
// The Boss Theme POD buy box (0.20.0, pod-engine.ts): lazy chunks like the motion effects. idle schedules gem-runtime.js's
|
|
3692
|
+
// stub, which imports the chunk when its hook comes near (personalize, delivery-estimate) or is first touched (print-preview),
|
|
3693
|
+
// never on a page without one. personalize holds the product's add (gem:cart:add / add-many, a form submit) until every
|
|
3694
|
+
// required field is filled, and answers a held add with gem:cart:add-failed.
|
|
3695
|
+
{
|
|
3696
|
+
key: "personalize",
|
|
3697
|
+
load_strategy: "idle",
|
|
3698
|
+
events: ["gem:personalize:ready", "gem:personalize:change", "gem:personalize:blocked", "gem:cart:add-failed"]
|
|
3699
|
+
},
|
|
3700
|
+
{
|
|
3701
|
+
key: "print-preview",
|
|
3702
|
+
load_strategy: "idle",
|
|
3703
|
+
events: ["gem:print-preview:ready"]
|
|
3704
|
+
},
|
|
3705
|
+
{
|
|
3706
|
+
key: "delivery-estimate",
|
|
3707
|
+
load_strategy: "idle",
|
|
3708
|
+
events: ["gem:delivery-estimate:ready"]
|
|
1470
3709
|
}
|
|
1471
3710
|
];
|
|
1472
3711
|
var gemFrameworkValidationRules = [
|
|
@@ -1572,6 +3811,24 @@ var GEM_POPUP_SHELL_CSS_LINES = [
|
|
|
1572
3811
|
" .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); }",
|
|
1573
3812
|
" .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; }",
|
|
1574
3813
|
"}",
|
|
3814
|
+
// 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
|
|
3815
|
+
// 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
|
|
3816
|
+
// 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ừ đó
|
|
3817
|
+
// (--gem-motion-base / --gem-ease-enter, như ngăn giỏ của framework). Chỉ là bố cục desktop: mobile vẫn takeover / sheet.
|
|
3818
|
+
"@keyframes gem-popup-drawer-in { from { translate: calc(100% * var(--gem-popup-drawer-dir, 1)) 0; } }",
|
|
3819
|
+
".gem-popup-shell__dialog:dir(rtl) { --gem-popup-drawer-dir: -1; }",
|
|
3820
|
+
"@media (min-width: 750px) {",
|
|
3821
|
+
" .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__dialog {",
|
|
3822
|
+
" width: min(var(--gem-popup-drawer-width, 640px), 100vw); max-width: 100vw; height: 100dvh; max-height: 100dvh;",
|
|
3823
|
+
" margin-block: 0; margin-inline: auto 0; border-radius: 0;",
|
|
3824
|
+
" animation: gem-popup-drawer-in var(--gem-motion-base, 180ms) var(--gem-ease-enter, ease-out);",
|
|
3825
|
+
" }",
|
|
3826
|
+
" .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__frame { grid-template-columns: 1fr; align-content: start; min-height: 100%; }",
|
|
3827
|
+
" .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__media { max-height: 240px; }",
|
|
3828
|
+
" .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__panel { justify-content: flex-start; align-items: stretch; text-align: start; }",
|
|
3829
|
+
"}",
|
|
3830
|
+
// 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).
|
|
3831
|
+
"@media (prefers-reduced-motion: reduce) { .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__dialog { animation: none; } }",
|
|
1575
3832
|
"@media (max-width: 749px) {",
|
|
1576
3833
|
// Mobile: takeover is the whole screen; a sheet rises from the bottom to at most 88% of it. The image
|
|
1577
3834
|
// sits under the text (top when media=top), capped at a third of the screen; with no image or the image
|
|
@@ -1607,6 +3864,9 @@ var GEM_POPUP_SHELL_CSS_LINES = [
|
|
|
1607
3864
|
"@media (min-width: 750px) {",
|
|
1608
3865
|
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] { padding: 0; }",
|
|
1609
3866
|
" :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; }",
|
|
3867
|
+
// 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).
|
|
3868
|
+
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='drawer-end'] { padding-inline: 0; }",
|
|
3869
|
+
" :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; }",
|
|
1610
3870
|
"}",
|
|
1611
3871
|
"@media (max-width: 749px) {",
|
|
1612
3872
|
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'] { padding: 0; }",
|
|
@@ -1619,7 +3879,7 @@ var GEM_POPUP_SHELL_CSS = GEM_POPUP_SHELL_CSS_LINES.join("\n");
|
|
|
1619
3879
|
|
|
1620
3880
|
// ../../packages/gem-framework/src/primitives.ts
|
|
1621
3881
|
var dividerMask = (path) => `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 10' preserveAspectRatio='none'%3E%3Cpath fill='%23fff' d='${path}'/%3E%3C/svg%3E")`;
|
|
1622
|
-
var knownValues = (
|
|
3882
|
+
var knownValues = (attr5, values) => `:where(${values.map((v) => `[${attr5}='${v}']`).join(", ")})`;
|
|
1623
3883
|
var DIVIDER_SHAPES = Object.keys(GEM_SECTION_DIVIDER_PATHS);
|
|
1624
3884
|
var GEM_DIVIDER_CSS_LINES = [
|
|
1625
3885
|
// The clip is WIDENED, not removed. `.gem-section` sets `overflow: clip` and the wedge is drawn
|
|
@@ -1802,7 +4062,7 @@ var GEM_INLINE_MARK_CSS_LINES = [
|
|
|
1802
4062
|
":where(.gem-mark--alert) { color: var(--gem-color-danger, #b42318); }",
|
|
1803
4063
|
":where(.gem-mark--highlight.gem-mark--accent), :where(.gem-mark--highlight.gem-mark--alert) { color: var(--gem-mark-highlight-ink, #1f1d18); }",
|
|
1804
4064
|
".gem-mark-link { color: var(--gem-color-accent); text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }",
|
|
1805
|
-
".gem-mark-link:hover { text-decoration-thickness: 2px; }",
|
|
4065
|
+
".gem-mark-link:hover { text-decoration-thickness: var(--gem-underline-thickness, 2px); }",
|
|
1806
4066
|
// 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
|
|
1807
4067
|
// 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).
|
|
1808
4068
|
".gem-mark-link.gem-mark--highlight, .gem-mark--highlight .gem-mark-link { color: var(--gem-mark-highlight-ink, #1f1d18); }"
|
|
@@ -2028,6 +4288,14 @@ function createPrimitiveCss() {
|
|
|
2028
4288
|
" cursor: not-allowed;",
|
|
2029
4289
|
" opacity: 0.55;",
|
|
2030
4290
|
"}",
|
|
4291
|
+
// Button shape of ONE section (GemFactory gap #69, 22/10/2026): matching a source, a section needs square, softly rounded
|
|
4292
|
+
// or pill buttons whatever the preset's button radius is, and writing border-radius on .gem-button takes the button
|
|
4293
|
+
// away from the preset for good (GemFactory's button_token_shadowed). `data-gem-button-shape` on the section root (or on
|
|
4294
|
+
// one button) sets the radius the button already reads; with no attribute the preset decides, as before. Square and
|
|
4295
|
+
// pill are shapes, so they do not scale; soft is a size, so it follows the preset's radius scale (--gem-style-radius).
|
|
4296
|
+
"[data-gem-button-shape='square'] { --gem-radius-button: 0px; }",
|
|
4297
|
+
"[data-gem-button-shape='soft'] { --gem-radius-button: var(--gem-radius-sm); }",
|
|
4298
|
+
"[data-gem-button-shape='pill'] { --gem-radius-button: 999px; }",
|
|
2031
4299
|
".gem-input:disabled, .gem-select:disabled, .gem-textarea:disabled, .gem-quantity-input__button:disabled {",
|
|
2032
4300
|
" cursor: not-allowed;",
|
|
2033
4301
|
" opacity: 0.6;",
|
|
@@ -3405,8 +5673,8 @@ function createPrimitiveCss() {
|
|
|
3405
5673
|
function createGemSectionRuntimeContractCss(selector = "[data-gem-section-library-id]", options = {}) {
|
|
3406
5674
|
const background = options.backgroundVar ?? "var(--gem-color-background)";
|
|
3407
5675
|
const surface = options.surfaceVar ?? "var(--gem-color-surface)";
|
|
3408
|
-
const
|
|
3409
|
-
const mutedText = options.mutedTextVar ?? `color-mix(in srgb, ${
|
|
5676
|
+
const text2 = options.textVar ?? "var(--gem-color-text)";
|
|
5677
|
+
const mutedText = options.mutedTextVar ?? `color-mix(in srgb, ${text2} 72%, transparent)`;
|
|
3410
5678
|
const accent = options.accentVar ?? "var(--gem-color-accent)";
|
|
3411
5679
|
const accentText = options.accentTextVar ?? "var(--gem-color-accent-text, #ffffff)";
|
|
3412
5680
|
const border = options.borderVar ?? "var(--gem-color-border)";
|
|
@@ -3416,7 +5684,7 @@ function createGemSectionRuntimeContractCss(selector = "[data-gem-section-librar
|
|
|
3416
5684
|
`${selector} {`,
|
|
3417
5685
|
" box-sizing: border-box;",
|
|
3418
5686
|
` background: ${background};`,
|
|
3419
|
-
` color: ${
|
|
5687
|
+
` color: ${text2};`,
|
|
3420
5688
|
"}",
|
|
3421
5689
|
`${selector} *,`,
|
|
3422
5690
|
`${selector} *::before,`,
|
|
@@ -3475,7 +5743,7 @@ function createGemSectionRuntimeContractCss(selector = "[data-gem-section-librar
|
|
|
3475
5743
|
" border-radius: var(--gem-radius-pill, 999px);",
|
|
3476
5744
|
" padding: 6px 10px;",
|
|
3477
5745
|
` background: ${surface};`,
|
|
3478
|
-
` color: ${
|
|
5746
|
+
` color: ${text2};`,
|
|
3479
5747
|
" font-size: 0.8125rem;",
|
|
3480
5748
|
" font-weight: 700;",
|
|
3481
5749
|
"}",
|
|
@@ -4833,8 +7101,8 @@ function resolveGift(gift, totalAmount) {
|
|
|
4833
7101
|
const remaining = `${symbol}${remainingAmount.toFixed(2)}`;
|
|
4834
7102
|
const percent = gift.threshold > 0 ? Math.max(0, Math.min(100, Math.round(totalAmount / gift.threshold * 100))) : 100;
|
|
4835
7103
|
const template = unlocked ? gift.unlocked_text ?? "\u{1F381} {label} unlocked!" : gift.locked_text ?? "Add {remaining} more to unlock {label}";
|
|
4836
|
-
const
|
|
4837
|
-
return { unlocked, remaining, remaining_amount: remainingAmount, percent, text };
|
|
7104
|
+
const text2 = template.replace("{remaining}", remaining).replace("{label}", gift.label);
|
|
7105
|
+
return { unlocked, remaining, remaining_amount: remainingAmount, percent, text: text2 };
|
|
4838
7106
|
}
|
|
4839
7107
|
function matches(when, sel) {
|
|
4840
7108
|
return Object.keys(when).every((k) => sel[k] === when[k]);
|
|
@@ -5980,7 +8248,13 @@ var runtimePluginReadyEvents = {
|
|
|
5980
8248
|
"context-adapter": "gem:context:ready",
|
|
5981
8249
|
"offer-engine": "gem:offer:ready",
|
|
5982
8250
|
reviews: "gem:reviews:ready",
|
|
5983
|
-
bundle: "gem:bundle:ready"
|
|
8251
|
+
bundle: "gem:bundle:ready",
|
|
8252
|
+
"size-guide": "gem:size-guide:ready",
|
|
8253
|
+
orbit: "gem:orbit:ready",
|
|
8254
|
+
rail: "gem:rail:ready",
|
|
8255
|
+
personalize: "gem:personalize:ready",
|
|
8256
|
+
"print-preview": "gem:print-preview:ready",
|
|
8257
|
+
"delivery-estimate": "gem:delivery-estimate:ready"
|
|
5984
8258
|
};
|
|
5985
8259
|
var cartDrawerPluginRegistrationLines = [
|
|
5986
8260
|
" registerPlugin('cart-drawer', ({ api }) => {",
|
|
@@ -6474,8 +8748,7 @@ var heroSlideshowPluginRegistrationLines = [
|
|
|
6474
8748
|
" api.emit('gem:hero-slideshow:ready', { source: 'gem-runtime', count: bound });",
|
|
6475
8749
|
" });"
|
|
6476
8750
|
];
|
|
6477
|
-
var
|
|
6478
|
-
" registerPlugin('quick-view', ({ api }) => {",
|
|
8751
|
+
var legacyQuickViewLines = [
|
|
6479
8752
|
` const triggers = document.querySelectorAll('[data-gem-quick-view], [data-gem-action="quick-view"]');`,
|
|
6480
8753
|
` const surface = document.querySelector('[data-gem-quick-view-surface], .gem-quick-view, .gem-modal[data-gem-component="quick-view"]');`,
|
|
6481
8754
|
" let bound = 0;",
|
|
@@ -6525,10 +8798,18 @@ var quickViewPluginRegistrationLines = [
|
|
|
6525
8798
|
" });",
|
|
6526
8799
|
" if (canListen(surface)) surface.addEventListener('keydown', (event) => { if (event.key === 'Escape') close('escape'); });",
|
|
6527
8800
|
" }",
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
"
|
|
8801
|
+
// A mount on one late trigger (gem-runtime.js binds a section added after boot on its own) finds no surface: it keeps the
|
|
8802
|
+
// page's api.quickView.
|
|
8803
|
+
" if (surface || !api.quickView) api.quickView = { open: (detail = {}) => openSurface({ ...detail }), close };"
|
|
6531
8804
|
];
|
|
8805
|
+
var runtimeChunks = {
|
|
8806
|
+
...gemMotionChunks({
|
|
8807
|
+
hook: '[data-gem-quick-view], [data-gem-action="quick-view"]',
|
|
8808
|
+
lines: legacyQuickViewLines,
|
|
8809
|
+
readyDetail: "count: bound + count, hasSurface: Boolean(surface)"
|
|
8810
|
+
}),
|
|
8811
|
+
...gemPodChunks()
|
|
8812
|
+
};
|
|
6532
8813
|
var promoPopupPluginRegistrationLines = [
|
|
6533
8814
|
" registerPlugin('promo-popup', ({ api }) => {",
|
|
6534
8815
|
// Only the explicit hook opts in. Every promo-popup section carries the archetype class .gem-promo-popup, and
|
|
@@ -7068,6 +9349,30 @@ var bundlePluginRegistrationLines = [
|
|
|
7068
9349
|
" " + bundleEngineRuntimeScript(),
|
|
7069
9350
|
" });"
|
|
7070
9351
|
];
|
|
9352
|
+
var sizeGuidePluginRegistrationLines = [
|
|
9353
|
+
" registerPlugin('size-guide', ({ api }) => {",
|
|
9354
|
+
" const page = document.documentElement && typeof document.documentElement.querySelectorAll === 'function' ? document.documentElement : document;",
|
|
9355
|
+
" const guides = Array.prototype.slice.call(page.querySelectorAll('[data-gem-size-guide]'));",
|
|
9356
|
+
" const opens = (target) => guides.some((guide) => !target || guide.getAttribute('data-gem-size-guide') === target || guide.id === target);",
|
|
9357
|
+
" let bound = 0;",
|
|
9358
|
+
" document.querySelectorAll('[data-gem-size-guide-trigger]').forEach((trigger) => {",
|
|
9359
|
+
" const target = trigger.getAttribute('data-gem-size-guide-trigger') || '';",
|
|
9360
|
+
" if (!opens(target)) { trigger.hidden = true; return; }",
|
|
9361
|
+
" if (!mark(trigger, 'size-guide')) return;",
|
|
9362
|
+
" trigger.hidden = false;",
|
|
9363
|
+
" if (!canListen(trigger)) return;",
|
|
9364
|
+
" trigger.addEventListener('click', (event) => {",
|
|
9365
|
+
" event.preventDefault && event.preventDefault();",
|
|
9366
|
+
" const holder = trigger.closest ? trigger.closest('[data-product-id]') : null;",
|
|
9367
|
+
" const detail = { source: 'gem-runtime', sectionId: target || null, productId: holder ? holder.getAttribute('data-product-id') : null };",
|
|
9368
|
+
" if (typeof window.CustomEvent === 'function') window.dispatchEvent(new window.CustomEvent('gem:size-guide:open', { detail }));",
|
|
9369
|
+
" api.emit('gem:size-guide:open', detail);",
|
|
9370
|
+
" });",
|
|
9371
|
+
" bound += 1;",
|
|
9372
|
+
" });",
|
|
9373
|
+
" api.emit('gem:size-guide:ready', { source: 'gem-runtime', count: bound, guides: guides.length });",
|
|
9374
|
+
" });"
|
|
9375
|
+
];
|
|
7071
9376
|
var runtimePluginSourceLines = [
|
|
7072
9377
|
["cart-drawer", cartDrawerPluginRegistrationLines],
|
|
7073
9378
|
["mobile-menu", mobileMenuPluginRegistrationLines],
|
|
@@ -7080,7 +9385,7 @@ var runtimePluginSourceLines = [
|
|
|
7080
9385
|
["media-gallery", mediaGalleryPluginRegistrationLines],
|
|
7081
9386
|
["carousel", carouselPluginRegistrationLines],
|
|
7082
9387
|
["hero-slideshow", heroSlideshowPluginRegistrationLines],
|
|
7083
|
-
["quick-view",
|
|
9388
|
+
["quick-view", runtimeChunks["quick-view"].body],
|
|
7084
9389
|
["promo-popup", promoPopupPluginRegistrationLines],
|
|
7085
9390
|
["sticky-atc", stickyAtcPluginRegistrationLines],
|
|
7086
9391
|
["before_after", beforeAfterPluginRegistrationLines],
|
|
@@ -7091,16 +9396,97 @@ var runtimePluginSourceLines = [
|
|
|
7091
9396
|
["runtime-slot-loader", runtimeSlotLoaderPluginRegistrationLines],
|
|
7092
9397
|
["offer-engine", offerEnginePluginRegistrationLines],
|
|
7093
9398
|
["reviews", reviewsPluginRegistrationLines],
|
|
7094
|
-
["bundle", bundlePluginRegistrationLines]
|
|
9399
|
+
["bundle", bundlePluginRegistrationLines],
|
|
9400
|
+
["size-guide", sizeGuidePluginRegistrationLines],
|
|
9401
|
+
["orbit", runtimeChunks.orbit.body],
|
|
9402
|
+
["rail", runtimeChunks.rail.body],
|
|
9403
|
+
["personalize", runtimeChunks.personalize.body],
|
|
9404
|
+
["print-preview", runtimeChunks["print-preview"].body],
|
|
9405
|
+
["delivery-estimate", runtimeChunks["delivery-estimate"].body]
|
|
7095
9406
|
];
|
|
7096
9407
|
var noopPluginRegistrationLine = (key, readyEvent) => ` registerNoopPlugin(${JSON.stringify(key)}, ${JSON.stringify(readyEvent)});`;
|
|
9408
|
+
var chunkStubLine = (key, chunk) => ` registerPlugin(${JSON.stringify(key)}, ({ api }) => loadChunk(${JSON.stringify(key)}, ${JSON.stringify(chunk.hook)}, ${JSON.stringify(chunk.when)}, api));`;
|
|
9409
|
+
var chunkLoaderLines = [
|
|
9410
|
+
" const chunkBase = (document.currentScript && document.currentScript.src) || '';",
|
|
9411
|
+
" const chunks = {};",
|
|
9412
|
+
" let chunkWatch = null;",
|
|
9413
|
+
" const loadChunkModule = (chunk) => {",
|
|
9414
|
+
" if (chunk.state) return;",
|
|
9415
|
+
" chunk.state = 1;",
|
|
9416
|
+
" Promise.resolve().then(() => {",
|
|
9417
|
+
" const url = new URL('plugins/' + chunk.key + '.js', chunk.api.pluginBase || chunkBase || document.baseURI).href;",
|
|
9418
|
+
" return chunk.api.importChunk ? chunk.api.importChunk(url) : import(url);",
|
|
9419
|
+
" }).then((module) => {",
|
|
9420
|
+
" chunk.module = module;",
|
|
9421
|
+
" module.mount(document, { api: chunk.api, window: target });",
|
|
9422
|
+
" chunk.state = 2;",
|
|
9423
|
+
" if (chunk.replay) chunk.replay.click();",
|
|
9424
|
+
" chunk.replay = null;",
|
|
9425
|
+
" }, (error) => { chunk.state = 0; chunk.replay = null; chunk.api.emit('gem:plugin:error', { key: chunk.key, error: String(error) }); });",
|
|
9426
|
+
" };",
|
|
9427
|
+
" const armChunk = (chunk, hooks) => {",
|
|
9428
|
+
" const load = () => loadChunkModule(chunk);",
|
|
9429
|
+
" if (chunk.when === 'viewport') {",
|
|
9430
|
+
" if (!target.IntersectionObserver) { load(); return; }",
|
|
9431
|
+
" chunk.io = chunk.io || new target.IntersectionObserver((entries) => { if (entries.some((entry) => entry.isIntersecting)) { chunk.io.disconnect(); load(); } }, { rootMargin: '200px' });",
|
|
9432
|
+
" hooks.forEach((hookEl) => chunk.io.observe(hookEl));",
|
|
9433
|
+
" return;",
|
|
9434
|
+
" }",
|
|
9435
|
+
" hooks.forEach((hookEl) => {",
|
|
9436
|
+
" ['pointerover', 'focusin', 'touchstart'].forEach((type) => hookEl.addEventListener(type, load, { once: true, passive: true }));",
|
|
9437
|
+
" hookEl.addEventListener('click', (event) => {",
|
|
9438
|
+
" if (chunk.state === 2 || event.defaultPrevented) return;",
|
|
9439
|
+
" const host = hookEl.closest && hookEl.closest('[data-gem-quick-view-host]');",
|
|
9440
|
+
" if (host && host.gemQuickViewHost && !host.gemQuickViewHost.opens(hookEl)) return;",
|
|
9441
|
+
" event.preventDefault();",
|
|
9442
|
+
" chunk.replay = hookEl;",
|
|
9443
|
+
" load();",
|
|
9444
|
+
" });",
|
|
9445
|
+
" });",
|
|
9446
|
+
" if (!chunk.idle) { chunk.idle = true; onIdle(load); }",
|
|
9447
|
+
" };",
|
|
9448
|
+
" const scanChunk = (chunk, scope) => {",
|
|
9449
|
+
" const hooks = Array.prototype.slice.call(scope.querySelectorAll ? scope.querySelectorAll(chunk.hook) : []);",
|
|
9450
|
+
" if (scope !== document && scope.matches && scope.matches(chunk.hook)) hooks.unshift(scope);",
|
|
9451
|
+
" if (!hooks.length) return false;",
|
|
9452
|
+
" if (chunk.state === 2) hooks.forEach((hookEl) => chunk.module.mount(hookEl, { api: chunk.api, window: target }));",
|
|
9453
|
+
" else armChunk(chunk, hooks);",
|
|
9454
|
+
" return true;",
|
|
9455
|
+
" };",
|
|
9456
|
+
" const watchChunks = () => {",
|
|
9457
|
+
" if (chunkWatch || !target.MutationObserver) return;",
|
|
9458
|
+
" let added = [];",
|
|
9459
|
+
" let timer = 0;",
|
|
9460
|
+
" const flush = () => {",
|
|
9461
|
+
" timer = 0;",
|
|
9462
|
+
" const nodes = added;",
|
|
9463
|
+
" added = [];",
|
|
9464
|
+
" nodes.forEach((node) => { if (node.isConnected) Object.keys(chunks).forEach((key) => scanChunk(chunks[key], node)); });",
|
|
9465
|
+
" };",
|
|
9466
|
+
" chunkWatch = new target.MutationObserver((records) => {",
|
|
9467
|
+
" records.forEach((record) => record.addedNodes.forEach((node) => { if (node.nodeType === 1) added.push(node); }));",
|
|
9468
|
+
" if (added.length && !timer) timer = setTimeout(flush, 50);",
|
|
9469
|
+
" });",
|
|
9470
|
+
" chunkWatch.observe(document.documentElement, { childList: true, subtree: true });",
|
|
9471
|
+
" if (canListen(document)) document.addEventListener('shopify:section:load', (event) => { if (event.target && event.target.nodeType === 1) Object.keys(chunks).forEach((key) => scanChunk(chunks[key], event.target)); });",
|
|
9472
|
+
" };",
|
|
9473
|
+
" const loadChunk = (key, hook, when, api) => {",
|
|
9474
|
+
" const chunk = (chunks[key] = { key, hook, when, api, state: 0, module: null, replay: null });",
|
|
9475
|
+
" watchChunks();",
|
|
9476
|
+
" if (!scanChunk(chunk, document)) api.emit('gem:' + key + ':ready', { source: 'gem-runtime', count: 0 });",
|
|
9477
|
+
" };"
|
|
9478
|
+
];
|
|
7097
9479
|
function gemRuntimePluginSources() {
|
|
7098
|
-
const withBody = runtimePluginSourceLines.map(([key, lines]) =>
|
|
7099
|
-
key
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
9480
|
+
const withBody = runtimePluginSourceLines.map(([key, lines]) => {
|
|
9481
|
+
const chunk = runtimeChunks[key];
|
|
9482
|
+
return {
|
|
9483
|
+
key,
|
|
9484
|
+
readyEvent: runtimePluginReadyEvents[key] ?? `gem:${key}:ready`,
|
|
9485
|
+
hasBody: true,
|
|
9486
|
+
lines: chunk ? [chunkStubLine(key, chunk)] : lines,
|
|
9487
|
+
...chunk ? { chunk } : {}
|
|
9488
|
+
};
|
|
9489
|
+
});
|
|
7104
9490
|
const noop = Object.entries(runtimePluginReadyEvents).filter(([key]) => !runtimePluginSourceLines.some(([sourceKey]) => sourceKey === key)).map(([key, readyEvent]) => ({ key, readyEvent, hasBody: false, lines: [noopPluginRegistrationLine(key, readyEvent)] }));
|
|
7105
9491
|
return [...withBody, ...noop];
|
|
7106
9492
|
}
|
|
@@ -7235,7 +9621,12 @@ function createGemRuntimeBootstrap(configArg) {
|
|
|
7235
9621
|
" if (readyEvent) api.emit(readyEvent, { source: 'gem-runtime' });",
|
|
7236
9622
|
" });",
|
|
7237
9623
|
" };",
|
|
7238
|
-
...runtimePluginSourceLines.
|
|
9624
|
+
...runtimePluginSourceLines.some(([key]) => runtimeChunks[key] && enabledPluginKeys.has(key)) ? chunkLoaderLines : [],
|
|
9625
|
+
...runtimePluginSourceLines.flatMap(([key, lines]) => {
|
|
9626
|
+
if (!enabledPluginKeys.has(key)) return [];
|
|
9627
|
+
const chunk = runtimeChunks[key];
|
|
9628
|
+
return chunk ? [chunkStubLine(key, chunk)] : lines;
|
|
9629
|
+
}),
|
|
7239
9630
|
...pluginRegistrationLines,
|
|
7240
9631
|
// `data-gem-bound-<key>` is a CONTRACT, not private bookkeeping: markup may carry it to claim an element for its
|
|
7241
9632
|
// own behavior (promo-popup.email-capture.v1 renders data-gem-bound-promo-popup="section"). Any value counts as
|
|
@@ -7331,17 +9722,18 @@ function gemRuntimePluginModuleKeys() {
|
|
|
7331
9722
|
function createGemRuntimePluginModule(key) {
|
|
7332
9723
|
const source = gemRuntimePluginSources().find((candidate) => candidate.key === key);
|
|
7333
9724
|
if (!source) throw new Error(`gem-framework: no runtime plugin "${key}"`);
|
|
7334
|
-
const body = routeListenersThroughLedger(source.lines.join("\n"), key);
|
|
9725
|
+
const body = routeListenersThroughLedger((source.chunk ? source.chunk.body : source.lines).join("\n"), key);
|
|
7335
9726
|
const guard = PLUGIN_GUARDS[key];
|
|
7336
9727
|
return [
|
|
7337
9728
|
`// @gemboss/gem-framework/plugins/${key}: the gem-runtime plugin "${key}" as an ES module. Generated from`,
|
|
7338
|
-
"// packages/gem-framework/src/runtime.ts, the same lines gem-runtime.js carries; do not edit here.",
|
|
9729
|
+
source.chunk ? "// packages/gem-framework/src/runtime.ts (a lazy chunk: gem-runtime.js carries only its stub); do not edit here." : "// packages/gem-framework/src/runtime.ts, the same lines gem-runtime.js carries; do not edit here.",
|
|
7339
9730
|
'import { mountPlugin } from "../runtime-core.js";',
|
|
9731
|
+
...source.chunk ? source.chunk.imports : [],
|
|
7340
9732
|
"",
|
|
7341
9733
|
`export const key = ${JSON.stringify(key)};`,
|
|
7342
9734
|
`export const readyEvent = ${JSON.stringify(source.readyEvent)};`,
|
|
7343
9735
|
"",
|
|
7344
|
-
"function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {",
|
|
9736
|
+
"function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin, onUnmount }) {",
|
|
7345
9737
|
body,
|
|
7346
9738
|
"}",
|
|
7347
9739
|
"",
|
|
@@ -7381,7 +9773,7 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
|
7381
9773
|
const namespace = group === "typography" ? "font" : group === "spacing" ? "space" : group;
|
|
7382
9774
|
return keys.map((key) => `--gem-${namespace}-${slugify(key)}`);
|
|
7383
9775
|
}),
|
|
7384
|
-
// The governed type ramp (--gem-h1…h6, --gem-body, --gem-text-sm, --gem-type-base/-scale). The
|
|
9776
|
+
// The governed type ramp (--gem-h1…h6, --gem-body, --gem-text-sm, --gem-text-xs, --gem-type-base/-scale). The
|
|
7385
9777
|
// framework stylesheet declares it on every page, but it is not a token GROUP, so it was missing here
|
|
7386
9778
|
// and a section sizing its heading with `var(--gem-h2)` got `unknown_gem_css_variable`. Read from the
|
|
7387
9779
|
// function that emits it, so the ramp and this list cannot drift.
|
|
@@ -7400,7 +9792,15 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
|
7400
9792
|
// The row primitive's variables (primitives.ts GEM_ROW_CSS). Sections SET these on their grid.
|
|
7401
9793
|
...GEM_ROW_CSS_VARIABLES,
|
|
7402
9794
|
// The form set's variables (form-set.ts): set from a plan's `tokens.form`, read by fields under [data-gem-form-set].
|
|
7403
|
-
...GEM_FORM_CSS_VARIABLES
|
|
9795
|
+
...GEM_FORM_CSS_VARIABLES,
|
|
9796
|
+
// The motion engine's (motion-engine.ts, 0.19.0), all of them: a section copies the base CSS VERBATIM into its own CSS,
|
|
9797
|
+
// internals (--gem-orbit-a, --gem-rail-t) included, and sets --gem-orbit-i / -n, the settings and --gem-motion-tint.
|
|
9798
|
+
...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) ?? []),
|
|
9799
|
+
// …and every setting an effect reads from a CSS variable (`css: true`): a section sets it in its stylesheet, per breakpoint.
|
|
9800
|
+
...gemMotionEffectList.flatMap((effect) => Object.entries(effect.settings).filter(([, setting]) => setting.css).map(([name]) => `--gem-${effect.key}-${name}`)),
|
|
9801
|
+
// The POD buy box's (pod-engine.ts, 0.20.0): the print area and lines gemsite sets on a preview, the swatch colour of an
|
|
9802
|
+
// option, the chunks' sentinels, and --gem-font-accent, the accent font a skin may set (the preview falls back to heading).
|
|
9803
|
+
...GEM_POD_CSS_VARIABLES
|
|
7404
9804
|
]);
|
|
7405
9805
|
var TOKENIZED_PROPERTIES = [
|
|
7406
9806
|
"background",
|
|
@@ -7512,14 +9912,25 @@ function validateGemFrameworkMarkup(markup, artifactPath, options = {}) {
|
|
|
7512
9912
|
}
|
|
7513
9913
|
return result(issues);
|
|
7514
9914
|
}
|
|
9915
|
+
var NAMESPACE_URL = String.raw`https?:\/\/www\.w3\.org\/[\w.\/#:~%+-]*`;
|
|
9916
|
+
var SHARE_NETWORK = "facebook|twitter|x|pinterest|whatsapp|threads";
|
|
9917
|
+
var AS_A_LINK = String.raw`(?<=(?:<a\s[^<>]{0,400}?(?<![\w-])href=\{?|[{,]\s{0,40}["']?(?:${SHARE_NETWORK})["']?\s{0,8}:\s{0,8})["'\x60])`;
|
|
9918
|
+
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)\?`;
|
|
9919
|
+
var NEVER_FETCHED_URL = new RegExp(`${NAMESPACE_URL}|${AS_A_LINK}${SHARE_INTENT}`, "gi");
|
|
9920
|
+
var JSON_LD = /<script\b[^>]*type=["']application\/ld\+json["'][^>]*>[\s\S]*?<\/script>/gi;
|
|
9921
|
+
function namesExternalDependency(withoutJsonLd) {
|
|
9922
|
+
return /https?:\/\/|\/\/cdn\./i.test(withoutJsonLd.replace(NEVER_FETCHED_URL, ""));
|
|
9923
|
+
}
|
|
9924
|
+
function hasExternalDependency(content) {
|
|
9925
|
+
return namesExternalDependency(content.replace(JSON_LD, ""));
|
|
9926
|
+
}
|
|
7515
9927
|
function validateSharedSafety(content, artifactPath) {
|
|
7516
9928
|
const issues = [];
|
|
7517
|
-
const
|
|
7518
|
-
|
|
7519
|
-
if (/<script\b/i.test(contentWithoutJsonLd)) {
|
|
9929
|
+
const withoutJsonLd = content.replace(JSON_LD, "");
|
|
9930
|
+
if (/<script\b/i.test(withoutJsonLd)) {
|
|
7520
9931
|
issues.push(ruleIssue("inline_script", "Gem Framework artifacts must not include inline script tags.", artifactPath));
|
|
7521
9932
|
}
|
|
7522
|
-
if (
|
|
9933
|
+
if (namesExternalDependency(withoutJsonLd)) {
|
|
7523
9934
|
issues.push(ruleIssue("external_dependency", "Gem Framework artifacts must not include external runtime or CDN dependencies.", artifactPath));
|
|
7524
9935
|
}
|
|
7525
9936
|
return issues;
|
|
@@ -7548,6 +9959,9 @@ function gemFrameworkPublicCssVariables() {
|
|
|
7548
9959
|
String(createSectionCss()),
|
|
7549
9960
|
String(createShellCss()),
|
|
7550
9961
|
String(createGemSectionRuntimeContractCss()),
|
|
9962
|
+
// popup-shell.css (its own file since 0.8.3, so not in createPrimitiveCss): a popup section sets
|
|
9963
|
+
// --gem-popup-drawer-width (0.18.17, drawer-end) and must not be told the framework's own variable is unknown.
|
|
9964
|
+
GEM_POPUP_SHELL_CSS,
|
|
7551
9965
|
// The button case rule only exists when a token sets it; a section may still read its variables (a CTA rule
|
|
7552
9966
|
// beats the zero-specificity rule, so opting in means `text-transform: var(--gem-font-button-transform, none)`).
|
|
7553
9967
|
gemButtonCaseCss({ typography: { buttonTransform: "none", buttonLetterSpacing: "normal" } })
|
|
@@ -7558,11 +9972,13 @@ function gemFrameworkPublicCssVariables() {
|
|
|
7558
9972
|
}
|
|
7559
9973
|
var ROOT_SELECTOR = /^\s*(?::root|html|:host)(?:\[[^\]]*\]|:[a-z-]+(?:\([^()]*\))?)*\s*$/;
|
|
7560
9974
|
var EDITION_ROOT_SELECTOR = /^\s*:where\(\s*\[data-gem-edition=(["'])\d+\1\](?:\s*,\s*\[data-gem-edition=(["'])\d+\2\])*\s*\)\s*$/;
|
|
9975
|
+
var KNOB_SCOPE_SELECTOR = /^\s*\[data-gem-[a-z0-9-]+(?:=(["'])[^"']*\1)?\]\s*$/;
|
|
7561
9976
|
function gemPublicCssVariablesFrom(stylesheet) {
|
|
7562
9977
|
const css = stylesheet.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
7563
9978
|
const declaredOnRoot = /* @__PURE__ */ new Set();
|
|
7564
9979
|
const declaredElsewhere = /* @__PURE__ */ new Set();
|
|
7565
9980
|
for (const [, selector, body] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
9981
|
+
if (selector.split(",").every((part) => KNOB_SCOPE_SELECTOR.test(part))) continue;
|
|
7566
9982
|
const onRoot = EDITION_ROOT_SELECTOR.test(selector) || selector.split(",").every((part) => ROOT_SELECTOR.test(part));
|
|
7567
9983
|
for (const [, name] of body.matchAll(/(--gem-[a-z0-9-]+)\s*:/g)) (onRoot ? declaredOnRoot : declaredElsewhere).add(name);
|
|
7568
9984
|
}
|
|
@@ -7761,9 +10177,9 @@ function renderGemShellHeader(plan) {
|
|
|
7761
10177
|
header.show_cart ? `<a class="gem-shell-header__action gem-shell-header__action--cart" href="#" aria-label="Cart">${ICON_CART}<span class="gem-shell-header__cart-count cart-count-bubble">0</span></a>` : ""
|
|
7762
10178
|
].filter(Boolean).join("");
|
|
7763
10179
|
const bars = Array.isArray(header.bars) ? header.bars.filter((b) => (b?.text ?? "").trim()) : [];
|
|
7764
|
-
const barMarkup = (
|
|
10180
|
+
const barMarkup = (text2, bg, fg, segments) => {
|
|
7765
10181
|
const style = [bg ? `background:${bg}` : "", fg ? `color:${fg}` : ""].filter(Boolean).join(";");
|
|
7766
|
-
const body = (segments ?? []).length > 1 ? `<span class="gem-announcement-bar__segments">${(segments ?? []).map((x) => `<span>${escapeHtml(x)}</span>`).join("")}</span>` : `<span>${escapeHtml(
|
|
10182
|
+
const body = (segments ?? []).length > 1 ? `<span class="gem-announcement-bar__segments">${(segments ?? []).map((x) => `<span>${escapeHtml(x)}</span>`).join("")}</span>` : `<span>${escapeHtml(text2)}</span>`;
|
|
7767
10183
|
return `<div class="announcement-bar" role="region" aria-label="Announcement"${style ? ` style="${escapeAttribute(style)}"` : ""}><p class="announcement-bar__message">${body}</p></div>`;
|
|
7768
10184
|
};
|
|
7769
10185
|
const announcement = bars.length ? bars.map((b) => barMarkup(b.text ?? "", b.bg, b.fg, b.segments)).join("") : header.announcement_text?.trim() ? barMarkup(header.announcement_text.trim(), header.announcement_bg, header.announcement_fg) : "";
|
|
@@ -8748,10 +11164,25 @@ export {
|
|
|
8748
11164
|
GEM_IMG_IN_CSS,
|
|
8749
11165
|
GEM_INLINE_MARK_CSS,
|
|
8750
11166
|
GEM_LINE_HEIGHT_TOKEN_KEYS,
|
|
11167
|
+
GEM_MOTION_CLASSES,
|
|
8751
11168
|
GEM_MOTION_REDUCED,
|
|
11169
|
+
GEM_MOTION_RUNTIME_FILES,
|
|
8752
11170
|
GEM_MOTION_SCALE_DEFAULTS,
|
|
11171
|
+
TIERS as GEM_MOTION_TIERS,
|
|
11172
|
+
GEM_ORBIT_BASE_CSS,
|
|
11173
|
+
GEM_ORBIT_RUNTIME_CSS,
|
|
8753
11174
|
GEM_PART_CLASS,
|
|
11175
|
+
GEM_PERSONALIZE_RUNTIME_CSS,
|
|
11176
|
+
GEM_POD_BUYBOX_CSS,
|
|
11177
|
+
GEM_POD_CLASSES,
|
|
11178
|
+
GEM_POD_CSS_VARIABLES,
|
|
11179
|
+
GEM_POD_PLUGINS,
|
|
11180
|
+
GEM_POD_RUNTIME_FILES,
|
|
8754
11181
|
GEM_POPUP_SHELL_CSS,
|
|
11182
|
+
GEM_PRINT_PREVIEW_RUNTIME_CSS,
|
|
11183
|
+
GEM_QUICK_VIEW_CSS,
|
|
11184
|
+
GEM_RAIL_BASE_CSS,
|
|
11185
|
+
GEM_RAIL_RUNTIME_CSS,
|
|
8755
11186
|
GEM_REVEAL_CSS,
|
|
8756
11187
|
GEM_ROW_CSS,
|
|
8757
11188
|
GEM_ROW_CSS_VARIABLES,
|
|
@@ -8765,10 +11196,14 @@ export {
|
|
|
8765
11196
|
MODERN_MINERAL_HOMEPAGE_LINEAGE_SECTIONS,
|
|
8766
11197
|
MODERN_MINERAL_HOMEPAGE_PACK_ID,
|
|
8767
11198
|
MODERN_MINERAL_STYLE_PACK_ID,
|
|
11199
|
+
PERSONALIZE_MESSAGES,
|
|
8768
11200
|
aaLabelColor,
|
|
8769
11201
|
aaTextColor,
|
|
8770
11202
|
auditGemFrameworkClassName,
|
|
8771
11203
|
bundleEngineRuntimeScript,
|
|
11204
|
+
capabilityTier,
|
|
11205
|
+
checkEffect,
|
|
11206
|
+
compactCss,
|
|
8772
11207
|
createGemBaseCss,
|
|
8773
11208
|
createGemFrameworkManifest,
|
|
8774
11209
|
createGemRuntimeBootstrap,
|
|
@@ -8788,7 +11223,12 @@ export {
|
|
|
8788
11223
|
createThemeProfileCss,
|
|
8789
11224
|
defaultGemTokens,
|
|
8790
11225
|
defaultSelection,
|
|
11226
|
+
defineEffect,
|
|
11227
|
+
deliveryEstimate,
|
|
8791
11228
|
deriveFormSetId,
|
|
11229
|
+
fileAccepted,
|
|
11230
|
+
follow,
|
|
11231
|
+
formatDeliveryRange,
|
|
8792
11232
|
gemBandSchemeColors,
|
|
8793
11233
|
gemBandSchemeCssVars,
|
|
8794
11234
|
gemBandSchemeDeclarations,
|
|
@@ -8809,17 +11249,24 @@ export {
|
|
|
8809
11249
|
gemFrameworkValidationRules,
|
|
8810
11250
|
gemFrameworkVersion,
|
|
8811
11251
|
gemMobileTokenCssVars,
|
|
11252
|
+
gemMotionChunks,
|
|
11253
|
+
gemMotionEffectList,
|
|
11254
|
+
gemMotionEffectManifest,
|
|
8812
11255
|
gemMotionScaleCssVars,
|
|
11256
|
+
gemOrbitBaseCss,
|
|
8813
11257
|
gemPartClass,
|
|
8814
11258
|
gemPhoneTokenTarget,
|
|
8815
11259
|
gemPlanFormSet,
|
|
8816
11260
|
gemPlanShapeKnobs,
|
|
11261
|
+
gemPodChunks,
|
|
11262
|
+
gemRailBaseCss,
|
|
8817
11263
|
gemRuntimePluginModuleKeys,
|
|
8818
11264
|
gemRuntimePluginSources,
|
|
8819
11265
|
gemSectionDividerResponsiveStyle,
|
|
8820
11266
|
gemSectionDividerStyle,
|
|
8821
11267
|
gemSectionDividers,
|
|
8822
11268
|
getGemFrameworkValidationRule,
|
|
11269
|
+
hasExternalDependency,
|
|
8823
11270
|
hexContrastRatio,
|
|
8824
11271
|
hexRelativeLuminance,
|
|
8825
11272
|
isModernMineralFamilyLineagePlan,
|
|
@@ -8828,11 +11275,23 @@ export {
|
|
|
8828
11275
|
modernMineralHomepageLineageSections,
|
|
8829
11276
|
motionCssVarAliases,
|
|
8830
11277
|
motionCssVarName,
|
|
11278
|
+
mountDeliveryEstimate,
|
|
11279
|
+
mountPersonalize,
|
|
11280
|
+
mountPrintPreview,
|
|
8831
11281
|
normalizeGemTokens,
|
|
8832
11282
|
normalizeHex,
|
|
8833
11283
|
offerEngineRuntimeScript,
|
|
8834
11284
|
optionalGemTokenKeys,
|
|
11285
|
+
orbitEffect,
|
|
11286
|
+
parseDayRange,
|
|
11287
|
+
parsePersonalizeWhen,
|
|
11288
|
+
personalizeWhenHolds,
|
|
8835
11289
|
planToRuntimeBootstrapConfig,
|
|
11290
|
+
printPreviewContent,
|
|
11291
|
+
quickViewEffect,
|
|
11292
|
+
railEffect,
|
|
11293
|
+
readField,
|
|
11294
|
+
readScope,
|
|
8836
11295
|
renderGemFrameworkManifest,
|
|
8837
11296
|
renderGemPreviewDocument,
|
|
8838
11297
|
renderGemSectionMarkup,
|
|
@@ -8844,7 +11303,15 @@ export {
|
|
|
8844
11303
|
resolveGift,
|
|
8845
11304
|
resolveOffer,
|
|
8846
11305
|
reviewsEngineRuntimeScript,
|
|
11306
|
+
ringDelta,
|
|
11307
|
+
ringPose,
|
|
11308
|
+
scopeMotionCss,
|
|
11309
|
+
scopeOf,
|
|
11310
|
+
scrollProgress,
|
|
8847
11311
|
slugify,
|
|
11312
|
+
spring,
|
|
11313
|
+
storeHour,
|
|
11314
|
+
throwTarget,
|
|
8848
11315
|
toCssVars,
|
|
8849
11316
|
toMotionCssVars,
|
|
8850
11317
|
validateGemFrameworkArtifact,
|