@gemboss/gem-framework 0.6.0 → 0.9.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 +63 -1
- package/dist/SOURCE.json +2 -2
- package/dist/gem-framework.css +206 -147
- package/dist/gem-runtime.js +93 -15
- package/dist/index.js +629 -178
- package/dist/plugins/before_after.d.ts +9 -0
- package/dist/plugins/before_after.js +37 -0
- package/dist/plugins/carousel.d.ts +9 -0
- package/dist/plugins/carousel.js +49 -0
- package/dist/plugins/cart-drawer.d.ts +9 -0
- package/dist/plugins/cart-drawer.js +69 -0
- package/dist/plugins/context-adapter.d.ts +9 -0
- package/dist/plugins/context-adapter.js +17 -0
- package/dist/plugins/countdown.d.ts +9 -0
- package/dist/plugins/countdown.js +164 -0
- package/dist/plugins/disclosure.d.ts +9 -0
- package/dist/plugins/disclosure.js +40 -0
- package/dist/plugins/hero-slideshow.d.ts +9 -0
- package/dist/plugins/hero-slideshow.js +57 -0
- package/dist/plugins/localization.d.ts +9 -0
- package/dist/plugins/localization.js +37 -0
- package/dist/plugins/media-gallery.d.ts +9 -0
- package/dist/plugins/media-gallery.js +57 -0
- package/dist/plugins/mobile-menu.d.ts +9 -0
- package/dist/plugins/mobile-menu.js +51 -0
- package/dist/plugins/offer-engine.d.ts +9 -0
- package/dist/plugins/offer-engine.js +57 -0
- package/dist/plugins/predictive-search.d.ts +9 -0
- package/dist/plugins/predictive-search.js +77 -0
- package/dist/plugins/promo-popup.d.ts +9 -0
- package/dist/plugins/promo-popup.js +117 -0
- package/dist/plugins/quantity-input.d.ts +9 -0
- package/dist/plugins/quantity-input.js +44 -0
- package/dist/plugins/quick-view.d.ts +9 -0
- package/dist/plugins/quick-view.js +69 -0
- package/dist/plugins/recently-viewed.d.ts +9 -0
- package/dist/plugins/recently-viewed.js +71 -0
- package/dist/plugins/reveal-on-scroll.d.ts +9 -0
- package/dist/plugins/reveal-on-scroll.js +37 -0
- package/dist/plugins/runtime-slot-loader.d.ts +9 -0
- package/dist/plugins/runtime-slot-loader.js +29 -0
- package/dist/plugins/seasonal-effects.d.ts +9 -0
- package/dist/plugins/seasonal-effects.js +17 -0
- package/dist/plugins/sticky-atc.d.ts +9 -0
- package/dist/plugins/sticky-atc.js +43 -0
- package/dist/plugins/tabs.d.ts +9 -0
- package/dist/plugins/tabs.js +50 -0
- package/dist/plugins/variant-selects.d.ts +9 -0
- package/dist/plugins/variant-selects.js +70 -0
- package/dist/popup-shell.css +91 -0
- package/dist/primitives.css +1430 -0
- package/dist/runtime-core.d.ts +51 -0
- package/dist/runtime-core.js +176 -0
- package/dist/section-gate.js +315 -86
- package/dist/sections.css +1116 -0
- package/dist/tokens.css +67 -0
- package/package.json +21 -1
package/dist/index.js
CHANGED
|
@@ -19,6 +19,118 @@ function gemPartClass(part) {
|
|
|
19
19
|
return GEM_PART_CLASS[part] ?? null;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// ../../packages/gem-framework/src/form-set.ts
|
|
23
|
+
var GEM_FORM_TOKEN_KEYS = [
|
|
24
|
+
"radius",
|
|
25
|
+
"borderWidth",
|
|
26
|
+
"borderColor",
|
|
27
|
+
"fill",
|
|
28
|
+
"height",
|
|
29
|
+
"paddingInline",
|
|
30
|
+
"fontSize",
|
|
31
|
+
"focusRing",
|
|
32
|
+
"checkboxAccent",
|
|
33
|
+
"checkboxSize",
|
|
34
|
+
"shape",
|
|
35
|
+
"join"
|
|
36
|
+
];
|
|
37
|
+
var FORM_CSS_VAR = {
|
|
38
|
+
radius: "--gem-input-radius",
|
|
39
|
+
borderWidth: "--gem-input-border-width",
|
|
40
|
+
borderColor: "--gem-input-border-color",
|
|
41
|
+
fill: "--gem-input-bg",
|
|
42
|
+
height: "--gem-input-height",
|
|
43
|
+
paddingInline: "--gem-input-padding-inline",
|
|
44
|
+
fontSize: "--gem-input-font-size",
|
|
45
|
+
focusRing: "--gem-input-focus-ring",
|
|
46
|
+
checkboxAccent: "--gem-checkbox-accent",
|
|
47
|
+
checkboxSize: "--gem-checkbox-size"
|
|
48
|
+
};
|
|
49
|
+
var GEM_FORM_CSS_VARIABLES = Object.values(FORM_CSS_VAR);
|
|
50
|
+
var GEM_FIELD_SHAPES = ["outline", "filled", "underline"];
|
|
51
|
+
var GEM_FIELD_JOINS = ["separate", "joined"];
|
|
52
|
+
var safe = (v) => {
|
|
53
|
+
if (typeof v !== "string" || v.trim() === "" || v.length > 200 || /[;{}<>\\"'`]|\/\*/.test(v)) return false;
|
|
54
|
+
const open = [];
|
|
55
|
+
for (const ch of v) {
|
|
56
|
+
if (ch === "(" || ch === "[") open.push(ch);
|
|
57
|
+
else if (ch === ")" || ch === "]") {
|
|
58
|
+
if (open.pop() !== (ch === ")" ? "(" : "[")) return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return open.length === 0;
|
|
62
|
+
};
|
|
63
|
+
var FIELD_SHAPE_SET = /* @__PURE__ */ new Set(["outline", "filled", "underline"]);
|
|
64
|
+
var FIELD_JOIN_SET = /* @__PURE__ */ new Set(["separate", "joined"]);
|
|
65
|
+
var hasForm = (form) => !!form && typeof form === "object" && Object.keys(form).some((k) => GEM_FORM_TOKEN_KEYS.includes(k));
|
|
66
|
+
var GEM_FORM_DEFAULTS = {
|
|
67
|
+
shape: "outline",
|
|
68
|
+
join: "separate",
|
|
69
|
+
radius: "4px",
|
|
70
|
+
borderWidth: "1px",
|
|
71
|
+
borderColor: "color-mix(in srgb, currentColor 70%, transparent)",
|
|
72
|
+
fill: "var(--gem-color-surface)",
|
|
73
|
+
height: "44px",
|
|
74
|
+
paddingInline: "12px",
|
|
75
|
+
fontSize: "16px",
|
|
76
|
+
checkboxAccent: "var(--gem-color-accent)",
|
|
77
|
+
checkboxSize: "24px"
|
|
78
|
+
};
|
|
79
|
+
var GEM_FORM_VAR_SCOPE = "[data-gem-form-set], [data-gem-form-set] :is(input, select, textarea, button, form)";
|
|
80
|
+
function gemFormCssBlock(form) {
|
|
81
|
+
const vars = gemFormCssVars(form);
|
|
82
|
+
return vars.length ? `${GEM_FORM_VAR_SCOPE} {
|
|
83
|
+
${vars.map((d) => ` ${d}`).join("\n")}
|
|
84
|
+
}` : "";
|
|
85
|
+
}
|
|
86
|
+
function gemFormCssVars(form) {
|
|
87
|
+
if (!hasForm(form)) return [];
|
|
88
|
+
const out = [];
|
|
89
|
+
for (const [key, cssVar] of Object.entries(FORM_CSS_VAR)) {
|
|
90
|
+
const v = form[key];
|
|
91
|
+
const fallback = GEM_FORM_DEFAULTS[key];
|
|
92
|
+
if (safe(v)) out.push(`${cssVar}: ${v.trim()};`);
|
|
93
|
+
else if (fallback) out.push(`${cssVar}: ${fallback};`);
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
function gemFormAttributes(form) {
|
|
98
|
+
if (!hasForm(form)) return {};
|
|
99
|
+
const id = typeof form.id === "string" && /^[a-z0-9-]{1,40}$/.test(form.id) ? form.id : "custom";
|
|
100
|
+
return {
|
|
101
|
+
"data-gem-form-set": id,
|
|
102
|
+
"data-gem-field-shape": typeof form.shape === "string" && FIELD_SHAPE_SET.has(form.shape) ? form.shape : GEM_FORM_DEFAULTS.shape,
|
|
103
|
+
"data-gem-field-join": typeof form.join === "string" && FIELD_JOIN_SET.has(form.join) ? form.join : GEM_FORM_DEFAULTS.join
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
var GEM_FORM_SETS = [
|
|
107
|
+
{
|
|
108
|
+
id: "crisp-outline",
|
|
109
|
+
label: "Crisp outline",
|
|
110
|
+
postures: ["clinical", "technical", "utility"],
|
|
111
|
+
form: { id: "crisp-outline", shape: "outline", join: "separate", radius: "4px", borderWidth: "1px", borderColor: "color-mix(in srgb, currentColor 70%, transparent)", fill: "var(--gem-color-surface)", height: "44px", paddingInline: "12px", fontSize: "16px", checkboxSize: "24px" }
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "hairline-underline",
|
|
115
|
+
label: "Hairline underline",
|
|
116
|
+
postures: ["premium", "editorial"],
|
|
117
|
+
form: { id: "hairline-underline", shape: "underline", join: "separate", radius: "0px", borderWidth: "1px", borderColor: "color-mix(in srgb, currentColor 70%, transparent)", fill: "transparent", height: "48px", paddingInline: "2px", fontSize: "16px", checkboxSize: "24px" }
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "soft-filled",
|
|
121
|
+
label: "Soft filled",
|
|
122
|
+
postures: ["natural"],
|
|
123
|
+
form: { id: "soft-filled", shape: "filled", join: "separate", radius: "10px", borderWidth: "2px", borderColor: "color-mix(in srgb, currentColor 55%, transparent)", fill: "color-mix(in srgb, var(--gem-color-text) 6%, var(--gem-color-surface))", height: "48px", paddingInline: "14px", fontSize: "16px", checkboxSize: "24px" }
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "pill-joined",
|
|
127
|
+
label: "Pill joined",
|
|
128
|
+
postures: ["playful"],
|
|
129
|
+
form: { id: "pill-joined", shape: "outline", join: "joined", radius: "999px", borderWidth: "2px", borderColor: "currentColor", fill: "var(--gem-color-surface)", height: "48px", paddingInline: "20px", fontSize: "16px", checkboxSize: "24px" }
|
|
130
|
+
}
|
|
131
|
+
];
|
|
132
|
+
var gemFormSetsFor = (posture) => GEM_FORM_SETS.filter((s) => s.postures.includes(posture));
|
|
133
|
+
|
|
22
134
|
// ../../packages/gem-framework/src/plan-shape.ts
|
|
23
135
|
function gemStyleSpaceFor(plan) {
|
|
24
136
|
switch (plan.design_system?.spacing_density) {
|
|
@@ -130,7 +242,10 @@ var defaultGemTokens = {
|
|
|
130
242
|
background: "#f6f6f3",
|
|
131
243
|
surface: "#ffffff",
|
|
132
244
|
text: "#202223",
|
|
133
|
-
|
|
245
|
+
// AA on THIS background (#f6f6f3), not only on white: #6b7280 measures 4.47:1 there, under 4.5, and muted styles
|
|
246
|
+
// every secondary line. #656c79 measures 4.88:1 (5.28 on white). Upstreamed from gemsite app/globals.css (#212,
|
|
247
|
+
// found by axe) in gem-framework 0.8.0.
|
|
248
|
+
muted: "#656c79",
|
|
134
249
|
accent: "#008060",
|
|
135
250
|
accentText: "#ffffff",
|
|
136
251
|
border: "#d2d5d8",
|
|
@@ -270,12 +385,14 @@ function createGemTokenCss(tokens, selector = ":root") {
|
|
|
270
385
|
GEM_TAP_MIN_CSS_VAR,
|
|
271
386
|
GEM_FOCUS_OFFSET_CSS_VAR
|
|
272
387
|
];
|
|
388
|
+
const formBlock = gemFormCssBlock(tokens.form);
|
|
273
389
|
const baseMobile = typeRampValue(tokens.typography, "baseMobile");
|
|
274
390
|
return [
|
|
275
391
|
`${selector} {`,
|
|
276
392
|
...declarations.map((line) => ` ${line}`),
|
|
277
393
|
"}",
|
|
278
|
-
...baseMobile ? [`@media (max-width: ${GEM_TYPE_MOBILE_MAX}) {`, ` ${selector} {`, ` --gem-type-base: ${baseMobile};`, " }", "}"] : []
|
|
394
|
+
...baseMobile ? [`@media (max-width: ${GEM_TYPE_MOBILE_MAX}) {`, ` ${selector} {`, ` --gem-type-base: ${baseMobile};`, " }", "}"] : [],
|
|
395
|
+
...formBlock ? [formBlock] : []
|
|
279
396
|
].join("\n");
|
|
280
397
|
}
|
|
281
398
|
var GEM_TYPE_RAMP_KEYS = ["base", "scale", "baseMobile"];
|
|
@@ -496,15 +613,6 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
496
613
|
"gem-delivery-date",
|
|
497
614
|
"gem-stock-indicator",
|
|
498
615
|
"gem-popup",
|
|
499
|
-
// Popup shell (primitives.ts GEM_POPUP_SHELL_CSS): the dialog frame popup sections draw their content in.
|
|
500
|
-
"gem-popup-shell",
|
|
501
|
-
"gem-popup-shell__dialog",
|
|
502
|
-
"gem-popup-shell__frame",
|
|
503
|
-
"gem-popup-shell__media",
|
|
504
|
-
"gem-popup-shell__image",
|
|
505
|
-
"gem-popup-shell__panel",
|
|
506
|
-
"gem-popup-shell__close",
|
|
507
|
-
"gem-popup-shell__teaser",
|
|
508
616
|
"gem-back-to-top",
|
|
509
617
|
// measured commerce atoms mapped by parts.ts (GEM_PART_CLASS) alongside gem-price/gem-badge.
|
|
510
618
|
"gem-discount",
|
|
@@ -531,6 +639,22 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
531
639
|
"gem-cart-line-item__price"
|
|
532
640
|
]
|
|
533
641
|
},
|
|
642
|
+
{
|
|
643
|
+
// The popup shell: the dialog frame popup sections draw their content in. Styled by popup-shell.ts
|
|
644
|
+
// (GEM_POPUP_SHELL_CSS), which ships as its own popup-shell.css since 0.8.3, not in primitives.css: a page that
|
|
645
|
+
// draws one of these loads that file (gem-framework.css, the whole file, carries it too).
|
|
646
|
+
group: "popup-shell",
|
|
647
|
+
classes: [
|
|
648
|
+
"gem-popup-shell",
|
|
649
|
+
"gem-popup-shell__dialog",
|
|
650
|
+
"gem-popup-shell__frame",
|
|
651
|
+
"gem-popup-shell__media",
|
|
652
|
+
"gem-popup-shell__image",
|
|
653
|
+
"gem-popup-shell__panel",
|
|
654
|
+
"gem-popup-shell__close",
|
|
655
|
+
"gem-popup-shell__teaser"
|
|
656
|
+
]
|
|
657
|
+
},
|
|
534
658
|
{
|
|
535
659
|
group: "sections",
|
|
536
660
|
classes: [
|
|
@@ -772,58 +896,7 @@ function createGemFrameworkManifest() {
|
|
|
772
896
|
};
|
|
773
897
|
}
|
|
774
898
|
|
|
775
|
-
// ../../packages/gem-framework/src/
|
|
776
|
-
var dividerMask = (path) => `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 10' preserveAspectRatio='none'%3E%3Cpath fill='%23fff' d='${path}'/%3E%3C/svg%3E")`;
|
|
777
|
-
var spikePath = (() => {
|
|
778
|
-
const teeth = [];
|
|
779
|
-
for (let x = 95; x > 0; x -= 5) teeth.push(`L${x} ${teeth.length % 2 === 0 ? 10 : 3}`);
|
|
780
|
-
return `M0 0 H100 V3 ${teeth.join(" ")} L0 ${teeth.length % 2 === 0 ? 10 : 3} Z`;
|
|
781
|
-
})();
|
|
782
|
-
var DIVIDER_SHAPES = {
|
|
783
|
-
wave: "M0 0 H100 V3 C80 9 70 -1 50 4 C30 9 20 1 0 4 Z",
|
|
784
|
-
slope: "M0 0 H100 V10 Z",
|
|
785
|
-
spikes: spikePath,
|
|
786
|
-
arch: "M0 0 H100 V2 Q50 12 0 2 Z",
|
|
787
|
-
blob: "M0 0 H100 V4 C85 11 72 0 58 5 C44 10 30 2 16 6 C10 8 5 5 0 6 Z"
|
|
788
|
-
};
|
|
789
|
-
var GEM_DIVIDER_CSS_LINES = [
|
|
790
|
-
// The clip is WIDENED, not removed. `.gem-section` sets `overflow: clip` and the wedge is drawn
|
|
791
|
-
// at `top: 100%` / `bottom: 100%` — entirely outside the box — so the parent deleted every
|
|
792
|
-
// divider on every track. Measured on the live storefront 2026-08-25: a band with the documented
|
|
793
|
-
// `data-gem-section-background="dark"` + `data-gem-divider-bottom="wave"` rendered a FLAT edge,
|
|
794
|
-
// while the attribute, the rule and the fallback colour were all present and correct.
|
|
795
|
-
// NOT `overflow: visible`: `.gem-hero` uses the clip to hold its own border-radius, so releasing
|
|
796
|
-
// it entirely would paint square corners over that radius. A browser without
|
|
797
|
-
// `overflow-clip-margin` keeps plain `clip` — the divider stays hidden, which is today's
|
|
798
|
-
// behaviour rather than a broken layout.
|
|
799
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-top], :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-bottom] { position: relative; overflow: clip; overflow-clip-margin: var(--gem-divider-height, 48px); }",
|
|
800
|
-
// Shared geometry. `top: 100%` / `bottom: 100%` put it outside the box; z-index 1 keeps it above the
|
|
801
|
-
// neighbouring band's background. pointer-events: none so it never eats a click near the seam.
|
|
802
|
-
// Qualified by SHAPE. Unqualified, any value paints a solid 48px slab — including the literal
|
|
803
|
-
// "none", which the attribute writer forwards unchanged. The clip used to hide that; widening
|
|
804
|
-
// it above makes a stray value visible.
|
|
805
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-bottom='wave']::after, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-bottom='slope']::after, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-bottom='spikes']::after, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-bottom='arch']::after, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-bottom='blob']::after, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-top='wave']::before, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-top='slope']::before, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-top='spikes']::before, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-top='arch']::before, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-top='blob']::before { content: ''; position: absolute; left: 0; right: 0; z-index: 1; pointer-events: none; height: var(--gem-divider-height, 48px); background: var(--gem-section-bg, var(--gem-color-background)); -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; -webkit-mask-size: 100% 100%; mask-size: 100% 100%; }",
|
|
806
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-bottom]::after { top: 100%; }",
|
|
807
|
-
// The top edge reuses the same masks, mirrored, so five shapes cover both edges.
|
|
808
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-top]::before { bottom: 100%; transform: scaleY(-1); }",
|
|
809
|
-
...Object.entries(DIVIDER_SHAPES).flatMap(([name, path]) => {
|
|
810
|
-
const mask = dividerMask(path);
|
|
811
|
-
return [
|
|
812
|
-
`:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-bottom='${name}']::after { -webkit-mask-image: ${mask}; mask-image: ${mask}; }`,
|
|
813
|
-
`:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-top='${name}']::before { -webkit-mask-image: ${mask}; mask-image: ${mask}; }`
|
|
814
|
-
];
|
|
815
|
-
})
|
|
816
|
-
];
|
|
817
|
-
var GEM_DIVIDER_CSS = GEM_DIVIDER_CSS_LINES.join("\n");
|
|
818
|
-
var GEM_CARD_CSS_LINES = [
|
|
819
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-card] :is(.gem-section__inner, [class*='__inner']) { border-radius: var(--gem-radius-md); padding: var(--gem-space-xl); }",
|
|
820
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-card='outline'] :is(.gem-section__inner, [class*='__inner']) { border: 1px solid var(--gem-color-border); }",
|
|
821
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-card='shadow'] :is(.gem-section__inner, [class*='__inner']) { background: var(--gem-color-surface); box-shadow: var(--gem-shadow-md); }",
|
|
822
|
-
// `filled` tints the section's own ground rather than reaching for a fixed colour, so it stays
|
|
823
|
-
// legible on an accent or dark band instead of punching a light panel into it.
|
|
824
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-card='filled'] :is(.gem-section__inner, [class*='__inner']) { background: color-mix(in srgb, currentColor 6%, transparent); }"
|
|
825
|
-
];
|
|
826
|
-
var GEM_CARD_CSS = GEM_CARD_CSS_LINES.join("\n");
|
|
899
|
+
// ../../packages/gem-framework/src/popup-shell.ts
|
|
827
900
|
var GEM_POPUP_SHELL_CSS_LINES = [
|
|
828
901
|
".gem-popup-shell { padding: 0; }",
|
|
829
902
|
".gem-popup-shell__dialog {",
|
|
@@ -843,8 +916,12 @@ var GEM_POPUP_SHELL_CSS_LINES = [
|
|
|
843
916
|
".gem-popup-shell[data-gem-popup-desktop='split-right'] .gem-popup-shell__media { order: 2; }",
|
|
844
917
|
".gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__frame,",
|
|
845
918
|
".gem-popup-shell[data-gem-popup-desktop='card'] .gem-popup-shell__frame { grid-template-columns: 1fr; }",
|
|
846
|
-
|
|
847
|
-
|
|
919
|
+
// Thẻ hẹp chỉ là bố cục DESKTOP. Viết ngoài media query thì nó (độ ưu tiên 0,3,0) thắng rule takeover 100vw của mobile
|
|
920
|
+
// (0,1,0): đo 04/10/2026, popup chọn desktop "card" hay không ảnh chỉ rộng 92vw trên điện thoại, không phủ kín.
|
|
921
|
+
"@media (min-width: 750px) {",
|
|
922
|
+
" .gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__dialog,",
|
|
923
|
+
" .gem-popup-shell[data-gem-popup-desktop='card'] .gem-popup-shell__dialog { width: min(480px, 92vw); }",
|
|
924
|
+
"}",
|
|
848
925
|
".gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__media { display: none; }",
|
|
849
926
|
".gem-popup-shell[data-gem-popup-desktop='card'] .gem-popup-shell__media { max-height: 200px; }",
|
|
850
927
|
".gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__frame { grid-template-columns: 1fr; place-items: center; }",
|
|
@@ -883,6 +960,12 @@ var GEM_POPUP_SHELL_CSS_LINES = [
|
|
|
883
960
|
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-media='background'] .gem-popup-shell__media { position: absolute; inset: 0; }",
|
|
884
961
|
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-media='background'] .gem-popup-shell__panel { position: relative; z-index: 1; }",
|
|
885
962
|
" .gem-popup-shell[data-gem-popup-desktop='card-over-image'][data-gem-popup-media='background'] .gem-popup-shell__panel { color: var(--gem-color-text); }",
|
|
963
|
+
// Ở split-left / split-right / card ảnh nằm CẠNH chữ trên desktop, không nằm sau (chỉ mobile, fullscreen, card-over-image
|
|
964
|
+
// đặt ảnh ra sau): chữ sáng và lớp tối của ảnh nền ở đây là chữ trắng trên nền sáng. Quét tương phản 629 bản ghép
|
|
965
|
+
// 04/10/2026: 1,04:1 ở mọi bản ghép như vậy.
|
|
966
|
+
" .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__panel,",
|
|
967
|
+
" .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); }",
|
|
968
|
+
" .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; }",
|
|
886
969
|
"}",
|
|
887
970
|
"@media (max-width: 749px) {",
|
|
888
971
|
// Mobile: takeover is the whole screen; a sheet rises from the bottom to at most 88% of it. The image
|
|
@@ -894,7 +977,9 @@ var GEM_POPUP_SHELL_CSS_LINES = [
|
|
|
894
977
|
" .gem-popup-shell__panel { justify-content: flex-start; padding: var(--gem-space-3xl) var(--gem-space-lg) var(--gem-space-xl); }",
|
|
895
978
|
" .gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__panel,",
|
|
896
979
|
" .gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__panel { justify-content: center; }",
|
|
897
|
-
|
|
980
|
+
// Mobile không có thẻ nổi: panel thôi nền riêng. Giữ nền thẻ (--gem-color-background) mà ảnh làm nền thì chữ sáng của
|
|
981
|
+
// ảnh nền nằm trên nền nhạt của thẻ: đo 04/10/2026, preset soft-pastel ra chữ trắng trên hồng nhạt, không đọc được.
|
|
982
|
+
" .gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__panel { width: 100%; margin: 0; border-radius: 0; box-shadow: none; background: transparent; }",
|
|
898
983
|
" .gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__media { position: relative; }",
|
|
899
984
|
" .gem-popup-shell__media { order: 2; max-height: 34dvh; }",
|
|
900
985
|
" .gem-popup-shell[data-gem-popup-media='top'] .gem-popup-shell__media { order: 0; }",
|
|
@@ -903,20 +988,160 @@ var GEM_POPUP_SHELL_CSS_LINES = [
|
|
|
903
988
|
" .gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__frame { grid-template-rows: 1fr; }",
|
|
904
989
|
" .gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__media { position: absolute; inset: 0; max-height: none; }",
|
|
905
990
|
"}",
|
|
906
|
-
|
|
907
|
-
|
|
991
|
+
// Bản xem trước (thẻ thư viện, trang ghép, lab). Ngoài canvas editor nó PHỦ CẢ KHUNG NHÌN như popup thật mở bằng
|
|
992
|
+
// showModal (top layer, che cả header và thanh thông báo): Chris 04/10/2026 thấy popup của trang ghép nằm dưới header,
|
|
993
|
+
// trong luồng trang, không giống store. Trong canvas editor nó vẫn nằm trong luồng trang trên một nền mờ giả, để chọn
|
|
994
|
+
// và sửa được như mọi band khác.
|
|
995
|
+
// Dialog định vị ĐÚNG như modal của trình duyệt (position: fixed; inset: 0; margin: auto), nên mọi bố cục (fullscreen,
|
|
996
|
+
// thẻ, takeover, sheet kéo tới 88dvh) ra cùng khung chữ nhật với popup mở thật; nền chỉ thay ::backdrop (không có ở
|
|
997
|
+
// dialog không-modal).
|
|
998
|
+
":root:not(:has([data-gem-canvas])) .gem-popup-shell[data-gem-popup-preview='true'] { position: fixed; inset: 0; z-index: 1000; padding: 0; background: rgb(0 0 0 / 0.55); }",
|
|
999
|
+
":root:not(:has([data-gem-canvas])) .gem-popup-shell[data-gem-popup-preview='true'] .gem-popup-shell__dialog { position: fixed; inset: 0; display: block; }",
|
|
1000
|
+
":root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'] .gem-popup-shell__dialog { position: relative; display: block; inset: auto; }",
|
|
1001
|
+
":root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'] { padding: var(--gem-space-xl) var(--gem-layout-gutter); background: rgb(0 0 0 / 0.55); }",
|
|
908
1002
|
"@media (min-width: 750px) {",
|
|
909
|
-
" .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] { padding: 0; }",
|
|
910
|
-
" .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] .gem-popup-shell__dialog { height: 720px; max-height: none; }",
|
|
1003
|
+
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] { padding: 0; }",
|
|
1004
|
+
" :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; }",
|
|
911
1005
|
"}",
|
|
912
1006
|
"@media (max-width: 749px) {",
|
|
913
|
-
" .gem-popup-shell[data-gem-popup-preview='true'] { padding: 0; }",
|
|
914
|
-
" .gem-popup-shell[data-gem-popup-preview='true'] .gem-popup-shell__dialog { height: 844px; max-height: none; width: 100%; max-width: 100%; }",
|
|
915
|
-
" .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-mobile='sheet'] { padding-top: 160px; }",
|
|
916
|
-
" .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-mobile='sheet'] .gem-popup-shell__dialog { min-height: 0; }",
|
|
1007
|
+
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'] { padding: 0; }",
|
|
1008
|
+
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'] .gem-popup-shell__dialog { height: 844px; max-height: none; width: 100%; max-width: 100%; }",
|
|
1009
|
+
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-mobile='sheet'] { padding-top: 160px; }",
|
|
1010
|
+
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-mobile='sheet'] .gem-popup-shell__dialog { min-height: 0; }",
|
|
917
1011
|
"}"
|
|
918
1012
|
];
|
|
919
1013
|
var GEM_POPUP_SHELL_CSS = GEM_POPUP_SHELL_CSS_LINES.join("\n");
|
|
1014
|
+
|
|
1015
|
+
// ../../packages/gem-framework/src/primitives.ts
|
|
1016
|
+
var dividerMask = (path) => `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 10' preserveAspectRatio='none'%3E%3Cpath fill='%23fff' d='${path}'/%3E%3C/svg%3E")`;
|
|
1017
|
+
var spikePath = (() => {
|
|
1018
|
+
const teeth = [];
|
|
1019
|
+
for (let x = 95; x > 0; x -= 5) teeth.push(`L${x} ${teeth.length % 2 === 0 ? 10 : 3}`);
|
|
1020
|
+
return `M0 0 H100 V3 ${teeth.join(" ")} L0 ${teeth.length % 2 === 0 ? 10 : 3} Z`;
|
|
1021
|
+
})();
|
|
1022
|
+
var DIVIDER_SHAPES = {
|
|
1023
|
+
wave: "M0 0 H100 V3 C80 9 70 -1 50 4 C30 9 20 1 0 4 Z",
|
|
1024
|
+
slope: "M0 0 H100 V10 Z",
|
|
1025
|
+
spikes: spikePath,
|
|
1026
|
+
arch: "M0 0 H100 V2 Q50 12 0 2 Z",
|
|
1027
|
+
blob: "M0 0 H100 V4 C85 11 72 0 58 5 C44 10 30 2 16 6 C10 8 5 5 0 6 Z"
|
|
1028
|
+
};
|
|
1029
|
+
var GEM_DIVIDER_CSS_LINES = [
|
|
1030
|
+
// The clip is WIDENED, not removed. `.gem-section` sets `overflow: clip` and the wedge is drawn
|
|
1031
|
+
// at `top: 100%` / `bottom: 100%` — entirely outside the box — so the parent deleted every
|
|
1032
|
+
// divider on every track. Measured on the live storefront 2026-08-25: a band with the documented
|
|
1033
|
+
// `data-gem-section-background="dark"` + `data-gem-divider-bottom="wave"` rendered a FLAT edge,
|
|
1034
|
+
// while the attribute, the rule and the fallback colour were all present and correct.
|
|
1035
|
+
// NOT `overflow: visible`: `.gem-hero` uses the clip to hold its own border-radius, so releasing
|
|
1036
|
+
// it entirely would paint square corners over that radius. A browser without
|
|
1037
|
+
// `overflow-clip-margin` keeps plain `clip` — the divider stays hidden, which is today's
|
|
1038
|
+
// behaviour rather than a broken layout.
|
|
1039
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-top], :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom] { position: relative; overflow: clip; overflow-clip-margin: var(--gem-divider-height, 48px); }",
|
|
1040
|
+
// Shared geometry. `top: 100%` / `bottom: 100%` put it outside the box; z-index 1 keeps it above the
|
|
1041
|
+
// neighbouring band's background. pointer-events: none so it never eats a click near the seam.
|
|
1042
|
+
// Qualified by SHAPE. Unqualified, any value paints a solid 48px slab — including the literal
|
|
1043
|
+
// "none", which the attribute writer forwards unchanged. The clip used to hide that; widening
|
|
1044
|
+
// it above makes a stray value visible.
|
|
1045
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom='wave']::after, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom='slope']::after, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom='spikes']::after, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom='arch']::after, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom='blob']::after, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-top='wave']::before, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-top='slope']::before, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-top='spikes']::before, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-top='arch']::before, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-top='blob']::before { content: ''; position: absolute; left: 0; right: 0; z-index: 1; pointer-events: none; height: var(--gem-divider-height, 48px); background: var(--gem-section-bg, var(--gem-color-background)); -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; -webkit-mask-size: 100% 100%; mask-size: 100% 100%; }",
|
|
1046
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom]::after { top: 100%; }",
|
|
1047
|
+
// The top edge reuses the same masks, mirrored, so five shapes cover both edges.
|
|
1048
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-top]::before { bottom: 100%; transform: scaleY(-1); }",
|
|
1049
|
+
...Object.entries(DIVIDER_SHAPES).flatMap(([name, path]) => {
|
|
1050
|
+
const mask = dividerMask(path);
|
|
1051
|
+
return [
|
|
1052
|
+
`:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom='${name}']::after { -webkit-mask-image: ${mask}; mask-image: ${mask}; }`,
|
|
1053
|
+
`:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-top='${name}']::before { -webkit-mask-image: ${mask}; mask-image: ${mask}; }`
|
|
1054
|
+
];
|
|
1055
|
+
})
|
|
1056
|
+
];
|
|
1057
|
+
var GEM_DIVIDER_CSS = GEM_DIVIDER_CSS_LINES.join("\n");
|
|
1058
|
+
var GEM_CARD_CSS_LINES = [
|
|
1059
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-card] :is(.gem-section__inner, [class*='__inner']) { border-radius: var(--gem-radius-md); padding: var(--gem-space-xl); }",
|
|
1060
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-card='outline'] :is(.gem-section__inner, [class*='__inner']) { border: 1px solid var(--gem-color-border); }",
|
|
1061
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-card='shadow'] :is(.gem-section__inner, [class*='__inner']) { background: var(--gem-color-surface); box-shadow: var(--gem-shadow-md); }",
|
|
1062
|
+
// `filled` tints the section's own ground rather than reaching for a fixed colour, so it stays
|
|
1063
|
+
// legible on an accent or dark band instead of punching a light panel into it.
|
|
1064
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-card='filled'] :is(.gem-section__inner, [class*='__inner']) { background: color-mix(in srgb, currentColor 6%, transparent); }"
|
|
1065
|
+
];
|
|
1066
|
+
var GEM_CARD_CSS = GEM_CARD_CSS_LINES.join("\n");
|
|
1067
|
+
var GEM_FORM_SET_CSS_LINES = [
|
|
1068
|
+
// FORM SET (form-set.ts). Every rule is scoped under [data-gem-form-set], which <body> carries only when the plan
|
|
1069
|
+
// sets `tokens.form`: a store without one renders as before. The variables are always COMPLETE (missing keys take
|
|
1070
|
+
// GEM_FORM_DEFAULTS) and are declared on the controls themselves (GEM_FORM_VAR_SCOPE), so these rules never fall back
|
|
1071
|
+
// to a value nobody chose and a band that redefines its palette recolours its fields.
|
|
1072
|
+
// THE FIELD LIST names the dawn-parity footer field (0,2,0, later in the sheet) so :is() — which takes its most
|
|
1073
|
+
// specific argument — outranks it: before, that field ignored every form set (review of #665). The main search is
|
|
1074
|
+
// LEFT OUT (`:where(:not(…))`, no specificity): it is a field and a button drawn as one block whose button belongs to
|
|
1075
|
+
// the profile, and restyling only the field half left it open on one side.
|
|
1076
|
+
// .gem-field-join is joined by construction (the seller chose the join element), so `data-gem-field-join` does not
|
|
1077
|
+
// open it; gemsite's own field + button pairs read that attribute.
|
|
1078
|
+
"[data-gem-form-set] :is(.gem-input, .gem-select, .gem-textarea, .gem-shell-footer__newsletter .gem-input):where(:not(.gem-search-form .gem-input)) {",
|
|
1079
|
+
" min-height: var(--gem-input-height);",
|
|
1080
|
+
" height: auto;",
|
|
1081
|
+
" border: var(--gem-input-border-width) solid var(--gem-input-border-color);",
|
|
1082
|
+
" border-radius: var(--gem-input-radius);",
|
|
1083
|
+
" background: var(--gem-input-bg);",
|
|
1084
|
+
" padding-inline: var(--gem-input-padding-inline);",
|
|
1085
|
+
" font-size: var(--gem-input-font-size);",
|
|
1086
|
+
"}",
|
|
1087
|
+
// A textarea is two rows tall and never a pill: a 999px radius grows with the box as the shopper drags it. Keyed on
|
|
1088
|
+
// the ELEMENT, not .gem-textarea, so a copy whose list adds its own fields (gemsite's cart note) reaches them too.
|
|
1089
|
+
"[data-gem-form-set] textarea:is(.gem-input, .gem-select, .gem-textarea, .gem-shell-footer__newsletter .gem-input):where(:not(.gem-search-form .gem-input)) {",
|
|
1090
|
+
" min-height: calc(var(--gem-input-height) * 2);",
|
|
1091
|
+
" border-radius: min(var(--gem-input-radius), 12px);",
|
|
1092
|
+
" padding-block: var(--gem-space-sm);",
|
|
1093
|
+
"}",
|
|
1094
|
+
"[data-gem-form-set] :is(.gem-input, .gem-select, .gem-textarea, .gem-shell-footer__newsletter .gem-input):where(:not(.gem-search-form .gem-input)):focus-visible {",
|
|
1095
|
+
" outline: var(--gem-input-focus-ring, var(--gem-focus-ring, 2px solid var(--gem-color-accent)));",
|
|
1096
|
+
" outline-offset: var(--gem-focus-offset, 2px);",
|
|
1097
|
+
"}",
|
|
1098
|
+
// underline: only the bottom rule, no box. filled: the tinted field keeps its top corners and a bottom rule.
|
|
1099
|
+
'[data-gem-form-set][data-gem-field-shape="underline"] :is(.gem-input, .gem-select, .gem-textarea, .gem-shell-footer__newsletter .gem-input):where(:not(.gem-search-form .gem-input)) {',
|
|
1100
|
+
" border-width: 0 0 var(--gem-input-border-width);",
|
|
1101
|
+
" border-radius: 0;",
|
|
1102
|
+
" background: transparent;",
|
|
1103
|
+
// Transparent, so it reads on whatever is behind it: the ink of the band it sits on, not the page's. A dark section
|
|
1104
|
+
// changes `color` and leaves --gem-color-text dark (review of #665: dark text on #202223, 1:1).
|
|
1105
|
+
" color: inherit;",
|
|
1106
|
+
"}",
|
|
1107
|
+
// On a band its placeholder must follow that ink too (the UA grey does not: 3.47:1 on a dark band, the footer field's
|
|
1108
|
+
// only visible label), and a select's options keep the system's own pair: the popup on Windows/Linux takes the
|
|
1109
|
+
// select's colours, so inherited light ink on a transparent field drew light options on a light list.
|
|
1110
|
+
'[data-gem-form-set][data-gem-field-shape="underline"] :is(.gem-input, .gem-select, .gem-textarea, .gem-shell-footer__newsletter .gem-input):where(:not(.gem-search-form .gem-input))::placeholder {',
|
|
1111
|
+
" color: color-mix(in srgb, currentColor 70%, transparent);",
|
|
1112
|
+
"}",
|
|
1113
|
+
'[data-gem-form-set][data-gem-field-shape="underline"] :is(.gem-input, .gem-select, .gem-textarea, .gem-shell-footer__newsletter .gem-input):where(:not(.gem-search-form .gem-input)) option {',
|
|
1114
|
+
" color: CanvasText;",
|
|
1115
|
+
" background: Canvas;",
|
|
1116
|
+
"}",
|
|
1117
|
+
'[data-gem-form-set][data-gem-field-shape="filled"] :is(.gem-input, .gem-select, .gem-textarea, .gem-shell-footer__newsletter .gem-input):where(:not(.gem-search-form .gem-input)) {',
|
|
1118
|
+
" border-width: 0 0 var(--gem-input-border-width);",
|
|
1119
|
+
" border-end-start-radius: 0;",
|
|
1120
|
+
" border-end-end-radius: 0;",
|
|
1121
|
+
"}",
|
|
1122
|
+
// The main search stays the profile's: the variables are declared on (and inherited by) every input, and the
|
|
1123
|
+
// profile's base .gem-input rule reads two of them as hooks, so set those back to "unset" here (morph: 1 → 2px).
|
|
1124
|
+
// DISABLED keeps a tinted fill (the base :disabled rule is 0,2,0 and the field rule above outranks it): the tint goes
|
|
1125
|
+
// OVER the field's own fill, so a white field on a dark band stays white under its dark text (review of #665: a
|
|
1126
|
+
// transparent tint there was 1:1). §9.2 state matrix: a disabled field must still look disabled, not only fainter.
|
|
1127
|
+
"[data-gem-form-set] :is(.gem-input, .gem-select, .gem-textarea, .gem-shell-footer__newsletter .gem-input):where(:not(.gem-search-form .gem-input)):disabled {",
|
|
1128
|
+
" background: color-mix(in srgb, currentColor 6%, var(--gem-input-bg));",
|
|
1129
|
+
"}",
|
|
1130
|
+
// An underline field has no fill of its own (its ink is the band's): tint the band, not a fill it does not draw.
|
|
1131
|
+
'[data-gem-form-set][data-gem-field-shape="underline"] :is(.gem-input, .gem-select, .gem-textarea, .gem-shell-footer__newsletter .gem-input):where(:not(.gem-search-form .gem-input)):disabled {',
|
|
1132
|
+
" background: color-mix(in srgb, currentColor 6%, transparent);",
|
|
1133
|
+
"}",
|
|
1134
|
+
"[data-gem-form-set] .gem-search-form .gem-input {",
|
|
1135
|
+
" --gem-input-border-width: initial;",
|
|
1136
|
+
" --gem-input-radius: initial;",
|
|
1137
|
+
"}",
|
|
1138
|
+
"[data-gem-form-set] .gem-checkbox {",
|
|
1139
|
+
" inline-size: var(--gem-checkbox-size);",
|
|
1140
|
+
" block-size: var(--gem-checkbox-size);",
|
|
1141
|
+
" accent-color: var(--gem-checkbox-accent);",
|
|
1142
|
+
"}"
|
|
1143
|
+
];
|
|
1144
|
+
var GEM_FORM_SET_CSS = GEM_FORM_SET_CSS_LINES.join("\n");
|
|
920
1145
|
var GEM_COUNTDOWN_ENDED_CSS_LINES = [
|
|
921
1146
|
":root:not(:has([data-gem-canvas])) [data-gem-section-id][data-gem-countdown-ended='true'] { display: none; }",
|
|
922
1147
|
":root:has([data-gem-canvas]) [data-gem-section-id][data-gem-countdown-ended='true'] { opacity: .55; }",
|
|
@@ -979,12 +1204,12 @@ var GEM_ROW_CSS_LINES = [
|
|
|
979
1204
|
var GEM_ROW_GAP_CSS = GEM_ROW_GAP_CSS_LINES.join("\n");
|
|
980
1205
|
var GEM_ROW_CSS = GEM_ROW_CSS_LINES.join("\n");
|
|
981
1206
|
var GEM_GRID_COLUMNS_KNOB_CSS_LINES = [
|
|
982
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
983
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
984
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
985
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
986
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
987
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
1207
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-grid-columns='2'] :is(.gem-section__grid, [class*='__grid']):not(.gem-row) { grid-template-columns: repeat(2, minmax(0, 1fr)); }",
|
|
1208
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-grid-columns='2'] .gem-row:is(.gem-section__grid, [class*='__grid']):not(.gem-row .gem-row) { --gem-row-cols: 2; --gem-row-cols-md: 2; --gem-row-tracks: initial; --gem-row-tracks-md: initial; }",
|
|
1209
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-grid-columns='3'] :is(.gem-section__grid, [class*='__grid']):not(.gem-row) { grid-template-columns: repeat(3, minmax(0, 1fr)); }",
|
|
1210
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-grid-columns='3'] .gem-row:is(.gem-section__grid, [class*='__grid']):not(.gem-row .gem-row) { --gem-row-cols: 3; --gem-row-cols-md: 3; --gem-row-tracks: initial; --gem-row-tracks-md: initial; }",
|
|
1211
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-grid-columns='4'] :is(.gem-section__grid, [class*='__grid']):not(.gem-row) { grid-template-columns: repeat(4, minmax(0, 1fr)); }",
|
|
1212
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-grid-columns='4'] .gem-row:is(.gem-section__grid, [class*='__grid']):not(.gem-row .gem-row) { --gem-row-cols: 4; --gem-row-cols-md: 4; --gem-row-tracks: initial; --gem-row-tracks-md: initial; }"
|
|
988
1213
|
];
|
|
989
1214
|
var GEM_GRID_COLUMNS_ROW_KNOB_CSS = GEM_GRID_COLUMNS_KNOB_CSS_LINES.filter((line) => line.includes(" .gem-row:is(")).join("\n");
|
|
990
1215
|
function createGemStyleKnobCss(options = {}, selector = ":root") {
|
|
@@ -1001,20 +1226,34 @@ function createGemStyleKnobCss(options = {}, selector = ":root") {
|
|
|
1001
1226
|
});
|
|
1002
1227
|
}
|
|
1003
1228
|
function createGemThemeCss(tokens, selector = ":root", options = {}) {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1229
|
+
const parts = createGemThemeCssParts(tokens, selector, options);
|
|
1230
|
+
return parts.tokens + parts.primitives + parts.popupShell + parts.sections;
|
|
1231
|
+
}
|
|
1232
|
+
function createGemThemeCssParts(tokens, selector = ":root", options = {}) {
|
|
1233
|
+
const base = createGemBaseCssParts(options.profile ?? "dawn-parity");
|
|
1234
|
+
return {
|
|
1235
|
+
tokens: [
|
|
1236
|
+
createGemTokenCss(tokens, selector),
|
|
1237
|
+
// Set the knobs the space / radius / border / type ramps read (only when they differ from the
|
|
1238
|
+
// default 1, so a standard-shaped storefront emits nothing extra).
|
|
1239
|
+
...createGemStyleKnobCss(options, selector),
|
|
1240
|
+
"",
|
|
1241
|
+
""
|
|
1242
|
+
].join("\n"),
|
|
1243
|
+
primitives: base.primitives,
|
|
1244
|
+
popupShell: [GEM_POPUP_SHELL_CSS, "", ""].join("\n"),
|
|
1245
|
+
sections: base.sections
|
|
1246
|
+
};
|
|
1012
1247
|
}
|
|
1013
1248
|
function createSectionClassName(section) {
|
|
1014
1249
|
return `gem-section gem-${slugify(section.archetype)} gem-${slugify(section.archetype)}--${slugify(section.variant)}`;
|
|
1015
1250
|
}
|
|
1016
1251
|
function createGemBaseCss(profile = "dawn-parity") {
|
|
1017
|
-
|
|
1252
|
+
const parts = createGemBaseCssParts(profile);
|
|
1253
|
+
return parts.primitives + parts.sections;
|
|
1254
|
+
}
|
|
1255
|
+
function createGemBaseCssParts(profile) {
|
|
1256
|
+
const reset = [
|
|
1018
1257
|
"*, *::before, *::after {",
|
|
1019
1258
|
" box-sizing: border-box;",
|
|
1020
1259
|
"}",
|
|
@@ -1089,16 +1328,12 @@ function createGemBaseCss(profile = "dawn-parity") {
|
|
|
1089
1328
|
" overflow: hidden !important;",
|
|
1090
1329
|
" clip: rect(0, 0, 0, 0) !important;",
|
|
1091
1330
|
" white-space: nowrap !important;",
|
|
1092
|
-
"}"
|
|
1093
|
-
"",
|
|
1094
|
-
createPrimitiveCss(),
|
|
1095
|
-
"",
|
|
1096
|
-
createShellCss(),
|
|
1097
|
-
"",
|
|
1098
|
-
createSectionCss(),
|
|
1099
|
-
"",
|
|
1100
|
-
createThemeProfileCss(profile)
|
|
1331
|
+
"}"
|
|
1101
1332
|
].join("\n");
|
|
1333
|
+
return {
|
|
1334
|
+
primitives: [reset, "", createPrimitiveCss(), "", ""].join("\n"),
|
|
1335
|
+
sections: [createShellCss(), "", createSectionCss(), "", createThemeProfileCss(profile)].join("\n")
|
|
1336
|
+
};
|
|
1102
1337
|
}
|
|
1103
1338
|
function createThemeProfileCss(profile = "dawn-parity") {
|
|
1104
1339
|
return profile === "dawn-parity" ? createDawnParityCss() : createMorphProfileCss();
|
|
@@ -1338,6 +1573,7 @@ function createPrimitiveCss() {
|
|
|
1338
1573
|
" block-size: var(--gem-space-lg);",
|
|
1339
1574
|
" accent-color: var(--gem-color-accent);",
|
|
1340
1575
|
"}",
|
|
1576
|
+
...GEM_FORM_SET_CSS_LINES,
|
|
1341
1577
|
".gem-submit {",
|
|
1342
1578
|
" cursor: pointer;",
|
|
1343
1579
|
"}",
|
|
@@ -2092,7 +2328,6 @@ function createPrimitiveCss() {
|
|
|
2092
2328
|
...GEM_DIVIDER_CSS_LINES,
|
|
2093
2329
|
...GEM_CARD_CSS_LINES,
|
|
2094
2330
|
...GEM_COUNTDOWN_ENDED_CSS_LINES,
|
|
2095
|
-
...GEM_POPUP_SHELL_CSS_LINES,
|
|
2096
2331
|
".gem-promo-banner, .gem-promo-tile {",
|
|
2097
2332
|
" border: var(--gem-border-subtle, 1px solid var(--gem-color-border));",
|
|
2098
2333
|
" border-radius: var(--gem-radius-md);",
|
|
@@ -2634,17 +2869,17 @@ function createSectionCss() {
|
|
|
2634
2869
|
// theme + published) and on the sim-preview markup. Grid items justify; text aligns. The text
|
|
2635
2870
|
// rule also reaches hero copy + section content so heroes honor it (their absolute-positioned
|
|
2636
2871
|
// copy box keeps its tuned position; only the text inside re-aligns).
|
|
2637
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2638
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2639
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2640
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2641
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2642
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2872
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-content-alignment='left'] .gem-section__inner { justify-items: start; }",
|
|
2873
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-content-alignment='left'] :is(.gem-section__inner, .gem-hero-banner__copy, [class*='__content']) { text-align: left; }",
|
|
2874
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-content-alignment='center'] .gem-section__inner { justify-items: center; }",
|
|
2875
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-content-alignment='center'] :is(.gem-section__inner, .gem-hero-banner__copy, [class*='__content']) { text-align: center; }",
|
|
2876
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-content-alignment='right'] .gem-section__inner { justify-items: end; }",
|
|
2877
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-content-alignment='right'] :is(.gem-section__inner, .gem-hero-banner__copy, [class*='__content']) { text-align: right; }",
|
|
2643
2878
|
// SAFETY: justify-items above content-sizes the inner grid's items, which would COLLAPSE a
|
|
2644
2879
|
// full-bleed grid child (a mosaic/collage/product grid that must span the row) to min-content —
|
|
2645
2880
|
// i.e. the section visually disappears. Re-stretch any grid/media/mosaic direct child so alignment
|
|
2646
2881
|
// only ever moves TEXT, never breaks a section's own layout (fix: collage vanished in wf_0102).
|
|
2647
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2882
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-content-alignment] .gem-section__inner > :is([class*='__grid'], [class*='__media'], [class*='__mosaic'], [class*='__masonry'], [class*='__frame'], [class*='__collage'], [class*='__row']) { justify-self: stretch; width: 100%; }",
|
|
2648
2883
|
// Text-position knob (the VERTICAL sibling of content_alignment) — set by set_layout text_position
|
|
2649
2884
|
// (gemboss override key gem_text_position), emitted as data-gem-text-position by the lean-hero
|
|
2650
2885
|
// renderer (shopify-adapter leanHeroLayoutKeys). It had NO CSS anywhere: the agent could pick it,
|
|
@@ -2656,16 +2891,16 @@ function createSectionCss() {
|
|
|
2656
2891
|
// (0,3,0) beats the component's own `.gem-hero-banner--default .gem-hero-banner__copy` (0,2,0),
|
|
2657
2892
|
// so this wins only when the operator sets the knob; unset, the tuned default position stands.
|
|
2658
2893
|
// `bottom`/`top: auto` is required on each: leaving both insets set pins the box to BOTH edges.
|
|
2659
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2660
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2661
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2894
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-text-position='top'] .gem-hero-banner__copy { top: 50px; bottom: auto; }",
|
|
2895
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-text-position='center'] .gem-hero-banner__copy { top: 50%; bottom: auto; transform: translate(-50%, -50%); }",
|
|
2896
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-text-position='bottom'] .gem-hero-banner__copy { bottom: 50px; top: auto; }",
|
|
2662
2897
|
// Non-hero sections: move the content stack within whatever spare height the section has (a
|
|
2663
2898
|
// `section_height: tall` section, say). `align-content` is a no-op when the grid has no spare
|
|
2664
2899
|
// space, so this never disturbs a normally-sized section — it only answers the same vertical
|
|
2665
2900
|
// question on the sections where the question can even be asked.
|
|
2666
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2667
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2668
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2901
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-text-position='top'] .gem-section__inner { align-content: start; }",
|
|
2902
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-text-position='center'] .gem-section__inner { align-content: center; }",
|
|
2903
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-text-position='bottom'] .gem-section__inner { align-content: end; }",
|
|
2669
2904
|
// Media-position knob (section LAYOUT axis) — set by set_layout media_position (gemboss override key
|
|
2670
2905
|
// gem_media_position). On a genuine two-column section it swaps the image column to the chosen side
|
|
2671
2906
|
// by re-ordering the media child. TWO shapes qualify: (a) `__media` is a DIRECT child of the inner
|
|
@@ -2677,16 +2912,16 @@ function createSectionCss() {
|
|
|
2677
2912
|
// __media), so `grid > __media` never matches a card's image → we swap real 2-col layouts and never
|
|
2678
2913
|
// reorder cards. A full-bleed background-image hero has its `__media` absolutely positioned, so
|
|
2679
2914
|
// `order` is a natural no-op there — exactly right.
|
|
2680
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2681
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2915
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-media-position='left'] .gem-section__inner > [class*='__media'], :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-media-position='left'] .gem-section__inner > [class*='__grid'] > [class*='__media'] { order: -1; }",
|
|
2916
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-media-position='right'] .gem-section__inner > [class*='__media'], :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-media-position='right'] .gem-section__inner > [class*='__grid'] > [class*='__media'] { order: 1; }",
|
|
2682
2917
|
// Spacing/density knob — set by set_layout content_spacing. Scales the inner grid gap (the gap
|
|
2683
2918
|
// between columns + stacked rows), the direct answer to wf_0101's 'khoảng cách giữa 2 cột'.
|
|
2684
2919
|
// Density knob — set by set_layout content_spacing. Scales the gap of the inner content stack.
|
|
2685
2920
|
// (Per-card GRID spacing is governed by the tuned card-mode/variant rules — changed via
|
|
2686
2921
|
// swap_variant, not this knob — so we don't fight their specificity here.)
|
|
2687
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2688
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2689
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2922
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-content-spacing='tight'] .gem-section__inner { gap: var(--gem-space-sm); }",
|
|
2923
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-content-spacing='normal'] .gem-section__inner { gap: var(--gem-space-lg); }",
|
|
2924
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-content-spacing='roomy'] .gem-section__inner { gap: var(--gem-space-3xl); }",
|
|
2690
2925
|
// HERO COPY STACK — same rules as gemsite app/globals.css (gemsite #609), kept in sync by hand.
|
|
2691
2926
|
// On a full-bleed hero the inner grid has ONE in-flow child (the media is absolutely positioned), so the
|
|
2692
2927
|
// inner `gap` above spaces nothing and the knob was MUTE: probed on the live storefront (gemboss acceptance
|
|
@@ -2709,18 +2944,18 @@ function createSectionCss() {
|
|
|
2709
2944
|
":is(.gem-hero-banner--default, .gem-hero-banner--dawn-image-banner)[data-gem-content-spacing='roomy'] .gem-hero-banner__copy > :not(:last-child), .gem-hero-banner[data-gem-content-spacing='roomy'] .gem-hero-slideshow__copy > :not(:last-child) { margin-bottom: 0; }",
|
|
2710
2945
|
// Width knob — set by set_layout content_width. Overrides the inner container width: full-bleed
|
|
2711
2946
|
// edge-to-edge, the default contained width, or a narrow reading measure.
|
|
2712
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2713
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2714
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2947
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-content-width='full'] .gem-section__inner { width: 100%; max-width: none; }",
|
|
2948
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-content-width='contained'] .gem-section__inner { width: min(var(--gem-layout-page, 1180px), calc(100% - var(--gem-layout-gutter, 32px))); max-width: none; }",
|
|
2949
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-content-width='narrow'] .gem-section__inner { width: min(56rem, calc(100% - var(--gem-layout-gutter, 32px))); }",
|
|
2715
2950
|
// Height knob — set by set_layout section_height. Sets min-height on BOTH the section element
|
|
2716
2951
|
// and its inner: the section-level rule (attr+class = (0,2,0)) beats a component's single-class
|
|
2717
2952
|
// floor like `.gem-hero-banner--default { min-height: 720px }` (0,1,0), so "make the hero
|
|
2718
2953
|
// shorter/taller" actually re-sizes the section (the inner-only rule alone left the section floored).
|
|
2719
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2720
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2721
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2722
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2723
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2954
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-height='short'] { min-height: 420px; }",
|
|
2955
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-height='tall'] { min-height: 760px; }",
|
|
2956
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-height='short'] .gem-section__inner { min-height: 420px; }",
|
|
2957
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-height='normal'] .gem-section__inner { min-height: auto; }",
|
|
2958
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-height='tall'] .gem-section__inner { min-height: 760px; }",
|
|
2724
2959
|
// Columns knob — set by set_layout grid_columns. Overrides the grid container's column count. The
|
|
2725
2960
|
// contract advertises grid_columns for any section with a `columns` setting (layoutCapabilitiesOf),
|
|
2726
2961
|
// but MOST gem sections name their grid `{section}__grid` (gem-multicolumn__grid, gem-stats__grid,
|
|
@@ -2743,25 +2978,25 @@ function createSectionCss() {
|
|
|
2743
2978
|
// The shape-divider below paints the section's OWN
|
|
2744
2979
|
// colour into the neighbouring section, so it needs a single name for "whatever this band is",
|
|
2745
2980
|
// rather than re-deriving the swatch in every divider rule.
|
|
2746
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2747
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2748
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2749
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2750
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2981
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='default'] { --gem-section-bg: var(--gem-color-background); background: transparent; }",
|
|
2982
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='surface'] { --gem-section-bg: var(--gem-color-surface); background: var(--gem-section-bg); padding-block: var(--gem-space-xl); }",
|
|
2983
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='accent'] { --gem-section-bg: var(--gem-color-accent); background: var(--gem-section-bg); color: var(--gem-color-accent-text); --gem-band-bg: var(--gem-color-accent); --gem-focus-ring: 2px solid var(--gem-color-accent-text); padding-block: var(--gem-space-xl); }",
|
|
2984
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='dark'] { --gem-section-bg: var(--gem-color-text); background: var(--gem-section-bg); color: var(--gem-color-background); --gem-band-bg: var(--gem-color-text); --gem-focus-ring: 2px solid var(--gem-color-background); padding-block: var(--gem-space-xl); }",
|
|
2985
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='light'] { --gem-section-bg: var(--gem-color-background); background: var(--gem-section-bg); color: var(--gem-color-text); padding-block: var(--gem-space-xl); }",
|
|
2751
2986
|
// On accent/dark bands, keep muted sub-copy + headings legible against the inverted ground.
|
|
2752
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2987
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='accent'] :is(.gem-section__heading, .gem-section__body, .gem-section__eyebrow), :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='dark'] :is(.gem-section__heading, .gem-section__body, .gem-section__eyebrow) { color: inherit; }",
|
|
2753
2988
|
// Price-colour knob (set by set_style price_color) — recolours the PRICE text on a product/collection
|
|
2754
2989
|
// section. red/green are literal sale/savings semantics; accent uses the brand token; default inherits.
|
|
2755
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2756
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2757
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2990
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-price-color='accent'] :is(.gem-price, .gem-price__regular, [class*='__price']) { color: var(--gem-color-accent); }",
|
|
2991
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-price-color='red'] :is(.gem-price, .gem-price__regular, [class*='__price']) { color: #d61f1f; }",
|
|
2992
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-price-color='green'] :is(.gem-price, .gem-price__regular, [class*='__price']) { color: #1a7f37; }",
|
|
2758
2993
|
// Text-weight knob (set by set_layout text_weight) — scales the section's --gem-style-weight ramp
|
|
2759
2994
|
// (which weight-bearing type multiplies) AND sets the heading font-weight directly, so 'bold'
|
|
2760
2995
|
// emboldens the headings on any section. default/normal inherit.
|
|
2761
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2762
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2763
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2764
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
2996
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-text-weight='bold'] { --gem-style-weight: 1.25; }",
|
|
2997
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-text-weight='bold'] :is(.gem-section__heading, .gem-hero-banner__copy h1, .gem-hero-banner__copy h2) { font-weight: 800; }",
|
|
2998
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-text-weight='light'] { --gem-style-weight: 0.8; }",
|
|
2999
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-text-weight='light'] :is(.gem-section__heading, .gem-hero-banner__copy h1, .gem-hero-banner__copy h2) { font-weight: 350; }",
|
|
2765
3000
|
// G4 — per-element colour (set by set_style element_color {target, colour}). One mechanism colours
|
|
2766
3001
|
// the heading / eyebrow / badge in a section; swatches map to brand token or literal sale/dark.
|
|
2767
3002
|
// (price uses data-gem-price-color above.) accent=brand, red/green=sale/savings, dark=text token.
|
|
@@ -4493,37 +4728,58 @@ var promoPopupPluginRegistrationLines = [
|
|
|
4493
4728
|
" registerPlugin('promo-popup', ({ api }) => {",
|
|
4494
4729
|
// Only the explicit hook opts in. Every promo-popup section carries the archetype class .gem-promo-popup, and
|
|
4495
4730
|
// binding by class took over variants that run their own behavior.js (GemFactory gap #16, 03/10/2026).
|
|
4731
|
+
// A section that has the hook AND its own behavior.js (promo-popup.email-capture.v1) renders the plugin's bound
|
|
4732
|
+
// mark, data-gem-bound-promo-popup, so mark() below skips it (gem-framework 0.8.3).
|
|
4496
4733
|
" const popups = document.querySelectorAll('[data-gem-promo-popup]');",
|
|
4497
4734
|
" let bound = 0;",
|
|
4735
|
+
" const shown = [];",
|
|
4498
4736
|
" const storagePrefix = 'gem:promo-popup-dismissed:';",
|
|
4499
4737
|
" const now = () => Date.now ? Date.now() : new Date().getTime();",
|
|
4500
|
-
|
|
4501
|
-
|
|
4738
|
+
// ONE storage key, two meanings in the wild (gem-framework 0.8.0). gemsite's port stores WHEN the shopper
|
|
4739
|
+
// dismissed; runtimes up to 0.7 stored when the dismissal ENDS. Read both: a value in the future can only be an
|
|
4740
|
+
// end, so it holds until then (and is rewritten once as a dismissal time); a value in the past is a dismissal time and
|
|
4741
|
+
// the popup's own window applies. Written from now on: the dismissal time (gemsite's meaning), which also lets a
|
|
4742
|
+
// seller who shortens the window shorten it for past dismissals. An end written by 0.7 that has already passed
|
|
4743
|
+
// reads as a dismissal time: that popup stays away one more window at most, never less than the shopper asked.
|
|
4744
|
+
" const readDismissal = (key) => {",
|
|
4745
|
+
" try { const value = Number(target.localStorage && target.localStorage.getItem(storagePrefix + key) || '0'); return Number.isFinite(value) && value > 0 ? value : 0; }",
|
|
4502
4746
|
" catch { return 0; }",
|
|
4503
4747
|
" };",
|
|
4504
|
-
" const
|
|
4748
|
+
" const writeDismissedAt = (key, timestamp) => {",
|
|
4505
4749
|
" try { if (target.localStorage) target.localStorage.setItem(storagePrefix + key, String(timestamp)); }",
|
|
4506
4750
|
" catch { /* storage can be unavailable in private contexts */ }",
|
|
4507
4751
|
" };",
|
|
4752
|
+
" const isDismissed = (key, windowMs) => {",
|
|
4753
|
+
" const stored = readDismissal(key);",
|
|
4754
|
+
" if (!stored) return false;",
|
|
4755
|
+
" const at = now();",
|
|
4756
|
+
// Converted ONCE: only when the end minus this window is already past (it is then the dismissal time). An end
|
|
4757
|
+
// further out than one window (the 0.7 window was longer) is left as it is and holds until it passes.
|
|
4758
|
+
" if (stored > at) { if (stored - windowMs <= at) writeDismissedAt(key, stored - windowMs); return true; }",
|
|
4759
|
+
" return at - stored < windowMs;",
|
|
4760
|
+
" };",
|
|
4508
4761
|
" popups.forEach((popup, index) => {",
|
|
4509
4762
|
" if (!mark(popup, 'promo-popup')) return;",
|
|
4510
4763
|
" const key = popup.id || popup.getAttribute && popup.getAttribute('data-gem-section-library-id') || String(index);",
|
|
4511
4764
|
" const delaySeconds = Number(popup.getAttribute && (popup.getAttribute('data-gem-promo-delay') || '0'));",
|
|
4512
4765
|
" const frequencyDays = Number(popup.getAttribute && (popup.getAttribute('data-gem-promo-frequency-days') || '7'));",
|
|
4766
|
+
" const windowMs = (Number.isFinite(frequencyDays) ? Math.max(1, frequencyDays) : 7) * 86400000;",
|
|
4513
4767
|
" const dialog = popup.querySelector && popup.querySelector('[data-gem-promo-popup-dialog]');",
|
|
4514
4768
|
" const overlay = popup.querySelector && popup.querySelector('[data-gem-promo-popup-overlay]');",
|
|
4515
|
-
" const
|
|
4516
|
-
" if (readDismissedUntil(key) > now()) { popup.dataset.gemPromoPopupState = 'dismissed'; return false; }",
|
|
4769
|
+
" const show = (reason) => {",
|
|
4517
4770
|
" popup.hidden = false;",
|
|
4518
4771
|
" if (dialog) dialog.hidden = false;",
|
|
4519
4772
|
" if (overlay) overlay.hidden = false;",
|
|
4520
4773
|
" popup.dataset.gemPromoPopupState = 'open';",
|
|
4521
4774
|
" api.emit('gem:promo-popup:open', { source: 'gem-runtime', id: popup.id || undefined, reason });",
|
|
4775
|
+
" };",
|
|
4776
|
+
" const open = (reason = 'timer') => {",
|
|
4777
|
+
" if (isDismissed(key, windowMs)) { popup.dataset.gemPromoPopupState = 'dismissed'; return false; }",
|
|
4778
|
+
" show(reason);",
|
|
4522
4779
|
" return true;",
|
|
4523
4780
|
" };",
|
|
4524
4781
|
" const close = (reason = 'dismiss') => {",
|
|
4525
|
-
"
|
|
4526
|
-
" writeDismissedUntil(key, now() + days * 86400000);",
|
|
4782
|
+
" writeDismissedAt(key, now());",
|
|
4527
4783
|
" if (dialog) dialog.hidden = true;",
|
|
4528
4784
|
" if (overlay) overlay.hidden = true;",
|
|
4529
4785
|
" popup.dataset.gemPromoPopupState = 'closed';",
|
|
@@ -4536,10 +4792,63 @@ var promoPopupPluginRegistrationLines = [
|
|
|
4536
4792
|
" if (canListen(overlay)) overlay.addEventListener('click', () => close('overlay'));",
|
|
4537
4793
|
" popup.dataset.gemPromoPopupState = 'ready';",
|
|
4538
4794
|
" const delay = Math.max(0, Number.isFinite(delaySeconds) ? delaySeconds : 0) * 1000;",
|
|
4539
|
-
|
|
4795
|
+
// WHEN it opens, gemsite's contract (components/behaviors/promoPopup.js): data-gem-promo-trigger = "delay"
|
|
4796
|
+
// (default, and any unknown value) | "exit_intent" (the pointer leaves through the top 10px of the window; a
|
|
4797
|
+
// touch screen has no pointer to leave with, so it falls back to the delay) | "scroll" (the shopper has scrolled
|
|
4798
|
+
// data-gem-promo-scroll-percent of the page, 10 to 90, default 50; a page that still cannot scroll at the delay
|
|
4799
|
+
// opens then). It opens once per page view, whichever fires first. data-gem-promo-armed says which one is live.
|
|
4800
|
+
// The delay keeps the runtime's default of 0 seconds (gemsite's port defaults to 6).
|
|
4801
|
+
" const trigger = String(popup.getAttribute && popup.getAttribute('data-gem-promo-trigger') || 'delay').trim();",
|
|
4802
|
+
" const rawPercent = String(popup.getAttribute && popup.getAttribute('data-gem-promo-scroll-percent') || '').trim();",
|
|
4803
|
+
" const parsedPercent = rawPercent ? Number(rawPercent) : NaN;",
|
|
4804
|
+
" const scrollPercent = Number.isFinite(parsedPercent) ? Math.min(90, Math.max(10, parsedPercent)) : 50;",
|
|
4805
|
+
" const canHover = () => { try { return Boolean(target.matchMedia && target.matchMedia('(hover: hover) and (pointer: fine)').matches); } catch { return false; } };",
|
|
4806
|
+
" const armed = trigger === 'exit_intent' && canHover() ? 'exit_intent' : trigger === 'scroll' ? 'scroll' : 'delay';",
|
|
4807
|
+
" if (popup.setAttribute) popup.setAttribute('data-gem-promo-armed', armed);",
|
|
4808
|
+
" let opened = false;",
|
|
4809
|
+
" const disarm = [];",
|
|
4810
|
+
" const openOnce = (reason) => { if (opened) return; opened = true; disarm.splice(0).forEach((off) => off()); open(reason); };",
|
|
4811
|
+
// An explicit open (api.promoPopup.open) counts as THE open of this page view: it disarms the pending trigger, so the
|
|
4812
|
+
// delay or exit intent does not show the popup a second time (code review of #675), and it does nothing on a popup
|
|
4813
|
+
// that is already open.
|
|
4814
|
+
" shown.push((reason) => { opened = true; disarm.splice(0).forEach((off) => off()); if (popup.dataset.gemPromoPopupState !== 'open') show(reason); });",
|
|
4815
|
+
" if (armed === 'exit_intent') {",
|
|
4816
|
+
" const onMouseOut = (event) => { if (event.relatedTarget || event.clientY >= 10) return; openOnce('exit_intent'); };",
|
|
4817
|
+
" if (canListen(document)) {",
|
|
4818
|
+
" document.addEventListener('mouseout', onMouseOut);",
|
|
4819
|
+
" disarm.push(() => document.removeEventListener && document.removeEventListener('mouseout', onMouseOut));",
|
|
4820
|
+
" }",
|
|
4821
|
+
" } else if (armed === 'scroll') {",
|
|
4822
|
+
" const room = () => Number(document.documentElement && document.documentElement.scrollHeight || 0) - Number(target.innerHeight || 0);",
|
|
4823
|
+
" const check = () => { const r = room(); if (r > 0 && (Number(target.scrollY || target.pageYOffset || 0) / r) * 100 >= scrollPercent) openOnce('scroll'); };",
|
|
4824
|
+
" let ticking = false;",
|
|
4825
|
+
" const onScroll = () => {",
|
|
4826
|
+
" if (ticking) return;",
|
|
4827
|
+
" ticking = true;",
|
|
4828
|
+
" const frame = target.requestAnimationFrame ? (fn) => target.requestAnimationFrame(fn) : (fn) => target.setTimeout && target.setTimeout(fn, 16);",
|
|
4829
|
+
" frame(() => { ticking = false; check(); });",
|
|
4830
|
+
" };",
|
|
4831
|
+
" if (canListen(target)) {",
|
|
4832
|
+
" target.addEventListener('scroll', onScroll, { passive: true });",
|
|
4833
|
+
" target.addEventListener('resize', check);",
|
|
4834
|
+
" target.addEventListener('load', check);",
|
|
4835
|
+
" disarm.push(() => { if (!target.removeEventListener) return; target.removeEventListener('scroll', onScroll); target.removeEventListener('resize', check); target.removeEventListener('load', check); });",
|
|
4836
|
+
" }",
|
|
4837
|
+
" if (target.setTimeout) {",
|
|
4838
|
+
" const fallback = target.setTimeout(() => { if (room() <= 0) openOnce('timer'); }, delay);",
|
|
4839
|
+
" disarm.push(() => target.clearTimeout && target.clearTimeout(fallback));",
|
|
4840
|
+
" }",
|
|
4841
|
+
" check();",
|
|
4842
|
+
" } else {",
|
|
4843
|
+
" target.setTimeout ? target.setTimeout(() => openOnce('timer'), delay) : openOnce('timer');",
|
|
4844
|
+
" }",
|
|
4540
4845
|
" bound += 1;",
|
|
4541
4846
|
" });",
|
|
4542
|
-
|
|
4847
|
+
// The popups THIS plugin bound, never every [data-gem-promo-popup]: one a section's own behavior.js drives carries the
|
|
4848
|
+
// bound mark already (email-capture renders data-gem-bound-promo-popup="section") and is not the plugin's to open. An
|
|
4849
|
+
// explicit open (a teaser, a test) shows the dialog itself, not only the root, and ignores the dismissal window,
|
|
4850
|
+
// which is about opening on its own. Before 0.8.3 it unhid the root only, so the dialog stayed hidden.
|
|
4851
|
+
" api.promoPopup = { open: () => shown.forEach((show) => show('api')) };",
|
|
4543
4852
|
" api.emit('gem:promo-popup:ready', { source: 'gem-runtime', count: bound });",
|
|
4544
4853
|
" });"
|
|
4545
4854
|
];
|
|
@@ -4555,7 +4864,14 @@ var stickyAtcPluginRegistrationLines = [
|
|
|
4555
4864
|
" };",
|
|
4556
4865
|
" bars.forEach((bar) => {",
|
|
4557
4866
|
" if (!mark(bar, 'sticky-atc')) return;",
|
|
4558
|
-
|
|
4867
|
+
// Two attribute names (gem-framework 0.8.0), each with its own default. data-gem-sticky-threshold is the
|
|
4868
|
+
// runtime's (default 240). data-gem-sticky-atc-threshold is gemsite's (StickyAtcCompactBar.tsx): when the bar
|
|
4869
|
+
// carries it, gemsite's rule decides: that number, else the --gem-sticky-atc-threshold CSS variable, else 360.
|
|
4870
|
+
// A bar carrying both follows gemsite's attribute.
|
|
4871
|
+
" const cssThreshold = () => { try { return target.getComputedStyle ? Number(target.getComputedStyle(bar).getPropertyValue('--gem-sticky-atc-threshold')) : 0; } catch { return 0; } };",
|
|
4872
|
+
" const threshold = bar.hasAttribute && bar.hasAttribute('data-gem-sticky-atc-threshold')",
|
|
4873
|
+
" ? (Number(bar.getAttribute('data-gem-sticky-atc-threshold')) || cssThreshold() || 360)",
|
|
4874
|
+
" : Number(bar.getAttribute && (bar.getAttribute('data-gem-sticky-threshold') || '240'));",
|
|
4559
4875
|
" const update = () => {",
|
|
4560
4876
|
" const scrollY = Number(target.scrollY || target.pageYOffset || 0);",
|
|
4561
4877
|
" setState(bar, scrollY >= threshold ? 'visible' : 'hidden');",
|
|
@@ -4639,6 +4955,13 @@ var countdownPluginRegistrationLines = [
|
|
|
4639
4955
|
" let endAt = 0;",
|
|
4640
4956
|
" let handle = null;",
|
|
4641
4957
|
" let state = 'idle';",
|
|
4958
|
+
// gem:countdown:start means the N seconds began: once. A visible-mode timer pauses and resumes as it leaves and
|
|
4959
|
+
// re-enters the screen, and each resume used to announce a new start (gemboss PR #3758 review, item 7).
|
|
4960
|
+
" let started = false;",
|
|
4961
|
+
// The visible-mode observer, released as soon as the timer can no longer run (complete or stopped), not at the
|
|
4962
|
+
// next intersection change after it.
|
|
4963
|
+
" let observer = null;",
|
|
4964
|
+
" const release = () => { if (observer) { observer.disconnect(); observer = null; } };",
|
|
4642
4965
|
" const detail = () => ({ source: 'gem-runtime', id: timer.id || undefined, mode: 'duration', duration: total, remainingSeconds: Math.max(0, Math.ceil(left / 1000)) });",
|
|
4643
4966
|
" const render = () => {",
|
|
4644
4967
|
" const s = Math.max(0, Math.ceil(left / 1000));",
|
|
@@ -4654,6 +4977,7 @@ var countdownPluginRegistrationLines = [
|
|
|
4654
4977
|
" const halt = () => { if (handle !== null && target.clearInterval) target.clearInterval(handle); handle = null; };",
|
|
4655
4978
|
" const finish = () => {",
|
|
4656
4979
|
" halt();",
|
|
4980
|
+
" release();",
|
|
4657
4981
|
" left = 0;",
|
|
4658
4982
|
" state = 'complete';",
|
|
4659
4983
|
" render();",
|
|
@@ -4672,20 +4996,20 @@ var countdownPluginRegistrationLines = [
|
|
|
4672
4996
|
" endAt = Date.now() + left;",
|
|
4673
4997
|
" render();",
|
|
4674
4998
|
" if (target.setInterval) handle = target.setInterval(tick, 250);",
|
|
4675
|
-
" api.emit('gem:countdown:start', detail());",
|
|
4999
|
+
" if (!started) { started = true; api.emit('gem:countdown:start', detail()); }",
|
|
4676
5000
|
" };",
|
|
4677
5001
|
" const pause = (next) => {",
|
|
4678
5002
|
" if (state === 'running') { left = Math.max(0, endAt - Date.now()); halt(); }",
|
|
4679
5003
|
" if (state === 'complete' || state === 'stopped') return;",
|
|
4680
5004
|
" state = next;",
|
|
5005
|
+
" if (next === 'stopped') release();",
|
|
4681
5006
|
" render();",
|
|
4682
5007
|
" };",
|
|
4683
5008
|
" render();",
|
|
4684
5009
|
" if (stopEvent && canListen(scope)) scope.addEventListener(stopEvent, () => pause('stopped'));",
|
|
4685
5010
|
" if (mode === 'visible' && target.IntersectionObserver) {",
|
|
4686
|
-
"
|
|
5011
|
+
" observer = new target.IntersectionObserver((entries) => {",
|
|
4687
5012
|
" for (const entry of entries) entry.isIntersecting ? run() : pause('paused');",
|
|
4688
|
-
" if (state === 'complete' || state === 'stopped') observer.disconnect();",
|
|
4689
5013
|
" });",
|
|
4690
5014
|
" observer.observe(timer);",
|
|
4691
5015
|
" } else if (mode === 'mount' || mode === 'visible') run();",
|
|
@@ -4702,7 +5026,18 @@ var countdownPluginRegistrationLines = [
|
|
|
4702
5026
|
" const targetTime = countdownEnd(timer.getAttribute && timer.getAttribute('data-gem-countdown-target'));",
|
|
4703
5027
|
" const section = timer.closest ? timer.closest('[data-gem-section-id]') : null;",
|
|
4704
5028
|
" const band = section && section.hasAttribute && section.hasAttribute('data-gem-countdown-end') ? section : null;",
|
|
5029
|
+
// Two readout schemes, one countdown (gem-framework 0.8.0). The runtime's own cells are
|
|
5030
|
+
// [data-gem-countdown-days|hours|minutes|seconds]; the Liquid theme's initCountdown and gemsite write
|
|
5031
|
+
// [data-gem-cd-days|hours|mins|secs] (gemsite CountdownBannerOfferBar.tsx). Both are written; a cell the markup
|
|
5032
|
+
// does not carry is skipped. The readout ticks every second only when it shows seconds, else once a minute ON the
|
|
5033
|
+
// minute of the remaining time (0.8.2): the next update is set for the instant the minute digit changes,
|
|
5034
|
+
// (remaining % 60000) + a few ms, and re-aimed from the clock each time, so the digit is never a minute late.
|
|
5035
|
+
// In the last minute the next change IS the end, so the chain aims at the end itself and stops there: nothing is
|
|
5036
|
+
// left scheduled after it, and a deadline too far off for the one-shot end timer below still ends on time.
|
|
5037
|
+
// A once-a-minute interval from mount time (0.8.0, 0.8.1) let it lag the true remaining time by up to 59 s.
|
|
5038
|
+
" const hasSecondsCell = Boolean(timer.querySelector && (timer.querySelector('[data-gem-cd-secs]') || timer.querySelector('[data-gem-countdown-seconds]')));",
|
|
4705
5039
|
" let handle = null;",
|
|
5040
|
+
" let announced = '';",
|
|
4706
5041
|
" const update = () => {",
|
|
4707
5042
|
" if (!Number.isFinite(targetTime)) {",
|
|
4708
5043
|
" timer.dataset.gemCountdownState = 'invalid';",
|
|
@@ -4713,20 +5048,43 @@ var countdownPluginRegistrationLines = [
|
|
|
4713
5048
|
" const days = Math.floor(remainingSeconds / 86400);",
|
|
4714
5049
|
" const hours = Math.floor((remainingSeconds % 86400) / 3600);",
|
|
4715
5050
|
" const minutes = Math.floor((remainingSeconds % 3600) / 60);",
|
|
5051
|
+
" const seconds = remainingSeconds % 60;",
|
|
5052
|
+
// ONE end state: data-gem-countdown-state="complete" (the runtime's word, same as the event
|
|
5053
|
+
// gem:countdown:complete and the duration timer). gemsite's port wrote "ended"; its meaning travels as the
|
|
5054
|
+
// attribute data-gem-countdown-ended="true" on the timer, the same mark a duration timer gets at zero. A timer
|
|
5055
|
+
// that is itself a section root is left to the band rule above, so it never hides a band that does not own a deadline.
|
|
4716
5056
|
" timer.dataset.gemCountdownState = ms > 0 ? 'running' : 'complete';",
|
|
4717
5057
|
" timer.dataset.gemCountdownRemainingSeconds = String(remainingSeconds);",
|
|
4718
5058
|
" write(timer, '[data-gem-countdown-days]', days);",
|
|
4719
5059
|
" write(timer, '[data-gem-countdown-hours]', hours);",
|
|
4720
5060
|
" write(timer, '[data-gem-countdown-minutes]', minutes);",
|
|
5061
|
+
" write(timer, '[data-gem-countdown-seconds]', seconds);",
|
|
5062
|
+
" write(timer, '[data-gem-cd-days]', days);",
|
|
5063
|
+
" write(timer, '[data-gem-cd-hours]', hours);",
|
|
5064
|
+
" write(timer, '[data-gem-cd-mins]', minutes);",
|
|
5065
|
+
" write(timer, '[data-gem-cd-secs]', seconds);",
|
|
4721
5066
|
" if (ms <= 0) {",
|
|
4722
5067
|
" if (band) band.setAttribute('data-gem-countdown-ended', 'true');",
|
|
5068
|
+
" if (timer.setAttribute && !(timer.hasAttribute && timer.hasAttribute('data-gem-section-id'))) timer.setAttribute('data-gem-countdown-ended', 'true');",
|
|
4723
5069
|
" if (handle !== null && target.clearInterval) { target.clearInterval(handle); handle = null; }",
|
|
4724
5070
|
" }",
|
|
4725
|
-
|
|
5071
|
+
// start: once a minute as before, but a per-second readout announces it once; complete: once.
|
|
5072
|
+
" const event = ms > 0 ? 'gem:countdown:start' : 'gem:countdown:complete';",
|
|
5073
|
+
" if (event === announced && (event === 'gem:countdown:complete' || hasSecondsCell)) return;",
|
|
5074
|
+
" announced = event;",
|
|
5075
|
+
" api.emit(event, { source: 'gem-runtime', id: timer.id || undefined, remainingSeconds });",
|
|
5076
|
+
" };",
|
|
5077
|
+
" const onTheMinute = () => {",
|
|
5078
|
+
" const ms = targetTime - Date.now();",
|
|
5079
|
+
" if (!(ms > 0) || !target.setTimeout) return;",
|
|
5080
|
+
" target.setTimeout(() => { update(); onTheMinute(); }, Math.min((ms % 60000) + 5, ms));",
|
|
4726
5081
|
" };",
|
|
4727
5082
|
" update();",
|
|
4728
|
-
" if (
|
|
4729
|
-
|
|
5083
|
+
" if (Number.isFinite(targetTime) && targetTime - Date.now() > 0) {",
|
|
5084
|
+
" if (!hasSecondsCell) onTheMinute();",
|
|
5085
|
+
" else if (target.setInterval) handle = target.setInterval(update, 1000);",
|
|
5086
|
+
" }",
|
|
5087
|
+
// A minute readout moves once a minute, but the END is not allowed to wait for the next minute: one timer
|
|
4730
5088
|
// for the exact instant (while it fits a setTimeout, ~24.8 days; past that the minute tick gets there).
|
|
4731
5089
|
" const left = targetTime - Date.now();",
|
|
4732
5090
|
" if (target.setTimeout && Number.isFinite(left) && left > 0 && left < 2147483647) target.setTimeout(update, left);",
|
|
@@ -4836,6 +5194,39 @@ var offerEnginePluginRegistrationLines = [
|
|
|
4836
5194
|
" api.emit('gem:offer:ready', { source: 'gem-runtime' });",
|
|
4837
5195
|
" });"
|
|
4838
5196
|
];
|
|
5197
|
+
var runtimePluginSourceLines = [
|
|
5198
|
+
["cart-drawer", cartDrawerPluginRegistrationLines],
|
|
5199
|
+
["mobile-menu", mobileMenuPluginRegistrationLines],
|
|
5200
|
+
["quantity-input", quantityInputPluginRegistrationLines],
|
|
5201
|
+
["variant-selects", variantSelectsPluginRegistrationLines],
|
|
5202
|
+
["predictive-search", predictiveSearchPluginRegistrationLines],
|
|
5203
|
+
["localization", localizationPluginRegistrationLines],
|
|
5204
|
+
["disclosure", disclosurePluginRegistrationLines],
|
|
5205
|
+
["tabs", tabsPluginRegistrationLines],
|
|
5206
|
+
["media-gallery", mediaGalleryPluginRegistrationLines],
|
|
5207
|
+
["carousel", carouselPluginRegistrationLines],
|
|
5208
|
+
["hero-slideshow", heroSlideshowPluginRegistrationLines],
|
|
5209
|
+
["quick-view", quickViewPluginRegistrationLines],
|
|
5210
|
+
["promo-popup", promoPopupPluginRegistrationLines],
|
|
5211
|
+
["sticky-atc", stickyAtcPluginRegistrationLines],
|
|
5212
|
+
["before_after", beforeAfterPluginRegistrationLines],
|
|
5213
|
+
["countdown", countdownPluginRegistrationLines],
|
|
5214
|
+
["reveal-on-scroll", revealOnScrollPluginRegistrationLines],
|
|
5215
|
+
["recently-viewed", recentlyViewedPluginRegistrationLines],
|
|
5216
|
+
["runtime-slot-loader", runtimeSlotLoaderPluginRegistrationLines],
|
|
5217
|
+
["offer-engine", offerEnginePluginRegistrationLines]
|
|
5218
|
+
];
|
|
5219
|
+
var noopPluginRegistrationLine = (key, readyEvent) => ` registerNoopPlugin(${JSON.stringify(key)}, ${JSON.stringify(readyEvent)});`;
|
|
5220
|
+
function gemRuntimePluginSources() {
|
|
5221
|
+
const withBody = runtimePluginSourceLines.map(([key, lines]) => ({
|
|
5222
|
+
key,
|
|
5223
|
+
readyEvent: runtimePluginReadyEvents[key] ?? `gem:${key}:ready`,
|
|
5224
|
+
hasBody: true,
|
|
5225
|
+
lines
|
|
5226
|
+
}));
|
|
5227
|
+
const noop = Object.entries(runtimePluginReadyEvents).filter(([key]) => !runtimePluginSourceLines.some(([sourceKey]) => sourceKey === key)).map(([key, readyEvent]) => ({ key, readyEvent, hasBody: false, lines: [noopPluginRegistrationLine(key, readyEvent)] }));
|
|
5228
|
+
return [...withBody, ...noop];
|
|
5229
|
+
}
|
|
4839
5230
|
function pushPlugin(plugins, spec) {
|
|
4840
5231
|
if (plugins.some((plugin) => plugin.key === spec.key)) return;
|
|
4841
5232
|
plugins.push(spec);
|
|
@@ -4894,7 +5285,7 @@ function createHeaderRuntimeGrammar(header) {
|
|
|
4894
5285
|
}
|
|
4895
5286
|
function createGemRuntimeBootstrap(configArg) {
|
|
4896
5287
|
const config = configArg.plugins.some((plugin) => plugin.key === "hero-slideshow") ? configArg : { ...configArg, plugins: [...configArg.plugins, { key: "hero-slideshow", loadStrategy: "idle" }] };
|
|
4897
|
-
const pluginRegistrationLines = [...new Set(config.plugins.map((plugin) => plugin.key))].filter((key) =>
|
|
5288
|
+
const pluginRegistrationLines = [...new Set(config.plugins.map((plugin) => plugin.key))].filter((key) => !runtimePluginSourceLines.some(([sourceKey]) => sourceKey === key)).filter((key) => runtimePluginReadyEvents[key]).map((key) => noopPluginRegistrationLine(key, runtimePluginReadyEvents[key]));
|
|
4898
5289
|
const enabledPluginKeys = new Set(config.plugins.map((plugin) => plugin.key));
|
|
4899
5290
|
return [
|
|
4900
5291
|
"(() => {",
|
|
@@ -4967,28 +5358,11 @@ function createGemRuntimeBootstrap(configArg) {
|
|
|
4967
5358
|
" if (readyEvent) api.emit(readyEvent, { source: 'gem-runtime' });",
|
|
4968
5359
|
" });",
|
|
4969
5360
|
" };",
|
|
4970
|
-
...enabledPluginKeys.has(
|
|
4971
|
-
...enabledPluginKeys.has("mobile-menu") ? mobileMenuPluginRegistrationLines : [],
|
|
4972
|
-
...enabledPluginKeys.has("quantity-input") ? quantityInputPluginRegistrationLines : [],
|
|
4973
|
-
...enabledPluginKeys.has("variant-selects") ? variantSelectsPluginRegistrationLines : [],
|
|
4974
|
-
...enabledPluginKeys.has("predictive-search") ? predictiveSearchPluginRegistrationLines : [],
|
|
4975
|
-
...enabledPluginKeys.has("localization") ? localizationPluginRegistrationLines : [],
|
|
4976
|
-
...enabledPluginKeys.has("disclosure") ? disclosurePluginRegistrationLines : [],
|
|
4977
|
-
...enabledPluginKeys.has("tabs") ? tabsPluginRegistrationLines : [],
|
|
4978
|
-
...enabledPluginKeys.has("media-gallery") ? mediaGalleryPluginRegistrationLines : [],
|
|
4979
|
-
...enabledPluginKeys.has("carousel") ? carouselPluginRegistrationLines : [],
|
|
4980
|
-
// hero-slideshow is always scheduled (added to config above) + self-guards, so always register it.
|
|
4981
|
-
...enabledPluginKeys.has("hero-slideshow") ? heroSlideshowPluginRegistrationLines : [],
|
|
4982
|
-
...enabledPluginKeys.has("quick-view") ? quickViewPluginRegistrationLines : [],
|
|
4983
|
-
...enabledPluginKeys.has("promo-popup") ? promoPopupPluginRegistrationLines : [],
|
|
4984
|
-
...enabledPluginKeys.has("sticky-atc") ? stickyAtcPluginRegistrationLines : [],
|
|
4985
|
-
...enabledPluginKeys.has("before_after") ? beforeAfterPluginRegistrationLines : [],
|
|
4986
|
-
...enabledPluginKeys.has("countdown") ? countdownPluginRegistrationLines : [],
|
|
4987
|
-
...enabledPluginKeys.has("reveal-on-scroll") ? revealOnScrollPluginRegistrationLines : [],
|
|
4988
|
-
...enabledPluginKeys.has("recently-viewed") ? recentlyViewedPluginRegistrationLines : [],
|
|
4989
|
-
...enabledPluginKeys.has("runtime-slot-loader") ? runtimeSlotLoaderPluginRegistrationLines : [],
|
|
4990
|
-
...enabledPluginKeys.has("offer-engine") ? offerEnginePluginRegistrationLines : [],
|
|
5361
|
+
...runtimePluginSourceLines.flatMap(([key, lines]) => enabledPluginKeys.has(key) ? lines : []),
|
|
4991
5362
|
...pluginRegistrationLines,
|
|
5363
|
+
// `data-gem-bound-<key>` is a CONTRACT, not private bookkeeping: markup may carry it to claim an element for its
|
|
5364
|
+
// own behavior (promo-popup.email-capture.v1 renders data-gem-bound-promo-popup="section"). Any value counts as
|
|
5365
|
+
// bound; runtime-core.js mark() keeps the same rule, and its unmount removes only the marks it set.
|
|
4992
5366
|
" const mark = (element, key) => {",
|
|
4993
5367
|
" const attr = 'data-gem-bound-' + key;",
|
|
4994
5368
|
" if (!element || element.hasAttribute(attr)) return false;",
|
|
@@ -5063,6 +5437,63 @@ function createGemRuntimeBootstrap(configArg) {
|
|
|
5063
5437
|
].join("\n");
|
|
5064
5438
|
}
|
|
5065
5439
|
|
|
5440
|
+
// ../../packages/gem-framework/src/runtime-modules.ts
|
|
5441
|
+
var LISTEN_CALL = /(?<![\w$.\])?])([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.addEventListener\(/g;
|
|
5442
|
+
function routeListenersThroughLedger(body, key = "plugin") {
|
|
5443
|
+
const routed = body.replace(LISTEN_CALL, "__gemListen($1, ");
|
|
5444
|
+
const unseen = routed.match(/.{0,40}\.addEventListener\(/);
|
|
5445
|
+
if (unseen) throw new Error(`gem-framework: plugin "${key}" adds a listener the module cannot track: ${unseen[0]}`);
|
|
5446
|
+
return routed;
|
|
5447
|
+
}
|
|
5448
|
+
var PLUGIN_GUARDS = {
|
|
5449
|
+
"offer-engine": { selector: "[data-gem-offer-engine]", prop: "__gemOffer" }
|
|
5450
|
+
};
|
|
5451
|
+
function gemRuntimePluginModuleKeys() {
|
|
5452
|
+
return gemRuntimePluginSources().map((source) => source.key);
|
|
5453
|
+
}
|
|
5454
|
+
function createGemRuntimePluginModule(key) {
|
|
5455
|
+
const source = gemRuntimePluginSources().find((candidate) => candidate.key === key);
|
|
5456
|
+
if (!source) throw new Error(`gem-framework: no runtime plugin "${key}"`);
|
|
5457
|
+
const body = routeListenersThroughLedger(source.lines.join("\n"), key);
|
|
5458
|
+
const guard = PLUGIN_GUARDS[key];
|
|
5459
|
+
return [
|
|
5460
|
+
`// @gemboss/gem-framework/plugins/${key}: the gem-runtime plugin "${key}" as an ES module. Generated from`,
|
|
5461
|
+
"// packages/gem-framework/src/runtime.ts, the same lines gem-runtime.js carries; do not edit here.",
|
|
5462
|
+
'import { mountPlugin } from "../runtime-core.js";',
|
|
5463
|
+
"",
|
|
5464
|
+
`export const key = ${JSON.stringify(key)};`,
|
|
5465
|
+
`export const readyEvent = ${JSON.stringify(source.readyEvent)};`,
|
|
5466
|
+
"",
|
|
5467
|
+
"function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {",
|
|
5468
|
+
body,
|
|
5469
|
+
"}",
|
|
5470
|
+
"",
|
|
5471
|
+
`/** Bind every "${key}" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */`,
|
|
5472
|
+
"export function mount(root, options) {",
|
|
5473
|
+
` return mountPlugin(key, install, root, options${guard ? `, ${JSON.stringify(guard)}` : ""});`,
|
|
5474
|
+
"}",
|
|
5475
|
+
"",
|
|
5476
|
+
"export default mount;",
|
|
5477
|
+
""
|
|
5478
|
+
].join("\n");
|
|
5479
|
+
}
|
|
5480
|
+
function createGemRuntimePluginModuleTypes(key) {
|
|
5481
|
+
const source = gemRuntimePluginSources().find((candidate) => candidate.key === key);
|
|
5482
|
+
if (!source) throw new Error(`gem-framework: no runtime plugin "${key}"`);
|
|
5483
|
+
return [
|
|
5484
|
+
`// @gemboss/gem-framework/plugins/${key}: types of the ES module next to this file. Generated; do not edit here.`,
|
|
5485
|
+
'import type { MountOptions, Unmount } from "../runtime-core.js";',
|
|
5486
|
+
"",
|
|
5487
|
+
"export type { MountOptions, Unmount };",
|
|
5488
|
+
`export declare const key: ${JSON.stringify(key)};`,
|
|
5489
|
+
`export declare const readyEvent: ${JSON.stringify(source.readyEvent)};`,
|
|
5490
|
+
`/** Bind every "${key}" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */`,
|
|
5491
|
+
"export declare function mount(root?: ParentNode | null, options?: MountOptions): Unmount;",
|
|
5492
|
+
"export default mount;",
|
|
5493
|
+
""
|
|
5494
|
+
].join("\n");
|
|
5495
|
+
}
|
|
5496
|
+
|
|
5066
5497
|
// ../../packages/gem-framework/src/validation.ts
|
|
5067
5498
|
var validationRulesByCode = new Map(gemFrameworkValidationRules.map((rule) => [rule.code, rule]));
|
|
5068
5499
|
var primitiveClassNames = new Set(
|
|
@@ -5088,7 +5519,9 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
|
5088
5519
|
"--gem-icon-size",
|
|
5089
5520
|
"--gem-drawer-offset",
|
|
5090
5521
|
// The row primitive's variables (primitives.ts GEM_ROW_CSS). Sections SET these on their grid.
|
|
5091
|
-
...GEM_ROW_CSS_VARIABLES
|
|
5522
|
+
...GEM_ROW_CSS_VARIABLES,
|
|
5523
|
+
// The form set's variables (form-set.ts): set from a plan's `tokens.form`, read by fields under [data-gem-form-set].
|
|
5524
|
+
...GEM_FORM_CSS_VARIABLES
|
|
5092
5525
|
]);
|
|
5093
5526
|
var TOKENIZED_PROPERTIES = [
|
|
5094
5527
|
"background",
|
|
@@ -5310,6 +5743,7 @@ function renderGemPreviewDocument(plan, options = {}) {
|
|
|
5310
5743
|
const footer = renderGemShellFooter(plan);
|
|
5311
5744
|
const sectionsHtml = renderGemPageSections(plan, page, options.sectionMarkup ?? {});
|
|
5312
5745
|
const title = options.title ?? `${plan.name} preview`;
|
|
5746
|
+
const formAttrs = Object.entries(gemFormAttributes(plan.tokens.form)).map(([k, v]) => ` ${k}="${escapeAttribute(v)}"`).join("");
|
|
5313
5747
|
const bodyClass = ["gem-theme", `gem-theme--${slugify(profile)}`, options.bodyClassName].filter(Boolean).join(" ");
|
|
5314
5748
|
return [
|
|
5315
5749
|
"<!doctype html>",
|
|
@@ -5320,7 +5754,7 @@ function renderGemPreviewDocument(plan, options = {}) {
|
|
|
5320
5754
|
` <title>${escapeHtml(title)}</title>`,
|
|
5321
5755
|
` <style>${css}</style>`,
|
|
5322
5756
|
"</head>",
|
|
5323
|
-
`<body class="${escapeAttribute(bodyClass)}" data-gem-plan-id="${escapeAttribute(plan.id)}" data-gem-style-pack-id="${escapeAttribute(plan.style_pack?.id ?? "none")}" data-gem-button-style="${escapeAttribute(plan.global_settings?.button_style ?? "solid")}" data-gem-preview-page="${escapeAttribute(page)}">`,
|
|
5757
|
+
`<body class="${escapeAttribute(bodyClass)}" data-gem-plan-id="${escapeAttribute(plan.id)}" data-gem-style-pack-id="${escapeAttribute(plan.style_pack?.id ?? "none")}" data-gem-button-style="${escapeAttribute(plan.global_settings?.button_style ?? "solid")}" data-gem-preview-page="${escapeAttribute(page)}"${formAttrs}>`,
|
|
5324
5758
|
' <a class="gem-skip-to-content-link gem-u-visually-hidden" href="#MainContent">Skip to content</a>',
|
|
5325
5759
|
header,
|
|
5326
5760
|
` <main id="MainContent" class="gem-main" data-gem-page="${escapeAttribute(page)}">`,
|
|
@@ -6370,6 +6804,14 @@ export {
|
|
|
6370
6804
|
COUNTDOWN_END_RUNTIME_LINES,
|
|
6371
6805
|
GEM_AMBIENT_CSS,
|
|
6372
6806
|
GEM_COUNTDOWN_ENDED_CSS,
|
|
6807
|
+
GEM_FIELD_JOINS,
|
|
6808
|
+
GEM_FIELD_SHAPES,
|
|
6809
|
+
GEM_FORM_CSS_VARIABLES,
|
|
6810
|
+
GEM_FORM_DEFAULTS,
|
|
6811
|
+
GEM_FORM_SETS,
|
|
6812
|
+
GEM_FORM_SET_CSS,
|
|
6813
|
+
GEM_FORM_TOKEN_KEYS,
|
|
6814
|
+
GEM_FORM_VAR_SCOPE,
|
|
6373
6815
|
GEM_GRID_COLUMNS_ROW_KNOB_CSS,
|
|
6374
6816
|
GEM_PART_CLASS,
|
|
6375
6817
|
GEM_POPUP_SHELL_CSS,
|
|
@@ -6386,9 +6828,12 @@ export {
|
|
|
6386
6828
|
createGemBaseCss,
|
|
6387
6829
|
createGemFrameworkManifest,
|
|
6388
6830
|
createGemRuntimeBootstrap,
|
|
6831
|
+
createGemRuntimePluginModule,
|
|
6832
|
+
createGemRuntimePluginModuleTypes,
|
|
6389
6833
|
createGemSectionRuntimeContractCss,
|
|
6390
6834
|
createGemStyleKnobCss,
|
|
6391
6835
|
createGemThemeCss,
|
|
6836
|
+
createGemThemeCssParts,
|
|
6392
6837
|
createGemTokenCss as createGemTokenVariablesCss,
|
|
6393
6838
|
createHeaderRuntimeGrammar,
|
|
6394
6839
|
createMorphProfileCss,
|
|
@@ -6399,12 +6844,18 @@ export {
|
|
|
6399
6844
|
createThemeProfileCss,
|
|
6400
6845
|
defaultGemTokens,
|
|
6401
6846
|
defaultSelection,
|
|
6847
|
+
gemFormAttributes,
|
|
6848
|
+
gemFormCssBlock,
|
|
6849
|
+
gemFormCssVars,
|
|
6850
|
+
gemFormSetsFor,
|
|
6402
6851
|
gemFrameworkPrimitiveGroups,
|
|
6403
6852
|
gemFrameworkRuntimePlugins,
|
|
6404
6853
|
gemFrameworkValidationRules,
|
|
6405
6854
|
gemFrameworkVersion,
|
|
6406
6855
|
gemPartClass,
|
|
6407
6856
|
gemPlanShapeKnobs,
|
|
6857
|
+
gemRuntimePluginModuleKeys,
|
|
6858
|
+
gemRuntimePluginSources,
|
|
6408
6859
|
getGemFrameworkValidationRule,
|
|
6409
6860
|
isModernMineralFamilyLineagePlan,
|
|
6410
6861
|
modernMineralFamilyLineage,
|