@gemboss/gem-framework 0.18.16 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -1
- package/dist/SOURCE.json +3 -3
- package/dist/gem-framework.css +23 -1
- package/dist/gem-runtime.js +94 -54
- package/dist/index.js +1668 -43
- package/dist/motion/core.d.ts +208 -0
- package/dist/motion/core.js +1 -0
- package/dist/motion/orbit.d.ts +4 -0
- package/dist/motion/orbit.js +1 -0
- package/dist/motion/quick-view.d.ts +4 -0
- package/dist/motion/quick-view.js +1 -0
- package/dist/motion/rail.d.ts +5 -0
- package/dist/motion/rail.js +1 -0
- package/dist/orbit-base.css +39 -0
- package/dist/plugins/before_after.js +1 -1
- package/dist/plugins/bundle.js +1 -1
- package/dist/plugins/carousel.js +1 -1
- package/dist/plugins/cart-drawer.js +1 -1
- package/dist/plugins/context-adapter.js +1 -1
- package/dist/plugins/countdown.js +1 -1
- package/dist/plugins/disclosure.js +1 -1
- package/dist/plugins/hero-slideshow.js +1 -1
- package/dist/plugins/img-in.js +1 -1
- package/dist/plugins/localization.js +1 -1
- package/dist/plugins/media-gallery.js +1 -1
- package/dist/plugins/mobile-menu.js +1 -1
- package/dist/plugins/offer-engine.js +1 -1
- package/dist/plugins/orbit.d.ts +9 -0
- package/dist/plugins/orbit.js +1 -0
- package/dist/plugins/predictive-search.js +1 -1
- package/dist/plugins/promo-popup.js +1 -1
- package/dist/plugins/quantity-input.js +1 -1
- package/dist/plugins/quick-view.js +1 -69
- package/dist/plugins/rail.d.ts +9 -0
- package/dist/plugins/rail.js +1 -0
- package/dist/plugins/recently-viewed.js +1 -1
- package/dist/plugins/reveal-on-scroll.js +1 -1
- package/dist/plugins/reviews.js +1 -1
- package/dist/plugins/runtime-slot-loader.js +1 -1
- package/dist/plugins/seasonal-effects.js +1 -1
- package/dist/plugins/size-guide.d.ts +9 -0
- package/dist/plugins/size-guide.js +38 -0
- package/dist/plugins/sticky-atc.js +1 -1
- package/dist/plugins/tabs.js +1 -1
- package/dist/plugins/variant-selects.js +1 -1
- package/dist/popup-shell.css +15 -0
- package/dist/primitives.css +4 -1
- package/dist/rail-base.css +48 -0
- package/dist/runtime-core.d.ts +2 -0
- package/dist/runtime-core.js +26 -15
- package/dist/section-gate.js +2368 -36
- package/dist/tokens.css +4 -0
- package/package.json +11 -1
package/dist/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.19.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,1398 @@ 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, attr] 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(`[${attr}]`);
|
|
2098
|
+
if (b && !b.disabled) pick(group, Number(b.getAttribute(attr)));
|
|
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
|
+
|
|
1053
2459
|
// ../../packages/gem-framework/src/manifest.ts
|
|
1054
2460
|
var gemFrameworkPrimitiveGroups = [
|
|
1055
2461
|
{
|
|
@@ -1245,6 +2651,12 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
1245
2651
|
"gem-popup-shell__teaser"
|
|
1246
2652
|
]
|
|
1247
2653
|
},
|
|
2654
|
+
{
|
|
2655
|
+
// The motion engine (motion-engine.ts, 0.19.0): the orbit, rail and turn markup a section draws, and the quick view panel
|
|
2656
|
+
// the quick-view plugin builds. Styled by their own base / runtime stylesheets, never by gem-framework.css.
|
|
2657
|
+
group: "motion",
|
|
2658
|
+
classes: [...GEM_MOTION_CLASSES]
|
|
2659
|
+
},
|
|
1248
2660
|
{
|
|
1249
2661
|
group: "sections",
|
|
1250
2662
|
classes: [
|
|
@@ -1399,9 +2811,11 @@ var gemFrameworkRuntimePlugins = [
|
|
|
1399
2811
|
events: ["gem:hero-slideshow:ready", "gem:hero-slideshow:change"]
|
|
1400
2812
|
},
|
|
1401
2813
|
{
|
|
2814
|
+
// 0.19.0: a lazy chunk with the motion engine's quick view; idle schedules only gem-runtime.js's stub, which imports
|
|
2815
|
+
// the chunk on the first interaction with a trigger (or at idle), never on a page without one.
|
|
1402
2816
|
key: "quick-view",
|
|
1403
|
-
load_strategy: "
|
|
1404
|
-
events: ["gem:quick-view:open", "gem:quick-view:close"]
|
|
2817
|
+
load_strategy: "idle",
|
|
2818
|
+
events: ["gem:quick-view:ready", "gem:quick-view:open", "gem:quick-view:close", "gem:cart:add"]
|
|
1405
2819
|
},
|
|
1406
2820
|
{
|
|
1407
2821
|
key: "promo-popup",
|
|
@@ -1467,6 +2881,24 @@ var gemFrameworkRuntimePlugins = [
|
|
|
1467
2881
|
key: "bundle",
|
|
1468
2882
|
load_strategy: "idle",
|
|
1469
2883
|
events: ["gem:bundle:ready", "gem:bundle:change", "gem:bundle:complete"]
|
|
2884
|
+
},
|
|
2885
|
+
{
|
|
2886
|
+
// idle, not interaction: the button it shows has to be there before anyone interacts.
|
|
2887
|
+
key: "size-guide",
|
|
2888
|
+
load_strategy: "idle",
|
|
2889
|
+
events: ["gem:size-guide:ready", "gem:size-guide:open"]
|
|
2890
|
+
},
|
|
2891
|
+
// The motion engine's effects (0.19.0): lazy chunks. idle schedules gem-runtime.js's stub, which imports the chunk when a
|
|
2892
|
+
// hook comes near the viewport and never on a page without one.
|
|
2893
|
+
{
|
|
2894
|
+
key: "orbit",
|
|
2895
|
+
load_strategy: "idle",
|
|
2896
|
+
events: ["gem:orbit:ready", "gem:motion:front", "gem:motion:fallback"]
|
|
2897
|
+
},
|
|
2898
|
+
{
|
|
2899
|
+
key: "rail",
|
|
2900
|
+
load_strategy: "idle",
|
|
2901
|
+
events: ["gem:rail:ready", "gem:motion:front", "gem:motion:fallback"]
|
|
1470
2902
|
}
|
|
1471
2903
|
];
|
|
1472
2904
|
var gemFrameworkValidationRules = [
|
|
@@ -1572,6 +3004,24 @@ var GEM_POPUP_SHELL_CSS_LINES = [
|
|
|
1572
3004
|
" .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
3005
|
" .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
3006
|
"}",
|
|
3007
|
+
// 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
|
|
3008
|
+
// 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
|
|
3009
|
+
// 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ừ đó
|
|
3010
|
+
// (--gem-motion-base / --gem-ease-enter, như ngăn giỏ của framework). Chỉ là bố cục desktop: mobile vẫn takeover / sheet.
|
|
3011
|
+
"@keyframes gem-popup-drawer-in { from { translate: calc(100% * var(--gem-popup-drawer-dir, 1)) 0; } }",
|
|
3012
|
+
".gem-popup-shell__dialog:dir(rtl) { --gem-popup-drawer-dir: -1; }",
|
|
3013
|
+
"@media (min-width: 750px) {",
|
|
3014
|
+
" .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__dialog {",
|
|
3015
|
+
" width: min(var(--gem-popup-drawer-width, 640px), 100vw); max-width: 100vw; height: 100dvh; max-height: 100dvh;",
|
|
3016
|
+
" margin-block: 0; margin-inline: auto 0; border-radius: 0;",
|
|
3017
|
+
" animation: gem-popup-drawer-in var(--gem-motion-base, 180ms) var(--gem-ease-enter, ease-out);",
|
|
3018
|
+
" }",
|
|
3019
|
+
" .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__frame { grid-template-columns: 1fr; align-content: start; min-height: 100%; }",
|
|
3020
|
+
" .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__media { max-height: 240px; }",
|
|
3021
|
+
" .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__panel { justify-content: flex-start; align-items: stretch; text-align: start; }",
|
|
3022
|
+
"}",
|
|
3023
|
+
// 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).
|
|
3024
|
+
"@media (prefers-reduced-motion: reduce) { .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__dialog { animation: none; } }",
|
|
1575
3025
|
"@media (max-width: 749px) {",
|
|
1576
3026
|
// Mobile: takeover is the whole screen; a sheet rises from the bottom to at most 88% of it. The image
|
|
1577
3027
|
// sits under the text (top when media=top), capped at a third of the screen; with no image or the image
|
|
@@ -1607,6 +3057,9 @@ var GEM_POPUP_SHELL_CSS_LINES = [
|
|
|
1607
3057
|
"@media (min-width: 750px) {",
|
|
1608
3058
|
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] { padding: 0; }",
|
|
1609
3059
|
" :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; }",
|
|
3060
|
+
// 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).
|
|
3061
|
+
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='drawer-end'] { padding-inline: 0; }",
|
|
3062
|
+
" :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
3063
|
"}",
|
|
1611
3064
|
"@media (max-width: 749px) {",
|
|
1612
3065
|
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'] { padding: 0; }",
|
|
@@ -1802,7 +3255,7 @@ var GEM_INLINE_MARK_CSS_LINES = [
|
|
|
1802
3255
|
":where(.gem-mark--alert) { color: var(--gem-color-danger, #b42318); }",
|
|
1803
3256
|
":where(.gem-mark--highlight.gem-mark--accent), :where(.gem-mark--highlight.gem-mark--alert) { color: var(--gem-mark-highlight-ink, #1f1d18); }",
|
|
1804
3257
|
".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; }",
|
|
3258
|
+
".gem-mark-link:hover { text-decoration-thickness: var(--gem-underline-thickness, 2px); }",
|
|
1806
3259
|
// 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
3260
|
// 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
3261
|
".gem-mark-link.gem-mark--highlight, .gem-mark--highlight .gem-mark-link { color: var(--gem-mark-highlight-ink, #1f1d18); }"
|
|
@@ -2028,6 +3481,14 @@ function createPrimitiveCss() {
|
|
|
2028
3481
|
" cursor: not-allowed;",
|
|
2029
3482
|
" opacity: 0.55;",
|
|
2030
3483
|
"}",
|
|
3484
|
+
// Button shape of ONE section (GemFactory gap #69, 22/10/2026): matching a source, a section needs square, softly rounded
|
|
3485
|
+
// or pill buttons whatever the preset's button radius is, and writing border-radius on .gem-button takes the button
|
|
3486
|
+
// away from the preset for good (GemFactory's button_token_shadowed). `data-gem-button-shape` on the section root (or on
|
|
3487
|
+
// one button) sets the radius the button already reads; with no attribute the preset decides, as before. Square and
|
|
3488
|
+
// pill are shapes, so they do not scale; soft is a size, so it follows the preset's radius scale (--gem-style-radius).
|
|
3489
|
+
"[data-gem-button-shape='square'] { --gem-radius-button: 0px; }",
|
|
3490
|
+
"[data-gem-button-shape='soft'] { --gem-radius-button: var(--gem-radius-sm); }",
|
|
3491
|
+
"[data-gem-button-shape='pill'] { --gem-radius-button: 999px; }",
|
|
2031
3492
|
".gem-input:disabled, .gem-select:disabled, .gem-textarea:disabled, .gem-quantity-input__button:disabled {",
|
|
2032
3493
|
" cursor: not-allowed;",
|
|
2033
3494
|
" opacity: 0.6;",
|
|
@@ -3405,8 +4866,8 @@ function createPrimitiveCss() {
|
|
|
3405
4866
|
function createGemSectionRuntimeContractCss(selector = "[data-gem-section-library-id]", options = {}) {
|
|
3406
4867
|
const background = options.backgroundVar ?? "var(--gem-color-background)";
|
|
3407
4868
|
const surface = options.surfaceVar ?? "var(--gem-color-surface)";
|
|
3408
|
-
const
|
|
3409
|
-
const mutedText = options.mutedTextVar ?? `color-mix(in srgb, ${
|
|
4869
|
+
const text2 = options.textVar ?? "var(--gem-color-text)";
|
|
4870
|
+
const mutedText = options.mutedTextVar ?? `color-mix(in srgb, ${text2} 72%, transparent)`;
|
|
3410
4871
|
const accent = options.accentVar ?? "var(--gem-color-accent)";
|
|
3411
4872
|
const accentText = options.accentTextVar ?? "var(--gem-color-accent-text, #ffffff)";
|
|
3412
4873
|
const border = options.borderVar ?? "var(--gem-color-border)";
|
|
@@ -3416,7 +4877,7 @@ function createGemSectionRuntimeContractCss(selector = "[data-gem-section-librar
|
|
|
3416
4877
|
`${selector} {`,
|
|
3417
4878
|
" box-sizing: border-box;",
|
|
3418
4879
|
` background: ${background};`,
|
|
3419
|
-
` color: ${
|
|
4880
|
+
` color: ${text2};`,
|
|
3420
4881
|
"}",
|
|
3421
4882
|
`${selector} *,`,
|
|
3422
4883
|
`${selector} *::before,`,
|
|
@@ -3475,7 +4936,7 @@ function createGemSectionRuntimeContractCss(selector = "[data-gem-section-librar
|
|
|
3475
4936
|
" border-radius: var(--gem-radius-pill, 999px);",
|
|
3476
4937
|
" padding: 6px 10px;",
|
|
3477
4938
|
` background: ${surface};`,
|
|
3478
|
-
` color: ${
|
|
4939
|
+
` color: ${text2};`,
|
|
3479
4940
|
" font-size: 0.8125rem;",
|
|
3480
4941
|
" font-weight: 700;",
|
|
3481
4942
|
"}",
|
|
@@ -4833,8 +6294,8 @@ function resolveGift(gift, totalAmount) {
|
|
|
4833
6294
|
const remaining = `${symbol}${remainingAmount.toFixed(2)}`;
|
|
4834
6295
|
const percent = gift.threshold > 0 ? Math.max(0, Math.min(100, Math.round(totalAmount / gift.threshold * 100))) : 100;
|
|
4835
6296
|
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 };
|
|
6297
|
+
const text2 = template.replace("{remaining}", remaining).replace("{label}", gift.label);
|
|
6298
|
+
return { unlocked, remaining, remaining_amount: remainingAmount, percent, text: text2 };
|
|
4838
6299
|
}
|
|
4839
6300
|
function matches(when, sel) {
|
|
4840
6301
|
return Object.keys(when).every((k) => sel[k] === when[k]);
|
|
@@ -5980,7 +7441,10 @@ var runtimePluginReadyEvents = {
|
|
|
5980
7441
|
"context-adapter": "gem:context:ready",
|
|
5981
7442
|
"offer-engine": "gem:offer:ready",
|
|
5982
7443
|
reviews: "gem:reviews:ready",
|
|
5983
|
-
bundle: "gem:bundle:ready"
|
|
7444
|
+
bundle: "gem:bundle:ready",
|
|
7445
|
+
"size-guide": "gem:size-guide:ready",
|
|
7446
|
+
orbit: "gem:orbit:ready",
|
|
7447
|
+
rail: "gem:rail:ready"
|
|
5984
7448
|
};
|
|
5985
7449
|
var cartDrawerPluginRegistrationLines = [
|
|
5986
7450
|
" registerPlugin('cart-drawer', ({ api }) => {",
|
|
@@ -6474,8 +7938,7 @@ var heroSlideshowPluginRegistrationLines = [
|
|
|
6474
7938
|
" api.emit('gem:hero-slideshow:ready', { source: 'gem-runtime', count: bound });",
|
|
6475
7939
|
" });"
|
|
6476
7940
|
];
|
|
6477
|
-
var
|
|
6478
|
-
" registerPlugin('quick-view', ({ api }) => {",
|
|
7941
|
+
var legacyQuickViewLines = [
|
|
6479
7942
|
` const triggers = document.querySelectorAll('[data-gem-quick-view], [data-gem-action="quick-view"]');`,
|
|
6480
7943
|
` const surface = document.querySelector('[data-gem-quick-view-surface], .gem-quick-view, .gem-modal[data-gem-component="quick-view"]');`,
|
|
6481
7944
|
" let bound = 0;",
|
|
@@ -6525,10 +7988,15 @@ var quickViewPluginRegistrationLines = [
|
|
|
6525
7988
|
" });",
|
|
6526
7989
|
" if (canListen(surface)) surface.addEventListener('keydown', (event) => { if (event.key === 'Escape') close('escape'); });",
|
|
6527
7990
|
" }",
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
"
|
|
7991
|
+
// A mount on one late trigger (gem-runtime.js binds a section added after boot on its own) finds no surface: it keeps the
|
|
7992
|
+
// page's api.quickView.
|
|
7993
|
+
" if (surface || !api.quickView) api.quickView = { open: (detail = {}) => openSurface({ ...detail }), close };"
|
|
6531
7994
|
];
|
|
7995
|
+
var runtimeChunks = gemMotionChunks({
|
|
7996
|
+
hook: '[data-gem-quick-view], [data-gem-action="quick-view"]',
|
|
7997
|
+
lines: legacyQuickViewLines,
|
|
7998
|
+
readyDetail: "count: bound + count, hasSurface: Boolean(surface)"
|
|
7999
|
+
});
|
|
6532
8000
|
var promoPopupPluginRegistrationLines = [
|
|
6533
8001
|
" registerPlugin('promo-popup', ({ api }) => {",
|
|
6534
8002
|
// Only the explicit hook opts in. Every promo-popup section carries the archetype class .gem-promo-popup, and
|
|
@@ -7068,6 +8536,30 @@ var bundlePluginRegistrationLines = [
|
|
|
7068
8536
|
" " + bundleEngineRuntimeScript(),
|
|
7069
8537
|
" });"
|
|
7070
8538
|
];
|
|
8539
|
+
var sizeGuidePluginRegistrationLines = [
|
|
8540
|
+
" registerPlugin('size-guide', ({ api }) => {",
|
|
8541
|
+
" const page = document.documentElement && typeof document.documentElement.querySelectorAll === 'function' ? document.documentElement : document;",
|
|
8542
|
+
" const guides = Array.prototype.slice.call(page.querySelectorAll('[data-gem-size-guide]'));",
|
|
8543
|
+
" const opens = (target) => guides.some((guide) => !target || guide.getAttribute('data-gem-size-guide') === target || guide.id === target);",
|
|
8544
|
+
" let bound = 0;",
|
|
8545
|
+
" document.querySelectorAll('[data-gem-size-guide-trigger]').forEach((trigger) => {",
|
|
8546
|
+
" const target = trigger.getAttribute('data-gem-size-guide-trigger') || '';",
|
|
8547
|
+
" if (!opens(target)) { trigger.hidden = true; return; }",
|
|
8548
|
+
" if (!mark(trigger, 'size-guide')) return;",
|
|
8549
|
+
" trigger.hidden = false;",
|
|
8550
|
+
" if (!canListen(trigger)) return;",
|
|
8551
|
+
" trigger.addEventListener('click', (event) => {",
|
|
8552
|
+
" event.preventDefault && event.preventDefault();",
|
|
8553
|
+
" const holder = trigger.closest ? trigger.closest('[data-product-id]') : null;",
|
|
8554
|
+
" const detail = { source: 'gem-runtime', sectionId: target || null, productId: holder ? holder.getAttribute('data-product-id') : null };",
|
|
8555
|
+
" if (typeof window.CustomEvent === 'function') window.dispatchEvent(new window.CustomEvent('gem:size-guide:open', { detail }));",
|
|
8556
|
+
" api.emit('gem:size-guide:open', detail);",
|
|
8557
|
+
" });",
|
|
8558
|
+
" bound += 1;",
|
|
8559
|
+
" });",
|
|
8560
|
+
" api.emit('gem:size-guide:ready', { source: 'gem-runtime', count: bound, guides: guides.length });",
|
|
8561
|
+
" });"
|
|
8562
|
+
];
|
|
7071
8563
|
var runtimePluginSourceLines = [
|
|
7072
8564
|
["cart-drawer", cartDrawerPluginRegistrationLines],
|
|
7073
8565
|
["mobile-menu", mobileMenuPluginRegistrationLines],
|
|
@@ -7080,7 +8572,7 @@ var runtimePluginSourceLines = [
|
|
|
7080
8572
|
["media-gallery", mediaGalleryPluginRegistrationLines],
|
|
7081
8573
|
["carousel", carouselPluginRegistrationLines],
|
|
7082
8574
|
["hero-slideshow", heroSlideshowPluginRegistrationLines],
|
|
7083
|
-
["quick-view",
|
|
8575
|
+
["quick-view", runtimeChunks["quick-view"].body],
|
|
7084
8576
|
["promo-popup", promoPopupPluginRegistrationLines],
|
|
7085
8577
|
["sticky-atc", stickyAtcPluginRegistrationLines],
|
|
7086
8578
|
["before_after", beforeAfterPluginRegistrationLines],
|
|
@@ -7091,16 +8583,94 @@ var runtimePluginSourceLines = [
|
|
|
7091
8583
|
["runtime-slot-loader", runtimeSlotLoaderPluginRegistrationLines],
|
|
7092
8584
|
["offer-engine", offerEnginePluginRegistrationLines],
|
|
7093
8585
|
["reviews", reviewsPluginRegistrationLines],
|
|
7094
|
-
["bundle", bundlePluginRegistrationLines]
|
|
8586
|
+
["bundle", bundlePluginRegistrationLines],
|
|
8587
|
+
["size-guide", sizeGuidePluginRegistrationLines],
|
|
8588
|
+
["orbit", runtimeChunks.orbit.body],
|
|
8589
|
+
["rail", runtimeChunks.rail.body]
|
|
7095
8590
|
];
|
|
7096
8591
|
var noopPluginRegistrationLine = (key, readyEvent) => ` registerNoopPlugin(${JSON.stringify(key)}, ${JSON.stringify(readyEvent)});`;
|
|
8592
|
+
var chunkStubLine = (key, chunk) => ` registerPlugin(${JSON.stringify(key)}, ({ api }) => loadChunk(${JSON.stringify(key)}, ${JSON.stringify(chunk.hook)}, ${JSON.stringify(chunk.when)}, api));`;
|
|
8593
|
+
var chunkLoaderLines = [
|
|
8594
|
+
" const chunkBase = (document.currentScript && document.currentScript.src) || '';",
|
|
8595
|
+
" const chunks = {};",
|
|
8596
|
+
" let chunkWatch = null;",
|
|
8597
|
+
" const loadChunkModule = (chunk) => {",
|
|
8598
|
+
" if (chunk.state) return;",
|
|
8599
|
+
" chunk.state = 1;",
|
|
8600
|
+
" Promise.resolve().then(() => {",
|
|
8601
|
+
" const url = new URL('plugins/' + chunk.key + '.js', chunk.api.pluginBase || chunkBase || document.baseURI).href;",
|
|
8602
|
+
" return chunk.api.importChunk ? chunk.api.importChunk(url) : import(url);",
|
|
8603
|
+
" }).then((module) => {",
|
|
8604
|
+
" chunk.module = module;",
|
|
8605
|
+
" module.mount(document, { api: chunk.api, window: target });",
|
|
8606
|
+
" chunk.state = 2;",
|
|
8607
|
+
" if (chunk.replay) chunk.replay.click();",
|
|
8608
|
+
" chunk.replay = null;",
|
|
8609
|
+
" }, (error) => { chunk.state = 0; chunk.replay = null; chunk.api.emit('gem:plugin:error', { key: chunk.key, error: String(error) }); });",
|
|
8610
|
+
" };",
|
|
8611
|
+
" const armChunk = (chunk, hooks) => {",
|
|
8612
|
+
" const load = () => loadChunkModule(chunk);",
|
|
8613
|
+
" if (chunk.when === 'viewport') {",
|
|
8614
|
+
" if (!target.IntersectionObserver) { load(); return; }",
|
|
8615
|
+
" chunk.io = chunk.io || new target.IntersectionObserver((entries) => { if (entries.some((entry) => entry.isIntersecting)) { chunk.io.disconnect(); load(); } }, { rootMargin: '200px' });",
|
|
8616
|
+
" hooks.forEach((hookEl) => chunk.io.observe(hookEl));",
|
|
8617
|
+
" return;",
|
|
8618
|
+
" }",
|
|
8619
|
+
" hooks.forEach((hookEl) => {",
|
|
8620
|
+
" ['pointerover', 'focusin', 'touchstart'].forEach((type) => hookEl.addEventListener(type, load, { once: true, passive: true }));",
|
|
8621
|
+
" hookEl.addEventListener('click', (event) => {",
|
|
8622
|
+
" if (chunk.state === 2 || event.defaultPrevented) return;",
|
|
8623
|
+
" const host = hookEl.closest && hookEl.closest('[data-gem-quick-view-host]');",
|
|
8624
|
+
" if (host && host.gemQuickViewHost && !host.gemQuickViewHost.opens(hookEl)) return;",
|
|
8625
|
+
" event.preventDefault();",
|
|
8626
|
+
" chunk.replay = hookEl;",
|
|
8627
|
+
" load();",
|
|
8628
|
+
" });",
|
|
8629
|
+
" });",
|
|
8630
|
+
" if (!chunk.idle) { chunk.idle = true; onIdle(load); }",
|
|
8631
|
+
" };",
|
|
8632
|
+
" const scanChunk = (chunk, scope) => {",
|
|
8633
|
+
" const hooks = Array.prototype.slice.call(scope.querySelectorAll ? scope.querySelectorAll(chunk.hook) : []);",
|
|
8634
|
+
" if (scope !== document && scope.matches && scope.matches(chunk.hook)) hooks.unshift(scope);",
|
|
8635
|
+
" if (!hooks.length) return false;",
|
|
8636
|
+
" if (chunk.state === 2) hooks.forEach((hookEl) => chunk.module.mount(hookEl, { api: chunk.api, window: target }));",
|
|
8637
|
+
" else armChunk(chunk, hooks);",
|
|
8638
|
+
" return true;",
|
|
8639
|
+
" };",
|
|
8640
|
+
" const watchChunks = () => {",
|
|
8641
|
+
" if (chunkWatch || !target.MutationObserver) return;",
|
|
8642
|
+
" let added = [];",
|
|
8643
|
+
" let timer = 0;",
|
|
8644
|
+
" const flush = () => {",
|
|
8645
|
+
" timer = 0;",
|
|
8646
|
+
" const nodes = added;",
|
|
8647
|
+
" added = [];",
|
|
8648
|
+
" nodes.forEach((node) => { if (node.isConnected) Object.keys(chunks).forEach((key) => scanChunk(chunks[key], node)); });",
|
|
8649
|
+
" };",
|
|
8650
|
+
" chunkWatch = new target.MutationObserver((records) => {",
|
|
8651
|
+
" records.forEach((record) => record.addedNodes.forEach((node) => { if (node.nodeType === 1) added.push(node); }));",
|
|
8652
|
+
" if (added.length && !timer) timer = setTimeout(flush, 50);",
|
|
8653
|
+
" });",
|
|
8654
|
+
" chunkWatch.observe(document.documentElement, { childList: true, subtree: true });",
|
|
8655
|
+
" 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)); });",
|
|
8656
|
+
" };",
|
|
8657
|
+
" const loadChunk = (key, hook, when, api) => {",
|
|
8658
|
+
" const chunk = (chunks[key] = { key, hook, when, api, state: 0, module: null, replay: null });",
|
|
8659
|
+
" watchChunks();",
|
|
8660
|
+
" if (!scanChunk(chunk, document)) api.emit('gem:' + key + ':ready', { source: 'gem-runtime', count: 0 });",
|
|
8661
|
+
" };"
|
|
8662
|
+
];
|
|
7097
8663
|
function gemRuntimePluginSources() {
|
|
7098
|
-
const withBody = runtimePluginSourceLines.map(([key, lines]) =>
|
|
7099
|
-
key
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
8664
|
+
const withBody = runtimePluginSourceLines.map(([key, lines]) => {
|
|
8665
|
+
const chunk = runtimeChunks[key];
|
|
8666
|
+
return {
|
|
8667
|
+
key,
|
|
8668
|
+
readyEvent: runtimePluginReadyEvents[key] ?? `gem:${key}:ready`,
|
|
8669
|
+
hasBody: true,
|
|
8670
|
+
lines: chunk ? [chunkStubLine(key, chunk)] : lines,
|
|
8671
|
+
...chunk ? { chunk } : {}
|
|
8672
|
+
};
|
|
8673
|
+
});
|
|
7104
8674
|
const noop = Object.entries(runtimePluginReadyEvents).filter(([key]) => !runtimePluginSourceLines.some(([sourceKey]) => sourceKey === key)).map(([key, readyEvent]) => ({ key, readyEvent, hasBody: false, lines: [noopPluginRegistrationLine(key, readyEvent)] }));
|
|
7105
8675
|
return [...withBody, ...noop];
|
|
7106
8676
|
}
|
|
@@ -7235,7 +8805,12 @@ function createGemRuntimeBootstrap(configArg) {
|
|
|
7235
8805
|
" if (readyEvent) api.emit(readyEvent, { source: 'gem-runtime' });",
|
|
7236
8806
|
" });",
|
|
7237
8807
|
" };",
|
|
7238
|
-
...runtimePluginSourceLines.
|
|
8808
|
+
...runtimePluginSourceLines.some(([key]) => runtimeChunks[key] && enabledPluginKeys.has(key)) ? chunkLoaderLines : [],
|
|
8809
|
+
...runtimePluginSourceLines.flatMap(([key, lines]) => {
|
|
8810
|
+
if (!enabledPluginKeys.has(key)) return [];
|
|
8811
|
+
const chunk = runtimeChunks[key];
|
|
8812
|
+
return chunk ? [chunkStubLine(key, chunk)] : lines;
|
|
8813
|
+
}),
|
|
7239
8814
|
...pluginRegistrationLines,
|
|
7240
8815
|
// `data-gem-bound-<key>` is a CONTRACT, not private bookkeeping: markup may carry it to claim an element for its
|
|
7241
8816
|
// own behavior (promo-popup.email-capture.v1 renders data-gem-bound-promo-popup="section"). Any value counts as
|
|
@@ -7331,17 +8906,18 @@ function gemRuntimePluginModuleKeys() {
|
|
|
7331
8906
|
function createGemRuntimePluginModule(key) {
|
|
7332
8907
|
const source = gemRuntimePluginSources().find((candidate) => candidate.key === key);
|
|
7333
8908
|
if (!source) throw new Error(`gem-framework: no runtime plugin "${key}"`);
|
|
7334
|
-
const body = routeListenersThroughLedger(source.lines.join("\n"), key);
|
|
8909
|
+
const body = routeListenersThroughLedger((source.chunk ? source.chunk.body : source.lines).join("\n"), key);
|
|
7335
8910
|
const guard = PLUGIN_GUARDS[key];
|
|
7336
8911
|
return [
|
|
7337
8912
|
`// @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.",
|
|
8913
|
+
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
8914
|
'import { mountPlugin } from "../runtime-core.js";',
|
|
8915
|
+
...source.chunk ? source.chunk.imports : [],
|
|
7340
8916
|
"",
|
|
7341
8917
|
`export const key = ${JSON.stringify(key)};`,
|
|
7342
8918
|
`export const readyEvent = ${JSON.stringify(source.readyEvent)};`,
|
|
7343
8919
|
"",
|
|
7344
|
-
"function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {",
|
|
8920
|
+
"function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin, onUnmount }) {",
|
|
7345
8921
|
body,
|
|
7346
8922
|
"}",
|
|
7347
8923
|
"",
|
|
@@ -7381,7 +8957,7 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
|
7381
8957
|
const namespace = group === "typography" ? "font" : group === "spacing" ? "space" : group;
|
|
7382
8958
|
return keys.map((key) => `--gem-${namespace}-${slugify(key)}`);
|
|
7383
8959
|
}),
|
|
7384
|
-
// The governed type ramp (--gem-h1…h6, --gem-body, --gem-text-sm, --gem-type-base/-scale). The
|
|
8960
|
+
// The governed type ramp (--gem-h1…h6, --gem-body, --gem-text-sm, --gem-text-xs, --gem-type-base/-scale). The
|
|
7385
8961
|
// framework stylesheet declares it on every page, but it is not a token GROUP, so it was missing here
|
|
7386
8962
|
// and a section sizing its heading with `var(--gem-h2)` got `unknown_gem_css_variable`. Read from the
|
|
7387
8963
|
// function that emits it, so the ramp and this list cannot drift.
|
|
@@ -7400,7 +8976,12 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
|
7400
8976
|
// The row primitive's variables (primitives.ts GEM_ROW_CSS). Sections SET these on their grid.
|
|
7401
8977
|
...GEM_ROW_CSS_VARIABLES,
|
|
7402
8978
|
// 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
|
|
8979
|
+
...GEM_FORM_CSS_VARIABLES,
|
|
8980
|
+
// The motion engine's (motion-engine.ts, 0.19.0), all of them: a section copies the base CSS VERBATIM into its own CSS,
|
|
8981
|
+
// internals (--gem-orbit-a, --gem-rail-t) included, and sets --gem-orbit-i / -n, the settings and --gem-motion-tint.
|
|
8982
|
+
...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) ?? []),
|
|
8983
|
+
// …and every setting an effect reads from a CSS variable (`css: true`): a section sets it in its stylesheet, per breakpoint.
|
|
8984
|
+
...gemMotionEffectList.flatMap((effect) => Object.entries(effect.settings).filter(([, setting]) => setting.css).map(([name]) => `--gem-${effect.key}-${name}`))
|
|
7404
8985
|
]);
|
|
7405
8986
|
var TOKENIZED_PROPERTIES = [
|
|
7406
8987
|
"background",
|
|
@@ -7512,14 +9093,25 @@ function validateGemFrameworkMarkup(markup, artifactPath, options = {}) {
|
|
|
7512
9093
|
}
|
|
7513
9094
|
return result(issues);
|
|
7514
9095
|
}
|
|
9096
|
+
var NAMESPACE_URL = String.raw`https?:\/\/www\.w3\.org\/[\w.\/#:~%+-]*`;
|
|
9097
|
+
var SHARE_NETWORK = "facebook|twitter|x|pinterest|whatsapp|threads";
|
|
9098
|
+
var AS_A_LINK = String.raw`(?<=(?:<a\s[^<>]{0,400}?(?<![\w-])href=\{?|[{,]\s{0,40}["']?(?:${SHARE_NETWORK})["']?\s{0,8}:\s{0,8})["'\x60])`;
|
|
9099
|
+
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)\?`;
|
|
9100
|
+
var NEVER_FETCHED_URL = new RegExp(`${NAMESPACE_URL}|${AS_A_LINK}${SHARE_INTENT}`, "gi");
|
|
9101
|
+
var JSON_LD = /<script\b[^>]*type=["']application\/ld\+json["'][^>]*>[\s\S]*?<\/script>/gi;
|
|
9102
|
+
function namesExternalDependency(withoutJsonLd) {
|
|
9103
|
+
return /https?:\/\/|\/\/cdn\./i.test(withoutJsonLd.replace(NEVER_FETCHED_URL, ""));
|
|
9104
|
+
}
|
|
9105
|
+
function hasExternalDependency(content) {
|
|
9106
|
+
return namesExternalDependency(content.replace(JSON_LD, ""));
|
|
9107
|
+
}
|
|
7515
9108
|
function validateSharedSafety(content, artifactPath) {
|
|
7516
9109
|
const issues = [];
|
|
7517
|
-
const
|
|
7518
|
-
|
|
7519
|
-
if (/<script\b/i.test(contentWithoutJsonLd)) {
|
|
9110
|
+
const withoutJsonLd = content.replace(JSON_LD, "");
|
|
9111
|
+
if (/<script\b/i.test(withoutJsonLd)) {
|
|
7520
9112
|
issues.push(ruleIssue("inline_script", "Gem Framework artifacts must not include inline script tags.", artifactPath));
|
|
7521
9113
|
}
|
|
7522
|
-
if (
|
|
9114
|
+
if (namesExternalDependency(withoutJsonLd)) {
|
|
7523
9115
|
issues.push(ruleIssue("external_dependency", "Gem Framework artifacts must not include external runtime or CDN dependencies.", artifactPath));
|
|
7524
9116
|
}
|
|
7525
9117
|
return issues;
|
|
@@ -7548,6 +9140,9 @@ function gemFrameworkPublicCssVariables() {
|
|
|
7548
9140
|
String(createSectionCss()),
|
|
7549
9141
|
String(createShellCss()),
|
|
7550
9142
|
String(createGemSectionRuntimeContractCss()),
|
|
9143
|
+
// popup-shell.css (its own file since 0.8.3, so not in createPrimitiveCss): a popup section sets
|
|
9144
|
+
// --gem-popup-drawer-width (0.18.17, drawer-end) and must not be told the framework's own variable is unknown.
|
|
9145
|
+
GEM_POPUP_SHELL_CSS,
|
|
7551
9146
|
// The button case rule only exists when a token sets it; a section may still read its variables (a CTA rule
|
|
7552
9147
|
// beats the zero-specificity rule, so opting in means `text-transform: var(--gem-font-button-transform, none)`).
|
|
7553
9148
|
gemButtonCaseCss({ typography: { buttonTransform: "none", buttonLetterSpacing: "normal" } })
|
|
@@ -7558,11 +9153,13 @@ function gemFrameworkPublicCssVariables() {
|
|
|
7558
9153
|
}
|
|
7559
9154
|
var ROOT_SELECTOR = /^\s*(?::root|html|:host)(?:\[[^\]]*\]|:[a-z-]+(?:\([^()]*\))?)*\s*$/;
|
|
7560
9155
|
var EDITION_ROOT_SELECTOR = /^\s*:where\(\s*\[data-gem-edition=(["'])\d+\1\](?:\s*,\s*\[data-gem-edition=(["'])\d+\2\])*\s*\)\s*$/;
|
|
9156
|
+
var KNOB_SCOPE_SELECTOR = /^\s*\[data-gem-[a-z0-9-]+(?:=(["'])[^"']*\1)?\]\s*$/;
|
|
7561
9157
|
function gemPublicCssVariablesFrom(stylesheet) {
|
|
7562
9158
|
const css = stylesheet.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
7563
9159
|
const declaredOnRoot = /* @__PURE__ */ new Set();
|
|
7564
9160
|
const declaredElsewhere = /* @__PURE__ */ new Set();
|
|
7565
9161
|
for (const [, selector, body] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
9162
|
+
if (selector.split(",").every((part) => KNOB_SCOPE_SELECTOR.test(part))) continue;
|
|
7566
9163
|
const onRoot = EDITION_ROOT_SELECTOR.test(selector) || selector.split(",").every((part) => ROOT_SELECTOR.test(part));
|
|
7567
9164
|
for (const [, name] of body.matchAll(/(--gem-[a-z0-9-]+)\s*:/g)) (onRoot ? declaredOnRoot : declaredElsewhere).add(name);
|
|
7568
9165
|
}
|
|
@@ -7761,9 +9358,9 @@ function renderGemShellHeader(plan) {
|
|
|
7761
9358
|
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
9359
|
].filter(Boolean).join("");
|
|
7763
9360
|
const bars = Array.isArray(header.bars) ? header.bars.filter((b) => (b?.text ?? "").trim()) : [];
|
|
7764
|
-
const barMarkup = (
|
|
9361
|
+
const barMarkup = (text2, bg, fg, segments) => {
|
|
7765
9362
|
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(
|
|
9363
|
+
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
9364
|
return `<div class="announcement-bar" role="region" aria-label="Announcement"${style ? ` style="${escapeAttribute(style)}"` : ""}><p class="announcement-bar__message">${body}</p></div>`;
|
|
7768
9365
|
};
|
|
7769
9366
|
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 +10345,18 @@ export {
|
|
|
8748
10345
|
GEM_IMG_IN_CSS,
|
|
8749
10346
|
GEM_INLINE_MARK_CSS,
|
|
8750
10347
|
GEM_LINE_HEIGHT_TOKEN_KEYS,
|
|
10348
|
+
GEM_MOTION_CLASSES,
|
|
8751
10349
|
GEM_MOTION_REDUCED,
|
|
10350
|
+
GEM_MOTION_RUNTIME_FILES,
|
|
8752
10351
|
GEM_MOTION_SCALE_DEFAULTS,
|
|
10352
|
+
TIERS as GEM_MOTION_TIERS,
|
|
10353
|
+
GEM_ORBIT_BASE_CSS,
|
|
10354
|
+
GEM_ORBIT_RUNTIME_CSS,
|
|
8753
10355
|
GEM_PART_CLASS,
|
|
8754
10356
|
GEM_POPUP_SHELL_CSS,
|
|
10357
|
+
GEM_QUICK_VIEW_CSS,
|
|
10358
|
+
GEM_RAIL_BASE_CSS,
|
|
10359
|
+
GEM_RAIL_RUNTIME_CSS,
|
|
8755
10360
|
GEM_REVEAL_CSS,
|
|
8756
10361
|
GEM_ROW_CSS,
|
|
8757
10362
|
GEM_ROW_CSS_VARIABLES,
|
|
@@ -8769,6 +10374,9 @@ export {
|
|
|
8769
10374
|
aaTextColor,
|
|
8770
10375
|
auditGemFrameworkClassName,
|
|
8771
10376
|
bundleEngineRuntimeScript,
|
|
10377
|
+
capabilityTier,
|
|
10378
|
+
checkEffect,
|
|
10379
|
+
compactCss,
|
|
8772
10380
|
createGemBaseCss,
|
|
8773
10381
|
createGemFrameworkManifest,
|
|
8774
10382
|
createGemRuntimeBootstrap,
|
|
@@ -8788,7 +10396,9 @@ export {
|
|
|
8788
10396
|
createThemeProfileCss,
|
|
8789
10397
|
defaultGemTokens,
|
|
8790
10398
|
defaultSelection,
|
|
10399
|
+
defineEffect,
|
|
8791
10400
|
deriveFormSetId,
|
|
10401
|
+
follow,
|
|
8792
10402
|
gemBandSchemeColors,
|
|
8793
10403
|
gemBandSchemeCssVars,
|
|
8794
10404
|
gemBandSchemeDeclarations,
|
|
@@ -8809,17 +10419,23 @@ export {
|
|
|
8809
10419
|
gemFrameworkValidationRules,
|
|
8810
10420
|
gemFrameworkVersion,
|
|
8811
10421
|
gemMobileTokenCssVars,
|
|
10422
|
+
gemMotionChunks,
|
|
10423
|
+
gemMotionEffectList,
|
|
10424
|
+
gemMotionEffectManifest,
|
|
8812
10425
|
gemMotionScaleCssVars,
|
|
10426
|
+
gemOrbitBaseCss,
|
|
8813
10427
|
gemPartClass,
|
|
8814
10428
|
gemPhoneTokenTarget,
|
|
8815
10429
|
gemPlanFormSet,
|
|
8816
10430
|
gemPlanShapeKnobs,
|
|
10431
|
+
gemRailBaseCss,
|
|
8817
10432
|
gemRuntimePluginModuleKeys,
|
|
8818
10433
|
gemRuntimePluginSources,
|
|
8819
10434
|
gemSectionDividerResponsiveStyle,
|
|
8820
10435
|
gemSectionDividerStyle,
|
|
8821
10436
|
gemSectionDividers,
|
|
8822
10437
|
getGemFrameworkValidationRule,
|
|
10438
|
+
hasExternalDependency,
|
|
8823
10439
|
hexContrastRatio,
|
|
8824
10440
|
hexRelativeLuminance,
|
|
8825
10441
|
isModernMineralFamilyLineagePlan,
|
|
@@ -8832,7 +10448,10 @@ export {
|
|
|
8832
10448
|
normalizeHex,
|
|
8833
10449
|
offerEngineRuntimeScript,
|
|
8834
10450
|
optionalGemTokenKeys,
|
|
10451
|
+
orbitEffect,
|
|
8835
10452
|
planToRuntimeBootstrapConfig,
|
|
10453
|
+
quickViewEffect,
|
|
10454
|
+
railEffect,
|
|
8836
10455
|
renderGemFrameworkManifest,
|
|
8837
10456
|
renderGemPreviewDocument,
|
|
8838
10457
|
renderGemSectionMarkup,
|
|
@@ -8844,7 +10463,13 @@ export {
|
|
|
8844
10463
|
resolveGift,
|
|
8845
10464
|
resolveOffer,
|
|
8846
10465
|
reviewsEngineRuntimeScript,
|
|
10466
|
+
ringDelta,
|
|
10467
|
+
ringPose,
|
|
10468
|
+
scopeMotionCss,
|
|
10469
|
+
scrollProgress,
|
|
8847
10470
|
slugify,
|
|
10471
|
+
spring,
|
|
10472
|
+
throwTarget,
|
|
8848
10473
|
toCssVars,
|
|
8849
10474
|
toMotionCssVars,
|
|
8850
10475
|
validateGemFrameworkArtifact,
|