@gemboss/gem-framework 0.18.15 → 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 +106 -1
- package/dist/SOURCE.json +3 -3
- package/dist/gem-framework.css +23 -1
- package/dist/gem-runtime.js +473 -55
- package/dist/index.js +2062 -44
- package/dist/motion/core.d.ts +208 -0
- package/dist/motion/core.js +1 -0
- package/dist/motion/orbit.d.ts +4 -0
- package/dist/motion/orbit.js +1 -0
- package/dist/motion/quick-view.d.ts +4 -0
- package/dist/motion/quick-view.js +1 -0
- package/dist/motion/rail.d.ts +5 -0
- package/dist/motion/rail.js +1 -0
- package/dist/orbit-base.css +39 -0
- package/dist/plugins/before_after.js +1 -1
- package/dist/plugins/bundle.d.ts +9 -0
- package/dist/plugins/bundle.js +394 -0
- 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 +2 -2
- 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 +2754 -37
- 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",
|
|
@@ -1462,6 +2876,29 @@ var gemFrameworkRuntimePlugins = [
|
|
|
1462
2876
|
key: "reviews",
|
|
1463
2877
|
load_strategy: "idle",
|
|
1464
2878
|
events: ["gem:reviews:ready", "gem:reviews:render", "gem:review:vote"]
|
|
2879
|
+
},
|
|
2880
|
+
{
|
|
2881
|
+
key: "bundle",
|
|
2882
|
+
load_strategy: "idle",
|
|
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"]
|
|
1465
2902
|
}
|
|
1466
2903
|
];
|
|
1467
2904
|
var gemFrameworkValidationRules = [
|
|
@@ -1567,6 +3004,24 @@ var GEM_POPUP_SHELL_CSS_LINES = [
|
|
|
1567
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); }",
|
|
1568
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; }",
|
|
1569
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; } }",
|
|
1570
3025
|
"@media (max-width: 749px) {",
|
|
1571
3026
|
// Mobile: takeover is the whole screen; a sheet rises from the bottom to at most 88% of it. The image
|
|
1572
3027
|
// sits under the text (top when media=top), capped at a third of the screen; with no image or the image
|
|
@@ -1602,6 +3057,9 @@ var GEM_POPUP_SHELL_CSS_LINES = [
|
|
|
1602
3057
|
"@media (min-width: 750px) {",
|
|
1603
3058
|
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] { padding: 0; }",
|
|
1604
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; }",
|
|
1605
3063
|
"}",
|
|
1606
3064
|
"@media (max-width: 749px) {",
|
|
1607
3065
|
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'] { padding: 0; }",
|
|
@@ -1797,7 +3255,7 @@ var GEM_INLINE_MARK_CSS_LINES = [
|
|
|
1797
3255
|
":where(.gem-mark--alert) { color: var(--gem-color-danger, #b42318); }",
|
|
1798
3256
|
":where(.gem-mark--highlight.gem-mark--accent), :where(.gem-mark--highlight.gem-mark--alert) { color: var(--gem-mark-highlight-ink, #1f1d18); }",
|
|
1799
3257
|
".gem-mark-link { color: var(--gem-color-accent); text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }",
|
|
1800
|
-
".gem-mark-link:hover { text-decoration-thickness: 2px; }",
|
|
3258
|
+
".gem-mark-link:hover { text-decoration-thickness: var(--gem-underline-thickness, 2px); }",
|
|
1801
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
|
|
1802
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).
|
|
1803
3261
|
".gem-mark-link.gem-mark--highlight, .gem-mark--highlight .gem-mark-link { color: var(--gem-mark-highlight-ink, #1f1d18); }"
|
|
@@ -2023,6 +3481,14 @@ function createPrimitiveCss() {
|
|
|
2023
3481
|
" cursor: not-allowed;",
|
|
2024
3482
|
" opacity: 0.55;",
|
|
2025
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; }",
|
|
2026
3492
|
".gem-input:disabled, .gem-select:disabled, .gem-textarea:disabled, .gem-quantity-input__button:disabled {",
|
|
2027
3493
|
" cursor: not-allowed;",
|
|
2028
3494
|
" opacity: 0.6;",
|
|
@@ -3400,8 +4866,8 @@ function createPrimitiveCss() {
|
|
|
3400
4866
|
function createGemSectionRuntimeContractCss(selector = "[data-gem-section-library-id]", options = {}) {
|
|
3401
4867
|
const background = options.backgroundVar ?? "var(--gem-color-background)";
|
|
3402
4868
|
const surface = options.surfaceVar ?? "var(--gem-color-surface)";
|
|
3403
|
-
const
|
|
3404
|
-
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)`;
|
|
3405
4871
|
const accent = options.accentVar ?? "var(--gem-color-accent)";
|
|
3406
4872
|
const accentText = options.accentTextVar ?? "var(--gem-color-accent-text, #ffffff)";
|
|
3407
4873
|
const border = options.borderVar ?? "var(--gem-color-border)";
|
|
@@ -3411,7 +4877,7 @@ function createGemSectionRuntimeContractCss(selector = "[data-gem-section-librar
|
|
|
3411
4877
|
`${selector} {`,
|
|
3412
4878
|
" box-sizing: border-box;",
|
|
3413
4879
|
` background: ${background};`,
|
|
3414
|
-
` color: ${
|
|
4880
|
+
` color: ${text2};`,
|
|
3415
4881
|
"}",
|
|
3416
4882
|
`${selector} *,`,
|
|
3417
4883
|
`${selector} *::before,`,
|
|
@@ -3470,7 +4936,7 @@ function createGemSectionRuntimeContractCss(selector = "[data-gem-section-librar
|
|
|
3470
4936
|
" border-radius: var(--gem-radius-pill, 999px);",
|
|
3471
4937
|
" padding: 6px 10px;",
|
|
3472
4938
|
` background: ${surface};`,
|
|
3473
|
-
` color: ${
|
|
4939
|
+
` color: ${text2};`,
|
|
3474
4940
|
" font-size: 0.8125rem;",
|
|
3475
4941
|
" font-weight: 700;",
|
|
3476
4942
|
"}",
|
|
@@ -4828,8 +6294,8 @@ function resolveGift(gift, totalAmount) {
|
|
|
4828
6294
|
const remaining = `${symbol}${remainingAmount.toFixed(2)}`;
|
|
4829
6295
|
const percent = gift.threshold > 0 ? Math.max(0, Math.min(100, Math.round(totalAmount / gift.threshold * 100))) : 100;
|
|
4830
6296
|
const template = unlocked ? gift.unlocked_text ?? "\u{1F381} {label} unlocked!" : gift.locked_text ?? "Add {remaining} more to unlock {label}";
|
|
4831
|
-
const
|
|
4832
|
-
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 };
|
|
4833
6299
|
}
|
|
4834
6300
|
function matches(when, sel) {
|
|
4835
6301
|
return Object.keys(when).every((k) => sel[k] === when[k]);
|
|
@@ -5445,7 +6911,7 @@ function reviewsEngineRuntimeScript() {
|
|
|
5445
6911
|
const b = document.createElement('button');
|
|
5446
6912
|
b.type = 'button'; b.className = strip.getAttribute('data-rv-thumb-class') || ''; b.setAttribute('data-rv-lb-thumb', String(k));
|
|
5447
6913
|
b.setAttribute('aria-label', (strip.getAttribute('data-rv-thumb-label') || 'Media {n} of {total}').replace('{n}', String(k + 1)).replace('{total}', String(lbSet.length)));
|
|
5448
|
-
const img = document.createElement('img'); img.
|
|
6914
|
+
const img = document.createElement('img'); img.loading = 'lazy'; img.decoding = 'async'; img.alt = ''; img.src = x.thumb;
|
|
5449
6915
|
b.appendChild(img); strip.appendChild(b);
|
|
5450
6916
|
});
|
|
5451
6917
|
strip.hidden = lbSet.length <= 1;
|
|
@@ -5570,6 +7036,386 @@ function reviewsEngineRuntimeScript() {
|
|
|
5570
7036
|
api.emit('gem:reviews:ready', { source: 'gem-runtime', count: boundReviews });`;
|
|
5571
7037
|
}
|
|
5572
7038
|
|
|
7039
|
+
// ../../packages/gem-framework/src/bundle-engine.ts
|
|
7040
|
+
function bundleEngineRuntimeScript() {
|
|
7041
|
+
return `let boundBundles = 0;
|
|
7042
|
+
const BB_WORDS = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'];
|
|
7043
|
+
document.querySelectorAll('[data-bb-root]').forEach((root) => {
|
|
7044
|
+
if (!mark(root, 'bundle')) return;
|
|
7045
|
+
boundBundles += 1;
|
|
7046
|
+
const on = (el, type, fn, opt) => { if (canListen(el)) el.addEventListener(type, fn, opt); };
|
|
7047
|
+
const isEl = (t) => t instanceof window.Element;
|
|
7048
|
+
const parse = (attr, fallback) => { try { const v = JSON.parse(root.getAttribute(attr) || 'null'); return v == null ? fallback : v; } catch (e) { return fallback; } };
|
|
7049
|
+
let rules = parse('data-bb-rules', {});
|
|
7050
|
+
const text = root.hasAttribute('data-bb-messages') ? parse('data-bb-messages', {}) : parse('data-bb-labels', {});
|
|
7051
|
+
const viMoney = root.getAttribute('data-bb-money') === 'vi';
|
|
7052
|
+
const currency = root.getAttribute('data-bb-currency') || (viMoney ? 'VND' : 'USD');
|
|
7053
|
+
let nf = null;
|
|
7054
|
+
if (!viMoney) { try { nf = new Intl.NumberFormat('en-US', { style: 'currency', currency, maximumFractionDigits: currency === 'VND' ? 0 : 2 }); } catch (e) { nf = null; } }
|
|
7055
|
+
const money = (n) => (viMoney ? String(Math.round(n)).replace(/\\B(?=(\\d{3})+(?!\\d))/g, '.') + '\u0111' : nf ? nf.format(Math.round(n * 100) / 100) : (Math.round(n * 100) / 100).toFixed(2));
|
|
7056
|
+
// A strike price as stores show it: a whole number drops its cents ("$42", not "$42.00").
|
|
7057
|
+
const moneyShort = (n) => { const s = money(n); return Math.abs(n - Math.round(n)) < 0.005 ? s.replace(/[.,]00(?=\\D*$)/, '') : s; };
|
|
7058
|
+
const fill = (tpl, vars) => String(tpl || '').replace(/\\{(\\w+)\\}/g, (m, k) => (Object.hasOwn(vars, k) ? String(vars[k]) : m));
|
|
7059
|
+
// A sentence with {reward}: the reward is a <strong>, the rest plain text. Never innerHTML.
|
|
7060
|
+
const say = (el, tpl, vars) => {
|
|
7061
|
+
if (!el) return;
|
|
7062
|
+
const parts = String(tpl || '').split('{reward}');
|
|
7063
|
+
el.replaceChildren();
|
|
7064
|
+
parts.forEach((part, i) => {
|
|
7065
|
+
if (part) el.append(document.createTextNode(fill(part, vars)));
|
|
7066
|
+
if (i < parts.length - 1 && vars.reward) { const s = document.createElement('strong'); s.textContent = vars.reward; el.append(s); }
|
|
7067
|
+
});
|
|
7068
|
+
};
|
|
7069
|
+
let min = Math.max(1, Number(rules.min) || 1);
|
|
7070
|
+
let cap = Number(rules.max) > 0 ? Number(rules.max) : Infinity;
|
|
7071
|
+
const per = Number(rules.per) > 0 ? Number(rules.per) : Infinity;
|
|
7072
|
+
let tiers = Array.isArray(rules.tiers) ? rules.tiers : [];
|
|
7073
|
+
let goal = tiers.length ? tiers[tiers.length - 1] : null;
|
|
7074
|
+
// The catalog: one entry per item, plus one per extra variant of a card (<key>~<index>).
|
|
7075
|
+
const catalog = new Map();
|
|
7076
|
+
const cards = new Map();
|
|
7077
|
+
root.querySelectorAll('[data-bb-item]').forEach((el) => {
|
|
7078
|
+
const key = el.getAttribute('data-bb-item');
|
|
7079
|
+
const base = {
|
|
7080
|
+
el,
|
|
7081
|
+
variantId: el.getAttribute('data-bb-variant') || '',
|
|
7082
|
+
price: Number(el.getAttribute('data-bb-price')) || 0,
|
|
7083
|
+
compare: Number(el.getAttribute('data-bb-compare')) || 0,
|
|
7084
|
+
title: el.getAttribute('data-bb-title') || '',
|
|
7085
|
+
subtitle: el.getAttribute('data-bb-subtitle') || '',
|
|
7086
|
+
image: el.getAttribute('data-bb-image') || '',
|
|
7087
|
+
group: el.getAttribute('data-bb-group') || '',
|
|
7088
|
+
step: el.getAttribute('data-bb-step') || '',
|
|
7089
|
+
short: el.getAttribute('data-bb-short') || '',
|
|
7090
|
+
available: el.getAttribute('data-available') !== 'false',
|
|
7091
|
+
};
|
|
7092
|
+
catalog.set(key, base);
|
|
7093
|
+
let vs = [];
|
|
7094
|
+
try { vs = JSON.parse(el.getAttribute('data-bb-variants') || '[]'); } catch (e) { vs = []; }
|
|
7095
|
+
if (!Array.isArray(vs)) vs = [];
|
|
7096
|
+
vs.forEach((v, i) => {
|
|
7097
|
+
if (!i) return;
|
|
7098
|
+
catalog.set(key + '~' + i, { ...base, variantId: String(v.id || ''), price: Number(v.price) || 0, compare: Number(v.compare) || 0, image: v.image || base.image, variantTitle: v.title || '' });
|
|
7099
|
+
});
|
|
7100
|
+
if (vs[0]) base.variantTitle = vs[0].title || '';
|
|
7101
|
+
cards.set(key, { el, variants: vs, gift: el.hasAttribute('data-bb-gift') });
|
|
7102
|
+
});
|
|
7103
|
+
const gifts = Array.from(cards.entries()).filter(([, c]) => c.gift).map(([k]) => k);
|
|
7104
|
+
// The variant a card shows now (data-bb-current), as the key an action picks.
|
|
7105
|
+
const currentKey = (key) => { const c = cards.get(key); const i = c ? Number(c.el.getAttribute('data-bb-current')) || 0 : 0; return i ? key + '~' + i : key; };
|
|
7106
|
+
let picks = [];
|
|
7107
|
+
let plan = 'one_time';
|
|
7108
|
+
const planOf = (el) => (el.type === 'checkbox' ? (el.checked ? 'subscribe' : 'one_time') : el.getAttribute('data-bb-plan') || 'one_time');
|
|
7109
|
+
const planInputs = Array.from(root.querySelectorAll('input[data-bb-plan]'));
|
|
7110
|
+
const checkedPlan = planInputs.find((el) => el.checked);
|
|
7111
|
+
if (checkedPlan) plan = planOf(checkedPlan);
|
|
7112
|
+
const tierFor = (n) => tiers.filter((t) => n >= t.min).pop() || null;
|
|
7113
|
+
const pctOf = (t, p) => (!t ? 0 : p === 'subscribe' ? Number(t.sub) || Number(t.pct) || 0 : Number(t.pct) || 0);
|
|
7114
|
+
// The reward in a progress sentence is always "N% off" for the plan in force; a tier's label is the text on its milestone.
|
|
7115
|
+
const rewardOf = (t, p) => (!t ? '' : pctOf(t, p) + '% off');
|
|
7116
|
+
const qtyOf = (key) => picks.filter((p) => p === key).length;
|
|
7117
|
+
const canAdd = (key) => picks.length < cap && (rules.repeat !== false || !picks.includes(key)) && qtyOf(key) < per;
|
|
7118
|
+
|
|
7119
|
+
// Box sizes, when the root declares them (data-bb-sizes): choosing a size sets floor = ceiling = its item count. "trim":
|
|
7120
|
+
// a smaller box drops the LAST picks; "queue": they wait, in order, and come back when there is room again.
|
|
7121
|
+
const sizeMode = root.getAttribute('data-bb-sizes');
|
|
7122
|
+
const sizes = Array.isArray(rules.sizes) ? rules.sizes : [];
|
|
7123
|
+
let size = null;
|
|
7124
|
+
const waiting = [];
|
|
7125
|
+
const refill = () => { while (sizeMode === 'queue' && picks.length < cap && waiting.length) picks.push(waiting.shift()); };
|
|
7126
|
+
const applySize = (n) => {
|
|
7127
|
+
const s = sizes.find((x) => Number(x.items) === n);
|
|
7128
|
+
if (!s) return false;
|
|
7129
|
+
size = s; min = Number(s.items); cap = Number(s.items);
|
|
7130
|
+
goal = tierFor(min) || (tiers.length ? tiers[tiers.length - 1] : null);
|
|
7131
|
+
if (picks.length > cap) { const out = picks.splice(cap); if (sizeMode === 'queue') waiting.unshift(...out); }
|
|
7132
|
+
refill();
|
|
7133
|
+
return true;
|
|
7134
|
+
};
|
|
7135
|
+
if (sizeMode) { const s0 = sizes.find((s) => Number(s.items) === Number(rules.size)) || sizes[0] || null; if (s0) applySize(Number(s0.items)); }
|
|
7136
|
+
|
|
7137
|
+
// Kits (rules.kits): each keeps its own picks and steps; a kit's own rules replace the bundle's while it is chosen.
|
|
7138
|
+
// A kit with steps fills each step's slots in order and moves on by itself; optional steps open after the required ones.
|
|
7139
|
+
const kits = Array.isArray(rules.kits) && rules.kits.length ? rules.kits : null;
|
|
7140
|
+
const stateOf = new Map();
|
|
7141
|
+
(kits || []).forEach((k) => {
|
|
7142
|
+
const steps = Array.isArray(k.steps) ? k.steps : [];
|
|
7143
|
+
const slots = {};
|
|
7144
|
+
steps.forEach((s) => { slots[s.id] = Array.from({ length: Math.max(1, Number(s.pick) || 1) }, () => null); });
|
|
7145
|
+
stateOf.set(k.id, { kit: k, steps, fill: slots, picks: [], current: steps.length ? steps[0].id : null });
|
|
7146
|
+
});
|
|
7147
|
+
let st = kits ? stateOf.get(kits[0].id) : { kit: null, steps: [], fill: {}, picks, current: null };
|
|
7148
|
+
const useKit = (id) => {
|
|
7149
|
+
if (!kits) return;
|
|
7150
|
+
st = stateOf.get(id) || stateOf.get(kits[0].id);
|
|
7151
|
+
const k = st.kit;
|
|
7152
|
+
rules = { ...rules, min: k.min, max: k.max, tiers: k.tiers, repeat: k.repeat, price_mode: k.price_mode, fixed_price: k.fixed_price };
|
|
7153
|
+
min = Math.max(1, Number(k.min) || 1);
|
|
7154
|
+
cap = Number(k.max) > 0 ? Number(k.max) : Infinity;
|
|
7155
|
+
tiers = Array.isArray(k.tiers) ? k.tiers : [];
|
|
7156
|
+
goal = tiers.length ? tiers[tiers.length - 1] : null;
|
|
7157
|
+
picks = st.picks;
|
|
7158
|
+
};
|
|
7159
|
+
if (kits) useKit(kits[0].id);
|
|
7160
|
+
const stepsMode = () => st.steps.length > 0;
|
|
7161
|
+
const required = () => st.steps.filter((s) => !s.optional);
|
|
7162
|
+
const isFull = (s) => st.fill[s.id].every(Boolean);
|
|
7163
|
+
// picks (in step order) are rebuilt from the slots after every change: price and cart read picks.
|
|
7164
|
+
const syncPicks = () => { picks.length = 0; st.steps.forEach((s) => st.fill[s.id].forEach((k) => { if (k) picks.push(k); })); };
|
|
7165
|
+
const openRequired = () => required().find((s) => !isFull(s)) || null;
|
|
7166
|
+
const nextOpen = () => openRequired() || st.steps.find((s) => s.optional && !isFull(s)) || null;
|
|
7167
|
+
const doneCount = () => required().filter(isFull).length;
|
|
7168
|
+
const isComplete = () => (stepsMode() ? doneCount() === required().length : picks.length >= min);
|
|
7169
|
+
const reachable = (s) => { const open = openRequired(); return !open || st.steps.indexOf(s) <= st.steps.indexOf(open); };
|
|
7170
|
+
const slotRef = (idx) => {
|
|
7171
|
+
let n = 0;
|
|
7172
|
+
for (const s of st.steps) for (let j = 0; j < st.fill[s.id].length; j++) { if (n === idx) return [s, j]; n++; }
|
|
7173
|
+
return null;
|
|
7174
|
+
};
|
|
7175
|
+
// Into the card's step: its first empty slot; a full step swaps its last pick (a full optional step refuses).
|
|
7176
|
+
const addToStep = (key) => {
|
|
7177
|
+
const it = catalog.get(key);
|
|
7178
|
+
const step = (it && st.steps.find((s) => s.id === it.step)) || st.steps.find((s) => s.id === st.current);
|
|
7179
|
+
if (!step || (rules.repeat === false && picks.includes(key))) return false;
|
|
7180
|
+
const row = st.fill[step.id];
|
|
7181
|
+
let i = row.indexOf(null);
|
|
7182
|
+
if (i < 0) { if (step.optional) return false; i = row.length - 1; }
|
|
7183
|
+
row[i] = key;
|
|
7184
|
+
syncPicks();
|
|
7185
|
+
if (isFull(step)) { const n = nextOpen(); if (n) st.current = n.id; }
|
|
7186
|
+
return true;
|
|
7187
|
+
};
|
|
7188
|
+
// "Edit" on a required step empties the slot, closes the gap and goes back to that step; on an optional step it only removes.
|
|
7189
|
+
const editSlot = (idx) => {
|
|
7190
|
+
const ref = slotRef(idx);
|
|
7191
|
+
if (!ref) return;
|
|
7192
|
+
const [s, j] = ref;
|
|
7193
|
+
const row = st.fill[s.id];
|
|
7194
|
+
row.splice(j, 1); row.push(null);
|
|
7195
|
+
syncPicks();
|
|
7196
|
+
if (!s.optional) st.current = s.id;
|
|
7197
|
+
};
|
|
7198
|
+
const clearAll = () => {
|
|
7199
|
+
st.steps.forEach((s) => { st.fill[s.id] = st.fill[s.id].map(() => null); });
|
|
7200
|
+
picks.length = 0; waiting.length = 0;
|
|
7201
|
+
st.current = st.steps.length ? st.steps[0].id : null;
|
|
7202
|
+
};
|
|
7203
|
+
|
|
7204
|
+
// Prices. totals(): the bundle price at the tier the bundle WILL reach once it has its minimum (stores show the bundle
|
|
7205
|
+
// price from the first pick), plus a container's base price. totalsNow(): at the tier reached now, rounded to the cent
|
|
7206
|
+
// first so total + saved add up.
|
|
7207
|
+
let basePrice = 0;
|
|
7208
|
+
let cartHead = null;
|
|
7209
|
+
const sumOf = () => {
|
|
7210
|
+
let sum = 0; let compare = 0;
|
|
7211
|
+
picks.forEach((k) => { const it = catalog.get(k); if (!it) return; sum += it.price; compare += Math.max(it.compare, it.price); });
|
|
7212
|
+
return { sum, compare };
|
|
7213
|
+
};
|
|
7214
|
+
const totals = (p) => {
|
|
7215
|
+
const { sum, compare } = sumOf();
|
|
7216
|
+
if (rules.price_mode === 'fixed' && !basePrice) {
|
|
7217
|
+
// A box size with a fixed one-time price; or a kit with one price whatever is picked (no strike price then).
|
|
7218
|
+
if (size && Number(size.fixed) > 0) { if (p !== 'subscribe') return { sum, compare, total: Number(size.fixed) }; }
|
|
7219
|
+
else if (Number(rules.fixed_price) > 0) return { sum, compare: 0, total: Number(rules.fixed_price) };
|
|
7220
|
+
}
|
|
7221
|
+
const t = tierFor(Math.max(picks.length, min)) || goal;
|
|
7222
|
+
return { sum, compare, total: basePrice + sum * (1 - pctOf(t, p) / 100) };
|
|
7223
|
+
};
|
|
7224
|
+
const totalsNow = (p) => {
|
|
7225
|
+
const { sum, compare } = sumOf();
|
|
7226
|
+
return { sum, compare, total: Math.round(sum * (1 - pctOf(tierFor(picks.length), p) / 100) * 100 + 1e-6) / 100 };
|
|
7227
|
+
};
|
|
7228
|
+
// One item's price on its card at the bundle's current tier, counting at least one item.
|
|
7229
|
+
const unitNow = (it, p) => it.price * (1 - pctOf(tierFor(Math.max(picks.length, 1)), p) / 100);
|
|
7230
|
+
|
|
7231
|
+
const frequency = () => root.querySelector('[data-bb-frequency]');
|
|
7232
|
+
const planId = () => {
|
|
7233
|
+
if (plan !== 'subscribe') return '';
|
|
7234
|
+
const f = frequency();
|
|
7235
|
+
if (!f) return '';
|
|
7236
|
+
if (f.matches('select')) return f.value;
|
|
7237
|
+
const c = f.querySelector('input:checked');
|
|
7238
|
+
return c ? c.value : '';
|
|
7239
|
+
};
|
|
7240
|
+
const bundleName = () => {
|
|
7241
|
+
const n = (st.kit && st.kit.label) || (root.querySelector('[data-bb-name]') || {}).textContent || 'Bundle';
|
|
7242
|
+
return String(n).trim();
|
|
7243
|
+
};
|
|
7244
|
+
const toCart = () => {
|
|
7245
|
+
const counts = new Map();
|
|
7246
|
+
picks.concat(gifts).forEach((k) => { const it = catalog.get(k); if (it && it.variantId) counts.set(it.variantId, (counts.get(it.variantId) || 0) + 1); });
|
|
7247
|
+
const ruleKey = root.getAttribute('data-bb-discount-key') || '';
|
|
7248
|
+
const id = (root.getAttribute('data-gem-section-id') || 'bundle') + '-' + Date.now().toString(36);
|
|
7249
|
+
const props = [{ key: '_bundle_id', value: id }, { key: '_bundle', value: bundleName() }].concat(ruleKey ? [{ key: '_bundle_rule', value: ruleKey }] : []);
|
|
7250
|
+
const pid = planId();
|
|
7251
|
+
const lines = Array.from(counts.entries()).map(([variantId, quantity]) => ({ variantId, quantity, properties: props.slice(), ...(pid ? { sellingPlanId: pid } : {}) }));
|
|
7252
|
+
// A container line (a gift box) goes first and carries the section's own properties (lid, gift message).
|
|
7253
|
+
const head = cartHead ? cartHead() : null;
|
|
7254
|
+
if (head) {
|
|
7255
|
+
const hp = props.concat(Array.isArray(head.properties) ? head.properties : []);
|
|
7256
|
+
if (head.variantId) lines.unshift({ variantId: head.variantId, quantity: 1, properties: hp, ...(pid ? { sellingPlanId: pid } : {}) });
|
|
7257
|
+
else if (lines[0]) lines[0].properties = hp;
|
|
7258
|
+
}
|
|
7259
|
+
if (lines.length && typeof window.CustomEvent === 'function') window.dispatchEvent(new window.CustomEvent('gem:cart:add-many', { detail: { lines, after: 'drawer' } }));
|
|
7260
|
+
return lines.length;
|
|
7261
|
+
};
|
|
7262
|
+
|
|
7263
|
+
let busy = false;
|
|
7264
|
+
let wasComplete = false;
|
|
7265
|
+
const state = () => {
|
|
7266
|
+
const n = picks.length;
|
|
7267
|
+
const counts = {};
|
|
7268
|
+
picks.forEach((k) => { counts[k] = (counts[k] || 0) + 1; });
|
|
7269
|
+
return {
|
|
7270
|
+
picks, count: n, counts, min, cap, max: cap === Infinity ? null : cap, per, rules, text, tiers, goal, plan, size, sizes, waiting,
|
|
7271
|
+
catalog, cards, gifts, reached: tierFor(n), next: tiers.find((t) => n < t.min) || null, left: Math.max(0, min - n),
|
|
7272
|
+
kit: st.kit, kits, steps: st.steps, fill: st.fill, current: st.current, stepsMode: stepsMode(), done: doneCount(),
|
|
7273
|
+
required: required(), complete: isComplete(), busy, base: basePrice,
|
|
7274
|
+
};
|
|
7275
|
+
};
|
|
7276
|
+
const outs = () => root.querySelectorAll('[data-bb-out]');
|
|
7277
|
+
const emit = (action, extra) => {
|
|
7278
|
+
const n = picks.length;
|
|
7279
|
+
const complete = isComplete();
|
|
7280
|
+
root.setAttribute('data-bb-count', String(n));
|
|
7281
|
+
root.setAttribute('data-bb-state', n === 0 ? 'empty' : n >= cap ? 'full' : complete ? 'complete' : 'partial');
|
|
7282
|
+
if (outs().length) {
|
|
7283
|
+
const t = totals(plan); const reached = tierFor(n);
|
|
7284
|
+
const v = { count: String(n), left: String(Math.max(0, min - n)), total: money(t.total), compare: money(t.compare), saved: money(Math.max(0, t.compare - t.total)), reward: reached ? rewardOf(reached, plan) : '' };
|
|
7285
|
+
outs().forEach((el) => { const k = el.getAttribute('data-bb-out'); if (Object.hasOwn(v, k)) el.textContent = v[k]; });
|
|
7286
|
+
}
|
|
7287
|
+
const detail = { source: 'gem-runtime', action, ...(extra || {}), count: n, min, max: cap === Infinity ? null : cap, plan, size: size ? Number(size.items) : null, kit: st.kit ? st.kit.id : null, step: st.current, complete, reward: tierFor(n) ? rewardOf(tierFor(n), plan) : '', total: totals(plan).total };
|
|
7288
|
+
if (typeof window.CustomEvent === 'function') {
|
|
7289
|
+
root.dispatchEvent(new window.CustomEvent('gem:bundle:change', { bubbles: true, detail }));
|
|
7290
|
+
if (complete && !wasComplete) root.dispatchEvent(new window.CustomEvent('gem:bundle:complete', { bubbles: true, detail }));
|
|
7291
|
+
}
|
|
7292
|
+
wasComplete = complete;
|
|
7293
|
+
};
|
|
7294
|
+
|
|
7295
|
+
// Actions: the handlers below call these, and so may a section's own controls (root.gemBundle.add(key)\u2026).
|
|
7296
|
+
const add = (key) => {
|
|
7297
|
+
const before = st.current;
|
|
7298
|
+
if (root.hasAttribute('data-bb-add-toggle') && picks.includes(key)) { picks.splice(picks.indexOf(key), 1); emit('remove', { key }); return true; }
|
|
7299
|
+
if (stepsMode()) { if (!addToStep(key)) return false; }
|
|
7300
|
+
else { if (!canAdd(key)) return false; picks.push(key); }
|
|
7301
|
+
emit('add', { key, from: before });
|
|
7302
|
+
return true;
|
|
7303
|
+
};
|
|
7304
|
+
const removeAt = (idx) => { if (stepsMode()) editSlot(idx); else { picks.splice(idx, 1); refill(); } emit('remove', { index: idx }); };
|
|
7305
|
+
const dec = (key) => { const at = picks.lastIndexOf(key); if (at < 0) return false; picks.splice(at, 1); refill(); emit('dec', { key }); return true; };
|
|
7306
|
+
const uncheck = (key) => { for (let i = picks.length - 1; i >= 0; i -= 1) if (picks[i] === key) picks.splice(i, 1); refill(); emit('uncheck', { key }); };
|
|
7307
|
+
const clear = () => { clearAll(); emit('clear'); };
|
|
7308
|
+
// A typed quantity: exactly k picks of that item, within the bundle's ceiling, the item's own and no-repeat.
|
|
7309
|
+
const setQty = (key, want) => {
|
|
7310
|
+
if (!catalog.has(key)) return;
|
|
7311
|
+
const has = qtyOf(key);
|
|
7312
|
+
const room = cap === Infinity ? Infinity : cap - (picks.length - has);
|
|
7313
|
+
const k = Math.max(0, Math.min(Math.floor(Number(want)) || 0, room, per, rules.repeat === false ? 1 : Infinity));
|
|
7314
|
+
for (let i = picks.length - 1; i >= 0; i--) if (picks[i] === key) picks.splice(i, 1);
|
|
7315
|
+
for (let i = 0; i < k; i++) picks.push(key);
|
|
7316
|
+
emit('qty', { key });
|
|
7317
|
+
};
|
|
7318
|
+
const setPlan = (p) => { plan = p === 'subscribe' ? 'subscribe' : 'one_time'; emit('plan'); };
|
|
7319
|
+
const setSize = (n) => {
|
|
7320
|
+
if (!sizeMode || !applySize(Number(n))) return false;
|
|
7321
|
+
// A box size may name its default selling plan.
|
|
7322
|
+
const f = frequency();
|
|
7323
|
+
if (f && f.matches('select') && size && size.plan && Array.from(f.options).some((o) => o.value === size.plan)) f.value = size.plan;
|
|
7324
|
+
emit('size');
|
|
7325
|
+
return true;
|
|
7326
|
+
};
|
|
7327
|
+
// An item's variant radio (a pack size): the whole row changes variant and price, its count stays.
|
|
7328
|
+
const applyVariant = (radio) => {
|
|
7329
|
+
const it = catalog.get(radio.getAttribute('data-bb-size'));
|
|
7330
|
+
if (!it) return false;
|
|
7331
|
+
it.variantId = radio.getAttribute('data-bb-variant') || '';
|
|
7332
|
+
it.price = Number(radio.getAttribute('data-bb-price')) || 0;
|
|
7333
|
+
it.compare = Number(radio.getAttribute('data-bb-compare')) || 0;
|
|
7334
|
+
it.el.setAttribute('data-bb-variant', it.variantId);
|
|
7335
|
+
it.el.setAttribute('data-bb-price', String(it.price));
|
|
7336
|
+
it.el.setAttribute('data-bb-compare', it.compare ? String(it.compare) : '');
|
|
7337
|
+
return true;
|
|
7338
|
+
};
|
|
7339
|
+
const selectKit = (id) => { if (!kits) return; useKit(id); emit('kit'); };
|
|
7340
|
+
const goStep = (id) => { const s = st.steps.find((x) => x.id === id); if (!s || !reachable(s)) return false; st.current = id; emit('step'); return true; };
|
|
7341
|
+
const finish = (ok) => {
|
|
7342
|
+
busy = false;
|
|
7343
|
+
root.querySelectorAll('[data-bb-cta]').forEach((c) => { c.removeAttribute('data-state'); c.removeAttribute('aria-busy'); });
|
|
7344
|
+
if (ok) clearAll();
|
|
7345
|
+
emit(ok ? 'added' : 'add-failed');
|
|
7346
|
+
};
|
|
7347
|
+
const checkout = () => {
|
|
7348
|
+
if (!isComplete() || busy || !picks.length) return false;
|
|
7349
|
+
if (!toCart()) return false;
|
|
7350
|
+
// With gemsite's cart bridge: wait for its answer before clearing (a failure keeps the bundle to try again).
|
|
7351
|
+
if (window.__gemCartAddBridge) {
|
|
7352
|
+
busy = true;
|
|
7353
|
+
const box = st.kit ? root.querySelector('[data-bb-kit-box="' + st.kit.id + '"]') : null;
|
|
7354
|
+
(box || root).querySelectorAll('[data-bb-cta]').forEach((c) => { c.setAttribute('data-state', 'loading'); c.setAttribute('aria-busy', 'true'); });
|
|
7355
|
+
emit('checkout');
|
|
7356
|
+
} else finish(true);
|
|
7357
|
+
return true;
|
|
7358
|
+
};
|
|
7359
|
+
|
|
7360
|
+
on(root, 'click', (event) => {
|
|
7361
|
+
const t = isEl(event.target) ? event.target : null;
|
|
7362
|
+
if (!t) return;
|
|
7363
|
+
const addEl = t.closest('[data-bb-add], [data-bb-inc]');
|
|
7364
|
+
if (addEl) { add(currentKey(addEl.getAttribute('data-bb-add') || addEl.getAttribute('data-bb-inc'))); return; }
|
|
7365
|
+
const decEl = t.closest('[data-bb-dec], [data-bb-minus]');
|
|
7366
|
+
if (decEl) { dec(currentKey(decEl.getAttribute('data-bb-dec') || decEl.getAttribute('data-bb-minus'))); return; }
|
|
7367
|
+
const un = t.closest('[data-bb-uncheck]');
|
|
7368
|
+
if (un) { uncheck(currentKey(un.getAttribute('data-bb-uncheck'))); return; }
|
|
7369
|
+
const rm = t.closest('[data-bb-remove]');
|
|
7370
|
+
if (rm) { removeAt(Number(rm.getAttribute('data-bb-remove')) || 0); return; }
|
|
7371
|
+
if (t.closest('[data-bb-clear]')) { clear(); return; }
|
|
7372
|
+
const nav = t.closest('[data-bb-step-nav]');
|
|
7373
|
+
if (nav) { if (!nav.disabled) goStep(nav.getAttribute('data-bb-step-nav')); return; }
|
|
7374
|
+
const tab = t.closest('[data-bb-kit-tab]');
|
|
7375
|
+
if (tab) { selectKit(tab.getAttribute('data-bb-kit-tab')); return; }
|
|
7376
|
+
if (t.closest('[data-bb-skip]')) { checkout(); return; }
|
|
7377
|
+
const cta = t.closest('[data-bb-cta]');
|
|
7378
|
+
if (cta) { event.preventDefault(); if (cta.getAttribute('aria-disabled') !== 'true') checkout(); }
|
|
7379
|
+
});
|
|
7380
|
+
on(root, 'change', (event) => {
|
|
7381
|
+
const t = isEl(event.target) ? event.target : null;
|
|
7382
|
+
if (!t) return;
|
|
7383
|
+
const p = t.closest('input[data-bb-plan]');
|
|
7384
|
+
if (p) { if (p.type === 'checkbox' || p.checked) setPlan(planOf(p)); return; }
|
|
7385
|
+
const s = t.closest('input[data-bb-size]');
|
|
7386
|
+
if (s && s.checked) { if (s.hasAttribute('data-bb-variant')) { if (applyVariant(s)) emit('variant', { key: s.getAttribute('data-bb-size') }); } else setSize(Number(s.value)); return; }
|
|
7387
|
+
const q = t.closest('input[data-bb-count]');
|
|
7388
|
+
if (q) setQty(q.getAttribute('data-bb-count'), q.value);
|
|
7389
|
+
});
|
|
7390
|
+
// A typed quantity applies as soon as it is a number (an emptied box waits for "change" to go back to 0).
|
|
7391
|
+
on(root, 'input', (event) => {
|
|
7392
|
+
const q = isEl(event.target) ? event.target.closest('input[data-bb-count]') : null;
|
|
7393
|
+
if (q && q.value !== '' && Number.isFinite(Number(q.value))) setQty(q.getAttribute('data-bb-count'), q.value);
|
|
7394
|
+
});
|
|
7395
|
+
on(window, 'gem:cart:added', () => { if (busy) finish(true); });
|
|
7396
|
+
on(window, 'gem:cart:add-failed', () => { if (busy) finish(false); });
|
|
7397
|
+
|
|
7398
|
+
// A page brought back from history restores its form: read back the variant radios and typed quantities in view.
|
|
7399
|
+
root.querySelectorAll('input[data-bb-size][data-bb-variant]').forEach((r) => { if (r.checked) applyVariant(r); });
|
|
7400
|
+
root.querySelectorAll('input[data-bb-count]').forEach((inp) => {
|
|
7401
|
+
const v = Math.floor(Number(inp.value)) || 0;
|
|
7402
|
+
const key = inp.getAttribute('data-bb-count');
|
|
7403
|
+
if (v > 0 && catalog.has(key)) { const k = Math.min(v, per, rules.repeat === false ? 1 : Infinity, cap === Infinity ? Infinity : cap - picks.length); for (let i = 0; i < k; i++) picks.push(key); }
|
|
7404
|
+
});
|
|
7405
|
+
|
|
7406
|
+
root.gemBundle = {
|
|
7407
|
+
state, money, moneyShort, fill, say, words: BB_WORDS, tierFor, pctOf, rewardOf, canAdd, qtyOf, currentKey, totals, totalsNow, unitNow,
|
|
7408
|
+
reachable, isFull, slotRef, required, add, removeAt, dec, uncheck, clear, setQty, setPlan, setSize, selectKit, goStep, checkout,
|
|
7409
|
+
setBase: (n) => { basePrice = Number(n) || 0; emit('base'); },
|
|
7410
|
+
setCartHead: (fn) => { cartHead = typeof fn === 'function' ? fn : null; },
|
|
7411
|
+
refresh: () => emit('refresh'),
|
|
7412
|
+
};
|
|
7413
|
+
root.setAttribute('data-bb-ready', '');
|
|
7414
|
+
emit('init');
|
|
7415
|
+
});
|
|
7416
|
+
api.emit('gem:bundle:ready', { source: 'gem-runtime', count: boundBundles });`;
|
|
7417
|
+
}
|
|
7418
|
+
|
|
5573
7419
|
// ../../packages/gem-framework/src/runtime.ts
|
|
5574
7420
|
var runtimePluginReadyEvents = {
|
|
5575
7421
|
"cart-drawer": "gem:cart:ready",
|
|
@@ -5594,7 +7440,11 @@ var runtimePluginReadyEvents = {
|
|
|
5594
7440
|
"seasonal-effects": "gem:seasonal:ready",
|
|
5595
7441
|
"context-adapter": "gem:context:ready",
|
|
5596
7442
|
"offer-engine": "gem:offer:ready",
|
|
5597
|
-
reviews: "gem:reviews:ready"
|
|
7443
|
+
reviews: "gem:reviews:ready",
|
|
7444
|
+
bundle: "gem:bundle:ready",
|
|
7445
|
+
"size-guide": "gem:size-guide:ready",
|
|
7446
|
+
orbit: "gem:orbit:ready",
|
|
7447
|
+
rail: "gem:rail:ready"
|
|
5598
7448
|
};
|
|
5599
7449
|
var cartDrawerPluginRegistrationLines = [
|
|
5600
7450
|
" registerPlugin('cart-drawer', ({ api }) => {",
|
|
@@ -6088,8 +7938,7 @@ var heroSlideshowPluginRegistrationLines = [
|
|
|
6088
7938
|
" api.emit('gem:hero-slideshow:ready', { source: 'gem-runtime', count: bound });",
|
|
6089
7939
|
" });"
|
|
6090
7940
|
];
|
|
6091
|
-
var
|
|
6092
|
-
" registerPlugin('quick-view', ({ api }) => {",
|
|
7941
|
+
var legacyQuickViewLines = [
|
|
6093
7942
|
` const triggers = document.querySelectorAll('[data-gem-quick-view], [data-gem-action="quick-view"]');`,
|
|
6094
7943
|
` const surface = document.querySelector('[data-gem-quick-view-surface], .gem-quick-view, .gem-modal[data-gem-component="quick-view"]');`,
|
|
6095
7944
|
" let bound = 0;",
|
|
@@ -6139,10 +7988,15 @@ var quickViewPluginRegistrationLines = [
|
|
|
6139
7988
|
" });",
|
|
6140
7989
|
" if (canListen(surface)) surface.addEventListener('keydown', (event) => { if (event.key === 'Escape') close('escape'); });",
|
|
6141
7990
|
" }",
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
"
|
|
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 };"
|
|
6145
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
|
+
});
|
|
6146
8000
|
var promoPopupPluginRegistrationLines = [
|
|
6147
8001
|
" registerPlugin('promo-popup', ({ api }) => {",
|
|
6148
8002
|
// Only the explicit hook opts in. Every promo-popup section carries the archetype class .gem-promo-popup, and
|
|
@@ -6677,6 +8531,35 @@ var reviewsPluginRegistrationLines = [
|
|
|
6677
8531
|
" " + reviewsEngineRuntimeScript(),
|
|
6678
8532
|
" });"
|
|
6679
8533
|
];
|
|
8534
|
+
var bundlePluginRegistrationLines = [
|
|
8535
|
+
" registerPlugin('bundle', ({ api }) => {",
|
|
8536
|
+
" " + bundleEngineRuntimeScript(),
|
|
8537
|
+
" });"
|
|
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
|
+
];
|
|
6680
8563
|
var runtimePluginSourceLines = [
|
|
6681
8564
|
["cart-drawer", cartDrawerPluginRegistrationLines],
|
|
6682
8565
|
["mobile-menu", mobileMenuPluginRegistrationLines],
|
|
@@ -6689,7 +8572,7 @@ var runtimePluginSourceLines = [
|
|
|
6689
8572
|
["media-gallery", mediaGalleryPluginRegistrationLines],
|
|
6690
8573
|
["carousel", carouselPluginRegistrationLines],
|
|
6691
8574
|
["hero-slideshow", heroSlideshowPluginRegistrationLines],
|
|
6692
|
-
["quick-view",
|
|
8575
|
+
["quick-view", runtimeChunks["quick-view"].body],
|
|
6693
8576
|
["promo-popup", promoPopupPluginRegistrationLines],
|
|
6694
8577
|
["sticky-atc", stickyAtcPluginRegistrationLines],
|
|
6695
8578
|
["before_after", beforeAfterPluginRegistrationLines],
|
|
@@ -6699,16 +8582,95 @@ var runtimePluginSourceLines = [
|
|
|
6699
8582
|
["recently-viewed", recentlyViewedPluginRegistrationLines],
|
|
6700
8583
|
["runtime-slot-loader", runtimeSlotLoaderPluginRegistrationLines],
|
|
6701
8584
|
["offer-engine", offerEnginePluginRegistrationLines],
|
|
6702
|
-
["reviews", reviewsPluginRegistrationLines]
|
|
8585
|
+
["reviews", reviewsPluginRegistrationLines],
|
|
8586
|
+
["bundle", bundlePluginRegistrationLines],
|
|
8587
|
+
["size-guide", sizeGuidePluginRegistrationLines],
|
|
8588
|
+
["orbit", runtimeChunks.orbit.body],
|
|
8589
|
+
["rail", runtimeChunks.rail.body]
|
|
6703
8590
|
];
|
|
6704
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
|
+
];
|
|
6705
8663
|
function gemRuntimePluginSources() {
|
|
6706
|
-
const withBody = runtimePluginSourceLines.map(([key, lines]) =>
|
|
6707
|
-
key
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
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
|
+
});
|
|
6712
8674
|
const noop = Object.entries(runtimePluginReadyEvents).filter(([key]) => !runtimePluginSourceLines.some(([sourceKey]) => sourceKey === key)).map(([key, readyEvent]) => ({ key, readyEvent, hasBody: false, lines: [noopPluginRegistrationLine(key, readyEvent)] }));
|
|
6713
8675
|
return [...withBody, ...noop];
|
|
6714
8676
|
}
|
|
@@ -6843,7 +8805,12 @@ function createGemRuntimeBootstrap(configArg) {
|
|
|
6843
8805
|
" if (readyEvent) api.emit(readyEvent, { source: 'gem-runtime' });",
|
|
6844
8806
|
" });",
|
|
6845
8807
|
" };",
|
|
6846
|
-
...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
|
+
}),
|
|
6847
8814
|
...pluginRegistrationLines,
|
|
6848
8815
|
// `data-gem-bound-<key>` is a CONTRACT, not private bookkeeping: markup may carry it to claim an element for its
|
|
6849
8816
|
// own behavior (promo-popup.email-capture.v1 renders data-gem-bound-promo-popup="section"). Any value counts as
|
|
@@ -6939,17 +8906,18 @@ function gemRuntimePluginModuleKeys() {
|
|
|
6939
8906
|
function createGemRuntimePluginModule(key) {
|
|
6940
8907
|
const source = gemRuntimePluginSources().find((candidate) => candidate.key === key);
|
|
6941
8908
|
if (!source) throw new Error(`gem-framework: no runtime plugin "${key}"`);
|
|
6942
|
-
const body = routeListenersThroughLedger(source.lines.join("\n"), key);
|
|
8909
|
+
const body = routeListenersThroughLedger((source.chunk ? source.chunk.body : source.lines).join("\n"), key);
|
|
6943
8910
|
const guard = PLUGIN_GUARDS[key];
|
|
6944
8911
|
return [
|
|
6945
8912
|
`// @gemboss/gem-framework/plugins/${key}: the gem-runtime plugin "${key}" as an ES module. Generated from`,
|
|
6946
|
-
"// 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.",
|
|
6947
8914
|
'import { mountPlugin } from "../runtime-core.js";',
|
|
8915
|
+
...source.chunk ? source.chunk.imports : [],
|
|
6948
8916
|
"",
|
|
6949
8917
|
`export const key = ${JSON.stringify(key)};`,
|
|
6950
8918
|
`export const readyEvent = ${JSON.stringify(source.readyEvent)};`,
|
|
6951
8919
|
"",
|
|
6952
|
-
"function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {",
|
|
8920
|
+
"function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin, onUnmount }) {",
|
|
6953
8921
|
body,
|
|
6954
8922
|
"}",
|
|
6955
8923
|
"",
|
|
@@ -6989,7 +8957,7 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
|
6989
8957
|
const namespace = group === "typography" ? "font" : group === "spacing" ? "space" : group;
|
|
6990
8958
|
return keys.map((key) => `--gem-${namespace}-${slugify(key)}`);
|
|
6991
8959
|
}),
|
|
6992
|
-
// 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
|
|
6993
8961
|
// framework stylesheet declares it on every page, but it is not a token GROUP, so it was missing here
|
|
6994
8962
|
// and a section sizing its heading with `var(--gem-h2)` got `unknown_gem_css_variable`. Read from the
|
|
6995
8963
|
// function that emits it, so the ramp and this list cannot drift.
|
|
@@ -7008,7 +8976,12 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
|
7008
8976
|
// The row primitive's variables (primitives.ts GEM_ROW_CSS). Sections SET these on their grid.
|
|
7009
8977
|
...GEM_ROW_CSS_VARIABLES,
|
|
7010
8978
|
// The form set's variables (form-set.ts): set from a plan's `tokens.form`, read by fields under [data-gem-form-set].
|
|
7011
|
-
...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}`))
|
|
7012
8985
|
]);
|
|
7013
8986
|
var TOKENIZED_PROPERTIES = [
|
|
7014
8987
|
"background",
|
|
@@ -7120,14 +9093,25 @@ function validateGemFrameworkMarkup(markup, artifactPath, options = {}) {
|
|
|
7120
9093
|
}
|
|
7121
9094
|
return result(issues);
|
|
7122
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
|
+
}
|
|
7123
9108
|
function validateSharedSafety(content, artifactPath) {
|
|
7124
9109
|
const issues = [];
|
|
7125
|
-
const
|
|
7126
|
-
|
|
7127
|
-
if (/<script\b/i.test(contentWithoutJsonLd)) {
|
|
9110
|
+
const withoutJsonLd = content.replace(JSON_LD, "");
|
|
9111
|
+
if (/<script\b/i.test(withoutJsonLd)) {
|
|
7128
9112
|
issues.push(ruleIssue("inline_script", "Gem Framework artifacts must not include inline script tags.", artifactPath));
|
|
7129
9113
|
}
|
|
7130
|
-
if (
|
|
9114
|
+
if (namesExternalDependency(withoutJsonLd)) {
|
|
7131
9115
|
issues.push(ruleIssue("external_dependency", "Gem Framework artifacts must not include external runtime or CDN dependencies.", artifactPath));
|
|
7132
9116
|
}
|
|
7133
9117
|
return issues;
|
|
@@ -7156,6 +9140,9 @@ function gemFrameworkPublicCssVariables() {
|
|
|
7156
9140
|
String(createSectionCss()),
|
|
7157
9141
|
String(createShellCss()),
|
|
7158
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,
|
|
7159
9146
|
// The button case rule only exists when a token sets it; a section may still read its variables (a CTA rule
|
|
7160
9147
|
// beats the zero-specificity rule, so opting in means `text-transform: var(--gem-font-button-transform, none)`).
|
|
7161
9148
|
gemButtonCaseCss({ typography: { buttonTransform: "none", buttonLetterSpacing: "normal" } })
|
|
@@ -7166,11 +9153,13 @@ function gemFrameworkPublicCssVariables() {
|
|
|
7166
9153
|
}
|
|
7167
9154
|
var ROOT_SELECTOR = /^\s*(?::root|html|:host)(?:\[[^\]]*\]|:[a-z-]+(?:\([^()]*\))?)*\s*$/;
|
|
7168
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*$/;
|
|
7169
9157
|
function gemPublicCssVariablesFrom(stylesheet) {
|
|
7170
9158
|
const css = stylesheet.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
7171
9159
|
const declaredOnRoot = /* @__PURE__ */ new Set();
|
|
7172
9160
|
const declaredElsewhere = /* @__PURE__ */ new Set();
|
|
7173
9161
|
for (const [, selector, body] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
9162
|
+
if (selector.split(",").every((part) => KNOB_SCOPE_SELECTOR.test(part))) continue;
|
|
7174
9163
|
const onRoot = EDITION_ROOT_SELECTOR.test(selector) || selector.split(",").every((part) => ROOT_SELECTOR.test(part));
|
|
7175
9164
|
for (const [, name] of body.matchAll(/(--gem-[a-z0-9-]+)\s*:/g)) (onRoot ? declaredOnRoot : declaredElsewhere).add(name);
|
|
7176
9165
|
}
|
|
@@ -7369,9 +9358,9 @@ function renderGemShellHeader(plan) {
|
|
|
7369
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>` : ""
|
|
7370
9359
|
].filter(Boolean).join("");
|
|
7371
9360
|
const bars = Array.isArray(header.bars) ? header.bars.filter((b) => (b?.text ?? "").trim()) : [];
|
|
7372
|
-
const barMarkup = (
|
|
9361
|
+
const barMarkup = (text2, bg, fg, segments) => {
|
|
7373
9362
|
const style = [bg ? `background:${bg}` : "", fg ? `color:${fg}` : ""].filter(Boolean).join(";");
|
|
7374
|
-
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>`;
|
|
7375
9364
|
return `<div class="announcement-bar" role="region" aria-label="Announcement"${style ? ` style="${escapeAttribute(style)}"` : ""}><p class="announcement-bar__message">${body}</p></div>`;
|
|
7376
9365
|
};
|
|
7377
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) : "";
|
|
@@ -8356,10 +10345,18 @@ export {
|
|
|
8356
10345
|
GEM_IMG_IN_CSS,
|
|
8357
10346
|
GEM_INLINE_MARK_CSS,
|
|
8358
10347
|
GEM_LINE_HEIGHT_TOKEN_KEYS,
|
|
10348
|
+
GEM_MOTION_CLASSES,
|
|
8359
10349
|
GEM_MOTION_REDUCED,
|
|
10350
|
+
GEM_MOTION_RUNTIME_FILES,
|
|
8360
10351
|
GEM_MOTION_SCALE_DEFAULTS,
|
|
10352
|
+
TIERS as GEM_MOTION_TIERS,
|
|
10353
|
+
GEM_ORBIT_BASE_CSS,
|
|
10354
|
+
GEM_ORBIT_RUNTIME_CSS,
|
|
8361
10355
|
GEM_PART_CLASS,
|
|
8362
10356
|
GEM_POPUP_SHELL_CSS,
|
|
10357
|
+
GEM_QUICK_VIEW_CSS,
|
|
10358
|
+
GEM_RAIL_BASE_CSS,
|
|
10359
|
+
GEM_RAIL_RUNTIME_CSS,
|
|
8363
10360
|
GEM_REVEAL_CSS,
|
|
8364
10361
|
GEM_ROW_CSS,
|
|
8365
10362
|
GEM_ROW_CSS_VARIABLES,
|
|
@@ -8376,6 +10373,10 @@ export {
|
|
|
8376
10373
|
aaLabelColor,
|
|
8377
10374
|
aaTextColor,
|
|
8378
10375
|
auditGemFrameworkClassName,
|
|
10376
|
+
bundleEngineRuntimeScript,
|
|
10377
|
+
capabilityTier,
|
|
10378
|
+
checkEffect,
|
|
10379
|
+
compactCss,
|
|
8379
10380
|
createGemBaseCss,
|
|
8380
10381
|
createGemFrameworkManifest,
|
|
8381
10382
|
createGemRuntimeBootstrap,
|
|
@@ -8395,7 +10396,9 @@ export {
|
|
|
8395
10396
|
createThemeProfileCss,
|
|
8396
10397
|
defaultGemTokens,
|
|
8397
10398
|
defaultSelection,
|
|
10399
|
+
defineEffect,
|
|
8398
10400
|
deriveFormSetId,
|
|
10401
|
+
follow,
|
|
8399
10402
|
gemBandSchemeColors,
|
|
8400
10403
|
gemBandSchemeCssVars,
|
|
8401
10404
|
gemBandSchemeDeclarations,
|
|
@@ -8416,17 +10419,23 @@ export {
|
|
|
8416
10419
|
gemFrameworkValidationRules,
|
|
8417
10420
|
gemFrameworkVersion,
|
|
8418
10421
|
gemMobileTokenCssVars,
|
|
10422
|
+
gemMotionChunks,
|
|
10423
|
+
gemMotionEffectList,
|
|
10424
|
+
gemMotionEffectManifest,
|
|
8419
10425
|
gemMotionScaleCssVars,
|
|
10426
|
+
gemOrbitBaseCss,
|
|
8420
10427
|
gemPartClass,
|
|
8421
10428
|
gemPhoneTokenTarget,
|
|
8422
10429
|
gemPlanFormSet,
|
|
8423
10430
|
gemPlanShapeKnobs,
|
|
10431
|
+
gemRailBaseCss,
|
|
8424
10432
|
gemRuntimePluginModuleKeys,
|
|
8425
10433
|
gemRuntimePluginSources,
|
|
8426
10434
|
gemSectionDividerResponsiveStyle,
|
|
8427
10435
|
gemSectionDividerStyle,
|
|
8428
10436
|
gemSectionDividers,
|
|
8429
10437
|
getGemFrameworkValidationRule,
|
|
10438
|
+
hasExternalDependency,
|
|
8430
10439
|
hexContrastRatio,
|
|
8431
10440
|
hexRelativeLuminance,
|
|
8432
10441
|
isModernMineralFamilyLineagePlan,
|
|
@@ -8439,7 +10448,10 @@ export {
|
|
|
8439
10448
|
normalizeHex,
|
|
8440
10449
|
offerEngineRuntimeScript,
|
|
8441
10450
|
optionalGemTokenKeys,
|
|
10451
|
+
orbitEffect,
|
|
8442
10452
|
planToRuntimeBootstrapConfig,
|
|
10453
|
+
quickViewEffect,
|
|
10454
|
+
railEffect,
|
|
8443
10455
|
renderGemFrameworkManifest,
|
|
8444
10456
|
renderGemPreviewDocument,
|
|
8445
10457
|
renderGemSectionMarkup,
|
|
@@ -8451,7 +10463,13 @@ export {
|
|
|
8451
10463
|
resolveGift,
|
|
8452
10464
|
resolveOffer,
|
|
8453
10465
|
reviewsEngineRuntimeScript,
|
|
10466
|
+
ringDelta,
|
|
10467
|
+
ringPose,
|
|
10468
|
+
scopeMotionCss,
|
|
10469
|
+
scrollProgress,
|
|
8454
10470
|
slugify,
|
|
10471
|
+
spring,
|
|
10472
|
+
throwTarget,
|
|
8455
10473
|
toCssVars,
|
|
8456
10474
|
toMotionCssVars,
|
|
8457
10475
|
validateGemFrameworkArtifact,
|