@gemboss/gem-framework 0.9.3 → 0.10.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 +75 -0
- package/dist/SOURCE.json +3 -3
- package/dist/gem-framework.css +27 -37
- package/dist/gem-runtime.js +45 -2
- package/dist/index.js +300 -57
- package/dist/plugins/hero-slideshow.js +5 -1
- package/dist/plugins/media-gallery.js +25 -0
- package/dist/plugins/sticky-atc.js +15 -1
- package/dist/primitives.css +9 -23
- package/dist/section-gate.js +3287 -1
- package/dist/sections.css +18 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -191,7 +191,7 @@ function gemPlanShapeKnobs(plan) {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
// ../../packages/gem-framework/src/tokens.ts
|
|
194
|
-
var gemFrameworkVersion = "0.
|
|
194
|
+
var gemFrameworkVersion = "0.10.0";
|
|
195
195
|
var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
|
|
196
196
|
var GEM_TAP_MIN_CSS_VAR = "--gem-tap-min: 44px;";
|
|
197
197
|
var GEM_FOCUS_OFFSET_CSS_VAR = "--gem-focus-offset: 3px;";
|
|
@@ -225,6 +225,73 @@ var GEM_ROW_CSS_VARIABLES = [
|
|
|
225
225
|
"--gem-row-gap",
|
|
226
226
|
"--gem-row-gap-y"
|
|
227
227
|
];
|
|
228
|
+
var GEM_BUTTON_TOKEN_KEYS = {
|
|
229
|
+
radius: ["button"],
|
|
230
|
+
typography: ["buttonWeight", "buttonTransform", "buttonLetterSpacing"]
|
|
231
|
+
};
|
|
232
|
+
var GEM_BUTTON_TOKEN_SHAPES = {
|
|
233
|
+
"radius.button": /^(0|\d*\.?\d+(px|rem|em|%)|var\(--gem-radius-[a-z0-9-]+\))$/i,
|
|
234
|
+
"typography.buttonWeight": /^(normal|bold|\d{1,4})$/i,
|
|
235
|
+
"typography.buttonTransform": /^(none|uppercase|lowercase|capitalize)$/i,
|
|
236
|
+
"typography.buttonLetterSpacing": /^(normal|0|-?\d*\.?\d+(em|px|rem))$/i
|
|
237
|
+
};
|
|
238
|
+
var buttonKeyNorm = (k) => k.toLowerCase().replace(/[_-]/g, "");
|
|
239
|
+
function buttonTokenValue(group, key, value) {
|
|
240
|
+
const shape = GEM_BUTTON_TOKEN_SHAPES[`${group}.${key}`];
|
|
241
|
+
if (!shape) return null;
|
|
242
|
+
const v = typeof value === "number" ? String(value) : typeof value === "string" ? stripImportant(value).trim() : "";
|
|
243
|
+
if (!shape.test(v)) return null;
|
|
244
|
+
if (key === "buttonWeight" && /^\d+$/.test(v) && (Number(v) < 1 || Number(v) > 1e3)) return null;
|
|
245
|
+
if (key === "button" && v === "0") return "0px";
|
|
246
|
+
return /^[a-z]+$/i.test(v) ? v.toLowerCase() : v.replace(/(px|rem|em)$/i, (u) => u.toLowerCase());
|
|
247
|
+
}
|
|
248
|
+
function readButtonTokens(tokens) {
|
|
249
|
+
const out = { groups: {}, values: {}, invalid: [] };
|
|
250
|
+
for (const [group, keys] of Object.entries(GEM_BUTTON_TOKEN_KEYS)) {
|
|
251
|
+
const values = tokens?.[group];
|
|
252
|
+
if (!values || typeof values !== "object" || Array.isArray(values)) continue;
|
|
253
|
+
const byNorm = new Map(keys.map((k) => [buttonKeyNorm(k), k]));
|
|
254
|
+
let copy = null;
|
|
255
|
+
for (const [rawKey, raw] of Object.entries(values)) {
|
|
256
|
+
const key = byNorm.get(buttonKeyNorm(rawKey));
|
|
257
|
+
if (!key) continue;
|
|
258
|
+
copy ??= { ...values };
|
|
259
|
+
delete copy[rawKey];
|
|
260
|
+
const v = buttonTokenValue(group, key, raw);
|
|
261
|
+
if (v === null) out.invalid.push({ path: `${group}.${rawKey}`, value: raw });
|
|
262
|
+
else {
|
|
263
|
+
copy[key] = v;
|
|
264
|
+
out.values[key] = v;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (copy) out.groups[group] = copy;
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
function splitSelectorList(selector) {
|
|
272
|
+
const parts = [];
|
|
273
|
+
let depth = 0, cur = "";
|
|
274
|
+
for (const ch of selector) {
|
|
275
|
+
if (ch === "(" || ch === "[") depth++;
|
|
276
|
+
else if (ch === ")" || ch === "]") depth--;
|
|
277
|
+
if (ch === "," && depth === 0) {
|
|
278
|
+
parts.push(cur.trim());
|
|
279
|
+
cur = "";
|
|
280
|
+
} else cur += ch;
|
|
281
|
+
}
|
|
282
|
+
if (cur.trim()) parts.push(cur.trim());
|
|
283
|
+
return parts;
|
|
284
|
+
}
|
|
285
|
+
function gemButtonCaseCss(tokens, selector = ":root") {
|
|
286
|
+
const { values } = readButtonTokens(tokens);
|
|
287
|
+
const decls = [
|
|
288
|
+
values.buttonTransform && "text-transform: var(--gem-font-button-transform);",
|
|
289
|
+
values.buttonLetterSpacing && "letter-spacing: var(--gem-font-button-letter-spacing);"
|
|
290
|
+
].filter(Boolean);
|
|
291
|
+
if (!decls.length) return "";
|
|
292
|
+
const scope = splitSelectorList(selector).map((sel) => `${sel} .gem-button:not(.gem-button--link)`).join(", ");
|
|
293
|
+
return `:where(${scope}) { ${decls.join(" ")} }`;
|
|
294
|
+
}
|
|
228
295
|
var requiredGemTokenKeys = {
|
|
229
296
|
color: ["background", "surface", "text", "muted", "accent", "accentText", "border", "success", "warning", "danger"],
|
|
230
297
|
typography: ["body", "heading", "mono"],
|
|
@@ -237,6 +304,52 @@ var requiredGemTokenKeys = {
|
|
|
237
304
|
component: ["headerHeight", "cardPadding", "buttonPaddingInline", "buttonPaddingBlock", "inputHeight"],
|
|
238
305
|
morphology: ["campaignDensity", "mediaRhythm", "proofIntensity", "commerceEmphasis", "policyTone"]
|
|
239
306
|
};
|
|
307
|
+
var GEM_PHONE_TOKEN_TARGETS = {
|
|
308
|
+
spacing: [...requiredGemTokenKeys.spacing, ...Object.keys(GEM_DERIVED_SPACE_STEPS)],
|
|
309
|
+
layout: ["section"]
|
|
310
|
+
};
|
|
311
|
+
var tokenKeyNorm = (k) => k.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
312
|
+
var PHONE_TARGET_BY_KEY = Object.fromEntries(
|
|
313
|
+
Object.entries(GEM_PHONE_TOKEN_TARGETS).map(([group, targets]) => [group, new Map(targets.map((t) => [`${tokenKeyNorm(t)}mobile`, t]))])
|
|
314
|
+
);
|
|
315
|
+
function gemPhoneTokenTarget(group, key) {
|
|
316
|
+
return PHONE_TARGET_BY_KEY[group]?.get(tokenKeyNorm(key)) ?? null;
|
|
317
|
+
}
|
|
318
|
+
function readPhoneTokens(tokens) {
|
|
319
|
+
const out = { spacing: {}, layout: {}, invalid: [] };
|
|
320
|
+
for (const group of ["spacing", "layout"]) {
|
|
321
|
+
const ownGroup = group === "spacing" ? /var\(\s*--gem-space-/i : /var\(\s*--gem-layout-/i;
|
|
322
|
+
for (const [key, raw] of Object.entries(tokens?.[group] ?? {})) {
|
|
323
|
+
const target = gemPhoneTokenTarget(group, key);
|
|
324
|
+
if (target == null) continue;
|
|
325
|
+
const v = stripImportant(String(raw ?? "")).trim();
|
|
326
|
+
const kind = gemSpaceMdKind(v);
|
|
327
|
+
if (kind === "absent") continue;
|
|
328
|
+
if (kind === "zero") out[group][target] = "0px";
|
|
329
|
+
else if (kind === "length" && !ownGroup.test(v)) out[group][target] = v;
|
|
330
|
+
else out.invalid.push({ path: `${group}.${key}`, value: raw, why: kind === "length" ? `it reads a ${group} variable, which re-declared on :root is a cycle` : "not a length (a number needs a unit, e.g. 70px)" });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
var GEM_LINE_HEIGHT_TOKEN_KEYS = ["headingLineHeight", "bodyLineHeight"];
|
|
336
|
+
var LINE_HEIGHT_SHAPE = /^(normal|\d*\.?\d+(px|rem|em|%)?|(var|calc|clamp|min|max)\(.*\))$/i;
|
|
337
|
+
var isLineHeightKey = (k) => GEM_LINE_HEIGHT_TOKEN_KEYS.some((l) => tokenKeyNorm(l) === tokenKeyNorm(k));
|
|
338
|
+
var badLineHeight = (k, v) => isLineHeightKey(k) && !LINE_HEIGHT_SHAPE.test(stripImportant(String(v ?? "")).trim());
|
|
339
|
+
var desktopEntries = (group, values) => values ? Object.fromEntries(Object.entries(values).filter(([key]) => gemPhoneTokenTarget(group, key) == null)) : values;
|
|
340
|
+
var GEM_TYPE_RAMP_KEYS = ["base", "scale", "baseMobile"];
|
|
341
|
+
var optionalGemTokenKeys = {
|
|
342
|
+
typography: [...GEM_TYPE_RAMP_KEYS, ...GEM_LINE_HEIGHT_TOKEN_KEYS, ...GEM_BUTTON_TOKEN_KEYS.typography],
|
|
343
|
+
spacing: [...Object.keys(GEM_DERIVED_SPACE_STEPS), ...GEM_PHONE_TOKEN_TARGETS.spacing.map((k) => `${k}Mobile`)],
|
|
344
|
+
layout: GEM_PHONE_TOKEN_TARGETS.layout.map((k) => `${k}Mobile`),
|
|
345
|
+
radius: [...GEM_BUTTON_TOKEN_KEYS.radius]
|
|
346
|
+
};
|
|
347
|
+
var GEM_HEADING_SIZE_CSS_VARS = [
|
|
348
|
+
"--gem-section-heading-size: calc(var(--gem-h2) * var(--gem-typography-heading-scale, 1));",
|
|
349
|
+
"--gem-hero-heading-size: calc(var(--gem-h1) * var(--gem-typography-heading-scale, 1));",
|
|
350
|
+
"--gem-section-heading-size-mobile: min(var(--gem-section-heading-size), var(--gem-heading-phone-cap, clamp(1.75rem, 9vw, 2.75rem)));",
|
|
351
|
+
"--gem-hero-heading-size-mobile: min(var(--gem-hero-heading-size), var(--gem-heading-phone-cap, clamp(1.75rem, 9vw, 2.75rem)));"
|
|
352
|
+
];
|
|
240
353
|
var defaultGemTokens = {
|
|
241
354
|
color: {
|
|
242
355
|
background: "#f6f6f3",
|
|
@@ -342,23 +455,37 @@ function validateGemTokens(tokens) {
|
|
|
342
455
|
}
|
|
343
456
|
}
|
|
344
457
|
}
|
|
458
|
+
for (const bad of readButtonTokens(tokens).invalid) {
|
|
459
|
+
const shape = GEM_BUTTON_TOKEN_SHAPES[bad.path.replace(/\.[^.]+$/, (k) => "." + (Object.values(GEM_BUTTON_TOKEN_KEYS).flat().find((c) => buttonKeyNorm(c) === buttonKeyNorm(k.slice(1))) ?? k.slice(1)))];
|
|
460
|
+
issues.push(tokenIssue("warn", "invalid_button_token", `Gem token ${bad.path} = ${JSON.stringify(bad.value)} is not a valid value (${shape}); the button keeps its default.`, bad.path));
|
|
461
|
+
}
|
|
462
|
+
for (const bad of readPhoneTokens(tokens).invalid) {
|
|
463
|
+
issues.push(tokenIssue("warn", "invalid_phone_token", `Gem token ${bad.path} = ${JSON.stringify(bad.value)} is not used: ${bad.why}. Phones keep the desktop value.`, bad.path));
|
|
464
|
+
}
|
|
465
|
+
for (const [key, value] of Object.entries(tokens.typography ?? {})) {
|
|
466
|
+
if (badLineHeight(key, value)) issues.push(tokenIssue("warn", "invalid_line_height_token", `Gem token typography.${key} = ${JSON.stringify(value)} is not a line-height (1.2, 120%, 24px, normal); headings keep their default.`, `typography.${key}`));
|
|
467
|
+
}
|
|
345
468
|
return {
|
|
346
469
|
valid: !issues.some((issue) => issue.severity === "error"),
|
|
347
470
|
issues
|
|
348
471
|
};
|
|
349
472
|
}
|
|
350
473
|
function createGemTokenCss(tokens, selector = ":root") {
|
|
474
|
+
const button = readButtonTokens(tokens);
|
|
475
|
+
const phone = readPhoneTokens(tokens);
|
|
476
|
+
const spacing = desktopEntries("spacing", tokens.spacing);
|
|
351
477
|
const declarations = [
|
|
352
478
|
...toCssVars("color", tokens.color),
|
|
353
|
-
...toCssVars("font", Object.fromEntries(Object.entries(tokens.typography ?? {}).filter(([k]) => !isRampKey(k)))),
|
|
479
|
+
...toCssVars("font", Object.fromEntries(Object.entries(button.groups.typography ?? tokens.typography ?? {}).filter(([k, v]) => !isRampKey(k) && !badLineHeight(k, v)))),
|
|
354
480
|
// Space ramp is scaled by the global density knob --gem-style-space (default 1),
|
|
355
481
|
// so a style pack can make the WHOLE storefront airier or denser without touching
|
|
356
482
|
// any section — every section already consumes --gem-space-*. Knob unset → identical.
|
|
357
|
-
|
|
358
|
-
...
|
|
483
|
+
// Phone keys (`3xlMobile`) are not desktop variables: they go to the phone block below (GEM_PHONE_TOKEN_TARGETS).
|
|
484
|
+
...toScaledCssVars("space", spacing, "--gem-style-space"),
|
|
485
|
+
...gemDerivedSpaceCssVars(spacing),
|
|
359
486
|
// Radius ramp scaled by --gem-style-radius (default 1): a 'sharp' style sets it to
|
|
360
487
|
// 0 (square everything), 'pill' rounds further — across every section, no edits.
|
|
361
|
-
...toScaledCssVars("radius", tokens.radius, "--gem-style-radius"),
|
|
488
|
+
...toScaledCssVars("radius", button.groups.radius ?? tokens.radius, "--gem-style-radius"),
|
|
362
489
|
// Border widths get the --gem-style-border knob injected into their leading length,
|
|
363
490
|
// whatever value the plan provides (e.g. "1px solid …" → "calc(1px * var(...)) solid …").
|
|
364
491
|
...toBorderCssVars("border", tokens.border, "--gem-style-border"),
|
|
@@ -367,7 +494,8 @@ function createGemTokenCss(tokens, selector = ":root") {
|
|
|
367
494
|
// and marquee primitives consume; unrecognised keys fall back to the --gem-motion-* namespace,
|
|
368
495
|
// so duration/easing keep emitting --gem-motion-duration/easing exactly as before (back-compat).
|
|
369
496
|
...toMotionCssVars(tokens.motion),
|
|
370
|
-
...toCssVars("layout", tokens.layout),
|
|
497
|
+
...toCssVars("layout", desktopEntries("layout", tokens.layout)),
|
|
498
|
+
...phone.layout.section ? [`--gem-layout-section-mobile: ${phone.layout.section};`] : [],
|
|
371
499
|
...toCssVars("component", tokens.component),
|
|
372
500
|
...toCssVars("morphology", tokens.morphology),
|
|
373
501
|
// THE GOVERNED TYPE RAMP — --gem-h1..h6 / --gem-body / --gem-text-sm.
|
|
@@ -377,6 +505,8 @@ function createGemTokenCss(tokens, selector = ":root") {
|
|
|
377
505
|
// variables that resolved to nothing. One modular scale, retuned by overriding --gem-type-base
|
|
378
506
|
// or --gem-type-scale (see gemTypeRampCssVars).
|
|
379
507
|
...gemTypeRampCssVars(tokens.typography),
|
|
508
|
+
// Heading sizes from the ramp, only for a plan that chose a ramp (GEM_HEADING_SIZE_CSS_VARS).
|
|
509
|
+
...typeRampValue(tokens.typography, "base") || typeRampValue(tokens.typography, "scale") ? GEM_HEADING_SIZE_CSS_VARS : [],
|
|
380
510
|
// Focus ring (gemfactory request #3, 28/09/2026): 11 sections wrote `outline: 2px solid
|
|
381
511
|
// var(--gem-color-accent)` by hand. Not a token GROUP (a group would name it --gem-border-focus-ring
|
|
382
512
|
// and a required key would fail every stored plan/preset without it), so it is emitted here like the
|
|
@@ -386,16 +516,25 @@ function createGemTokenCss(tokens, selector = ":root") {
|
|
|
386
516
|
GEM_FOCUS_OFFSET_CSS_VAR
|
|
387
517
|
];
|
|
388
518
|
const formBlock = gemFormCssBlock(tokens.form);
|
|
389
|
-
const
|
|
519
|
+
const buttonCase = gemButtonCaseCss(tokens, selector);
|
|
520
|
+
const mobile = gemMobileTokenCssVars(tokens, phone);
|
|
390
521
|
return [
|
|
391
522
|
`${selector} {`,
|
|
392
523
|
...declarations.map((line) => ` ${line}`),
|
|
393
524
|
"}",
|
|
394
|
-
...
|
|
395
|
-
...formBlock ? [formBlock] : []
|
|
525
|
+
...mobile.length ? [`@media (max-width: ${GEM_TYPE_MOBILE_MAX}) {`, ` ${selector} {`, ...mobile.map((line) => ` ${line}`), " }", "}"] : [],
|
|
526
|
+
...formBlock ? [formBlock] : [],
|
|
527
|
+
...buttonCase ? [buttonCase] : []
|
|
396
528
|
].join("\n");
|
|
397
529
|
}
|
|
398
|
-
|
|
530
|
+
function gemMobileTokenCssVars(tokens, phone = readPhoneTokens(tokens)) {
|
|
531
|
+
const baseMobile = typeRampValue(tokens.typography, "baseMobile");
|
|
532
|
+
return [
|
|
533
|
+
...baseMobile ? [`--gem-type-base: ${baseMobile};`] : [],
|
|
534
|
+
...toScaledCssVars("space", phone.spacing, "--gem-style-space"),
|
|
535
|
+
...toCssVars("layout", phone.layout)
|
|
536
|
+
];
|
|
537
|
+
}
|
|
399
538
|
var rampKey = (k) => k.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
400
539
|
var isRampKey = (k) => GEM_TYPE_RAMP_KEYS.some((r) => rampKey(r) === rampKey(k));
|
|
401
540
|
var LENGTH = /^\d*\.?\d+(px|rem|em)$/;
|
|
@@ -879,10 +1018,10 @@ function createGemFrameworkManifest() {
|
|
|
879
1018
|
package_name: "@gemtheme/gem-framework",
|
|
880
1019
|
framework_version: gemFrameworkVersion,
|
|
881
1020
|
namespace: "gem",
|
|
882
|
-
token_groups: Object.entries(requiredGemTokenKeys).map(([group, keys]) =>
|
|
883
|
-
group
|
|
884
|
-
keys: [...keys]
|
|
885
|
-
})
|
|
1021
|
+
token_groups: Object.entries(requiredGemTokenKeys).map(([group, keys]) => {
|
|
1022
|
+
const optional = [...optionalGemTokenKeys[group] ?? []];
|
|
1023
|
+
return { group, keys: [...keys], ...optional.length ? { optional_keys: optional } : {} };
|
|
1024
|
+
}),
|
|
886
1025
|
primitive_groups: gemFrameworkPrimitiveGroups.map((group) => ({
|
|
887
1026
|
group: group.group,
|
|
888
1027
|
classes: [...group.classes]
|
|
@@ -1186,7 +1325,6 @@ var GEM_AMBIENT_CSS_LINES = [
|
|
|
1186
1325
|
"@media (prefers-reduced-motion: reduce) {",
|
|
1187
1326
|
" [data-gem-ambient] { --gem-ambient-amp: 2px; animation-duration: 12s; }",
|
|
1188
1327
|
" [data-gem-reveal] { transition: none; opacity: 1; transform: none; }",
|
|
1189
|
-
" .gem-marquee__track { animation-duration: calc(var(--gem-marquee-duration, 40s) * 2.5); }",
|
|
1190
1328
|
"}"
|
|
1191
1329
|
];
|
|
1192
1330
|
var GEM_AMBIENT_CSS = GEM_AMBIENT_CSS_LINES.join("\n");
|
|
@@ -1376,9 +1514,13 @@ function createPrimitiveCss() {
|
|
|
1376
1514
|
" justify-content: center;",
|
|
1377
1515
|
" min-height: var(--gem-component-input-height, 44px);",
|
|
1378
1516
|
" border: var(--gem-button-border-width, 1px) solid var(--gem-color-border);",
|
|
1379
|
-
|
|
1517
|
+
// Button tokens (GEM_BUTTON_TOKEN_KEYS, optional): unset, radius and weight fall back to exactly what this rule always
|
|
1518
|
+
// drew. --gem-radius-button is the button's own radius, so a pill button does not turn every card into a pill
|
|
1519
|
+
// (radius-md is shared). Case and tracking are NOT declared here: <button> gets `none` / `normal` from the UA sheet
|
|
1520
|
+
// while <a> inherits, so createGemTokenCss adds them only when a token sets them (gemButtonCaseCss).
|
|
1521
|
+
" border-radius: var(--gem-radius-button, var(--gem-radius-md));",
|
|
1380
1522
|
" padding: var(--gem-component-button-padding-block, 12px) var(--gem-component-button-padding-inline, 18px);",
|
|
1381
|
-
" font-weight: 700;",
|
|
1523
|
+
" font-weight: var(--gem-font-button-weight, 700);",
|
|
1382
1524
|
" text-decoration: none;",
|
|
1383
1525
|
" transition: background var(--gem-motion-duration, 160ms) var(--gem-motion-easing, ease), color var(--gem-motion-duration, 160ms) var(--gem-motion-easing, ease);",
|
|
1384
1526
|
"}",
|
|
@@ -1641,28 +1783,6 @@ function createPrimitiveCss() {
|
|
|
1641
1783
|
" border-radius: var(--gem-radius-md);",
|
|
1642
1784
|
" background: color-mix(in srgb, var(--gem-color-text) 8%, var(--gem-color-background));",
|
|
1643
1785
|
"}",
|
|
1644
|
-
".gem-marquee {",
|
|
1645
|
-
" overflow: hidden;",
|
|
1646
|
-
" border-block: var(--gem-border-subtle, 1px solid var(--gem-color-border));",
|
|
1647
|
-
"}",
|
|
1648
|
-
// Consumer contract: render the track's children TWICE (like the site's `.mq-row`) so the
|
|
1649
|
-
// -50% translate lands on an identical second copy and the loop is seamless with no visible jump.
|
|
1650
|
-
// The animation reuses the gem-marquee-scroll keyframe defined with the ambient primitive.
|
|
1651
|
-
".gem-marquee__track {",
|
|
1652
|
-
" display: flex;",
|
|
1653
|
-
" gap: var(--gem-space-lg);",
|
|
1654
|
-
" min-width: max-content;",
|
|
1655
|
-
" animation: gem-marquee-scroll var(--gem-marquee-duration, 40s) linear infinite;",
|
|
1656
|
-
" will-change: transform;",
|
|
1657
|
-
"}",
|
|
1658
|
-
// Pause-on-hover lets a shopper read a badge/logo they want to inspect.
|
|
1659
|
-
".gem-marquee:hover .gem-marquee__track {",
|
|
1660
|
-
" animation-play-state: paused;",
|
|
1661
|
-
"}",
|
|
1662
|
-
// Opt-in reverse direction (e.g. a second row scrolling the other way).
|
|
1663
|
-
".gem-marquee[data-gem-marquee-reverse] .gem-marquee__track {",
|
|
1664
|
-
" animation-direction: reverse;",
|
|
1665
|
-
"}",
|
|
1666
1786
|
".gem-carousel {",
|
|
1667
1787
|
" display: grid;",
|
|
1668
1788
|
" gap: var(--gem-space-md);",
|
|
@@ -1798,9 +1918,20 @@ function createPrimitiveCss() {
|
|
|
1798
1918
|
" overflow: hidden;",
|
|
1799
1919
|
" color: var(--gem-color-accent);",
|
|
1800
1920
|
"}",
|
|
1801
|
-
// Marquee primitive
|
|
1802
|
-
//
|
|
1803
|
-
|
|
1921
|
+
// Marquee primitive. CSS-only and deliberately so (no JS to init, works on every render path).
|
|
1922
|
+
// Contract: `.gem-marquee > .gem-marquee__viewport > .gem-marquee__track` TWICE (the second copy
|
|
1923
|
+
// aria-hidden); each track is at least the viewport wide and moves -100% of itself, so the second
|
|
1924
|
+
// copy is what makes the loop seamless. gemcapture's clone kit and GemFactory build exactly this.
|
|
1925
|
+
// ONE definition (0.9.11): until then a second, earlier one (gemtheme #124: one track with its
|
|
1926
|
+
// children doubled, -50%) sat above this and lost every property both set, so its contract never
|
|
1927
|
+
// worked. What it set that still took effect (the border, will-change, pause on hover of the whole
|
|
1928
|
+
// marquee, the data-attribute reverse) is kept here, so a marquee built to either contract computes
|
|
1929
|
+
// the same style as before (Chromium, 08/10/2026). The one exception would be a track that also
|
|
1930
|
+
// carries data-gem-ambient, which nothing renders.
|
|
1931
|
+
".gem-marquee {",
|
|
1932
|
+
" overflow: hidden;",
|
|
1933
|
+
" border-block: var(--gem-border-subtle, 1px solid var(--gem-color-border));",
|
|
1934
|
+
"}",
|
|
1804
1935
|
".gem-marquee__viewport { display: flex; overflow: hidden; }",
|
|
1805
1936
|
".gem-marquee__track {",
|
|
1806
1937
|
" flex: 0 0 auto;",
|
|
@@ -1811,9 +1942,12 @@ function createPrimitiveCss() {
|
|
|
1811
1942
|
" gap: var(--gem-space-lg);",
|
|
1812
1943
|
" white-space: nowrap;",
|
|
1813
1944
|
" animation: gem-marquee var(--gem-marquee-duration, 24s) linear infinite;",
|
|
1945
|
+
" will-change: transform;",
|
|
1814
1946
|
"}",
|
|
1815
|
-
|
|
1816
|
-
".gem-marquee
|
|
1947
|
+
// Pause-on-hover lets a shopper read a badge/logo they want to inspect.
|
|
1948
|
+
".gem-marquee:hover .gem-marquee__track, .gem-marquee__viewport:hover .gem-marquee__track { animation-play-state: paused; }",
|
|
1949
|
+
// Opt-in reverse direction (e.g. a second row scrolling the other way).
|
|
1950
|
+
".gem-marquee--reverse .gem-marquee__track, .gem-marquee[data-gem-marquee-reverse] .gem-marquee__track { animation-direction: reverse; }",
|
|
1817
1951
|
"@keyframes gem-marquee { from { transform: translateX(0); } to { transform: translateX(-100%); } }",
|
|
1818
1952
|
"@media (prefers-reduced-motion: reduce) {",
|
|
1819
1953
|
" .gem-marquee__track { animation: none; }",
|
|
@@ -2831,10 +2965,10 @@ function createGemSectionRuntimeContractCss(selector = "[data-gem-section-librar
|
|
|
2831
2965
|
"}"
|
|
2832
2966
|
].join("\n");
|
|
2833
2967
|
}
|
|
2834
|
-
var OPT_IN_EYEBROW = ":where(.gem-section[data-gem-eyebrow='show']) .gem-section__eyebrow { display: block; letter-spacing: 0.12em; }";
|
|
2835
|
-
var OPT_IN_HEADING = ":where(.gem-section[data-gem-heading='section']) .gem-section__heading { margin: 0; font-family: var(--gem-font-heading); font-size: var(--gem-h2); line-height: 1.1; letter-spacing: -0.01em; text-wrap: balance; }";
|
|
2968
|
+
var OPT_IN_EYEBROW = ":where(.gem-section[data-gem-eyebrow='show']) .gem-section__eyebrow { display: block; font-size: var(--gem-text-sm); letter-spacing: 0.12em; }";
|
|
2969
|
+
var OPT_IN_HEADING = ":where(.gem-section[data-gem-heading='section']) .gem-section__heading { margin: 0; font-family: var(--gem-font-heading); font-size: var(--gem-section-heading-size, var(--gem-h2)); line-height: var(--gem-font-heading-line-height, 1.1); letter-spacing: -0.01em; text-wrap: balance; }";
|
|
2836
2970
|
var OPT_IN_BODY = ":where(.gem-section[data-gem-body='section']) .gem-section__body { margin: 0; color: var(--gem-color-muted); font-size: var(--gem-body); line-height: var(--gem-font-body-line-height, 1.5); }";
|
|
2837
|
-
var OPT_IN_HEADING_MOBILE = " :where(.gem-section[data-gem-heading='section']) .gem-section__heading { font-size: min(var(--gem-h2), clamp(1.75rem, 9vw, 2.75rem)); }";
|
|
2971
|
+
var OPT_IN_HEADING_MOBILE = " :where(.gem-section[data-gem-heading='section']) .gem-section__heading { font-size: var(--gem-section-heading-size-mobile, min(var(--gem-h2), clamp(1.75rem, 9vw, 2.75rem))); }";
|
|
2838
2972
|
function createSectionCss() {
|
|
2839
2973
|
return [
|
|
2840
2974
|
".gem-section {",
|
|
@@ -3026,8 +3160,8 @@ function createSectionCss() {
|
|
|
3026
3160
|
".gem-section__heading {",
|
|
3027
3161
|
" margin: 0;",
|
|
3028
3162
|
" font-family: var(--gem-font-heading);",
|
|
3029
|
-
" font-size: clamp(calc(2rem * var(--gem-typography-heading-scale, 1)), calc(4vw * var(--gem-typography-heading-scale, 1)), calc(4rem * var(--gem-typography-heading-scale, 1)));",
|
|
3030
|
-
" line-height: 1.04;",
|
|
3163
|
+
" font-size: var(--gem-section-heading-size, clamp(calc(2rem * var(--gem-typography-heading-scale, 1)), calc(4vw * var(--gem-typography-heading-scale, 1)), calc(4rem * var(--gem-typography-heading-scale, 1))));",
|
|
3164
|
+
" line-height: var(--gem-font-heading-line-height, 1.04);",
|
|
3031
3165
|
"}",
|
|
3032
3166
|
".gem-section__body {",
|
|
3033
3167
|
" margin: 0;",
|
|
@@ -3144,9 +3278,10 @@ function createSectionCss() {
|
|
|
3144
3278
|
" }",
|
|
3145
3279
|
"}",
|
|
3146
3280
|
"@media (max-width: 760px) {",
|
|
3147
|
-
|
|
3281
|
+
// A declared layout.sectionMobile is final (tokens.ts GEM_PHONE_TOKEN_TARGETS); otherwise the desktop value × 0.72.
|
|
3282
|
+
" .gem-section { padding-block: var(--gem-layout-section-mobile, calc(var(--gem-layout-section, 64px) * 0.72)); }",
|
|
3148
3283
|
" .gem-section__inner, .gem-container { width: min(100% - 32px, var(--gem-layout-page, 1180px)); }",
|
|
3149
|
-
" .gem-section__heading { font-size: clamp(1.75rem, 9vw, 2.75rem); }",
|
|
3284
|
+
" .gem-section__heading { font-size: var(--gem-section-heading-size-mobile, clamp(1.75rem, 9vw, 2.75rem)); }",
|
|
3150
3285
|
" .gem-section__grid { grid-template-columns: minmax(0, 1fr); }",
|
|
3151
3286
|
" .gem-section:is([data-gem-composition-mode='split-collage'], [data-gem-editorial-band-mode='media-story']) .gem-section__grid { grid-template-columns: minmax(0, 1fr); }",
|
|
3152
3287
|
" .gem-product-main[data-gem-product-main-layout='dawn-main'] .gem-product-main__inner { grid-template-columns: minmax(0, 1fr); }",
|
|
@@ -3166,7 +3301,7 @@ function createSectionCss() {
|
|
|
3166
3301
|
function createDawnParityCss() {
|
|
3167
3302
|
return [
|
|
3168
3303
|
"/* Dawn parity profile: locked default baseline; morph presets must opt out before applying Gem visual identity. */",
|
|
3169
|
-
":root { --gem-profile-mode: dawn-parity; --gem-layout-page: 1100px; --gem-layout-gutter: 32px; --gem-layout-section: 52px; --gem-radius-md: 0px; --gem-card-radius: 0px; --gem-media-radius: 0px; --gem-grid-column-gap: 8px; --gem-grid-row-gap: 8px; }",
|
|
3304
|
+
":root { --gem-profile-mode: dawn-parity; --gem-layout-page: 1100px; --gem-layout-gutter: 32px; --gem-layout-section: 52px; --gem-radius-md: 0px; --gem-card-radius: 0px; --gem-media-radius: 0px; --gem-grid-column-gap: 8px; --gem-grid-row-gap: 8px; --gem-heading-phone-cap: clamp(2rem, 11vw, 3.6rem); }",
|
|
3170
3305
|
".utility-bar, .announcement-bar, .utility-bar__grid { min-height: 38px; }",
|
|
3171
3306
|
".announcement-bar__message { margin: 0; min-height: 38px; padding-block: 10px; font-size: 0.75rem; font-weight: 400; letter-spacing: 0.04em; }",
|
|
3172
3307
|
// The pair of rules here used to blank the real message (font-size: 0) and inject a FIXED
|
|
@@ -3242,7 +3377,12 @@ function createDawnParityCss() {
|
|
|
3242
3377
|
".gem-hero-banner--split-proof .gem-hero-banner__media, .gem-hero-banner--editorial .gem-hero-banner__media { position: relative; inset: auto; min-height: 26rem; height: 100%; overflow: hidden; border-radius: 6px; background: rgb(244 244 244); }",
|
|
3243
3378
|
".gem-hero-banner--split-proof .gem-hero-banner__image, .gem-hero-banner--split-proof .gem-hero-banner__placeholder, .gem-hero-banner--editorial .gem-hero-banner__image, .gem-hero-banner--editorial .gem-hero-banner__placeholder { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }",
|
|
3244
3379
|
".gem-hero-banner .gem-section__content { max-width: 78rem; margin-inline: auto; }",
|
|
3245
|
-
".gem-hero-banner h1, .gem-hero-banner h2, .gem-section__heading { margin: 0 0 1.5rem; font-
|
|
3380
|
+
".gem-hero-banner h1, .gem-hero-banner h2, .gem-section__heading { margin: 0 0 1.5rem; font-weight: 400; line-height: var(--gem-font-heading-line-height, 1.08); letter-spacing: 0; }",
|
|
3381
|
+
// Font size has ONE owner per selector (one-owner-per-selector.test.ts): section headings take H2 of the plan's ramp,
|
|
3382
|
+
// the hero's own title H1 (tokens.ts GEM_HEADING_SIZE_CSS_VARS). Same position and specificity as the shared rule above,
|
|
3383
|
+
// so with no ramp both resolve to the clamp that rule used to set.
|
|
3384
|
+
".gem-section__heading { font-size: var(--gem-section-heading-size, clamp(calc(2.5rem * var(--gem-typography-heading-scale, 1)), calc(4vw * var(--gem-typography-heading-scale, 1)), calc(5.2rem * var(--gem-typography-heading-scale, 1)))); }",
|
|
3385
|
+
".gem-hero-banner h1, .gem-hero-banner h2 { font-size: var(--gem-hero-heading-size, clamp(calc(2.5rem * var(--gem-typography-heading-scale, 1)), calc(4vw * var(--gem-typography-heading-scale, 1)), calc(5.2rem * var(--gem-typography-heading-scale, 1)))); }",
|
|
3246
3386
|
".gem-section__eyebrow { display: none; }",
|
|
3247
3387
|
".gem-hero-banner--compact-collection { min-height: auto; display: block; text-align: left; padding-block: 25px; }",
|
|
3248
3388
|
".gem-hero-banner--compact-collection .gem-section__content, .gem-product-card-grid--quick-view-grid > .gem-section__inner { width: min(var(--gem-layout-page, 1200px), calc(100% - var(--gem-layout-gutter, 32px))); margin-inline: auto; }",
|
|
@@ -3257,7 +3397,13 @@ function createDawnParityCss() {
|
|
|
3257
3397
|
".gem-rich-text--dawn-intro .gem-section__body { width: 100%; max-width: none; margin: 20px 0 0; font-size: 16px; line-height: 28.8px; }",
|
|
3258
3398
|
".gem-rich-text--dawn-intro .gem-section__body p { margin: 0; }",
|
|
3259
3399
|
".gem-rich-text--dawn-intro .gem-rich-text__blocks { display: block; width: 100%; }",
|
|
3260
|
-
|
|
3400
|
+
// Dawn's square, regular-weight button stays the default; the button tokens (radius.button, typography.buttonWeight)
|
|
3401
|
+
// override it, or this rule (same specificity, later) would pin every button square at 400 whatever the plan says.
|
|
3402
|
+
// The weight sits in its own rule that skips link buttons, which keep today's 400 here: the rule above it is what
|
|
3403
|
+
// overrides `.gem-button--link { font-weight: inherit }` in this profile, and a weight token must not reach a link.
|
|
3404
|
+
// `:where()` keeps both at one class of specificity, so a section's own CTA rule still wins.
|
|
3405
|
+
".gem-button { min-width: 12rem; border-radius: var(--gem-radius-button, 0); font-weight: 400; box-shadow: none; }",
|
|
3406
|
+
".gem-button:where(:not(.gem-button--link)) { font-weight: var(--gem-font-button-weight, 400); }",
|
|
3261
3407
|
".gem-product-card-wrapper { display: block; min-width: 0; }",
|
|
3262
3408
|
".gem-card, .gem-product-card { border: 0; border-radius: 0; box-shadow: none; background: transparent; padding: 0; }",
|
|
3263
3409
|
".gem-product-card__media, .gem-media { border: 0; border-radius: 0; box-shadow: none; background: rgb(244 244 244); }",
|
|
@@ -3453,7 +3599,9 @@ function createDawnParityMobileCss() {
|
|
|
3453
3599
|
" .gem-product-card__content { padding-top: 8px; gap: 4px; }",
|
|
3454
3600
|
" .gem-product-card__title { font-size: 15px; line-height: 24px; }",
|
|
3455
3601
|
" .gem-product-card__price, .gem-product-card__price .gem-price { font-size: 16px; line-height: 24px; }",
|
|
3456
|
-
|
|
3602
|
+
// Fallback = the profile's own phone heading, declared once as --gem-heading-phone-cap on the :root above.
|
|
3603
|
+
" .gem-section__heading { font-size: var(--gem-section-heading-size-mobile, var(--gem-heading-phone-cap)); }",
|
|
3604
|
+
" .gem-hero-banner h1, .gem-hero-banner h2 { font-size: var(--gem-hero-heading-size-mobile, var(--gem-heading-phone-cap)); }",
|
|
3457
3605
|
" .gem-featured-collection--editorial-grid .gem-section__grid, .gem-product-card-grid--quick-view-grid .gem-section__grid, .gem-main-search .gem-section__grid { grid-template-columns: repeat(var(--gem-product-card-grid-mobile-columns, 2), minmax(0, 1fr)); }",
|
|
3458
3606
|
" .gem-product-main[data-gem-product-main-layout='dawn-main'] > .gem-section__inner { width: calc(100% - 30px); }",
|
|
3459
3607
|
" .gem-product-main[data-gem-product-main-layout='dawn-main'] .gem-product-media-gallery__frame { display: flex; gap: 0; overflow-x: auto; scroll-snap-type: x mandatory; }",
|
|
@@ -4555,7 +4703,38 @@ var mediaGalleryPluginRegistrationLines = [
|
|
|
4555
4703
|
" const galleries = document.querySelectorAll('[data-gem-media-gallery], .gem-media-gallery, .gem-product-media-gallery');",
|
|
4556
4704
|
" let bound = 0;",
|
|
4557
4705
|
" galleries.forEach((gallery) => {",
|
|
4706
|
+
// A wrapper matched only by its class (the thumbnail-stack <section class="gem-product-media-gallery">) around a
|
|
4707
|
+
// [data-gem-media-gallery] hook is not a second gallery: binding both ran every click twice and the outer one
|
|
4708
|
+
// renumbered the thumbnails' data-gem-media-index by counting every <img>, the main image included.
|
|
4709
|
+
" const hooked = !!(gallery.hasAttribute && gallery.hasAttribute('data-gem-media-gallery'));",
|
|
4710
|
+
" if (!hooked && gallery.querySelector && gallery.querySelector('[data-gem-media-gallery]')) return;",
|
|
4558
4711
|
" if (!mark(gallery, 'media-gallery')) return;",
|
|
4712
|
+
// Main image + thumbnails (product-media-gallery.thumbnail-stack.v1): a thumbnail click puts its
|
|
4713
|
+
// data-gem-media-src / -alt on [data-gem-media-main], as the section's behavior.js did. Without this the plugin
|
|
4714
|
+
// only moved aria-current and the big image never changed. The markup's data-gem-media-index is kept.
|
|
4715
|
+
" const main = gallery.querySelector && gallery.querySelector('[data-gem-media-main]');",
|
|
4716
|
+
" const thumbs = main && gallery.querySelectorAll ? Array.prototype.slice.call(gallery.querySelectorAll('[data-gem-media-item]')) : [];",
|
|
4717
|
+
" if (main && thumbs.length) {",
|
|
4718
|
+
" gallery.dataset.gemMediaState = 'ready';",
|
|
4719
|
+
" const choose = (item, reason) => {",
|
|
4720
|
+
" const src = item.getAttribute('data-gem-media-src');",
|
|
4721
|
+
" const alt = item.getAttribute('data-gem-media-alt');",
|
|
4722
|
+
" if (src) { main.setAttribute('src', src); main.removeAttribute('srcset'); if (alt != null) main.setAttribute('alt', alt); }",
|
|
4723
|
+
" const index = Number(item.getAttribute('data-gem-media-index') || thumbs.indexOf(item));",
|
|
4724
|
+
" thumbs.forEach((thumb) => { const on = thumb === item; thumb.setAttribute('aria-current', String(on)); if (thumb.dataset) thumb.dataset.gemMediaActive = on ? 'true' : 'false'; });",
|
|
4725
|
+
" gallery.dataset.gemMediaActiveIndex = String(index);",
|
|
4726
|
+
" if (gallery.dispatchEvent && typeof window.CustomEvent === 'function') gallery.dispatchEvent(new window.CustomEvent('gem:media:select', { bubbles: true, detail: { index } }));",
|
|
4727
|
+
" api.emit('gem:media:change', { source: 'gem-runtime', index, galleryId: gallery.id || undefined, reason });",
|
|
4728
|
+
" };",
|
|
4729
|
+
" if (canListen(gallery)) gallery.addEventListener('click', (event) => {",
|
|
4730
|
+
" const item = event.target && event.target.closest ? event.target.closest('[data-gem-media-item]') : null;",
|
|
4731
|
+
" if (!item || !gallery.contains(item)) return;",
|
|
4732
|
+
" if (event.preventDefault) event.preventDefault();",
|
|
4733
|
+
" choose(item, 'click');",
|
|
4734
|
+
" });",
|
|
4735
|
+
" bound += 1;",
|
|
4736
|
+
" return;",
|
|
4737
|
+
" }",
|
|
4559
4738
|
" const items = gallery.querySelectorAll('[data-gem-media-item], img, video, model-viewer');",
|
|
4560
4739
|
" const thumbControls = gallery.querySelectorAll('[data-gem-media-thumb]');",
|
|
4561
4740
|
" const controls = thumbControls.length ? thumbControls : items;",
|
|
@@ -4667,7 +4846,11 @@ var heroSlideshowPluginRegistrationLines = [
|
|
|
4667
4846
|
" else if (k === 'Home') { if (e.preventDefault) e.preventDefault(); focusDot(0); }",
|
|
4668
4847
|
" else if (k === 'End') { if (e.preventDefault) e.preventDefault(); focusDot(dots.length - 1); }",
|
|
4669
4848
|
" }); });",
|
|
4670
|
-
"
|
|
4849
|
+
" // The visible label follows the state like the section's own behavior did: the button's own text while playing",
|
|
4850
|
+
" // (kept, so a translated label survives), data-gem-hero-play-label (or 'Play') while paused. Only a text-only button.",
|
|
4851
|
+
" const pauseText = pause && pause.childElementCount === 0 ? pause.textContent : null;",
|
|
4852
|
+
" const playText = (pause && pause.getAttribute && pause.getAttribute('data-gem-hero-play-label')) || 'Play';",
|
|
4853
|
+
" if (canListen(pause)) pause.addEventListener('click', () => { paused = !paused; if (pause.setAttribute) pause.setAttribute('aria-pressed', String(paused)); if (pauseText !== null) pause.textContent = paused ? playText : pauseText; start(); });",
|
|
4671
4854
|
" show(active); start(); bound += 1;",
|
|
4672
4855
|
" });",
|
|
4673
4856
|
" api.emit('gem:hero-slideshow:ready', { source: 'gem-runtime', count: bound });",
|
|
@@ -4882,8 +5065,27 @@ var stickyAtcPluginRegistrationLines = [
|
|
|
4882
5065
|
" };",
|
|
4883
5066
|
" update();",
|
|
4884
5067
|
" if (canListen(target)) target.addEventListener('scroll', update, { passive: true });",
|
|
5068
|
+
// The price follows the variant <select> (each <option> carries its own data-gem-variant-price), as the section's
|
|
5069
|
+
// behavior.js did: otherwise the bar shows the first variant's price whatever the shopper picked.
|
|
5070
|
+
" const priceEl = bar.querySelector && bar.querySelector('[data-gem-sticky-atc-price]');",
|
|
5071
|
+
" const select = bar.querySelector && bar.querySelector('select');",
|
|
5072
|
+
" const syncPrice = () => { if (!priceEl || !select) return; const opt = select.options && select.selectedIndex > -1 ? select.options[select.selectedIndex] : null; const price = opt && opt.getAttribute && opt.getAttribute('data-gem-variant-price'); if (price) priceEl.textContent = price; };",
|
|
5073
|
+
" if (canListen(select)) select.addEventListener('change', syncPrice);",
|
|
5074
|
+
" syncPrice();",
|
|
5075
|
+
// A form WITHOUT an action (the headless bar) has nowhere to post: the plugin adds to cart by dispatching
|
|
5076
|
+
// gem:cart:add with the SELECTED variant (the button's data-gem-variant-id is only the first one), which the
|
|
5077
|
+
// storefront's cart bridge consumes. A form WITH an action (a Liquid /cart/add form) is left to the browser.
|
|
4885
5078
|
" const form = bar.querySelector && bar.querySelector('form');",
|
|
4886
|
-
" if (canListen(form)) form.addEventListener('submit', () =>
|
|
5079
|
+
" if (canListen(form)) form.addEventListener('submit', (event) => {",
|
|
5080
|
+
" if (!(form.hasAttribute && form.hasAttribute('action'))) {",
|
|
5081
|
+
" if (event && event.preventDefault) event.preventDefault();",
|
|
5082
|
+
" const choice = form.querySelector && form.querySelector('select');",
|
|
5083
|
+
" const trigger = bar.querySelector && bar.querySelector('[data-gem-variant-id]');",
|
|
5084
|
+
" const variantId = (choice && choice.value) || (trigger && trigger.getAttribute('data-gem-variant-id')) || undefined;",
|
|
5085
|
+
" if (bar.dispatchEvent && typeof window.CustomEvent === 'function') bar.dispatchEvent(new window.CustomEvent('gem:cart:add', { bubbles: true, detail: { variantId, quantity: 1 } }));",
|
|
5086
|
+
" }",
|
|
5087
|
+
" api.emit('gem:sticky-atc:submit', { source: 'gem-runtime', id: bar.id || undefined });",
|
|
5088
|
+
" });",
|
|
4887
5089
|
" bound += 1;",
|
|
4888
5090
|
" });",
|
|
4889
5091
|
" api.emit('gem:sticky-atc:ready', { source: 'gem-runtime', count: bound });",
|
|
@@ -5662,8 +5864,41 @@ function isAllowedUntokenizedValue(property, value) {
|
|
|
5662
5864
|
function hasRawVisualValue(value) {
|
|
5663
5865
|
return /#[0-9a-f]{3,8}\b|rgb\(|hsl\(|\b\d+(?:\.\d+)?(?:px|rem|em)\b/i.test(value);
|
|
5664
5866
|
}
|
|
5867
|
+
var frameworkPublicVariables;
|
|
5868
|
+
function gemFrameworkPublicCssVariables() {
|
|
5869
|
+
if (!frameworkPublicVariables) {
|
|
5870
|
+
const css = [
|
|
5871
|
+
createGemThemeCss(defaultGemTokens, ":root", { profile: "dawn-parity" }),
|
|
5872
|
+
createGemThemeCss(defaultGemTokens, ":root", { profile: "morph" }),
|
|
5873
|
+
createThemeProfileCss("dawn-parity"),
|
|
5874
|
+
createMorphProfileCss(),
|
|
5875
|
+
createPrimitiveCss(),
|
|
5876
|
+
String(createSectionCss()),
|
|
5877
|
+
String(createShellCss()),
|
|
5878
|
+
String(createGemSectionRuntimeContractCss()),
|
|
5879
|
+
// The button case rule only exists when a token sets it; a section may still read its variables (a CTA rule
|
|
5880
|
+
// beats the zero-specificity rule, so opting in means `text-transform: var(--gem-font-button-transform, none)`).
|
|
5881
|
+
gemButtonCaseCss({ typography: { buttonTransform: "none", buttonLetterSpacing: "normal" } })
|
|
5882
|
+
].join("\n").replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
5883
|
+
frameworkPublicVariables = gemPublicCssVariablesFrom(css);
|
|
5884
|
+
}
|
|
5885
|
+
return frameworkPublicVariables;
|
|
5886
|
+
}
|
|
5887
|
+
var ROOT_SELECTOR = /^\s*(?::root|html|:host)(?:\[[^\]]*\]|:[a-z-]+(?:\([^()]*\))?)*\s*$/;
|
|
5888
|
+
function gemPublicCssVariablesFrom(stylesheet) {
|
|
5889
|
+
const css = stylesheet.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
5890
|
+
const declaredOnRoot = /* @__PURE__ */ new Set();
|
|
5891
|
+
const declaredElsewhere = /* @__PURE__ */ new Set();
|
|
5892
|
+
for (const [, selector, body] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
5893
|
+
const onRoot = selector.split(",").every((part) => ROOT_SELECTOR.test(part));
|
|
5894
|
+
for (const [, name] of body.matchAll(/(--gem-[a-z0-9-]+)\s*:/g)) (onRoot ? declaredOnRoot : declaredElsewhere).add(name);
|
|
5895
|
+
}
|
|
5896
|
+
const mentioned = new Set([...css.matchAll(/--gem-[a-z0-9-]+/g)].map((match) => match[0]));
|
|
5897
|
+
return new Set([...mentioned].filter((name) => declaredOnRoot.has(name) || !declaredElsewhere.has(name)));
|
|
5898
|
+
}
|
|
5665
5899
|
function isKnownGemCssVariable(variableName, options) {
|
|
5666
5900
|
if (canonicalCssVariableNames.has(variableName)) return true;
|
|
5901
|
+
if (gemFrameworkPublicCssVariables().has(variableName)) return true;
|
|
5667
5902
|
const allowedPrefixes = [
|
|
5668
5903
|
"--gem-seasonal-",
|
|
5669
5904
|
// Global style-variant knobs (--gem-style-space/radius/text/weight/stroke): the
|
|
@@ -6814,6 +7049,8 @@ function stringSetting(value) {
|
|
|
6814
7049
|
export {
|
|
6815
7050
|
COUNTDOWN_END_RUNTIME_LINES,
|
|
6816
7051
|
GEM_AMBIENT_CSS,
|
|
7052
|
+
GEM_BUTTON_TOKEN_KEYS,
|
|
7053
|
+
GEM_BUTTON_TOKEN_SHAPES,
|
|
6817
7054
|
GEM_COUNTDOWN_ENDED_CSS,
|
|
6818
7055
|
GEM_FIELD_JOINS,
|
|
6819
7056
|
GEM_FIELD_SHAPES,
|
|
@@ -6824,6 +7061,8 @@ export {
|
|
|
6824
7061
|
GEM_FORM_TOKEN_KEYS,
|
|
6825
7062
|
GEM_FORM_VAR_SCOPE,
|
|
6826
7063
|
GEM_GRID_COLUMNS_ROW_KNOB_CSS,
|
|
7064
|
+
GEM_HEADING_SIZE_CSS_VARS,
|
|
7065
|
+
GEM_LINE_HEIGHT_TOKEN_KEYS,
|
|
6827
7066
|
GEM_PART_CLASS,
|
|
6828
7067
|
GEM_POPUP_SHELL_CSS,
|
|
6829
7068
|
GEM_REVEAL_CSS,
|
|
@@ -6855,6 +7094,7 @@ export {
|
|
|
6855
7094
|
createThemeProfileCss,
|
|
6856
7095
|
defaultGemTokens,
|
|
6857
7096
|
defaultSelection,
|
|
7097
|
+
gemButtonCaseCss,
|
|
6858
7098
|
gemFormAttributes,
|
|
6859
7099
|
gemFormCssBlock,
|
|
6860
7100
|
gemFormCssVars,
|
|
@@ -6863,7 +7103,9 @@ export {
|
|
|
6863
7103
|
gemFrameworkRuntimePlugins,
|
|
6864
7104
|
gemFrameworkValidationRules,
|
|
6865
7105
|
gemFrameworkVersion,
|
|
7106
|
+
gemMobileTokenCssVars,
|
|
6866
7107
|
gemPartClass,
|
|
7108
|
+
gemPhoneTokenTarget,
|
|
6867
7109
|
gemPlanShapeKnobs,
|
|
6868
7110
|
gemRuntimePluginModuleKeys,
|
|
6869
7111
|
gemRuntimePluginSources,
|
|
@@ -6874,6 +7116,7 @@ export {
|
|
|
6874
7116
|
motionCssVarAliases,
|
|
6875
7117
|
normalizeGemTokens,
|
|
6876
7118
|
offerEngineRuntimeScript,
|
|
7119
|
+
optionalGemTokenKeys,
|
|
6877
7120
|
planToRuntimeBootstrapConfig,
|
|
6878
7121
|
renderGemFrameworkManifest,
|
|
6879
7122
|
renderGemPreviewDocument,
|
|
@@ -42,7 +42,11 @@ function install({ document, window, target, mark, canListen, listen: __gemListe
|
|
|
42
42
|
else if (k === 'Home') { if (e.preventDefault) e.preventDefault(); focusDot(0); }
|
|
43
43
|
else if (k === 'End') { if (e.preventDefault) e.preventDefault(); focusDot(dots.length - 1); }
|
|
44
44
|
}); });
|
|
45
|
-
|
|
45
|
+
// The visible label follows the state like the section's own behavior did: the button's own text while playing
|
|
46
|
+
// (kept, so a translated label survives), data-gem-hero-play-label (or 'Play') while paused. Only a text-only button.
|
|
47
|
+
const pauseText = pause && pause.childElementCount === 0 ? pause.textContent : null;
|
|
48
|
+
const playText = (pause && pause.getAttribute && pause.getAttribute('data-gem-hero-play-label')) || 'Play';
|
|
49
|
+
if (canListen(pause)) __gemListen(pause, 'click', () => { paused = !paused; if (pause.setAttribute) pause.setAttribute('aria-pressed', String(paused)); if (pauseText !== null) pause.textContent = paused ? playText : pauseText; start(); });
|
|
46
50
|
show(active); start(); bound += 1;
|
|
47
51
|
});
|
|
48
52
|
api.emit('gem:hero-slideshow:ready', { source: 'gem-runtime', count: bound });
|
|
@@ -10,7 +10,32 @@ function install({ document, window, target, mark, canListen, listen: __gemListe
|
|
|
10
10
|
const galleries = document.querySelectorAll('[data-gem-media-gallery], .gem-media-gallery, .gem-product-media-gallery');
|
|
11
11
|
let bound = 0;
|
|
12
12
|
galleries.forEach((gallery) => {
|
|
13
|
+
const hooked = !!(gallery.hasAttribute && gallery.hasAttribute('data-gem-media-gallery'));
|
|
14
|
+
if (!hooked && gallery.querySelector && gallery.querySelector('[data-gem-media-gallery]')) return;
|
|
13
15
|
if (!mark(gallery, 'media-gallery')) return;
|
|
16
|
+
const main = gallery.querySelector && gallery.querySelector('[data-gem-media-main]');
|
|
17
|
+
const thumbs = main && gallery.querySelectorAll ? Array.prototype.slice.call(gallery.querySelectorAll('[data-gem-media-item]')) : [];
|
|
18
|
+
if (main && thumbs.length) {
|
|
19
|
+
gallery.dataset.gemMediaState = 'ready';
|
|
20
|
+
const choose = (item, reason) => {
|
|
21
|
+
const src = item.getAttribute('data-gem-media-src');
|
|
22
|
+
const alt = item.getAttribute('data-gem-media-alt');
|
|
23
|
+
if (src) { main.setAttribute('src', src); main.removeAttribute('srcset'); if (alt != null) main.setAttribute('alt', alt); }
|
|
24
|
+
const index = Number(item.getAttribute('data-gem-media-index') || thumbs.indexOf(item));
|
|
25
|
+
thumbs.forEach((thumb) => { const on = thumb === item; thumb.setAttribute('aria-current', String(on)); if (thumb.dataset) thumb.dataset.gemMediaActive = on ? 'true' : 'false'; });
|
|
26
|
+
gallery.dataset.gemMediaActiveIndex = String(index);
|
|
27
|
+
if (gallery.dispatchEvent && typeof window.CustomEvent === 'function') gallery.dispatchEvent(new window.CustomEvent('gem:media:select', { bubbles: true, detail: { index } }));
|
|
28
|
+
api.emit('gem:media:change', { source: 'gem-runtime', index, galleryId: gallery.id || undefined, reason });
|
|
29
|
+
};
|
|
30
|
+
if (canListen(gallery)) __gemListen(gallery, 'click', (event) => {
|
|
31
|
+
const item = event.target && event.target.closest ? event.target.closest('[data-gem-media-item]') : null;
|
|
32
|
+
if (!item || !gallery.contains(item)) return;
|
|
33
|
+
if (event.preventDefault) event.preventDefault();
|
|
34
|
+
choose(item, 'click');
|
|
35
|
+
});
|
|
36
|
+
bound += 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
14
39
|
const items = gallery.querySelectorAll('[data-gem-media-item], img, video, model-viewer');
|
|
15
40
|
const thumbControls = gallery.querySelectorAll('[data-gem-media-thumb]');
|
|
16
41
|
const controls = thumbControls.length ? thumbControls : items;
|