@gemboss/gem-framework 0.4.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 +42 -0
- package/dist/SOURCE.json +7 -0
- package/dist/gem-framework.css +2493 -0
- package/dist/gem-runtime.js +880 -0
- package/dist/index.js +6061 -0
- package/dist/section-gate.js +1199 -0
- package/dist/sections.js +112 -0
- package/dist/settings-schema.js +254 -0
- package/package.json +34 -0
|
@@ -0,0 +1,1199 @@
|
|
|
1
|
+
// ../../packages/gem-framework/src/tokens.ts
|
|
2
|
+
var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
|
|
3
|
+
var GEM_TAP_MIN_CSS_VAR = "--gem-tap-min: 44px;";
|
|
4
|
+
var GEM_ROW_CSS_VARIABLES = [
|
|
5
|
+
"--gem-row-cols",
|
|
6
|
+
"--gem-row-cols-md",
|
|
7
|
+
"--gem-row-cols-sm",
|
|
8
|
+
"--gem-row-tracks",
|
|
9
|
+
"--gem-row-tracks-md",
|
|
10
|
+
"--gem-row-tracks-sm",
|
|
11
|
+
"--gem-row-gap",
|
|
12
|
+
"--gem-row-gap-y"
|
|
13
|
+
];
|
|
14
|
+
var requiredGemTokenKeys = {
|
|
15
|
+
color: ["background", "surface", "text", "muted", "accent", "accentText", "border", "success", "warning", "danger"],
|
|
16
|
+
typography: ["body", "heading", "mono"],
|
|
17
|
+
spacing: ["xs", "sm", "md", "lg", "xl", "2xl", "3xl"],
|
|
18
|
+
radius: ["sm", "md", "lg", "pill"],
|
|
19
|
+
border: ["subtle", "strong"],
|
|
20
|
+
shadow: ["sm", "md"],
|
|
21
|
+
motion: ["duration", "easing"],
|
|
22
|
+
layout: ["page", "gutter", "section"],
|
|
23
|
+
component: ["headerHeight", "cardPadding", "buttonPaddingInline", "buttonPaddingBlock", "inputHeight"],
|
|
24
|
+
morphology: ["campaignDensity", "mediaRhythm", "proofIntensity", "commerceEmphasis", "policyTone"]
|
|
25
|
+
};
|
|
26
|
+
function gemTypeRampCssVars() {
|
|
27
|
+
return [
|
|
28
|
+
"--gem-type-base: 1rem;",
|
|
29
|
+
"--gem-type-scale: 1.22;",
|
|
30
|
+
"--gem-text-sm: calc(var(--gem-type-base) / var(--gem-type-scale));",
|
|
31
|
+
"--gem-body: var(--gem-type-base);",
|
|
32
|
+
"--gem-h6: calc(var(--gem-type-base) * var(--gem-type-scale));",
|
|
33
|
+
"--gem-h5: calc(var(--gem-h6) * var(--gem-type-scale));",
|
|
34
|
+
"--gem-h4: calc(var(--gem-h5) * var(--gem-type-scale));",
|
|
35
|
+
"--gem-h3: calc(var(--gem-h4) * var(--gem-type-scale));",
|
|
36
|
+
"--gem-h2: calc(var(--gem-h3) * var(--gem-type-scale));",
|
|
37
|
+
"--gem-h1: calc(var(--gem-h2) * var(--gem-type-scale));"
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
function slugify(value) {
|
|
41
|
+
return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ../../packages/gem-framework/src/manifest.ts
|
|
45
|
+
var gemFrameworkPrimitiveGroups = [
|
|
46
|
+
{
|
|
47
|
+
group: "layout",
|
|
48
|
+
// `gem-product-grid` is the shared product-card grid six sections lay their cards out on
|
|
49
|
+
// (featured-collection, product-card-grid, product-bundle, recently-viewed, and both
|
|
50
|
+
// recommended-products variants). It is styled once, globally, in the headless storefront —
|
|
51
|
+
// it was never section-scoped — but it had no entry here, so every section using it failed
|
|
52
|
+
// markup validation with `gem_framework_unknown_gem_class`.
|
|
53
|
+
// `gem-row` is the column primitive (primitives.ts GEM_ROW_CSS): counts and ratios per breakpoint
|
|
54
|
+
// through --gem-row-* variables, shared by section CSS and the element-tree Row.
|
|
55
|
+
classes: ["gem-container", "gem-stack", "gem-cluster", "gem-grid", "gem-row", "gem-product-grid"]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
group: "actions",
|
|
59
|
+
classes: ["gem-button", "gem-button--primary", "gem-button--secondary", "gem-button--soft", "gem-button--link"]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
group: "surfaces",
|
|
63
|
+
classes: ["gem-card", "gem-media", "gem-badge", "gem-accordion", "gem-divider"]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
group: "forms",
|
|
67
|
+
classes: [
|
|
68
|
+
"gem-form-field",
|
|
69
|
+
"gem-input",
|
|
70
|
+
"gem-checkbox",
|
|
71
|
+
"gem-select",
|
|
72
|
+
"gem-textarea",
|
|
73
|
+
"gem-submit",
|
|
74
|
+
"gem-newsletter-form"
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
group: "content",
|
|
79
|
+
classes: [
|
|
80
|
+
"gem-heading",
|
|
81
|
+
"gem-text",
|
|
82
|
+
// Centred, max-width reading column for long-form copy — styled globally in the headless
|
|
83
|
+
// storefront alongside gem-section__ctas.
|
|
84
|
+
"gem-prose",
|
|
85
|
+
"gem-icon",
|
|
86
|
+
"gem-icon-list",
|
|
87
|
+
"gem-icon-list__item",
|
|
88
|
+
"gem-video",
|
|
89
|
+
"gem-marquee",
|
|
90
|
+
"gem-marquee__track",
|
|
91
|
+
"gem-marquee__viewport",
|
|
92
|
+
"gem-marquee--reverse",
|
|
93
|
+
"gem-carousel",
|
|
94
|
+
"gem-carousel__viewport",
|
|
95
|
+
"gem-carousel__track",
|
|
96
|
+
"gem-carousel__slide",
|
|
97
|
+
"gem-carousel__controls",
|
|
98
|
+
"gem-article-card",
|
|
99
|
+
"gem-article-card__media",
|
|
100
|
+
"gem-article-card__meta",
|
|
101
|
+
"gem-article-card__title",
|
|
102
|
+
"gem-article-card__excerpt",
|
|
103
|
+
"gem-article-card__content",
|
|
104
|
+
// measured editorial atoms mapped by parts.ts (GEM_PART_CLASS) — styled primitives a gemcapture
|
|
105
|
+
// rebuild pours copy into; declared here so they don't read as unknown_gem_class.
|
|
106
|
+
"gem-pull-quote",
|
|
107
|
+
"gem-byline",
|
|
108
|
+
"gem-disclaimer",
|
|
109
|
+
"gem-subhead"
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
group: "commerce",
|
|
114
|
+
classes: [
|
|
115
|
+
"gem-richtext",
|
|
116
|
+
"gem-price",
|
|
117
|
+
// BEM children + state of primitives that createPrimitiveCss ALREADY styles and runtime.ts / parts.ts
|
|
118
|
+
// already emit (parts.ts: compare_at_price → gem-price__compare, rating → gem-stars). Styled but
|
|
119
|
+
// unregistered, so a section using the framework's own price / stars / quantity markup failed
|
|
120
|
+
// `gem_framework_unknown_gem_class` — and at stable that warning is an error. Measured 28/09/2026
|
|
121
|
+
// porting four designlab (now gemlabs-gemfactory) sections: 75 warnings, every one of them a class/var this package defines.
|
|
122
|
+
"gem-price__regular",
|
|
123
|
+
"gem-price__compare",
|
|
124
|
+
"gem-price__unit",
|
|
125
|
+
"gem-price--on-sale",
|
|
126
|
+
"gem-rating",
|
|
127
|
+
"gem-proof",
|
|
128
|
+
"gem-product-card",
|
|
129
|
+
"gem-product-card__media",
|
|
130
|
+
// The framework's own CSS styles this (primitives.ts) and the Shopify adapter emits it, so it was a
|
|
131
|
+
// primitive everywhere except the list that decides what a primitive is. A section using it was
|
|
132
|
+
// refused by the validator for using a class the framework itself ships.
|
|
133
|
+
"gem-product-card__placeholder",
|
|
134
|
+
"gem-product-card__title",
|
|
135
|
+
"gem-product-card__price",
|
|
136
|
+
"gem-product-card__badges",
|
|
137
|
+
// Styled in primitives.ts alongside its siblings, but never listed here — so a section using
|
|
138
|
+
// it (product-card-grid.quick-view-grid) failed the gem-namespace check for a class the
|
|
139
|
+
// framework itself ships.
|
|
140
|
+
"gem-product-card__content",
|
|
141
|
+
"gem-product-card__quick-actions",
|
|
142
|
+
"gem-product-card__rating",
|
|
143
|
+
"gem-product-card__vendor",
|
|
144
|
+
"gem-product-card__description",
|
|
145
|
+
"gem-product-card__swatches",
|
|
146
|
+
"gem-product-card__secondary-image",
|
|
147
|
+
"gem-product-card--sold-out",
|
|
148
|
+
"gem-product-meta",
|
|
149
|
+
"gem-product-meta__item",
|
|
150
|
+
"gem-product-meta__label",
|
|
151
|
+
"gem-product-meta__value",
|
|
152
|
+
"gem-product-grid",
|
|
153
|
+
"gem-variant-picker",
|
|
154
|
+
"gem-swatch",
|
|
155
|
+
"gem-quantity-selector",
|
|
156
|
+
// What the quantity-input runtime plugin binds to (`[data-gem-quantity-input], .gem-quantity-input`).
|
|
157
|
+
"gem-quantity-input",
|
|
158
|
+
"gem-quantity-input__button",
|
|
159
|
+
"gem-quantity-input__field",
|
|
160
|
+
// What the media-gallery runtime plugin binds to (`.gem-product-media-gallery`).
|
|
161
|
+
"gem-product-media-gallery",
|
|
162
|
+
"gem-product-media-gallery__frame",
|
|
163
|
+
"gem-product-media-gallery__item",
|
|
164
|
+
"gem-product-media-gallery__thumbs",
|
|
165
|
+
"gem-product-media-gallery__thumb",
|
|
166
|
+
"gem-sticky-atc",
|
|
167
|
+
"gem-promo-banner",
|
|
168
|
+
"gem-promo-tile",
|
|
169
|
+
"gem-trust-list",
|
|
170
|
+
"gem-tabs",
|
|
171
|
+
"gem-breadcrumbs",
|
|
172
|
+
"gem-filter-bar",
|
|
173
|
+
"gem-drawer",
|
|
174
|
+
"gem-modal",
|
|
175
|
+
"gem-copy-code",
|
|
176
|
+
"gem-delivery-date",
|
|
177
|
+
"gem-stock-indicator",
|
|
178
|
+
"gem-popup",
|
|
179
|
+
"gem-back-to-top",
|
|
180
|
+
// measured commerce atoms mapped by parts.ts (GEM_PART_CLASS) alongside gem-price/gem-badge.
|
|
181
|
+
"gem-discount",
|
|
182
|
+
"gem-stars",
|
|
183
|
+
"gem-stars__base",
|
|
184
|
+
"gem-stars__fill",
|
|
185
|
+
"gem-review-count",
|
|
186
|
+
// The cart line: primitives.ts styles it (grid, media, details, meta, price, and the drawer's
|
|
187
|
+
// two-column override), and the Shopify adapter renders it inside SECTIONS
|
|
188
|
+
// (snippets/gem-cart-line-item.liquid in gem-cart-main-line-items and gem-main-order), yet it
|
|
189
|
+
// was never listed. GemFactory's cart-drawer used the framework's own line markup and got 9
|
|
190
|
+
// unknown_gem_class warnings; renaming it to a section-scoped class silenced them and dropped the
|
|
191
|
+
// framework layout (measured 28/09/2026: the screenshot changed). __variant / __plan are emitted
|
|
192
|
+
// by the adapter but not styled.
|
|
193
|
+
"gem-cart-line-item",
|
|
194
|
+
"gem-cart-line-item__media",
|
|
195
|
+
"gem-cart-line-item__details",
|
|
196
|
+
"gem-cart-line-item__title",
|
|
197
|
+
"gem-cart-line-item__variant",
|
|
198
|
+
"gem-cart-line-item__plan",
|
|
199
|
+
"gem-cart-line-item__meta",
|
|
200
|
+
"gem-cart-line-item__remove",
|
|
201
|
+
"gem-cart-line-item__quantity",
|
|
202
|
+
"gem-cart-line-item__price"
|
|
203
|
+
]
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
group: "sections",
|
|
207
|
+
classes: [
|
|
208
|
+
"gem-section",
|
|
209
|
+
"gem-section__inner",
|
|
210
|
+
"gem-section__content",
|
|
211
|
+
"gem-section__eyebrow",
|
|
212
|
+
"gem-section__heading",
|
|
213
|
+
"gem-section__body",
|
|
214
|
+
// The header block above a section's body, and its two variants. Styled in the SHARED bridge
|
|
215
|
+
// stylesheet (`gem-dawn-bridge.css`, right beside `gem-section__body` which was already here)
|
|
216
|
+
// and used by 51 shipping Liquid sections — so it belongs to no single section and cannot be
|
|
217
|
+
// section-scoped. It was simply never added: this list enumerates BEM children by hand, and
|
|
218
|
+
// three of the seven `gem-section__*` classes the bridge styles had drifted off it. Sections
|
|
219
|
+
// using the framework correctly were failing promotion for it.
|
|
220
|
+
"gem-section__header",
|
|
221
|
+
"gem-section__header--center",
|
|
222
|
+
"gem-section__subheading",
|
|
223
|
+
"gem-section__grid",
|
|
224
|
+
// The CTA row under a section's copy. Styled globally in the headless storefront
|
|
225
|
+
// (`.gem-section__ctas`, plus an `:empty` rule), never section-scoped.
|
|
226
|
+
"gem-section__ctas",
|
|
227
|
+
// Model B: the shell modifier SectionTreeView puts on a section whose body is an element tree
|
|
228
|
+
// rather than a vendored component. One shared modifier for every such section, so it is a
|
|
229
|
+
// primitive here and could never be section-scoped.
|
|
230
|
+
"gem-section--elements"
|
|
231
|
+
]
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
group: "shell",
|
|
235
|
+
classes: [
|
|
236
|
+
"gem-shell-header",
|
|
237
|
+
"gem-shell-header--sticky",
|
|
238
|
+
"gem-shell-header--editorial",
|
|
239
|
+
"gem-shell-header--search-led",
|
|
240
|
+
"gem-shell-header--mega-menu",
|
|
241
|
+
"gem-shell-header--split-nav",
|
|
242
|
+
"gem-shell-header--marketplace",
|
|
243
|
+
"gem-shell-header--promo-stack",
|
|
244
|
+
"gem-shell-header--sticky-search",
|
|
245
|
+
"gem-shell-header__announcement",
|
|
246
|
+
"gem-shell-header__utility",
|
|
247
|
+
"gem-shell-header__inner",
|
|
248
|
+
"gem-shell-header__logo",
|
|
249
|
+
"gem-shell-header__nav",
|
|
250
|
+
"gem-shell-header__nav--editorial",
|
|
251
|
+
"gem-shell-header__nav--mega",
|
|
252
|
+
"gem-shell-header__actions",
|
|
253
|
+
"gem-shell-header__action--search",
|
|
254
|
+
"gem-shell-header__action--cart",
|
|
255
|
+
"gem-shell-header__mega",
|
|
256
|
+
"gem-shell-header__departments",
|
|
257
|
+
"gem-shell-header__promo-stack",
|
|
258
|
+
"gem-shell-header__editorial-note",
|
|
259
|
+
"gem-shell-header__search",
|
|
260
|
+
"gem-shell-header__search--sticky",
|
|
261
|
+
"gem-shell-footer",
|
|
262
|
+
"gem-shell-footer--sitemap",
|
|
263
|
+
"gem-shell-footer--support-first",
|
|
264
|
+
"gem-shell-footer--community",
|
|
265
|
+
"gem-shell-footer--localization-rich",
|
|
266
|
+
"gem-shell-footer__inner",
|
|
267
|
+
"gem-shell-footer__newsletter",
|
|
268
|
+
"gem-shell-footer__grid",
|
|
269
|
+
"gem-shell-footer__story",
|
|
270
|
+
"gem-shell-footer__compliance",
|
|
271
|
+
"gem-shell-footer__offer",
|
|
272
|
+
"gem-shell-footer__support",
|
|
273
|
+
"gem-shell-footer__community",
|
|
274
|
+
"gem-shell-footer__markets",
|
|
275
|
+
"gem-shell-footer__trust"
|
|
276
|
+
]
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
group: "utilities",
|
|
280
|
+
classes: [
|
|
281
|
+
"gem-u-hidden",
|
|
282
|
+
"gem-u-visually-hidden",
|
|
283
|
+
"gem-u-center",
|
|
284
|
+
"gem-u-muted",
|
|
285
|
+
"gem-u-surface",
|
|
286
|
+
"gem-u-accent",
|
|
287
|
+
"gem-u-stack-sm",
|
|
288
|
+
"gem-u-stack-md",
|
|
289
|
+
"gem-u-stack-lg"
|
|
290
|
+
// gem-u-grid-2/3/4 removed 28/09/2026: no section, theme or renderer in the fleet used them
|
|
291
|
+
// (gemtheme, gemsite, gemboss, gemcapture, GemFactory: 0), and they could not work alone — a
|
|
292
|
+
// grid-template with no display:grid, no gap and one breakpoint. Columns are `gem-row` now.
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
];
|
|
296
|
+
var gemFrameworkValidationRules = [
|
|
297
|
+
{ code: "legacy_gs_class", severity: "error", surface: "artifact" },
|
|
298
|
+
{ code: "non_gem_class", severity: "error", surface: "artifact" },
|
|
299
|
+
{ code: "unknown_gem_class", severity: "warn", surface: "artifact" },
|
|
300
|
+
{ code: "unknown_gem_css_variable", severity: "warn", surface: "css" },
|
|
301
|
+
{ code: "untokenized_visual_value", severity: "warn", surface: "css" },
|
|
302
|
+
{ code: "missing_data_gem_marker", severity: "warn", surface: "markup" },
|
|
303
|
+
{ code: "inline_script", severity: "error", surface: "runtime" },
|
|
304
|
+
{ code: "external_dependency", severity: "error", surface: "runtime" }
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
// ../../packages/gem-framework/src/primitives.ts
|
|
308
|
+
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")`;
|
|
309
|
+
var spikePath = (() => {
|
|
310
|
+
const teeth = [];
|
|
311
|
+
for (let x = 95; x > 0; x -= 5) teeth.push(`L${x} ${teeth.length % 2 === 0 ? 10 : 3}`);
|
|
312
|
+
return `M0 0 H100 V3 ${teeth.join(" ")} L0 ${teeth.length % 2 === 0 ? 10 : 3} Z`;
|
|
313
|
+
})();
|
|
314
|
+
var DIVIDER_SHAPES = {
|
|
315
|
+
wave: "M0 0 H100 V3 C80 9 70 -1 50 4 C30 9 20 1 0 4 Z",
|
|
316
|
+
slope: "M0 0 H100 V10 Z",
|
|
317
|
+
spikes: spikePath,
|
|
318
|
+
arch: "M0 0 H100 V2 Q50 12 0 2 Z",
|
|
319
|
+
blob: "M0 0 H100 V4 C85 11 72 0 58 5 C44 10 30 2 16 6 C10 8 5 5 0 6 Z"
|
|
320
|
+
};
|
|
321
|
+
var GEM_DIVIDER_CSS_LINES = [
|
|
322
|
+
// The clip is WIDENED, not removed. `.gem-section` sets `overflow: clip` and the wedge is drawn
|
|
323
|
+
// at `top: 100%` / `bottom: 100%` — entirely outside the box — so the parent deleted every
|
|
324
|
+
// divider on every track. Measured on the live storefront 2026-08-25: a band with the documented
|
|
325
|
+
// `data-gem-section-background="dark"` + `data-gem-divider-bottom="wave"` rendered a FLAT edge,
|
|
326
|
+
// while the attribute, the rule and the fallback colour were all present and correct.
|
|
327
|
+
// NOT `overflow: visible`: `.gem-hero` uses the clip to hold its own border-radius, so releasing
|
|
328
|
+
// it entirely would paint square corners over that radius. A browser without
|
|
329
|
+
// `overflow-clip-margin` keeps plain `clip` — the divider stays hidden, which is today's
|
|
330
|
+
// behaviour rather than a broken layout.
|
|
331
|
+
":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); }",
|
|
332
|
+
// Shared geometry. `top: 100%` / `bottom: 100%` put it outside the box; z-index 1 keeps it above the
|
|
333
|
+
// neighbouring band's background. pointer-events: none so it never eats a click near the seam.
|
|
334
|
+
// Qualified by SHAPE. Unqualified, any value paints a solid 48px slab — including the literal
|
|
335
|
+
// "none", which the attribute writer forwards unchanged. The clip used to hide that; widening
|
|
336
|
+
// it above makes a stray value visible.
|
|
337
|
+
":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%; }",
|
|
338
|
+
":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%; }",
|
|
339
|
+
// The top edge reuses the same masks, mirrored, so five shapes cover both edges.
|
|
340
|
+
":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); }",
|
|
341
|
+
...Object.entries(DIVIDER_SHAPES).flatMap(([name, path]) => {
|
|
342
|
+
const mask = dividerMask(path);
|
|
343
|
+
return [
|
|
344
|
+
`: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}; }`,
|
|
345
|
+
`: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}; }`
|
|
346
|
+
];
|
|
347
|
+
})
|
|
348
|
+
];
|
|
349
|
+
var GEM_DIVIDER_CSS = GEM_DIVIDER_CSS_LINES.join("\n");
|
|
350
|
+
var GEM_CARD_CSS_LINES = [
|
|
351
|
+
":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); }",
|
|
352
|
+
":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); }",
|
|
353
|
+
":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); }",
|
|
354
|
+
// `filled` tints the section's own ground rather than reaching for a fixed colour, so it stays
|
|
355
|
+
// legible on an accent or dark band instead of punching a light panel into it.
|
|
356
|
+
":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); }"
|
|
357
|
+
];
|
|
358
|
+
var GEM_CARD_CSS = GEM_CARD_CSS_LINES.join("\n");
|
|
359
|
+
var GEM_REVEAL_CSS_LINES = [
|
|
360
|
+
// Longhand, so the per-element duration/delay knobs can reach it. The shorthand hard-coded 600ms,
|
|
361
|
+
// while `gem_animation_duration` and `gem_animation_delay` are declared on ALL FIFTY kinds in the
|
|
362
|
+
// element seam ("Empty keeps the default (600ms). Capped at 5000."). gemsite honours them; this
|
|
363
|
+
// copy could not, so the knob worked on the headless storefront and did nothing on the exported
|
|
364
|
+
// Shopify theme — offered to every seller, inert on one of the two tracks.
|
|
365
|
+
//
|
|
366
|
+
// The fallbacks are the values this rule shipped with, so a theme where nothing writes the
|
|
367
|
+
// variables renders EXACTLY as before. Note what this does not finish: the exported theme has no
|
|
368
|
+
// writer for `--gem-el-motion-*` yet (gemsite writes them in element-motion.ts), so the knob stays
|
|
369
|
+
// inert there until one exists. This removes the CSS half of that gap, not the whole gap.
|
|
370
|
+
".gem-reveal, [data-gem-reveal] { transition-property: opacity, transform, filter; transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1); transition-duration: var(--gem-el-motion-duration, 600ms); transition-delay: var(--gem-el-motion-delay, 0ms); }",
|
|
371
|
+
// Bare `data-gem-reveal` (no value) keeps its original rise-and-fade — the candidate sections and
|
|
372
|
+
// gempages blocks that already opt in this way must not change behaviour.
|
|
373
|
+
"[data-gem-reveal][data-gem-reveal-state='ready'] { opacity: 0; transform: translateY(24px); }",
|
|
374
|
+
"[data-gem-reveal][data-gem-reveal-state='in'] { opacity: 1; transform: none; filter: none; }",
|
|
375
|
+
// The set_layout `reveal` knob writes a VALUE, and each value picks its own resting pose. Only the
|
|
376
|
+
// 'ready' state differs — 'in' above returns every variant to the same neutral end state.
|
|
377
|
+
"[data-gem-reveal='fade'][data-gem-reveal-state='ready'] { opacity: 0; transform: none; }",
|
|
378
|
+
"[data-gem-reveal='rise'][data-gem-reveal-state='ready'] { opacity: 0; transform: translateY(24px); }",
|
|
379
|
+
"[data-gem-reveal='zoom'][data-gem-reveal-state='ready'] { opacity: 0; transform: scale(0.94); }",
|
|
380
|
+
"[data-gem-reveal='blur'][data-gem-reveal-state='ready'] { opacity: 0; filter: blur(10px); transform: none; }",
|
|
381
|
+
"@media (prefers-reduced-motion: reduce) { [data-gem-reveal] { opacity: 1 !important; transform: none !important; filter: none !important; transition: none; } }"
|
|
382
|
+
];
|
|
383
|
+
var GEM_REVEAL_CSS = GEM_REVEAL_CSS_LINES.join("\n");
|
|
384
|
+
var GEM_AMBIENT_CSS_LINES = [
|
|
385
|
+
"@keyframes gem-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(calc(-1 * var(--gem-ambient-amp, 7px))); } }",
|
|
386
|
+
"@keyframes gem-sheen { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }",
|
|
387
|
+
"@keyframes gem-marquee-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }",
|
|
388
|
+
"[data-gem-ambient='float'] { animation: gem-float var(--gem-ambient-mid, 7s) var(--gem-ease-soft, ease-in-out) infinite; will-change: transform; }",
|
|
389
|
+
"[data-gem-ambient='sheen'] { animation: gem-sheen var(--gem-ambient-slow, 9s) var(--gem-ease-soft, ease-in-out) infinite; will-change: opacity; }",
|
|
390
|
+
"[data-gem-ambient='marquee'] { animation: gem-marquee-scroll var(--gem-ambient-fast, 5.5s) linear infinite; will-change: transform; }",
|
|
391
|
+
// IO gate — the runtime toggles this attribute; unobserved nodes simply never pause (safe default).
|
|
392
|
+
"[data-gem-offscreen] { animation-play-state: paused; }",
|
|
393
|
+
"@media (prefers-reduced-motion: reduce) {",
|
|
394
|
+
" [data-gem-ambient] { --gem-ambient-amp: 2px; animation-duration: 12s; }",
|
|
395
|
+
" [data-gem-reveal] { transition: none; opacity: 1; transform: none; }",
|
|
396
|
+
" .gem-marquee__track { animation-duration: calc(var(--gem-marquee-duration, 40s) * 2.5); }",
|
|
397
|
+
"}"
|
|
398
|
+
];
|
|
399
|
+
var GEM_AMBIENT_CSS = GEM_AMBIENT_CSS_LINES.join("\n");
|
|
400
|
+
var GEM_ROW_GAP_CSS_LINES = [
|
|
401
|
+
".gem-row { column-gap: var(--gem-row-gap, var(--gem-space-lg)); row-gap: var(--gem-row-gap-y, var(--gem-row-gap, var(--gem-space-lg))); }"
|
|
402
|
+
];
|
|
403
|
+
var GEM_ROW_CSS_LINES = [
|
|
404
|
+
".gem-row:where(:not([hidden])) { --gem-row-auto-sm: repeat(var(--gem-row-cols-sm), minmax(0, 1fr)); --gem-row-auto-md: repeat(var(--gem-row-cols-md), minmax(0, 1fr)); --gem-row-auto-lg: repeat(var(--gem-row-cols), minmax(0, 1fr)); display: grid; grid-template-columns: var(--gem-row-tracks-sm, var(--gem-row-auto-sm, minmax(0, 1fr))); }",
|
|
405
|
+
// :where() = specificity 0: any rule that sets a count on a nested row wins over this reset, whatever
|
|
406
|
+
// its specificity or load order; the reset only has to beat inheritance, and any declaration does.
|
|
407
|
+
`:where(.gem-row > *) { min-width: 0; ${GEM_ROW_CSS_VARIABLES.map((name) => `${name}: initial;`).join(" ")} }`,
|
|
408
|
+
"@media (min-width: 750px) { .gem-row { grid-template-columns: var(--gem-row-tracks-md, var(--gem-row-auto-md, var(--gem-row-tracks, var(--gem-row-auto-lg, repeat(2, minmax(0, 1fr)))))); } }",
|
|
409
|
+
"@media (min-width: 990px) { .gem-row { grid-template-columns: var(--gem-row-tracks, var(--gem-row-auto-lg, repeat(2, minmax(0, 1fr)))); } }"
|
|
410
|
+
];
|
|
411
|
+
var GEM_ROW_GAP_CSS = GEM_ROW_GAP_CSS_LINES.join("\n");
|
|
412
|
+
var GEM_ROW_CSS = GEM_ROW_CSS_LINES.join("\n");
|
|
413
|
+
var GEM_GRID_COLUMNS_KNOB_CSS_LINES = [
|
|
414
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-grid-columns='2'] :is(.gem-section__grid, [class*='__grid']):not(.gem-row) { grid-template-columns: repeat(2, minmax(0, 1fr)); }",
|
|
415
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[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; }",
|
|
416
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-grid-columns='3'] :is(.gem-section__grid, [class*='__grid']):not(.gem-row) { grid-template-columns: repeat(3, minmax(0, 1fr)); }",
|
|
417
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[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; }",
|
|
418
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-grid-columns='4'] :is(.gem-section__grid, [class*='__grid']):not(.gem-row) { grid-template-columns: repeat(4, minmax(0, 1fr)); }",
|
|
419
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[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; }"
|
|
420
|
+
];
|
|
421
|
+
var GEM_GRID_COLUMNS_ROW_KNOB_CSS = GEM_GRID_COLUMNS_KNOB_CSS_LINES.filter((line) => line.includes(" .gem-row:is(")).join("\n");
|
|
422
|
+
|
|
423
|
+
// ../../packages/gem-framework/src/offer-engine.ts
|
|
424
|
+
function offerEngineRuntimeScript() {
|
|
425
|
+
return `(function(){
|
|
426
|
+
document.querySelectorAll('[data-gem-offer-engine]').forEach(function(root){
|
|
427
|
+
if(root.__gemOffer)return; root.__gemOffer=1;
|
|
428
|
+
var el=root.querySelector('[data-gem-offer-model]'); if(!el)return;
|
|
429
|
+
var model; try{model=JSON.parse(el.textContent);}catch(e){return;}
|
|
430
|
+
var dims=model.dimensions||{}, sel={};
|
|
431
|
+
Object.keys(dims).forEach(function(d){var o=(dims[d].options||[]),def=o.filter(function(x){return x.default;})[0]||o[0];sel[d]=(model.default_selection&&model.default_selection[d])||(def&&def.id);});
|
|
432
|
+
function m(w){return Object.keys(w).every(function(k){return sel[k]===w[k];});}
|
|
433
|
+
function sp(w){return Object.keys(w).length;}
|
|
434
|
+
function pick(list){return (list||[]).filter(function(r){return m(r.when);}).sort(function(a,b){return sp(b.when)-sp(a.when);})[0];}
|
|
435
|
+
function gift(g,t){if(!g||t==null)return null;var sym=g.currency_symbol||'$';var rem=Math.max(0,Math.round((g.threshold-t)*100)/100);var unl=rem<=0;var rs=sym+rem.toFixed(2);var pct=g.threshold>0?Math.max(0,Math.min(100,Math.round((t/g.threshold)*100))):100;var tpl=unl?(g.unlocked_text||'\\uD83C\\uDF81 {label} unlocked!'):(g.locked_text||'Add {remaining} more to unlock {label}');return {unlocked:unl,remaining:rs,remaining_amount:rem,percent:pct,text:tpl.replace('{remaining}',rs).replace('{label}',g.label)};}
|
|
436
|
+
function render(){
|
|
437
|
+
var row=pick(model.price_matrix)||{}, plan=pick(model.selling_plan_map);
|
|
438
|
+
var vis={}; Object.keys(dims).forEach(function(d){vis[d]=true;});
|
|
439
|
+
(model.conditions||[]).forEach(function(c){vis[c.show]=(sel[c.when_dim]===c.equals);});
|
|
440
|
+
(model.panels||[]).forEach(function(p){if(p.when)vis[p.id]=(sel[p.when.when_dim]===p.when.equals);});
|
|
441
|
+
var headline=row.total||row.price||'';
|
|
442
|
+
var gf=gift(model.gift,row.total_amount);
|
|
443
|
+
root.querySelectorAll('[data-gem-offer-out]').forEach(function(o){var k=o.getAttribute('data-gem-offer-out');
|
|
444
|
+
if(k==='price')o.textContent=row.price||'';
|
|
445
|
+
else if(k==='compare'){o.textContent=row.compare_at||'';o.style.display=row.compare_at?'':'none';}
|
|
446
|
+
else if(k==='save'){o.textContent=row.save_label||'';o.style.display=row.save_label?'':'none';}
|
|
447
|
+
else if(k==='gift'){o.textContent=gf?gf.text:'';if(o.classList)o.classList.toggle('unlocked',!!(gf&&gf.unlocked));}
|
|
448
|
+
else if(k==='cta')o.textContent=(model.cta_template||'Add to cart').replace('{price}',headline).replace('{total}',headline);});
|
|
449
|
+
root.querySelectorAll('[data-gem-offer-gift]').forEach(function(w){w.style.display=gf?'':'none';if(w.classList)w.classList.toggle('unlocked',!!(gf&&gf.unlocked));});
|
|
450
|
+
root.querySelectorAll('[data-gem-offer-gift-bar]').forEach(function(b){b.style.width=(gf?gf.percent:0)+'%';});
|
|
451
|
+
root.querySelectorAll('[data-gem-offer-group]').forEach(function(g){g.style.display=(vis[g.getAttribute('data-gem-offer-group')]===false)?'none':'';});
|
|
452
|
+
root.querySelectorAll('[data-gem-offer-dim]').forEach(function(b){var d=b.getAttribute('data-gem-offer-dim'),v=b.getAttribute('data-gem-offer-value');if(b.tagName==='OPTION'){b.selected=(sel[d]===v);}else{b.classList.toggle('sel',sel[d]===v);}});
|
|
453
|
+
var s=root.querySelector('[data-gem-offer-selling-plan]'); if(s)s.value=(plan&&plan.selling_plan_id)||'';
|
|
454
|
+
var ca=(plan&&plan.cart_attributes)||{};
|
|
455
|
+
root.querySelectorAll('[data-gem-offer-cart-field]').forEach(function(f){var n=f.getAttribute('data-gem-offer-cart-field');f.value=ca[n]||'';f.disabled=!(n in ca);});
|
|
456
|
+
root.dispatchEvent(new CustomEvent('gem:offer:change',{bubbles:true,detail:{selection:sel,row:row,selling_plan_id:(plan&&plan.selling_plan_id)||null,cart_attributes:ca,gift:gf}}));
|
|
457
|
+
}
|
|
458
|
+
root.addEventListener('click',function(e){var b=e.target.closest('[data-gem-offer-dim]');if(!b||b.tagName==='OPTION')return;sel[b.getAttribute('data-gem-offer-dim')]=b.getAttribute('data-gem-offer-value');render();});
|
|
459
|
+
root.addEventListener('change',function(e){var s=e.target.closest('select[data-gem-offer-select]');if(!s)return;var o=s.options[s.selectedIndex];sel[s.getAttribute('data-gem-offer-select')]=o.getAttribute('data-gem-offer-value');render();});
|
|
460
|
+
render();
|
|
461
|
+
});
|
|
462
|
+
})();`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ../../packages/gem-framework/src/runtime.ts
|
|
466
|
+
var offerEnginePluginRegistrationLines = [
|
|
467
|
+
" registerPlugin('offer-engine', ({ api }) => {",
|
|
468
|
+
" " + offerEngineRuntimeScript(),
|
|
469
|
+
" api.emit('gem:offer:ready', { source: 'gem-runtime' });",
|
|
470
|
+
" });"
|
|
471
|
+
];
|
|
472
|
+
|
|
473
|
+
// ../../packages/gem-framework/src/validation.ts
|
|
474
|
+
var validationRulesByCode = new Map(gemFrameworkValidationRules.map((rule) => [rule.code, rule]));
|
|
475
|
+
var primitiveClassNames = new Set(
|
|
476
|
+
gemFrameworkPrimitiveGroups.flatMap((group) => group.classes)
|
|
477
|
+
);
|
|
478
|
+
var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
479
|
+
...Object.entries(requiredGemTokenKeys).flatMap(([group, keys]) => {
|
|
480
|
+
const namespace = group === "typography" ? "font" : group === "spacing" ? "space" : group;
|
|
481
|
+
return keys.map((key) => `--gem-${namespace}-${slugify(key)}`);
|
|
482
|
+
}),
|
|
483
|
+
// The governed type ramp (--gem-h1…h6, --gem-body, --gem-text-sm, --gem-type-base/-scale). The
|
|
484
|
+
// framework stylesheet declares it on every page, but it is not a token GROUP, so it was missing here
|
|
485
|
+
// and a section sizing its heading with `var(--gem-h2)` got `unknown_gem_css_variable`. Read from the
|
|
486
|
+
// function that emits it, so the ramp and this list cannot drift.
|
|
487
|
+
...gemTypeRampCssVars().map((line) => line.split(":")[0].trim()),
|
|
488
|
+
// --gem-focus-ring: emitted with the tokens, not a group (tokens.ts GEM_FOCUS_RING_CSS_VAR).
|
|
489
|
+
GEM_FOCUS_RING_CSS_VAR.split(":")[0].trim(),
|
|
490
|
+
GEM_TAP_MIN_CSS_VAR.split(":")[0].trim(),
|
|
491
|
+
// The row primitive's variables (primitives.ts GEM_ROW_CSS). Sections SET these on their grid.
|
|
492
|
+
...GEM_ROW_CSS_VARIABLES
|
|
493
|
+
]);
|
|
494
|
+
var TOKENIZED_PROPERTIES = [
|
|
495
|
+
"background",
|
|
496
|
+
"background-color",
|
|
497
|
+
"border",
|
|
498
|
+
"border-color",
|
|
499
|
+
"box-shadow",
|
|
500
|
+
"color",
|
|
501
|
+
"font-family",
|
|
502
|
+
"gap",
|
|
503
|
+
"margin",
|
|
504
|
+
"margin-block",
|
|
505
|
+
"margin-inline",
|
|
506
|
+
"padding",
|
|
507
|
+
"padding-block",
|
|
508
|
+
"padding-inline",
|
|
509
|
+
"border-radius",
|
|
510
|
+
// The row's spacing variables end up as column-gap / row-gap: a raw length there must be caught the same
|
|
511
|
+
// way `gap: 37px` is, or moving a grid onto .gem-row quietly switches the token check off (#589 review).
|
|
512
|
+
"--gem-row-gap",
|
|
513
|
+
"--gem-row-gap-y"
|
|
514
|
+
];
|
|
515
|
+
function validateGemFrameworkArtifact(artifact, options = {}) {
|
|
516
|
+
if (artifact.path.endsWith(".css")) {
|
|
517
|
+
return validateGemFrameworkCss(artifact.content, artifact.path, options);
|
|
518
|
+
}
|
|
519
|
+
if (/\.(liquid|html|tsx)$/.test(artifact.path)) {
|
|
520
|
+
return validateGemFrameworkMarkup(artifact.content, artifact.path, options);
|
|
521
|
+
}
|
|
522
|
+
if (artifact.path.endsWith(".js")) {
|
|
523
|
+
return result(validateSharedSafety(artifact.content, artifact.path));
|
|
524
|
+
}
|
|
525
|
+
return result([]);
|
|
526
|
+
}
|
|
527
|
+
function validateGemFrameworkCss(css, artifactPath, options = {}) {
|
|
528
|
+
const issues = validateSharedSafety(css, artifactPath);
|
|
529
|
+
css = css.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
530
|
+
if (/\bgs-/.test(css)) {
|
|
531
|
+
issues.push(ruleIssue("legacy_gs_class", "Gem Framework CSS must not use legacy gs-* classes.", artifactPath));
|
|
532
|
+
}
|
|
533
|
+
const classNames = [...css.matchAll(/\.([_A-Za-z-][_A-Za-z0-9-]*)/g)].map((match) => match[1]);
|
|
534
|
+
for (const className of classNames) {
|
|
535
|
+
const audit = auditGemFrameworkClassName(className, options);
|
|
536
|
+
if (audit.kind === "non_gem") {
|
|
537
|
+
issues.push(ruleIssue("non_gem_class", `CSS class .${className} must use the gem-* namespace.`, artifactPath, {
|
|
538
|
+
class_name: className
|
|
539
|
+
}));
|
|
540
|
+
}
|
|
541
|
+
if (audit.kind === "unknown_gem") {
|
|
542
|
+
issues.push(ruleIssue("unknown_gem_class", `CSS class .${className} is not a manifest primitive or section-scoped class.`, artifactPath, {
|
|
543
|
+
class_name: className
|
|
544
|
+
}));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const cssVariables = [...css.matchAll(/--gem-[a-z0-9-]+/g)].map((match) => match[0]);
|
|
548
|
+
for (const variableName of [...new Set(cssVariables)]) {
|
|
549
|
+
if (isKnownGemCssVariable(variableName, options)) continue;
|
|
550
|
+
issues.push(ruleIssue("unknown_gem_css_variable", `${variableName} is not a canonical Gem token or allowed section-scoped CSS variable.`, artifactPath, {
|
|
551
|
+
value: variableName
|
|
552
|
+
}));
|
|
553
|
+
}
|
|
554
|
+
const declarations = [...css.matchAll(/([a-z-]+)\s*:\s*([^;{}]+);/gi)];
|
|
555
|
+
for (const [, property, value] of declarations) {
|
|
556
|
+
if (!TOKENIZED_PROPERTIES.includes(property)) continue;
|
|
557
|
+
if (isAllowedUntokenizedValue(property, value)) continue;
|
|
558
|
+
if (!value.includes("var(--gem-") && hasRawVisualValue(value)) {
|
|
559
|
+
issues.push(ruleIssue("untokenized_visual_value", `${property}: ${value.trim()} should prefer semantic --gem-* tokens.`, artifactPath, {
|
|
560
|
+
property,
|
|
561
|
+
value: value.trim()
|
|
562
|
+
}));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return result(issues);
|
|
566
|
+
}
|
|
567
|
+
function validateGemFrameworkMarkup(markup, artifactPath, options = {}) {
|
|
568
|
+
const issues = validateSharedSafety(markup, artifactPath);
|
|
569
|
+
if (/\bgs-/.test(markup)) {
|
|
570
|
+
issues.push(ruleIssue("legacy_gs_class", "Gem Framework markup must not use legacy gs-* classes.", artifactPath));
|
|
571
|
+
}
|
|
572
|
+
const classAttributes = [...markup.matchAll(/\bclass(?:Name)?=(["'`])([^"'`]+)\1/g)].map((match) => match[2]);
|
|
573
|
+
for (const classAttribute of classAttributes) {
|
|
574
|
+
if (classAttribute.includes("{{") || classAttribute.includes("{%") || classAttribute.includes("${")) continue;
|
|
575
|
+
for (const className of classAttribute.split(/\s+/).filter(Boolean)) {
|
|
576
|
+
const audit = auditGemFrameworkClassName(className, options);
|
|
577
|
+
if (audit.kind === "non_gem") {
|
|
578
|
+
issues.push(ruleIssue("non_gem_class", `Markup class ${className} must use the gem-* namespace.`, artifactPath, {
|
|
579
|
+
class_name: className
|
|
580
|
+
}));
|
|
581
|
+
}
|
|
582
|
+
if (audit.kind === "unknown_gem") {
|
|
583
|
+
issues.push(ruleIssue("unknown_gem_class", `Markup class ${className} is not a manifest primitive or section-scoped class.`, artifactPath, {
|
|
584
|
+
class_name: className
|
|
585
|
+
}));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (!/data-gem-[a-z0-9-]+=/.test(markup) && /<section\b/i.test(markup)) {
|
|
590
|
+
issues.push(ruleIssue("missing_data_gem_marker", "Section markup should expose a data-gem-* marker for provenance or runtime targeting.", artifactPath));
|
|
591
|
+
}
|
|
592
|
+
return result(issues);
|
|
593
|
+
}
|
|
594
|
+
function validateSharedSafety(content, artifactPath) {
|
|
595
|
+
const issues = [];
|
|
596
|
+
const contentWithoutJsonLd = content.replace(/<script\b[^>]*type=["']application\/ld\+json["'][^>]*>[\s\S]*?<\/script>/gi, "");
|
|
597
|
+
const contentWithoutNamespaces = contentWithoutJsonLd.replace(/https?:\/\/www\.w3\.org\/[^\s"'<>]*/gi, "");
|
|
598
|
+
if (/<script\b/i.test(contentWithoutJsonLd)) {
|
|
599
|
+
issues.push(ruleIssue("inline_script", "Gem Framework artifacts must not include inline script tags.", artifactPath));
|
|
600
|
+
}
|
|
601
|
+
if (/https?:\/\/|\/\/cdn\./i.test(contentWithoutNamespaces)) {
|
|
602
|
+
issues.push(ruleIssue("external_dependency", "Gem Framework artifacts must not include external runtime or CDN dependencies.", artifactPath));
|
|
603
|
+
}
|
|
604
|
+
return issues;
|
|
605
|
+
}
|
|
606
|
+
function isAllowedUntokenizedValue(property, value) {
|
|
607
|
+
const trimmed = value.trim();
|
|
608
|
+
if (property === "color" && ["inherit", "currentColor"].includes(trimmed)) return true;
|
|
609
|
+
if (property === "border" && trimmed === "0") return true;
|
|
610
|
+
if (property.startsWith("margin") && ["0", "auto"].includes(trimmed)) return true;
|
|
611
|
+
if (property.startsWith("padding") && trimmed === "0") return true;
|
|
612
|
+
if ((property === "gap" || property.startsWith("--gem-row-gap")) && trimmed === "0") return true;
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
function hasRawVisualValue(value) {
|
|
616
|
+
return /#[0-9a-f]{3,8}\b|rgb\(|hsl\(|\b\d+(?:\.\d+)?(?:px|rem|em)\b/i.test(value);
|
|
617
|
+
}
|
|
618
|
+
function isKnownGemCssVariable(variableName, options) {
|
|
619
|
+
if (canonicalCssVariableNames.has(variableName)) return true;
|
|
620
|
+
const allowedPrefixes = [
|
|
621
|
+
"--gem-seasonal-",
|
|
622
|
+
// Global style-variant knobs (--gem-style-space/radius/text/weight/stroke): the
|
|
623
|
+
// style-adaptivity family a section reads to adapt airy↔dense / thin↔bold.
|
|
624
|
+
// Global style-variant knobs (--gem-style-space/weight/radius/border/stroke):
|
|
625
|
+
// section-adaptivity tokens a style pack sets on :root; sections consume them via
|
|
626
|
+
// calc(... * var(--gem-style-*, 1)) with safe fallbacks. Canonical, not section-scoped.
|
|
627
|
+
"--gem-style-",
|
|
628
|
+
...options.allowedCssVariablePrefixes ?? [],
|
|
629
|
+
...(options.sectionClassPrefixes ?? []).map((prefix) => `--${prefix}-`)
|
|
630
|
+
];
|
|
631
|
+
return allowedPrefixes.some((prefix) => variableName.startsWith(prefix));
|
|
632
|
+
}
|
|
633
|
+
function getGemFrameworkValidationRule(code) {
|
|
634
|
+
return validationRulesByCode.get(code);
|
|
635
|
+
}
|
|
636
|
+
function auditGemFrameworkClassName(className, options = {}) {
|
|
637
|
+
if ((options.allowedClassNames ?? []).includes(className)) return { class_name: className, kind: "primitive" };
|
|
638
|
+
if (primitiveClassNames.has(className)) return { class_name: className, kind: "primitive" };
|
|
639
|
+
if (className === "gem-el" || className.startsWith("gem-el-")) return { class_name: className, kind: "primitive" };
|
|
640
|
+
if (className.startsWith("section-gem-")) return { class_name: className, kind: "section_scoped" };
|
|
641
|
+
if (!className.startsWith("gem-")) return { class_name: className, kind: "non_gem" };
|
|
642
|
+
if (!options.enforceKnownClasses) return { class_name: className, kind: "section_scoped" };
|
|
643
|
+
const sectionClassPrefixes = options.sectionClassPrefixes ?? [];
|
|
644
|
+
if (sectionClassPrefixes.some((prefix) => isSectionScopedClassName(className, prefix))) {
|
|
645
|
+
return { class_name: className, kind: "section_scoped" };
|
|
646
|
+
}
|
|
647
|
+
return { class_name: className, kind: "unknown_gem" };
|
|
648
|
+
}
|
|
649
|
+
function isSectionScopedClassName(className, prefix) {
|
|
650
|
+
return className === prefix || className.startsWith(`${prefix}__`) || className.startsWith(`${prefix}--`);
|
|
651
|
+
}
|
|
652
|
+
function ruleIssue(code, message, artifactPath, metadata = {}) {
|
|
653
|
+
const rule = getGemFrameworkValidationRule(code);
|
|
654
|
+
if (!rule) throw new Error(`Gem Framework validation rule is missing from the manifest: ${code}`);
|
|
655
|
+
return {
|
|
656
|
+
severity: rule.severity,
|
|
657
|
+
code,
|
|
658
|
+
message,
|
|
659
|
+
artifact_path: artifactPath,
|
|
660
|
+
...metadata
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function result(issues) {
|
|
664
|
+
return {
|
|
665
|
+
valid: !issues.some((item) => item.severity === "error"),
|
|
666
|
+
issues
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ../../packages/gem-framework/src/preview-document.ts
|
|
671
|
+
var PREVIEW_MEDIA_CSS = [
|
|
672
|
+
'body[data-gem-preview-page] :is([class*="__placeholder"], .gem-media:empty, .gem-product-card__media) {',
|
|
673
|
+
" background-color: color-mix(in srgb, var(--gem-color-accent) 9%, var(--gem-color-surface, var(--gem-color-background))) !important;",
|
|
674
|
+
// Two soft, percentage-sized colour pools (one accent, one neutral) over a base
|
|
675
|
+
// diagonal wash — reads as abstract brand art and scales to any tile, so even large
|
|
676
|
+
// empty heroes feel designed rather than blank.
|
|
677
|
+
" background-image:",
|
|
678
|
+
// Soft lit centre (studio product-shot backdrop) over two ambient colour pools and a
|
|
679
|
+
// base diagonal wash — reads as a styled set rather than a blank box, and scales to any tile.
|
|
680
|
+
" radial-gradient(58% 48% at 50% 40%, color-mix(in srgb, var(--gem-color-surface, var(--gem-color-background)) 78%, #fff) 0%, transparent 72%),",
|
|
681
|
+
" radial-gradient(72% 60% at 22% 26%, color-mix(in srgb, var(--gem-color-accent) 34%, var(--gem-color-surface, var(--gem-color-background))) 0%, transparent 70%),",
|
|
682
|
+
" radial-gradient(68% 70% at 84% 82%, color-mix(in srgb, var(--gem-color-text) 14%, var(--gem-color-surface, var(--gem-color-background))) 0%, transparent 66%),",
|
|
683
|
+
" linear-gradient(135deg, color-mix(in srgb, var(--gem-color-accent) 16%, var(--gem-color-surface, var(--gem-color-background))), color-mix(in srgb, var(--gem-color-accent) 6%, var(--gem-color-surface, var(--gem-color-background)))) !important;",
|
|
684
|
+
"}",
|
|
685
|
+
// Niche product-tile motif (injected line-art): centred, soft, in a palette-derived ink.
|
|
686
|
+
// Every empty media box (hero/collage/video/card) is position:relative + inked so the
|
|
687
|
+
// fallback motif (previewMediaImg) centres and reads as "image here" — never dead white space.
|
|
688
|
+
'body[data-gem-preview-page] [class*="__placeholder"] { position: relative; min-height: 140px; color: color-mix(in srgb, var(--gem-color-accent) 55%, var(--gem-color-text)); }',
|
|
689
|
+
"body[data-gem-preview-page] .gem-product-card__media { position: relative; color: color-mix(in srgb, var(--gem-color-accent) 55%, var(--gem-color-text)); }",
|
|
690
|
+
"body[data-gem-preview-page] .gem-pv-motif { position: absolute; top: 50%; left: 50%; width: clamp(44px, 13%, 132px); height: auto; aspect-ratio: 1; transform: translate(-50%, -50%); opacity: 0.5; }",
|
|
691
|
+
// Real reference photo (preview-only) covers the whole tile when present.
|
|
692
|
+
"body[data-gem-preview-page] .gem-pv-photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }",
|
|
693
|
+
// Hero overlay text readability (sketch-only): light hero copy on a light product/hero image
|
|
694
|
+
// renders invisible — a dark text-shadow keeps the heading/CTA legible on ANY image or variant,
|
|
695
|
+
// so the sketch's hero reads as a real hero (headline + CTA) rather than a bare image.
|
|
696
|
+
'body[data-gem-preview-page] :is([class*="gem-hero"], [data-gem-section-archetype^="hero"]) :is(.gem-section__heading, .gem-section__body, .gem-section__eyebrow) { text-shadow: 0 2px 18px rgba(0, 0, 0, 0.6), 0 1px 4px rgba(0, 0, 0, 0.55); }',
|
|
697
|
+
'body[data-gem-preview-page] :is([class*="gem-hero"], [data-gem-section-archetype^="hero"]) .gem-button--primary { text-shadow: 0 1px 6px rgba(0, 0, 0, 0.35); }'
|
|
698
|
+
].join("\n");
|
|
699
|
+
var CHROME_VARIANT_CSS = `
|
|
700
|
+
.gem-shell-header__inner{display:flex;align-items:center;gap:24px}
|
|
701
|
+
/* The shell header markup carries Dawn's header--middle-left grid classes too; force the
|
|
702
|
+
logo to size to its content (the grid otherwise pins it to a narrow column + overflow:hidden,
|
|
703
|
+
clipping the brand to "Baseline S..."). */
|
|
704
|
+
[data-gem-shell-header] .gem-shell-header__logo{flex:0 0 auto;width:auto;max-width:none;min-width:0;overflow:visible;text-decoration:none}
|
|
705
|
+
[data-gem-shell-header] .gem-shell-header__logo .h2{margin:0;font-size:1.25rem;line-height:1.1;white-space:nowrap;overflow:visible;text-overflow:clip}
|
|
706
|
+
.gem-shell-header__nav{display:flex;align-items:center;gap:18px;flex-wrap:wrap}
|
|
707
|
+
.gem-shell-header__actions{display:flex;align-items:center;gap:16px;margin-left:auto}
|
|
708
|
+
.gem-shell-header__action{display:inline-flex;align-items:center;justify-content:center;position:relative;color:inherit;text-decoration:none}
|
|
709
|
+
.gem-shell-header__icon{display:block;width:20px;height:20px}
|
|
710
|
+
.gem-shell-header__action--cart .gem-shell-header__cart-count{position:absolute;top:-7px;right:-9px;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:#1f1e1d;color:#fff;font-size:10px;font-weight:600;line-height:16px;text-align:center;box-sizing:border-box}
|
|
711
|
+
.gem-shell-header__search-input{min-width:200px;padding:8px 12px}
|
|
712
|
+
/* editorial: centered logo with the nav on a row beneath it */
|
|
713
|
+
.gem-shell-header--editorial .gem-shell-header__inner{flex-direction:column;align-items:center;gap:10px;text-align:center}
|
|
714
|
+
.gem-shell-header--editorial .gem-shell-header__logo{order:1}
|
|
715
|
+
.gem-shell-header--editorial .gem-shell-header__actions{order:2;margin-left:0;position:absolute;right:24px;top:18px}
|
|
716
|
+
.gem-shell-header--editorial .gem-shell-header__nav{order:3;justify-content:center}
|
|
717
|
+
/* search-led: prominent search field takes the centre, actions stay right */
|
|
718
|
+
.gem-shell-header--search-led .gem-shell-header__nav{order:3;flex-basis:100%;justify-content:center}
|
|
719
|
+
.gem-shell-header--search-led .gem-shell-header__search{flex:1;display:flex}
|
|
720
|
+
.gem-shell-header--search-led .gem-shell-header__search-input{width:100%}
|
|
721
|
+
/* mega-menu: roomier nav with hover affordance */
|
|
722
|
+
.gem-shell-header__nav--mega{gap:26px;font-weight:600}
|
|
723
|
+
.gem-shell-header__nav--mega a::after{content:"";display:inline-block;width:6px;height:6px;margin-left:6px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(45deg);vertical-align:middle;opacity:.55}
|
|
724
|
+
/* minimal: tighter, lighter nav */
|
|
725
|
+
.gem-shell-header--minimal .gem-shell-header__nav{gap:14px;font-size:.92em}
|
|
726
|
+
/* conversion: dense commerce actions */
|
|
727
|
+
.gem-shell-header--conversion .gem-shell-header__actions{gap:18px;font-weight:600}
|
|
728
|
+
/* footer variants */
|
|
729
|
+
.gem-shell-footer__trust{display:flex;flex-wrap:wrap;gap:18px;justify-content:center;margin:0 0 8px}
|
|
730
|
+
.gem-shell-footer__trust-item{font-size:.86em;opacity:.85}
|
|
731
|
+
.gem-shell-footer__social{display:flex;gap:14px;justify-content:center;margin:18px 0 0;padding:0}
|
|
732
|
+
.gem-shell-footer__social-link{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:999px;color:inherit;opacity:.9;text-decoration:none}
|
|
733
|
+
.gem-shell-footer__social-link:hover{opacity:1}
|
|
734
|
+
.gem-shell-footer__social-link svg{display:block;width:20px;height:20px}
|
|
735
|
+
/* bottom bar: copyright left, payment icons right (Dawn footer DNA) */
|
|
736
|
+
.gem-shell-footer__bottom{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:16px;margin-top:24px;padding-top:18px;border-top:1px solid rgba(18,18,18,.12)}
|
|
737
|
+
.gem-shell-footer__payment{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0}
|
|
738
|
+
.gem-shell-footer__payment-icon{display:inline-flex;align-items:center;justify-content:center;width:40px;height:26px;border-radius:4px;background:#fff;border:1px solid rgba(18,18,18,.14);box-shadow:0 1px 2px rgba(0,0,0,.06);color:#555;font-size:8px;font-weight:700;letter-spacing:-.02em;text-transform:uppercase}
|
|
739
|
+
.gem-shell-footer--newsletter-first .gem-shell-footer__newsletter{order:-1;text-align:center;padding-bottom:20px}
|
|
740
|
+
.gem-shell-footer--trust-heavy .gem-shell-footer__trust{order:-1;font-weight:600}
|
|
741
|
+
.gem-shell-footer--story-led .gem-shell-footer__grid{text-align:center}
|
|
742
|
+
.gem-shell-footer--brand-rich .gem-shell-footer__social{justify-content:flex-start}
|
|
743
|
+
`.trim();
|
|
744
|
+
|
|
745
|
+
// ../../packages/gem-sections/src/section-contract.ts
|
|
746
|
+
var LAYOUT_ATTRS = [
|
|
747
|
+
{ attr: "content-alignment", setting: "gem_content_alignment", knob: "content_alignment" },
|
|
748
|
+
{ attr: "text-position", setting: "gem_text_position", knob: "text_position" },
|
|
749
|
+
{ attr: "media-position", setting: "gem_media_position", knob: "media_position" },
|
|
750
|
+
{ attr: "content-spacing", setting: "gem_content_spacing", knob: "content_spacing" },
|
|
751
|
+
{ attr: "content-width", setting: "gem_content_width", knob: "content_width" },
|
|
752
|
+
{ attr: "section-height", setting: "gem_section_height", knob: "section_height" },
|
|
753
|
+
{ attr: "grid-columns", setting: "gem_grid_columns", knob: "grid_columns" },
|
|
754
|
+
{ attr: "section-background", setting: "gem_section_background", knob: "background" },
|
|
755
|
+
{ attr: "price-color", setting: "gem_price_color", knob: "price_color" },
|
|
756
|
+
// Written from BOTH sides: the style pack sets it, and set_layout gained a `text_weight` knob.
|
|
757
|
+
{ attr: "text-weight", setting: "gem_text_weight", knob: "text_weight" },
|
|
758
|
+
// Scroll-entrance: emitted only when the knob carries a value.
|
|
759
|
+
{ attr: "reveal", setting: "gem_reveal", knob: "reveal" },
|
|
760
|
+
// Shaped edges: same emit-only-when-set rule.
|
|
761
|
+
{ attr: "divider-top", setting: "gem_divider_top", knob: "divider_top" },
|
|
762
|
+
{ attr: "divider-bottom", setting: "gem_divider_bottom", knob: "divider_bottom" },
|
|
763
|
+
// Boxes the section's content: styles .gem-section__inner, so the root attribute alone is not enough.
|
|
764
|
+
{ attr: "card", setting: "gem_card", knob: "card", requires: "__inner" }
|
|
765
|
+
];
|
|
766
|
+
function sectionLibraryIdOf(tsx) {
|
|
767
|
+
return /data-gem-section-library-id="([^"]+)"/.exec(tsx)?.[1] ?? "";
|
|
768
|
+
}
|
|
769
|
+
function layoutKnobCheck(tsx, knobExempt = /* @__PURE__ */ new Set()) {
|
|
770
|
+
const id = sectionLibraryIdOf(tsx);
|
|
771
|
+
const missing = LAYOUT_ATTRS.filter((a) => {
|
|
772
|
+
if (knobExempt.has(`${id}::${a.attr}`)) return false;
|
|
773
|
+
if (!tsx.includes(`data-gem-${a.attr}`)) return true;
|
|
774
|
+
return Boolean(a.requires) && !tsx.includes(a.requires);
|
|
775
|
+
}).map((a) => a.attr);
|
|
776
|
+
const tag = /<section[\s\S]*?>/.exec(tsx)?.[0] ?? "";
|
|
777
|
+
const emitted = [...tag.matchAll(/(data-gem-[a-z-]+)=/g)].map((m) => m[1]);
|
|
778
|
+
const dupes = [...new Set(emitted.filter((a, i) => emitted.indexOf(a) !== i))];
|
|
779
|
+
return { id, missing, dupes };
|
|
780
|
+
}
|
|
781
|
+
function slotsReadBy(raw) {
|
|
782
|
+
const source = raw.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[^:])\/\/.*$/gm, "$1 ");
|
|
783
|
+
const names = /* @__PURE__ */ new Set(["slots"]);
|
|
784
|
+
for (const m of source.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:section\.)?content_slots\b/g)) names.add(m[1]);
|
|
785
|
+
const alt = [...names].map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
786
|
+
const keys = /* @__PURE__ */ new Set();
|
|
787
|
+
for (const m of source.matchAll(new RegExp(`(?:${alt})\\.([A-Za-z_][A-Za-z0-9_]*)`, "g"))) keys.add(m[1]);
|
|
788
|
+
for (const m of source.matchAll(new RegExp(`(?:${alt})\\[["']([^"']+)["']\\]`, "g"))) keys.add(m[1]);
|
|
789
|
+
const prefixes = [];
|
|
790
|
+
for (const m of source.matchAll(new RegExp("(?:" + alt + ")\\[`([^`]*)`\\]", "g"))) {
|
|
791
|
+
const literal = m[1].split("${")[0];
|
|
792
|
+
if (literal.length >= 3) prefixes.push(literal);
|
|
793
|
+
}
|
|
794
|
+
return { keys, prefixes };
|
|
795
|
+
}
|
|
796
|
+
function slotSourceFromElementTree(treeJson) {
|
|
797
|
+
return [...treeJson.matchAll(/"\$slot"\s*:\s*"([A-Za-z0-9_]+)"/g)].map((m) => `slots.${m[1]}`).join("\n");
|
|
798
|
+
}
|
|
799
|
+
function slotContractDrift(source, manifest) {
|
|
800
|
+
const declaredSlots = new Set(manifest.content_slots ?? []);
|
|
801
|
+
const declaredSettings = new Set(manifest.settings ?? []);
|
|
802
|
+
const read = slotsReadBy(source);
|
|
803
|
+
const isRead = (key) => read.keys.has(key) || read.prefixes.some((p) => key.startsWith(p));
|
|
804
|
+
return {
|
|
805
|
+
ghost: [...declaredSlots].filter((s) => !isRead(s)).sort(),
|
|
806
|
+
undeclared: [...read.keys].filter((s) => !declaredSlots.has(s) && !declaredSettings.has(s)).sort(),
|
|
807
|
+
settingsExcused: [...read.keys].filter((s) => !declaredSlots.has(s) && declaredSettings.has(s)).sort()
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
var SHARED_FRAMEWORK_PREFIXES = [
|
|
811
|
+
"gem-section",
|
|
812
|
+
"gem-button",
|
|
813
|
+
"gem-card",
|
|
814
|
+
"gem-price",
|
|
815
|
+
"gem-badge",
|
|
816
|
+
"gem-prose",
|
|
817
|
+
"gem-reveal",
|
|
818
|
+
"gem-countdown"
|
|
819
|
+
];
|
|
820
|
+
var SECTION_CLASS_RE = /^gem-[a-z0-9]+(?:-[a-z0-9]+)*(?:__[a-z0-9]+(?:-[a-z0-9]+)*)?(?:--[a-z0-9]+(?:-[a-z0-9]+)*)?$/;
|
|
821
|
+
function classesStyledBy(css) {
|
|
822
|
+
return new Set((css.match(/\.(-?[_a-zA-Z][\w-]*)/g) ?? []).map((s) => s.slice(1)));
|
|
823
|
+
}
|
|
824
|
+
function sectionClassesIn(tsx) {
|
|
825
|
+
const out = /* @__PURE__ */ new Set();
|
|
826
|
+
const src = tsx.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/^\s*\/\/.*$/gm, " ");
|
|
827
|
+
const re = /"([^"\n]*)"|'([^'\n]*)'|`([^`]*)`/g;
|
|
828
|
+
let m;
|
|
829
|
+
while ((m = re.exec(src)) !== null) {
|
|
830
|
+
for (const tok of (m[1] ?? m[2] ?? m[3] ?? "").split(/\s+/)) {
|
|
831
|
+
if (!tok.startsWith("gem-") || tok.includes("$") || tok.includes("{")) continue;
|
|
832
|
+
if (SECTION_CLASS_RE.test(tok)) out.add(tok);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return out;
|
|
836
|
+
}
|
|
837
|
+
function isFrameworkOwnedClass(cls, frameworkClasses) {
|
|
838
|
+
if (frameworkClasses.has(cls)) return true;
|
|
839
|
+
return SHARED_FRAMEWORK_PREFIXES.some((p) => cls === p || cls.startsWith(`${p}-`) || cls.startsWith(`${p}_`));
|
|
840
|
+
}
|
|
841
|
+
function cssPairingOrphans(tsx, css, frameworkClasses, archetype) {
|
|
842
|
+
const arch = archetype ?? /data-gem-section-archetype="([a-z0-9-]+)"/.exec(tsx)?.[1];
|
|
843
|
+
const ownBlock = arch ? `gem-${arch}` : null;
|
|
844
|
+
const emitted = [...sectionClassesIn(tsx)].filter((c) => c !== ownBlock && !isFrameworkOwnedClass(c, frameworkClasses));
|
|
845
|
+
const styled = classesStyledBy(css);
|
|
846
|
+
return { emitted: emitted.length, orphans: emitted.filter((c) => !styled.has(c)).sort() };
|
|
847
|
+
}
|
|
848
|
+
var CLAIM_PLAN_KEY = /^ratings?\d*$|ratingvalue|ratingscore|ratingscount|ratingcount|reviewscount|reviewcount|customerscount|customercount|unitssold|soldcount|testimonialcount|starrating/i;
|
|
849
|
+
var planKey = (key) => CLAIM_PLAN_KEY.test(key.replace(/_/g, ""));
|
|
850
|
+
var CLAIM_KEY = /(rating|review|customer|sold|purchas|verified|score|star|testimonial|backer|subscriber)/i;
|
|
851
|
+
var INVENTED_PERSON = /"[A-Z][a-z]+ [A-Z]\."|\b(Verified (Buyer|Customer)|Happy Customer)\b/;
|
|
852
|
+
var PROMISE = /[$£€]\s?\d|\b\d[\d.,]*\s?(?:USD|EUR|GBP)\b|\bfree,?(?:\s+(?:standard|express|expedited|worldwide|international|domestic|nationwide|ground|priority|tracked|insured|fast|economy|local|same[- ]day|next[- ]day|two[- ]day|overnight|\d+[- ](?:day|hour)|u\.?s\.?|uk|us|eu|au|ca|and|&),?){0,3}\s+(?:shipping|delivery|returns?|exchanges?)\b|\b(?:easy|hassle[- ]free|no[- ]hassle|risk[- ]free|no[- ]questions?[- ]asked)\s+(?:returns?|exchanges?)\b(?!\s+(?:faq|policy|information|info|page|portal)\b)|\b(?:returns?|exchanges?|refunds?)(?:\s+[\w${}.-]+){0,3}?\s+(?:(?:are|is)\s+)?(?:accepted|issued|processed)?\s*(?:within|in)\s+\d+\s*(?:business\s+)?(?:days?|weeks?)\b|\b(?:returns?|exchanges?|shipping|delivery)\s+(?:are|is)\s+(?:always\s+)?free\b|\bships?\s+free\b|\bcomplimentary\s+(?:shipping|delivery|returns?)\b|\b\d+\s+days\s+to\s+(?:return|exchange)\b|\b(?:processed|dispatched?|shipped)\s+within\s+\d|\bmoney[- ]back\b|\b\d+[- ]day (?:guarantee|returns?|exchanges?|trial)\b|\bdispatched? within\b|\bships? within\b|\b(?:lifetime|\d+[- ](?:year|month)s?(?:[- ]limited)?)[- ]warranty\b|\bwarranty:?\s*\d+\s*(?:years?|months?)\b|\b(?:delivered|ships?|arrives?) (?:in|within) \d+\s*(?:-\s*\d+\s*)?(?:h|hrs?|hours?|days?|business days|weeks?)\b|\bfull refund(?!\s+polic)|\brefunds? you\b|\btry (?:it )?(?:free )?(?:for )?\d+ (?:days|nights)\b|\b\d+[- ]nights? (?:trial|risk[- ]free)\b|(?<!\b(?:cannot|can\\?['’]t|can\s+not|won\\?['’]t|not|non|no|unable\s+to|need\s+to)[\s-]+(?:be\s+)?(?:\w+\s+(?:or|and)\s+)?)\bcancel\w*\b(?!\s+(?:fees?|charges?|penalt)|\s+(?:the\s+)?filters?\b)(?![^.!?\n]{0,40}\b(?:call|contact|message|email|reach|chat)\b)[^.!?\n]{0,40}\b(?:any ?-?time|at any time|whenever)\b|\bno\s+cancel\w*\s+(?:fees?|charges?|penalt\w*)\b|(?<!\b(?:cannot|can\\?['’]t|not|no|never)\s+)\bguarantee\w*\b[^.!?\n,&]{0,20}\b(?:deliver\w*|ship\w*)\b|\b(?:deliver\w*|ship\w*)\b[^.!?\n]{0,20}(?<!\b(?:not|never|no)\s+(?:be\s+)?)\bguaranteed\b(?!\s+[a-z])/i;
|
|
853
|
+
var CLAIM_TEXT = /\b\d[\d,.]*[kKmM]?\s*(\+\s*)?((?:verified|happy|satisfied|loyal)\s+)?(reviews?|ratings?|customers?|shoppers?|buyers?|sold|five[- ]stars?)\b|\b\d[\d,.]*[kKmM]?\s*(\+\s*)?(?:\d|five)[- ]?stars?\s+(?:reviews?|ratings?)\b|\brated\s+\d/i;
|
|
854
|
+
var CLAIM_TEXT_VI = /\d[.,]\d+\s*\/\s*5(?!\p{N})|(?<![\p{L}\p{N}])\d\s*\/\s*5\s*(?:sao|điểm)(?![\p{L}\p{N}])|\d[\d.,]*\s*(?:nghìn|ngàn|triệu|k)?\s*\+?\s*(?:lượt\s+)?(?:đánh giá|nhận xét|khách hàng|khách|người mua|người dùng|người đã mua|sao|đơn hàng)(?![\p{L}\p{N}])|(?<![\p{L}\p{N}])đã bán\s+(?:hơn\s+)?\d|\d[\d.,]*\s*(?:nghìn|ngàn|triệu|k)?\s*\+?\s*đã bán(?![\p{L}\p{N}])|(?<![\p{L}\p{N}])được đánh giá\s+\d|\d(?:[.,]\d{1,2})?\s*[★⭐]|[★⭐]\uFE0F?\s*\d/iu;
|
|
855
|
+
var VI_START = "(?<![\\p{L}\\p{N}])";
|
|
856
|
+
var VI_END = "(?![\\p{L}\\p{N}])";
|
|
857
|
+
var VI_DURATION = `\\d+(?:[.,]\\d+)?\\s*(?:(?:[\u2013\u2014~-]|\u0111\u1EBFn|t\u1EDBi|ho\u1EB7c)\\s*\\d+\\s*)?(?:h(?:\\d{2})?|gi\u1EDD|ti\u1EBFng|ph\xFAt|ng\xE0y|tu\u1EA7n|th\xE1ng)${VI_END}`;
|
|
858
|
+
var VI_DELIVERY_SPAN = `(?:\\d+(?:[.,]\\d+)?\\s*(?:h|gi\u1EDD|ti\u1EBFng|ph\xFAt|ng\xE0y|tu\u1EA7n)|\\d+\\s*(?:[\u2013\u2014~-]|\u0111\u1EBFn|t\u1EDBi)\\s*\\d+\\s*(?:ng\xE0y|tu\u1EA7n))${VI_END}`;
|
|
859
|
+
var PROMISE_VI_PARTS = {
|
|
860
|
+
freeShipping: `${VI_START}mi\u1EC5n ph\xED\\s+(?:v\u1EADn chuy\u1EC3n|giao h\xE0ng|ship|\u0111\u1ED5i tr\u1EA3|\u0111\u1ED5i h\xE0ng|tr\u1EA3 h\xE0ng)${VI_END}`,
|
|
861
|
+
shippingFree: `${VI_START}(?:v\u1EADn chuy\u1EC3n|giao h\xE0ng|\u0111\u1ED5i tr\u1EA3|\u0111\u1ED5i h\xE0ng|tr\u1EA3 h\xE0ng)\\s+mi\u1EC5n ph\xED${VI_END}`,
|
|
862
|
+
freeship: `${VI_START}free\\s?ship${VI_END}`,
|
|
863
|
+
price: `\\d[\\d.,]*\\s?(?:\u0111|\u20AB|vn\u0111|vnd|\u0111\u1ED3ng(?!\\s*h\u1ED3))${VI_END}`,
|
|
864
|
+
daysThenPolicy: `\\d+\\s*ng\xE0y\\s+(?:\u0111\u1ED5i tr\u1EA3|\u0111\u1ED5i h\xE0ng|ho\xE0n ti\u1EC1n|d\xF9ng th\u1EED|tr\u1EA3 h\xE0ng)${VI_END}`,
|
|
865
|
+
policyWindow: `${VI_START}(?:ho\xE0n ti\u1EC1n|\u0111\u1ED5i tr\u1EA3|\u0111\u1ED5i h\xE0ng|tr\u1EA3 h\xE0ng)\\s+(?:mi\u1EC5n ph\xED\\s+)?(?:trong\\s+(?:v\xF2ng\\s+)?)?${VI_DURATION}`,
|
|
866
|
+
refundPercent: `${VI_START}ho\xE0n ti\u1EC1n\\s+\\d+\\s*%`,
|
|
867
|
+
// Giới từ + số là đủ (như 0.3.5), trừ khi số đó đếm bước/lần/món ("Nhận hàng trong 3 bước").
|
|
868
|
+
deliveryWithin: `${VI_START}(?:giao|nh\u1EADn)(?:\\s+h\xE0ng)?(?:\\s+nhanh)?\\s+(?:trong v\xF2ng|trong|sau|ch\u1EC9)\\s+\\d(?!\\d*\\s*(?:b\u01B0\u1EDBc|l\u1EA7n|c\xE1ch|m\xF3n|s\u1EA3n ph\u1EA9m|m\u1EE5c)${VI_END})`,
|
|
869
|
+
// Không giới từ thì chỉ "giao": "Nhận 10h sáng" là giờ hẹn. Loại khung giờ và nhịp ("7h-21h", "3 ngày/tuần", "một lần").
|
|
870
|
+
deliveryTime: `${VI_START}giao(?:\\s+h\xE0ng)?(?:\\s+nhanh)?\\s*:?\\s+${VI_DELIVERY_SPAN}(?!\\s*(?:[-\u2013\u2014~/]|\u0111\u1EBFn|t\u1EDBi|s\xE1ng|chi\u1EC1u|t\u1ED1i|tr\u01B0a|\u0111\xEAm|m\u1ED9t l\u1EA7n|1 l\u1EA7n|m\u1ED7i))`,
|
|
871
|
+
trial: `${VI_START}(?:d\xF9ng th\u1EED|tr\u1EA3i nghi\u1EC7m)\\s+(?:mi\u1EC5n ph\xED\\s+)?(?:trong\\s+(?:v\xF2ng\\s+)?)?\\d+\\s*(?:ng\xE0y|\u0111\xEAm|tu\u1EA7n|th\xE1ng)${VI_END}`,
|
|
872
|
+
warranty: `${VI_START}b\u1EA3o h\xE0nh(?:\\s+(?:ch\xEDnh h\xE3ng|1 \u0111\u1ED5i 1|\u0111\u1ED5i m\u1EDBi|l\xEAn (?:\u0111\u1EBFn|t\u1EDBi)|t\u1EDBi|\u0111\u1EBFn|trong(?: v\xF2ng)?))*\\s+(?:tr\u1ECDn \u0111\u1EDDi|\\d+(?:[.,]\\d+)?\\s*(?:ng\xE0y|th\xE1ng|n\u0103m))${VI_END}`,
|
|
873
|
+
// Chính sách huỷ gói và cam kết giao hàng, theo KHOẢNG CÁCH chứ không theo một cụm cố định: "Huỷ đăng ký bất cứ
|
|
874
|
+
// khi nào", "Cam kết giao hàng đúng hẹn" (review gemsite #694 và gemtheme #604, 29/09/2026).
|
|
875
|
+
// "Không thể / không được / không hỗ trợ / không cho phép (đổi hoặc) huỷ bất cứ lúc nào" là lời từ chối (review
|
|
876
|
+
// gemboss #3499, gemtheme #605). "Không ràng buộc, huỷ bất cứ lúc nào" vẫn là lời hứa.
|
|
877
|
+
// "Huỷ (bỏ) lọc/chọn" là nhãn bộ lọc; "huỷ đăng ký" là bỏ nhận tin, như "Unsubscribe anytime" bên tiếng Anh (huỷ GÓI
|
|
878
|
+
// mới là chính sách); và một động từ liên hệ ở giữa ("Hủy đơn, gọi chúng tôi bất cứ lúc nào") nói "bất cứ lúc nào" về
|
|
879
|
+
// việc gọi, không về việc huỷ (review gemfactory #68, gemtheme #606). Dấu phẩy thì KHÔNG cắt: "Hủy miễn phí, bất cứ
|
|
880
|
+
// lúc nào" vẫn là lời hứa.
|
|
881
|
+
cancelAnytime: `${VI_START}(?<!kh\xF4ng(?:\\s+(?:th\u1EC3|\u0111\u01B0\u1EE3c|h\u1ED7 tr\u1EE3|cho ph\xE9p))?(?:\\s+(?:\u0111\u1ED5i|tr\u1EA3)\\s+(?:ho\u1EB7c|hay))?\\s+)(?:hu\u1EF7|h\u1EE7y)(?!\\s+(?:b\u1ECF\\s+)?(?:l\u1ECDc|ch\u1ECDn|t\xECm)|\\s+\u0111\u0103ng k\xFD(?!\\s+g\xF3i))(?![^.!?\\n]{0,20}(?:g\u1ECDi|li\xEAn h\u1EC7|nh\u1EAFn|h\u1ECFi|chat))[^.!?\\n]{0,20}b\u1EA5t (?:c\u1EE9|k\u1EF3|k\xEC) (?:l\xFAc|khi) n\xE0o${VI_END}`,
|
|
882
|
+
// Loại TRỪ các từ ghép không phải giao hàng ("giao dịch", "giao diện", "giao tiếp"…), đừng liệt kê từ được phép:
|
|
883
|
+
// danh sách cho phép làm lọt "Cam kết giao sớm", "Cam kết giao toàn quốc" (review gemtheme #605).
|
|
884
|
+
guaranteedDelivery: `${VI_START}(?:\u0111\u1EA3m b\u1EA3o|b\u1EA3o \u0111\u1EA3m|cam k\u1EBFt)\\s+(?:giao(?!\\s*(?:d\u1ECBch|di\u1EC7n|ti\u1EBFp|l\u01B0u|th\xF4ng|ban|th\u1EEBa|h\u1EEFu|k\xE8o|ca|tranh|thoa|ph\xF3|h\u1EB9n|ho\xE0|h\xF2a))|ship|v\u1EADn chuy\u1EC3n)${VI_END}|${VI_START}(?:giao h\xE0ng|giao nhanh|ship nhanh|ship h\xE0ng|v\u1EADn chuy\u1EC3n)\\s+(?:\u0111\u1EA3m b\u1EA3o|b\u1EA3o \u0111\u1EA3m|cam k\u1EBFt)${VI_END}`,
|
|
885
|
+
// "được hoàn tiền" là lời hứa ("Khách hàng được hoàn tiền"), trừ "để được" (câu hỏi cách làm) và "không được" (từ chối).
|
|
886
|
+
refundCommitment: `${VI_START}(?:cam k\u1EBFt|b\u1EA3o \u0111\u1EA3m|\u0111\u1EA3m b\u1EA3o|s\u1EBD|(?<!(?:\u0111\u1EC3|kh\xF4ng)\\s)\u0111\u01B0\u1EE3c)\\s+ho\xE0n ti\u1EC1n${VI_END}`,
|
|
887
|
+
refundCondition: `${VI_START}ho\xE0n ti\u1EC1n\\s+(?:n\u1EBFu|khi(?!\\s+n\xE0o)|ngay|to\xE0n b\u1ED9|\u0111\u1EA7y \u0111\u1EE7|100)(?![\\p{L}])`,
|
|
888
|
+
// "Không hợp, Lumen hoàn tiền". "không hợp lệ" là "invalid", và "… thì không hoàn tiền" là lời từ chối.
|
|
889
|
+
// "Nếu muốn/cần hoàn tiền" là hướng dẫn, không phải lời hứa. Một "không" ngay trước ("không được hoàn tiền") là từ chối.
|
|
890
|
+
refundIfUnhappy: `${VI_START}(?:kh\xF4ng\\s+(?:h\u1EE3p(?!\\s+l\u1EC7)|h\xE0i l\xF2ng|v\u1EEBa \xFD|\u01B0ng \xFD|\u01B0ng)${VI_END}|n\u1EBFu(?!\\s+(?:b\u1EA1n\\s+|qu\xFD kh\xE1ch\\s+)?(?:mu\u1ED1n|c\u1EA7n)))[^.!?\\n]{0,40}(?<!kh\xF4ng(?:\\s+\\p{L}+)?\\s)ho\xE0n ti\u1EC1n${VI_END}`,
|
|
891
|
+
// "…nhắn cho Lumen để nhận lại toàn bộ tiền", "Hoàn lại 100% tiền": lời hứa hoàn tiền không có chữ "hoàn tiền"
|
|
892
|
+
// liền nhau, lọt mọi mẫu trên (thân cam kết của gemfactory guarantee-band, 29/09/2026). "Không (được/thể) …" là từ
|
|
893
|
+
// chối; "tiền thừa/thối/lẻ" là tiền trả lại khi thanh toán.
|
|
894
|
+
moneyBack: `${VI_START}(?<!kh\xF4ng(?:\\s+(?:\u0111\u01B0\u1EE3c|th\u1EC3))?\\s)(?:nh\u1EADn|ho\xE0n|tr\u1EA3|g\u1EEDi)\\s+l\u1EA1i\\s+(?:(?:to\xE0n b\u1ED9|\u0111\u1EE7|\u0111\u1EA7y \u0111\u1EE7|100\\s*%)\\s+)?(?:s\u1ED1\\s+)?ti\u1EC1n(?!\\s+(?:th\u1EEBa|th\u1ED1i|l\u1EBB))${VI_END}`
|
|
895
|
+
};
|
|
896
|
+
var PROMISE_VI = new RegExp(Object.values(PROMISE_VI_PARTS).join("|"), "iu");
|
|
897
|
+
var INVENTED_PERSON_VI = /^\s*[-–—]?\s*(?:(?:Chị|Anh|Cô|Chú|Bác|Em|Bạn|Ông|Bà|Nguyễn|Trần|Lê|Phạm|Hoàng|Huỳnh|Phan|Vũ|Võ|Đặng|Bùi|Đỗ|Ngô|Dương|Đinh|Trương|Đoàn|Lương|Trịnh|Lý)(?:\s+\p{Lu}\p{Ll}*){1,3}\s*(?:,|\(|\s[-–—·])\s*\S|(?:Nguyễn|Trần|Lê|Phạm|Hoàng|Huỳnh|Phan|Vũ|Võ|Đặng|Bùi|Đỗ|Ngô|Dương|Đinh|Trương|Đoàn|Lương|Trịnh|Lý)\s+(?:Thị|Văn)(?:\s+\p{Lu}\p{Ll}*){1,2}\s*$)/u;
|
|
898
|
+
var PRICE_K_VI = /(?<![\p{L}\p{N}.,])\d{2,3}(?:[.,]\d{3})*\s?k(?![\p{L}\p{N}])/u;
|
|
899
|
+
var DISPLAY_QUANTITY = /(_to_show|_per_page|_per_row|_limit|_max|_visible|_columns?)$/;
|
|
900
|
+
function blankComments(raw) {
|
|
901
|
+
return raw.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")).replace(/(^|[^:])\/\/.*$/gm, (_m, p) => p);
|
|
902
|
+
}
|
|
903
|
+
function demoValues(src) {
|
|
904
|
+
const out = /* @__PURE__ */ new Map();
|
|
905
|
+
const head = /\bconst\s+DEMO\s*(?::[^=]+)?=\s*\{/.exec(src);
|
|
906
|
+
if (!head) return out;
|
|
907
|
+
let depth = 1;
|
|
908
|
+
let entry = "";
|
|
909
|
+
let entryStart = head.index + head[0].length;
|
|
910
|
+
const flush = (next) => {
|
|
911
|
+
const m = /^\s*["']?([A-Za-z_]\w*)["']?\s*:\s*([\s\S]+?)\s*$/.exec(entry);
|
|
912
|
+
const keyAt = entryStart + (entry.length - entry.trimStart().length);
|
|
913
|
+
if (m) out.set(m[1], { value: m[2], line: src.slice(0, keyAt).split("\n").length });
|
|
914
|
+
entry = "";
|
|
915
|
+
entryStart = next;
|
|
916
|
+
};
|
|
917
|
+
for (let i = entryStart; i < src.length; i++) {
|
|
918
|
+
const c = src[i];
|
|
919
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
920
|
+
let j = i + 1;
|
|
921
|
+
while (j < src.length && src[j] !== c) j += src[j] === "\\" ? 2 : 1;
|
|
922
|
+
entry += src.slice(i, j + 1);
|
|
923
|
+
i = j;
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
if (c === "{" || c === "[" || c === "(") depth++;
|
|
927
|
+
else if (c === "}" || c === "]" || c === ")") {
|
|
928
|
+
if (--depth === 0) {
|
|
929
|
+
flush(i + 1);
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
} else if (c === "," && depth === 1) {
|
|
933
|
+
flush(i + 1);
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
entry += c;
|
|
937
|
+
}
|
|
938
|
+
return out;
|
|
939
|
+
}
|
|
940
|
+
var PRICE_ANY = new RegExp(`[$\xA3\u20AC]\\s?\\d|\\b\\d[\\d.,]*\\s?(?:USD|EUR|GBP)\\b|${PROMISE_VI_PARTS.price}`, "iu");
|
|
941
|
+
function isQuestion(text) {
|
|
942
|
+
const t = text.trim();
|
|
943
|
+
if (!/\?$/.test(t) || /[.!?]\s+\S/.test(t)) return false;
|
|
944
|
+
return /^(?:can|do|does|did|how|what|when|where|why|who|which|is|are|will|would|should|could|may)\b/i.test(t) || /(?:không|chưa|nào|sao|gì|đâu|bao lâu|bao nhiêu|mấy|được không|à|hả|nhỉ)\s*\?$/iu.test(t) || /^(?:làm sao|tại sao|vì sao|khi nào|bao lâu|có)(?![\p{L}])/iu.test(t);
|
|
945
|
+
}
|
|
946
|
+
function claimIn(raw) {
|
|
947
|
+
const text = raw.normalize("NFC");
|
|
948
|
+
if (CLAIM_TEXT.test(text) || CLAIM_TEXT_VI.test(text)) return "hardcoded claim";
|
|
949
|
+
if (INVENTED_PERSON.test(`"${text}"`) || INVENTED_PERSON_VI.test(text)) return "invented person";
|
|
950
|
+
if (isQuestion(text)) return PRICE_ANY.test(text) || PRICE_K_VI.test(text) ? "promise the merchant never made" : null;
|
|
951
|
+
if (PRICE_K_VI.test(text) && /^\s*\d[\d.,]*\s?k\s*\+?\s*$/.test(text)) return "hardcoded figure";
|
|
952
|
+
if (PROMISE.test(text) || PROMISE_VI.test(text) || PRICE_K_VI.test(text)) return "promise the merchant never made";
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
function demoInvents(value) {
|
|
956
|
+
const v = value.trim();
|
|
957
|
+
if (/^-?\d/.test(v)) return "number";
|
|
958
|
+
if (/^[{[]/.test(v) && /(?:^|[:[,])\s*-?\d/.test(v.slice(1))) return "number";
|
|
959
|
+
for (const text of stringLiteralsIn(v)) {
|
|
960
|
+
const kind = claimIn(text);
|
|
961
|
+
if (kind) return kind;
|
|
962
|
+
}
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
var claimNamed = (key) => CLAIM_KEY.test(key) && !DISPLAY_QUANTITY.test(key);
|
|
966
|
+
function stringLiteralsIn(line) {
|
|
967
|
+
return [...line.matchAll(/(["'`])((?:\\.|(?!\1)[^\\])*)\1/g)].map((m) => m[2]);
|
|
968
|
+
}
|
|
969
|
+
function fabricatedProofInSource(raw) {
|
|
970
|
+
const src = blankComments(raw);
|
|
971
|
+
const out = [];
|
|
972
|
+
const demo = demoValues(src);
|
|
973
|
+
const lines = src.split("\n");
|
|
974
|
+
const caught = /* @__PURE__ */ new Set();
|
|
975
|
+
const demoReads = [];
|
|
976
|
+
const DANGLING = /(?:settings|slots|content_slots)\??\.([a-z_]+)\s*(?:\?\?|\|\|)\s*$/i;
|
|
977
|
+
lines.forEach((line, i) => {
|
|
978
|
+
const at = i + 1;
|
|
979
|
+
const slotKeys = [];
|
|
980
|
+
const carried = DANGLING.exec(lines[i - 1] ?? "")?.[1];
|
|
981
|
+
const reads = [...line.matchAll(/(?:settings|slots|content_slots)\??\.([a-z_]+)\s*(?:\?\?|\|\|)\s*([^;,)\]}]+)/gi)].map((m) => [m[1], m[2]]);
|
|
982
|
+
if (carried) reads.push([carried, /^\s*([^;,)\]}]+)/.exec(line)?.[1] ?? ""]);
|
|
983
|
+
for (const [key, rawFallback] of reads) {
|
|
984
|
+
slotKeys.push(key);
|
|
985
|
+
if (!claimNamed(key)) continue;
|
|
986
|
+
const fallback = rawFallback.trim();
|
|
987
|
+
const rest = fallback.replace(/\bDEMO\.\w+/g, "").trim();
|
|
988
|
+
if (/^(undefined|null|false|true|\[\]|\{\}|""|''|``)$/.test(rest)) continue;
|
|
989
|
+
if (!/\d/.test(rest)) continue;
|
|
990
|
+
out.push({ line: at, why: `${key} defaults to ${fallback}`, text: line.trim() });
|
|
991
|
+
}
|
|
992
|
+
for (const m of line.matchAll(/\bDEMO\.([A-Za-z_]\w*)/g)) {
|
|
993
|
+
const entry = demo.get(m[1]);
|
|
994
|
+
if (!entry) continue;
|
|
995
|
+
const kind = demoInvents(entry.value);
|
|
996
|
+
if (!kind) continue;
|
|
997
|
+
const slot = slotKeys.find(claimNamed);
|
|
998
|
+
if (kind === "number" && !claimNamed(m[1]) && !slot) continue;
|
|
999
|
+
demoReads.push({ at, key: m[1], entry, why: `${slot ?? m[1]} defaults to DEMO.${m[1]} = ${entry.value.trim()}`, text: line.trim() });
|
|
1000
|
+
}
|
|
1001
|
+
for (const m of line.matchAll(/(?<!\?\s*)["']?\b([A-Za-z_]\w*)["']?\s*:\s*["'`]?(-?\d(?:[\d.,]*\d)?)/g)) {
|
|
1002
|
+
const [, key, num] = m;
|
|
1003
|
+
if (!planKey(key)) continue;
|
|
1004
|
+
if (/^0(\.0+)?$/.test(num)) continue;
|
|
1005
|
+
caught.add(`${at}\0key\0${key}`);
|
|
1006
|
+
out.push({ line: at, why: `plan literal ${key}: ${num}`, text: line.trim() });
|
|
1007
|
+
}
|
|
1008
|
+
for (const text of stringLiteralsIn(line)) {
|
|
1009
|
+
if (text.includes("${")) continue;
|
|
1010
|
+
const kind = claimIn(text);
|
|
1011
|
+
if (!kind) continue;
|
|
1012
|
+
caught.add(`${at}\0str\0${text}`);
|
|
1013
|
+
out.push({ line: at, why: `${kind} ${JSON.stringify(text)}`, text: line.trim() });
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
for (const r of demoReads) {
|
|
1017
|
+
const onDemoLine = caught.has(`${r.entry.line}\0key\0${r.key}`) || stringLiteralsIn(r.entry.value).some((text) => caught.has(`${r.entry.line}\0str\0${text}`));
|
|
1018
|
+
if (!onDemoLine) out.push({ line: r.at, why: r.why, text: r.text });
|
|
1019
|
+
}
|
|
1020
|
+
return out.sort((a, b) => a.line - b.line);
|
|
1021
|
+
}
|
|
1022
|
+
function fabricatedProofInSchema(doc) {
|
|
1023
|
+
const out = [];
|
|
1024
|
+
const visit = (node) => {
|
|
1025
|
+
if (Array.isArray(node)) return node.forEach(visit);
|
|
1026
|
+
if (!node || typeof node !== "object") return;
|
|
1027
|
+
const rec = node;
|
|
1028
|
+
const id = typeof rec.id === "string" ? rec.id : null;
|
|
1029
|
+
const zero = /^0(\.0+)?$/.test(String(rec.default ?? "").trim());
|
|
1030
|
+
if (id && CLAIM_KEY.test(id) && !DISPLAY_QUANTITY.test(id) && rec.default != null && !zero && /\d/.test(String(rec.default))) {
|
|
1031
|
+
out.push(`schema default ${id} = ${JSON.stringify(rec.default)}`);
|
|
1032
|
+
}
|
|
1033
|
+
Object.values(rec).forEach(visit);
|
|
1034
|
+
};
|
|
1035
|
+
visit(doc);
|
|
1036
|
+
return out;
|
|
1037
|
+
}
|
|
1038
|
+
var TESTIMONY_NAME = /^(DEMO|STARTER|SAMPLE|PLACEHOLDER|FALLBACK)_.*(QUOTE|REVIEW|TESTIMONIAL|RATING|STARS?|CUSTOMER|PROOF|BUYER)/;
|
|
1039
|
+
var TESTIMONY_PROPERTY = /(?:^|_)(?:quotes?|testimonials?|reviews?|ratings?|stars?|customers?|reviewers?|authors?|buyers?)(?:_|$|\d)/;
|
|
1040
|
+
var LABEL_PROPERTY = /(?:^|_)(?:heading|title|label|eyebrow|kicker|intro|subheading|cta|button|link|caption)(?:_|$)/;
|
|
1041
|
+
function inventedTestimonyIn(raw) {
|
|
1042
|
+
const src = blankComments(raw);
|
|
1043
|
+
const usedAsFallback = (name) => (
|
|
1044
|
+
// The else-branch of ANY ternary, wrapped or not (`bound ? [] : DEMO_REVIEWS` slipped past `[\\w.]+`,
|
|
1045
|
+
// gemfactory #62). `?.` is optional chaining, not a ternary.
|
|
1046
|
+
new RegExp(`\\?(?![.?])[^:;?]*:\\s*${name}\\b|\\?\\?\\s*${name}\\b|\\|\\|\\s*${name}\\b`).test(src)
|
|
1047
|
+
);
|
|
1048
|
+
const out = [];
|
|
1049
|
+
for (const m of src.matchAll(/const\s+([A-Z][A-Z0-9_]*)\s*=/g)) {
|
|
1050
|
+
const name = m[1];
|
|
1051
|
+
if (TESTIMONY_NAME.test(name) && usedAsFallback(name)) out.push(name);
|
|
1052
|
+
}
|
|
1053
|
+
for (const [prop] of demoValues(src)) {
|
|
1054
|
+
if (!TESTIMONY_PROPERTY.test(prop) || LABEL_PROPERTY.test(prop) || DISPLAY_QUANTITY.test(prop)) continue;
|
|
1055
|
+
if (new RegExp(`\\bDEMO\\.${prop}\\b`).test(src)) out.push(`DEMO.${prop}`);
|
|
1056
|
+
}
|
|
1057
|
+
return out;
|
|
1058
|
+
}
|
|
1059
|
+
function demoOutsidePreview(raw) {
|
|
1060
|
+
const src = blankComments(raw);
|
|
1061
|
+
const out = [];
|
|
1062
|
+
const GUARDS = "hasSlots|pageBound|demoMode|hasAuthoredColumnData";
|
|
1063
|
+
for (const m of src.matchAll(/\bconst\s+(hasSlots|pageBound)\s*=\s*([^;]+);/g)) {
|
|
1064
|
+
if (!/^\s*section\.demo\s*!==\s*true\s*\|\|/.test(m[2])) out.push(`\`${m[1]}\` must start \`section.demo !== true || \u2026\` so a live store counts as filled: ${m[0].replace(/\s+/g, " ").trim()}`);
|
|
1065
|
+
}
|
|
1066
|
+
for (const m of src.matchAll(/\bconst\s+demoMode\s*=\s*([^;]+);/g)) {
|
|
1067
|
+
if (!/&&\s*section\.demo\s*===\s*true\b|\bsection\.demo\s*===\s*true\s*&&/.test(m[1])) out.push(`\`demoMode\` must require \`section.demo === true\`: ${m[0].replace(/\s+/g, " ").trim()}`);
|
|
1068
|
+
}
|
|
1069
|
+
const guard = new RegExp(`\\b(?:${GUARDS})\\b|\\bsection\\.demo\\b`);
|
|
1070
|
+
for (const m of src.matchAll(/\bDEMO(?:_[A-Z0-9_]+\b|\.[A-Za-z_])/g)) {
|
|
1071
|
+
const at = m.index ?? 0;
|
|
1072
|
+
const lineStart = src.lastIndexOf("\n", at) + 1;
|
|
1073
|
+
if (/^\s*(?:export\s+)?const\s+DEMO/.test(src.slice(lineStart, at + m[0].length + 40))) continue;
|
|
1074
|
+
let depth = 0;
|
|
1075
|
+
let start = 0;
|
|
1076
|
+
for (let k = at - 1; k >= 0; k--) {
|
|
1077
|
+
const c = src[k];
|
|
1078
|
+
if (c === "}" || c === ")" || c === "]") depth++;
|
|
1079
|
+
else if (c === "{" || c === "(" || c === "[") {
|
|
1080
|
+
if (depth === 0 && c === "{") {
|
|
1081
|
+
start = k;
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
if (depth > 0) depth--;
|
|
1085
|
+
} else if (c === ";" && depth === 0) {
|
|
1086
|
+
start = k;
|
|
1087
|
+
break;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (!guard.test(src.slice(start, at))) {
|
|
1091
|
+
const lineNo = src.slice(0, at).split("\n").length;
|
|
1092
|
+
const lineEnd = src.indexOf("\n", at);
|
|
1093
|
+
out.push(`line ${lineNo} reads starter copy with no preview guard: ${src.slice(lineStart, lineEnd < 0 ? void 0 : lineEnd).trim().slice(0, 120)}`);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return out;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// ../../packages/gem-sections/src/section-artifacts.ts
|
|
1100
|
+
function issue(severity, code, message, sectionId, metadata = {}) {
|
|
1101
|
+
return {
|
|
1102
|
+
severity,
|
|
1103
|
+
code,
|
|
1104
|
+
message,
|
|
1105
|
+
section_id: sectionId,
|
|
1106
|
+
...metadata
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
function requiredArtifactPaths(definition) {
|
|
1110
|
+
const shopify = definition.adapters.includes("shopify");
|
|
1111
|
+
const headlessSurface = definition.paths?.element_tree ?? (definition.element_tree ? void 0 : definition.paths?.headless);
|
|
1112
|
+
return [
|
|
1113
|
+
definition.paths?.manifest,
|
|
1114
|
+
...shopify ? [definition.paths?.shopify, definition.paths?.css, definition.paths?.preview] : [],
|
|
1115
|
+
...definition.adapters.includes("headless") ? [headlessSurface] : [],
|
|
1116
|
+
...definition.dependencies.length > 0 && definition.paths?.behavior ? [definition.paths.behavior] : []
|
|
1117
|
+
].filter((path) => Boolean(path));
|
|
1118
|
+
}
|
|
1119
|
+
function isGemFrameworkArtifactPath(path) {
|
|
1120
|
+
return /\.(css|liquid|html|tsx|js)$/.test(path);
|
|
1121
|
+
}
|
|
1122
|
+
function validateSectionArtifacts(definition, artifacts) {
|
|
1123
|
+
const issues = [];
|
|
1124
|
+
const byPath = new Map(artifacts.map((artifact) => [artifact.path, artifact]));
|
|
1125
|
+
const marker = `data-gem-section-library-id="${definition.id}"`;
|
|
1126
|
+
for (const artifactPath of requiredArtifactPaths(definition)) {
|
|
1127
|
+
if (!byPath.has(artifactPath)) {
|
|
1128
|
+
issues.push(issue("error", "missing_section_artifact", `Missing required section artifact: ${artifactPath}`, definition.id));
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
for (const path of [definition.paths?.shopify, definition.paths?.preview].filter(Boolean)) {
|
|
1132
|
+
const artifact = byPath.get(path);
|
|
1133
|
+
if (artifact && !artifact.content.includes(marker)) {
|
|
1134
|
+
issues.push(issue("error", "missing_section_provenance_marker", `${path} is missing ${marker}.`, definition.id));
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
for (const artifact of artifacts.filter((item) => isGemFrameworkArtifactPath(item.path))) {
|
|
1138
|
+
const validation = validateGemFrameworkArtifact(artifact, {
|
|
1139
|
+
enforceKnownClasses: true,
|
|
1140
|
+
sectionClassPrefixes: [
|
|
1141
|
+
`gem-${slugify(definition.archetype)}`,
|
|
1142
|
+
...definition.class_root ? [definition.class_root] : []
|
|
1143
|
+
]
|
|
1144
|
+
});
|
|
1145
|
+
for (const frameworkIssue of validation.issues) {
|
|
1146
|
+
const severity = definition.status === "stable" && frameworkIssue.severity === "warn" ? "error" : frameworkIssue.severity;
|
|
1147
|
+
issues.push(issue(
|
|
1148
|
+
severity,
|
|
1149
|
+
`gem_framework_${frameworkIssue.code}`,
|
|
1150
|
+
`${artifact.path}: ${frameworkIssue.message}`,
|
|
1151
|
+
definition.id,
|
|
1152
|
+
{
|
|
1153
|
+
artifact_path: frameworkIssue.artifact_path ?? artifact.path,
|
|
1154
|
+
...frameworkIssue.class_name ? { framework_class_name: frameworkIssue.class_name } : {},
|
|
1155
|
+
...frameworkIssue.property ? { framework_property: frameworkIssue.property } : {},
|
|
1156
|
+
...frameworkIssue.value ? { framework_value: frameworkIssue.value } : {}
|
|
1157
|
+
}
|
|
1158
|
+
));
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return {
|
|
1162
|
+
valid: !issues.some((item) => item.severity === "error"),
|
|
1163
|
+
issues
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
export {
|
|
1167
|
+
CLAIM_KEY,
|
|
1168
|
+
CLAIM_PLAN_KEY,
|
|
1169
|
+
CLAIM_TEXT,
|
|
1170
|
+
CLAIM_TEXT_VI,
|
|
1171
|
+
INVENTED_PERSON,
|
|
1172
|
+
INVENTED_PERSON_VI,
|
|
1173
|
+
LAYOUT_ATTRS,
|
|
1174
|
+
PRICE_K_VI,
|
|
1175
|
+
PROMISE,
|
|
1176
|
+
PROMISE_VI,
|
|
1177
|
+
SHARED_FRAMEWORK_PREFIXES,
|
|
1178
|
+
TESTIMONY_NAME,
|
|
1179
|
+
TESTIMONY_PROPERTY,
|
|
1180
|
+
claimIn,
|
|
1181
|
+
classesStyledBy,
|
|
1182
|
+
cssPairingOrphans,
|
|
1183
|
+
demoOutsidePreview,
|
|
1184
|
+
fabricatedProofInSchema,
|
|
1185
|
+
fabricatedProofInSource,
|
|
1186
|
+
inventedTestimonyIn,
|
|
1187
|
+
isFrameworkOwnedClass,
|
|
1188
|
+
isGemFrameworkArtifactPath,
|
|
1189
|
+
issue,
|
|
1190
|
+
layoutKnobCheck,
|
|
1191
|
+
requiredArtifactPaths,
|
|
1192
|
+
sectionClassesIn,
|
|
1193
|
+
sectionLibraryIdOf,
|
|
1194
|
+
slotContractDrift,
|
|
1195
|
+
slotSourceFromElementTree,
|
|
1196
|
+
slotsReadBy,
|
|
1197
|
+
stringLiteralsIn,
|
|
1198
|
+
validateSectionArtifacts
|
|
1199
|
+
};
|