@gemboss/gem-framework 0.6.0 → 0.9.2
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 +69 -1
- package/dist/SOURCE.json +2 -2
- package/dist/gem-framework.css +206 -147
- package/dist/gem-runtime.js +101 -16
- package/dist/index.js +637 -179
- 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 +44 -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/section-gate.js
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
// ../../packages/gem-framework/src/form-set.ts
|
|
2
|
+
var FORM_CSS_VAR = {
|
|
3
|
+
radius: "--gem-input-radius",
|
|
4
|
+
borderWidth: "--gem-input-border-width",
|
|
5
|
+
borderColor: "--gem-input-border-color",
|
|
6
|
+
fill: "--gem-input-bg",
|
|
7
|
+
height: "--gem-input-height",
|
|
8
|
+
paddingInline: "--gem-input-padding-inline",
|
|
9
|
+
fontSize: "--gem-input-font-size",
|
|
10
|
+
focusRing: "--gem-input-focus-ring",
|
|
11
|
+
checkboxAccent: "--gem-checkbox-accent",
|
|
12
|
+
checkboxSize: "--gem-checkbox-size"
|
|
13
|
+
};
|
|
14
|
+
var GEM_FORM_CSS_VARIABLES = Object.values(FORM_CSS_VAR);
|
|
15
|
+
|
|
1
16
|
// ../../packages/gem-framework/src/tokens.ts
|
|
2
17
|
var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
|
|
3
18
|
var GEM_TAP_MIN_CSS_VAR = "--gem-tap-min: 44px;";
|
|
@@ -203,15 +218,6 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
203
218
|
"gem-delivery-date",
|
|
204
219
|
"gem-stock-indicator",
|
|
205
220
|
"gem-popup",
|
|
206
|
-
// Popup shell (primitives.ts GEM_POPUP_SHELL_CSS): the dialog frame popup sections draw their content in.
|
|
207
|
-
"gem-popup-shell",
|
|
208
|
-
"gem-popup-shell__dialog",
|
|
209
|
-
"gem-popup-shell__frame",
|
|
210
|
-
"gem-popup-shell__media",
|
|
211
|
-
"gem-popup-shell__image",
|
|
212
|
-
"gem-popup-shell__panel",
|
|
213
|
-
"gem-popup-shell__close",
|
|
214
|
-
"gem-popup-shell__teaser",
|
|
215
221
|
"gem-back-to-top",
|
|
216
222
|
// measured commerce atoms mapped by parts.ts (GEM_PART_CLASS) alongside gem-price/gem-badge.
|
|
217
223
|
"gem-discount",
|
|
@@ -238,6 +244,22 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
238
244
|
"gem-cart-line-item__price"
|
|
239
245
|
]
|
|
240
246
|
},
|
|
247
|
+
{
|
|
248
|
+
// The popup shell: the dialog frame popup sections draw their content in. Styled by popup-shell.ts
|
|
249
|
+
// (GEM_POPUP_SHELL_CSS), which ships as its own popup-shell.css since 0.8.3, not in primitives.css: a page that
|
|
250
|
+
// draws one of these loads that file (gem-framework.css, the whole file, carries it too).
|
|
251
|
+
group: "popup-shell",
|
|
252
|
+
classes: [
|
|
253
|
+
"gem-popup-shell",
|
|
254
|
+
"gem-popup-shell__dialog",
|
|
255
|
+
"gem-popup-shell__frame",
|
|
256
|
+
"gem-popup-shell__media",
|
|
257
|
+
"gem-popup-shell__image",
|
|
258
|
+
"gem-popup-shell__panel",
|
|
259
|
+
"gem-popup-shell__close",
|
|
260
|
+
"gem-popup-shell__teaser"
|
|
261
|
+
]
|
|
262
|
+
},
|
|
241
263
|
{
|
|
242
264
|
group: "sections",
|
|
243
265
|
classes: [
|
|
@@ -340,58 +362,7 @@ var gemFrameworkValidationRules = [
|
|
|
340
362
|
{ code: "external_dependency", severity: "error", surface: "runtime" }
|
|
341
363
|
];
|
|
342
364
|
|
|
343
|
-
// ../../packages/gem-framework/src/
|
|
344
|
-
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")`;
|
|
345
|
-
var spikePath = (() => {
|
|
346
|
-
const teeth = [];
|
|
347
|
-
for (let x = 95; x > 0; x -= 5) teeth.push(`L${x} ${teeth.length % 2 === 0 ? 10 : 3}`);
|
|
348
|
-
return `M0 0 H100 V3 ${teeth.join(" ")} L0 ${teeth.length % 2 === 0 ? 10 : 3} Z`;
|
|
349
|
-
})();
|
|
350
|
-
var DIVIDER_SHAPES = {
|
|
351
|
-
wave: "M0 0 H100 V3 C80 9 70 -1 50 4 C30 9 20 1 0 4 Z",
|
|
352
|
-
slope: "M0 0 H100 V10 Z",
|
|
353
|
-
spikes: spikePath,
|
|
354
|
-
arch: "M0 0 H100 V2 Q50 12 0 2 Z",
|
|
355
|
-
blob: "M0 0 H100 V4 C85 11 72 0 58 5 C44 10 30 2 16 6 C10 8 5 5 0 6 Z"
|
|
356
|
-
};
|
|
357
|
-
var GEM_DIVIDER_CSS_LINES = [
|
|
358
|
-
// The clip is WIDENED, not removed. `.gem-section` sets `overflow: clip` and the wedge is drawn
|
|
359
|
-
// at `top: 100%` / `bottom: 100%` — entirely outside the box — so the parent deleted every
|
|
360
|
-
// divider on every track. Measured on the live storefront 2026-08-25: a band with the documented
|
|
361
|
-
// `data-gem-section-background="dark"` + `data-gem-divider-bottom="wave"` rendered a FLAT edge,
|
|
362
|
-
// while the attribute, the rule and the fallback colour were all present and correct.
|
|
363
|
-
// NOT `overflow: visible`: `.gem-hero` uses the clip to hold its own border-radius, so releasing
|
|
364
|
-
// it entirely would paint square corners over that radius. A browser without
|
|
365
|
-
// `overflow-clip-margin` keeps plain `clip` — the divider stays hidden, which is today's
|
|
366
|
-
// behaviour rather than a broken layout.
|
|
367
|
-
":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); }",
|
|
368
|
-
// Shared geometry. `top: 100%` / `bottom: 100%` put it outside the box; z-index 1 keeps it above the
|
|
369
|
-
// neighbouring band's background. pointer-events: none so it never eats a click near the seam.
|
|
370
|
-
// Qualified by SHAPE. Unqualified, any value paints a solid 48px slab — including the literal
|
|
371
|
-
// "none", which the attribute writer forwards unchanged. The clip used to hide that; widening
|
|
372
|
-
// it above makes a stray value visible.
|
|
373
|
-
":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%; }",
|
|
374
|
-
":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%; }",
|
|
375
|
-
// The top edge reuses the same masks, mirrored, so five shapes cover both edges.
|
|
376
|
-
":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); }",
|
|
377
|
-
...Object.entries(DIVIDER_SHAPES).flatMap(([name, path]) => {
|
|
378
|
-
const mask = dividerMask(path);
|
|
379
|
-
return [
|
|
380
|
-
`: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}; }`,
|
|
381
|
-
`: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}; }`
|
|
382
|
-
];
|
|
383
|
-
})
|
|
384
|
-
];
|
|
385
|
-
var GEM_DIVIDER_CSS = GEM_DIVIDER_CSS_LINES.join("\n");
|
|
386
|
-
var GEM_CARD_CSS_LINES = [
|
|
387
|
-
":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); }",
|
|
388
|
-
":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); }",
|
|
389
|
-
":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); }",
|
|
390
|
-
// `filled` tints the section's own ground rather than reaching for a fixed colour, so it stays
|
|
391
|
-
// legible on an accent or dark band instead of punching a light panel into it.
|
|
392
|
-
":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); }"
|
|
393
|
-
];
|
|
394
|
-
var GEM_CARD_CSS = GEM_CARD_CSS_LINES.join("\n");
|
|
365
|
+
// ../../packages/gem-framework/src/popup-shell.ts
|
|
395
366
|
var GEM_POPUP_SHELL_CSS_LINES = [
|
|
396
367
|
".gem-popup-shell { padding: 0; }",
|
|
397
368
|
".gem-popup-shell__dialog {",
|
|
@@ -411,8 +382,12 @@ var GEM_POPUP_SHELL_CSS_LINES = [
|
|
|
411
382
|
".gem-popup-shell[data-gem-popup-desktop='split-right'] .gem-popup-shell__media { order: 2; }",
|
|
412
383
|
".gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__frame,",
|
|
413
384
|
".gem-popup-shell[data-gem-popup-desktop='card'] .gem-popup-shell__frame { grid-template-columns: 1fr; }",
|
|
414
|
-
|
|
415
|
-
|
|
385
|
+
// 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
|
|
386
|
+
// (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.
|
|
387
|
+
"@media (min-width: 750px) {",
|
|
388
|
+
" .gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__dialog,",
|
|
389
|
+
" .gem-popup-shell[data-gem-popup-desktop='card'] .gem-popup-shell__dialog { width: min(480px, 92vw); }",
|
|
390
|
+
"}",
|
|
416
391
|
".gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__media { display: none; }",
|
|
417
392
|
".gem-popup-shell[data-gem-popup-desktop='card'] .gem-popup-shell__media { max-height: 200px; }",
|
|
418
393
|
".gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__frame { grid-template-columns: 1fr; place-items: center; }",
|
|
@@ -451,6 +426,12 @@ var GEM_POPUP_SHELL_CSS_LINES = [
|
|
|
451
426
|
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-media='background'] .gem-popup-shell__media { position: absolute; inset: 0; }",
|
|
452
427
|
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-media='background'] .gem-popup-shell__panel { position: relative; z-index: 1; }",
|
|
453
428
|
" .gem-popup-shell[data-gem-popup-desktop='card-over-image'][data-gem-popup-media='background'] .gem-popup-shell__panel { color: var(--gem-color-text); }",
|
|
429
|
+
// Ở 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
|
|
430
|
+
// đặ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
|
|
431
|
+
// 04/10/2026: 1,04:1 ở mọi bản ghép như vậy.
|
|
432
|
+
" .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,",
|
|
433
|
+
" .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); }",
|
|
434
|
+
" .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; }",
|
|
454
435
|
"}",
|
|
455
436
|
"@media (max-width: 749px) {",
|
|
456
437
|
// Mobile: takeover is the whole screen; a sheet rises from the bottom to at most 88% of it. The image
|
|
@@ -462,7 +443,9 @@ var GEM_POPUP_SHELL_CSS_LINES = [
|
|
|
462
443
|
" .gem-popup-shell__panel { justify-content: flex-start; padding: var(--gem-space-3xl) var(--gem-space-lg) var(--gem-space-xl); }",
|
|
463
444
|
" .gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__panel,",
|
|
464
445
|
" .gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__panel { justify-content: center; }",
|
|
465
|
-
|
|
446
|
+
// 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
|
|
447
|
+
// ả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.
|
|
448
|
+
" .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; }",
|
|
466
449
|
" .gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__media { position: relative; }",
|
|
467
450
|
" .gem-popup-shell__media { order: 2; max-height: 34dvh; }",
|
|
468
451
|
" .gem-popup-shell[data-gem-popup-media='top'] .gem-popup-shell__media { order: 0; }",
|
|
@@ -471,20 +454,160 @@ var GEM_POPUP_SHELL_CSS_LINES = [
|
|
|
471
454
|
" .gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__frame { grid-template-rows: 1fr; }",
|
|
472
455
|
" .gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__media { position: absolute; inset: 0; max-height: none; }",
|
|
473
456
|
"}",
|
|
474
|
-
|
|
475
|
-
|
|
457
|
+
// 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
|
|
458
|
+
// 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,
|
|
459
|
+
// 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
|
|
460
|
+
// và sửa được như mọi band khác.
|
|
461
|
+
// 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,
|
|
462
|
+
// 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ó ở
|
|
463
|
+
// dialog không-modal).
|
|
464
|
+
":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); }",
|
|
465
|
+
":root:not(:has([data-gem-canvas])) .gem-popup-shell[data-gem-popup-preview='true'] .gem-popup-shell__dialog { position: fixed; inset: 0; display: block; }",
|
|
466
|
+
":root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'] .gem-popup-shell__dialog { position: relative; display: block; inset: auto; }",
|
|
467
|
+
":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); }",
|
|
476
468
|
"@media (min-width: 750px) {",
|
|
477
|
-
" .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] { padding: 0; }",
|
|
478
|
-
" .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] .gem-popup-shell__dialog { height: 720px; max-height: none; }",
|
|
469
|
+
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] { padding: 0; }",
|
|
470
|
+
" :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; }",
|
|
479
471
|
"}",
|
|
480
472
|
"@media (max-width: 749px) {",
|
|
481
|
-
" .gem-popup-shell[data-gem-popup-preview='true'] { padding: 0; }",
|
|
482
|
-
" .gem-popup-shell[data-gem-popup-preview='true'] .gem-popup-shell__dialog { height: 844px; max-height: none; width: 100%; max-width: 100%; }",
|
|
483
|
-
" .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-mobile='sheet'] { padding-top: 160px; }",
|
|
484
|
-
" .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-mobile='sheet'] .gem-popup-shell__dialog { min-height: 0; }",
|
|
473
|
+
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'] { padding: 0; }",
|
|
474
|
+
" :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%; }",
|
|
475
|
+
" :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-mobile='sheet'] { padding-top: 160px; }",
|
|
476
|
+
" :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; }",
|
|
485
477
|
"}"
|
|
486
478
|
];
|
|
487
479
|
var GEM_POPUP_SHELL_CSS = GEM_POPUP_SHELL_CSS_LINES.join("\n");
|
|
480
|
+
|
|
481
|
+
// ../../packages/gem-framework/src/primitives.ts
|
|
482
|
+
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")`;
|
|
483
|
+
var spikePath = (() => {
|
|
484
|
+
const teeth = [];
|
|
485
|
+
for (let x = 95; x > 0; x -= 5) teeth.push(`L${x} ${teeth.length % 2 === 0 ? 10 : 3}`);
|
|
486
|
+
return `M0 0 H100 V3 ${teeth.join(" ")} L0 ${teeth.length % 2 === 0 ? 10 : 3} Z`;
|
|
487
|
+
})();
|
|
488
|
+
var DIVIDER_SHAPES = {
|
|
489
|
+
wave: "M0 0 H100 V3 C80 9 70 -1 50 4 C30 9 20 1 0 4 Z",
|
|
490
|
+
slope: "M0 0 H100 V10 Z",
|
|
491
|
+
spikes: spikePath,
|
|
492
|
+
arch: "M0 0 H100 V2 Q50 12 0 2 Z",
|
|
493
|
+
blob: "M0 0 H100 V4 C85 11 72 0 58 5 C44 10 30 2 16 6 C10 8 5 5 0 6 Z"
|
|
494
|
+
};
|
|
495
|
+
var GEM_DIVIDER_CSS_LINES = [
|
|
496
|
+
// The clip is WIDENED, not removed. `.gem-section` sets `overflow: clip` and the wedge is drawn
|
|
497
|
+
// at `top: 100%` / `bottom: 100%` — entirely outside the box — so the parent deleted every
|
|
498
|
+
// divider on every track. Measured on the live storefront 2026-08-25: a band with the documented
|
|
499
|
+
// `data-gem-section-background="dark"` + `data-gem-divider-bottom="wave"` rendered a FLAT edge,
|
|
500
|
+
// while the attribute, the rule and the fallback colour were all present and correct.
|
|
501
|
+
// NOT `overflow: visible`: `.gem-hero` uses the clip to hold its own border-radius, so releasing
|
|
502
|
+
// it entirely would paint square corners over that radius. A browser without
|
|
503
|
+
// `overflow-clip-margin` keeps plain `clip` — the divider stays hidden, which is today's
|
|
504
|
+
// behaviour rather than a broken layout.
|
|
505
|
+
":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); }",
|
|
506
|
+
// Shared geometry. `top: 100%` / `bottom: 100%` put it outside the box; z-index 1 keeps it above the
|
|
507
|
+
// neighbouring band's background. pointer-events: none so it never eats a click near the seam.
|
|
508
|
+
// Qualified by SHAPE. Unqualified, any value paints a solid 48px slab — including the literal
|
|
509
|
+
// "none", which the attribute writer forwards unchanged. The clip used to hide that; widening
|
|
510
|
+
// it above makes a stray value visible.
|
|
511
|
+
":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%; }",
|
|
512
|
+
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom]::after { top: 100%; }",
|
|
513
|
+
// The top edge reuses the same masks, mirrored, so five shapes cover both edges.
|
|
514
|
+
":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); }",
|
|
515
|
+
...Object.entries(DIVIDER_SHAPES).flatMap(([name, path]) => {
|
|
516
|
+
const mask = dividerMask(path);
|
|
517
|
+
return [
|
|
518
|
+
`: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}; }`,
|
|
519
|
+
`: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}; }`
|
|
520
|
+
];
|
|
521
|
+
})
|
|
522
|
+
];
|
|
523
|
+
var GEM_DIVIDER_CSS = GEM_DIVIDER_CSS_LINES.join("\n");
|
|
524
|
+
var GEM_CARD_CSS_LINES = [
|
|
525
|
+
":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); }",
|
|
526
|
+
":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); }",
|
|
527
|
+
":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); }",
|
|
528
|
+
// `filled` tints the section's own ground rather than reaching for a fixed colour, so it stays
|
|
529
|
+
// legible on an accent or dark band instead of punching a light panel into it.
|
|
530
|
+
":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); }"
|
|
531
|
+
];
|
|
532
|
+
var GEM_CARD_CSS = GEM_CARD_CSS_LINES.join("\n");
|
|
533
|
+
var GEM_FORM_SET_CSS_LINES = [
|
|
534
|
+
// FORM SET (form-set.ts). Every rule is scoped under [data-gem-form-set], which <body> carries only when the plan
|
|
535
|
+
// sets `tokens.form`: a store without one renders as before. The variables are always COMPLETE (missing keys take
|
|
536
|
+
// GEM_FORM_DEFAULTS) and are declared on the controls themselves (GEM_FORM_VAR_SCOPE), so these rules never fall back
|
|
537
|
+
// to a value nobody chose and a band that redefines its palette recolours its fields.
|
|
538
|
+
// THE FIELD LIST names the dawn-parity footer field (0,2,0, later in the sheet) so :is() — which takes its most
|
|
539
|
+
// specific argument — outranks it: before, that field ignored every form set (review of #665). The main search is
|
|
540
|
+
// LEFT OUT (`:where(:not(…))`, no specificity): it is a field and a button drawn as one block whose button belongs to
|
|
541
|
+
// the profile, and restyling only the field half left it open on one side.
|
|
542
|
+
// .gem-field-join is joined by construction (the seller chose the join element), so `data-gem-field-join` does not
|
|
543
|
+
// open it; gemsite's own field + button pairs read that attribute.
|
|
544
|
+
"[data-gem-form-set] :is(.gem-input, .gem-select, .gem-textarea, .gem-shell-footer__newsletter .gem-input):where(:not(.gem-search-form .gem-input)) {",
|
|
545
|
+
" min-height: var(--gem-input-height);",
|
|
546
|
+
" height: auto;",
|
|
547
|
+
" border: var(--gem-input-border-width) solid var(--gem-input-border-color);",
|
|
548
|
+
" border-radius: var(--gem-input-radius);",
|
|
549
|
+
" background: var(--gem-input-bg);",
|
|
550
|
+
" padding-inline: var(--gem-input-padding-inline);",
|
|
551
|
+
" font-size: var(--gem-input-font-size);",
|
|
552
|
+
"}",
|
|
553
|
+
// A textarea is two rows tall and never a pill: a 999px radius grows with the box as the shopper drags it. Keyed on
|
|
554
|
+
// the ELEMENT, not .gem-textarea, so a copy whose list adds its own fields (gemsite's cart note) reaches them too.
|
|
555
|
+
"[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)) {",
|
|
556
|
+
" min-height: calc(var(--gem-input-height) * 2);",
|
|
557
|
+
" border-radius: min(var(--gem-input-radius), 12px);",
|
|
558
|
+
" padding-block: var(--gem-space-sm);",
|
|
559
|
+
"}",
|
|
560
|
+
"[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 {",
|
|
561
|
+
" outline: var(--gem-input-focus-ring, var(--gem-focus-ring, 2px solid var(--gem-color-accent)));",
|
|
562
|
+
" outline-offset: var(--gem-focus-offset, 2px);",
|
|
563
|
+
"}",
|
|
564
|
+
// underline: only the bottom rule, no box. filled: the tinted field keeps its top corners and a bottom rule.
|
|
565
|
+
'[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)) {',
|
|
566
|
+
" border-width: 0 0 var(--gem-input-border-width);",
|
|
567
|
+
" border-radius: 0;",
|
|
568
|
+
" background: transparent;",
|
|
569
|
+
// Transparent, so it reads on whatever is behind it: the ink of the band it sits on, not the page's. A dark section
|
|
570
|
+
// changes `color` and leaves --gem-color-text dark (review of #665: dark text on #202223, 1:1).
|
|
571
|
+
" color: inherit;",
|
|
572
|
+
"}",
|
|
573
|
+
// 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
|
|
574
|
+
// only visible label), and a select's options keep the system's own pair: the popup on Windows/Linux takes the
|
|
575
|
+
// select's colours, so inherited light ink on a transparent field drew light options on a light list.
|
|
576
|
+
'[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 {',
|
|
577
|
+
" color: color-mix(in srgb, currentColor 70%, transparent);",
|
|
578
|
+
"}",
|
|
579
|
+
'[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 {',
|
|
580
|
+
" color: CanvasText;",
|
|
581
|
+
" background: Canvas;",
|
|
582
|
+
"}",
|
|
583
|
+
'[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)) {',
|
|
584
|
+
" border-width: 0 0 var(--gem-input-border-width);",
|
|
585
|
+
" border-end-start-radius: 0;",
|
|
586
|
+
" border-end-end-radius: 0;",
|
|
587
|
+
"}",
|
|
588
|
+
// The main search stays the profile's: the variables are declared on (and inherited by) every input, and the
|
|
589
|
+
// profile's base .gem-input rule reads two of them as hooks, so set those back to "unset" here (morph: 1 → 2px).
|
|
590
|
+
// DISABLED keeps a tinted fill (the base :disabled rule is 0,2,0 and the field rule above outranks it): the tint goes
|
|
591
|
+
// OVER the field's own fill, so a white field on a dark band stays white under its dark text (review of #665: a
|
|
592
|
+
// transparent tint there was 1:1). §9.2 state matrix: a disabled field must still look disabled, not only fainter.
|
|
593
|
+
"[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 {",
|
|
594
|
+
" background: color-mix(in srgb, currentColor 6%, var(--gem-input-bg));",
|
|
595
|
+
"}",
|
|
596
|
+
// 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.
|
|
597
|
+
'[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 {',
|
|
598
|
+
" background: color-mix(in srgb, currentColor 6%, transparent);",
|
|
599
|
+
"}",
|
|
600
|
+
"[data-gem-form-set] .gem-search-form .gem-input {",
|
|
601
|
+
" --gem-input-border-width: initial;",
|
|
602
|
+
" --gem-input-radius: initial;",
|
|
603
|
+
"}",
|
|
604
|
+
"[data-gem-form-set] .gem-checkbox {",
|
|
605
|
+
" inline-size: var(--gem-checkbox-size);",
|
|
606
|
+
" block-size: var(--gem-checkbox-size);",
|
|
607
|
+
" accent-color: var(--gem-checkbox-accent);",
|
|
608
|
+
"}"
|
|
609
|
+
];
|
|
610
|
+
var GEM_FORM_SET_CSS = GEM_FORM_SET_CSS_LINES.join("\n");
|
|
488
611
|
var GEM_COUNTDOWN_ENDED_CSS_LINES = [
|
|
489
612
|
":root:not(:has([data-gem-canvas])) [data-gem-section-id][data-gem-countdown-ended='true'] { display: none; }",
|
|
490
613
|
":root:has([data-gem-canvas]) [data-gem-section-id][data-gem-countdown-ended='true'] { opacity: .55; }",
|
|
@@ -547,12 +670,12 @@ var GEM_ROW_CSS_LINES = [
|
|
|
547
670
|
var GEM_ROW_GAP_CSS = GEM_ROW_GAP_CSS_LINES.join("\n");
|
|
548
671
|
var GEM_ROW_CSS = GEM_ROW_CSS_LINES.join("\n");
|
|
549
672
|
var GEM_GRID_COLUMNS_KNOB_CSS_LINES = [
|
|
550
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
551
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
552
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
553
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
554
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
555
|
-
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow
|
|
673
|
+
":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)); }",
|
|
674
|
+
":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; }",
|
|
675
|
+
":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)); }",
|
|
676
|
+
":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; }",
|
|
677
|
+
":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)); }",
|
|
678
|
+
":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; }"
|
|
556
679
|
];
|
|
557
680
|
var GEM_GRID_COLUMNS_ROW_KNOB_CSS = GEM_GRID_COLUMNS_KNOB_CSS_LINES.filter((line) => line.includes(" .gem-row:is(")).join("\n");
|
|
558
681
|
|
|
@@ -646,6 +769,13 @@ var countdownPluginRegistrationLines = [
|
|
|
646
769
|
" let endAt = 0;",
|
|
647
770
|
" let handle = null;",
|
|
648
771
|
" let state = 'idle';",
|
|
772
|
+
// gem:countdown:start means the N seconds began: once. A visible-mode timer pauses and resumes as it leaves and
|
|
773
|
+
// re-enters the screen, and each resume used to announce a new start (gemboss PR #3758 review, item 7).
|
|
774
|
+
" let started = false;",
|
|
775
|
+
// The visible-mode observer, released as soon as the timer can no longer run (complete or stopped), not at the
|
|
776
|
+
// next intersection change after it.
|
|
777
|
+
" let observer = null;",
|
|
778
|
+
" const release = () => { if (observer) { observer.disconnect(); observer = null; } };",
|
|
649
779
|
" const detail = () => ({ source: 'gem-runtime', id: timer.id || undefined, mode: 'duration', duration: total, remainingSeconds: Math.max(0, Math.ceil(left / 1000)) });",
|
|
650
780
|
" const render = () => {",
|
|
651
781
|
" const s = Math.max(0, Math.ceil(left / 1000));",
|
|
@@ -661,6 +791,7 @@ var countdownPluginRegistrationLines = [
|
|
|
661
791
|
" const halt = () => { if (handle !== null && target.clearInterval) target.clearInterval(handle); handle = null; };",
|
|
662
792
|
" const finish = () => {",
|
|
663
793
|
" halt();",
|
|
794
|
+
" release();",
|
|
664
795
|
" left = 0;",
|
|
665
796
|
" state = 'complete';",
|
|
666
797
|
" render();",
|
|
@@ -679,20 +810,20 @@ var countdownPluginRegistrationLines = [
|
|
|
679
810
|
" endAt = Date.now() + left;",
|
|
680
811
|
" render();",
|
|
681
812
|
" if (target.setInterval) handle = target.setInterval(tick, 250);",
|
|
682
|
-
" api.emit('gem:countdown:start', detail());",
|
|
813
|
+
" if (!started) { started = true; api.emit('gem:countdown:start', detail()); }",
|
|
683
814
|
" };",
|
|
684
815
|
" const pause = (next) => {",
|
|
685
816
|
" if (state === 'running') { left = Math.max(0, endAt - Date.now()); halt(); }",
|
|
686
817
|
" if (state === 'complete' || state === 'stopped') return;",
|
|
687
818
|
" state = next;",
|
|
819
|
+
" if (next === 'stopped') release();",
|
|
688
820
|
" render();",
|
|
689
821
|
" };",
|
|
690
822
|
" render();",
|
|
691
823
|
" if (stopEvent && canListen(scope)) scope.addEventListener(stopEvent, () => pause('stopped'));",
|
|
692
824
|
" if (mode === 'visible' && target.IntersectionObserver) {",
|
|
693
|
-
"
|
|
825
|
+
" observer = new target.IntersectionObserver((entries) => {",
|
|
694
826
|
" for (const entry of entries) entry.isIntersecting ? run() : pause('paused');",
|
|
695
|
-
" if (state === 'complete' || state === 'stopped') observer.disconnect();",
|
|
696
827
|
" });",
|
|
697
828
|
" observer.observe(timer);",
|
|
698
829
|
" } else if (mode === 'mount' || mode === 'visible') run();",
|
|
@@ -709,7 +840,18 @@ var countdownPluginRegistrationLines = [
|
|
|
709
840
|
" const targetTime = countdownEnd(timer.getAttribute && timer.getAttribute('data-gem-countdown-target'));",
|
|
710
841
|
" const section = timer.closest ? timer.closest('[data-gem-section-id]') : null;",
|
|
711
842
|
" const band = section && section.hasAttribute && section.hasAttribute('data-gem-countdown-end') ? section : null;",
|
|
843
|
+
// Two readout schemes, one countdown (gem-framework 0.8.0). The runtime's own cells are
|
|
844
|
+
// [data-gem-countdown-days|hours|minutes|seconds]; the Liquid theme's initCountdown and gemsite write
|
|
845
|
+
// [data-gem-cd-days|hours|mins|secs] (gemsite CountdownBannerOfferBar.tsx). Both are written; a cell the markup
|
|
846
|
+
// does not carry is skipped. The readout ticks every second only when it shows seconds, else once a minute ON the
|
|
847
|
+
// minute of the remaining time (0.8.2): the next update is set for the instant the minute digit changes,
|
|
848
|
+
// (remaining % 60000) + a few ms, and re-aimed from the clock each time, so the digit is never a minute late.
|
|
849
|
+
// In the last minute the next change IS the end, so the chain aims at the end itself and stops there: nothing is
|
|
850
|
+
// left scheduled after it, and a deadline too far off for the one-shot end timer below still ends on time.
|
|
851
|
+
// 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.
|
|
852
|
+
" const hasSecondsCell = Boolean(timer.querySelector && (timer.querySelector('[data-gem-cd-secs]') || timer.querySelector('[data-gem-countdown-seconds]')));",
|
|
712
853
|
" let handle = null;",
|
|
854
|
+
" let announced = '';",
|
|
713
855
|
" const update = () => {",
|
|
714
856
|
" if (!Number.isFinite(targetTime)) {",
|
|
715
857
|
" timer.dataset.gemCountdownState = 'invalid';",
|
|
@@ -720,20 +862,43 @@ var countdownPluginRegistrationLines = [
|
|
|
720
862
|
" const days = Math.floor(remainingSeconds / 86400);",
|
|
721
863
|
" const hours = Math.floor((remainingSeconds % 86400) / 3600);",
|
|
722
864
|
" const minutes = Math.floor((remainingSeconds % 3600) / 60);",
|
|
865
|
+
" const seconds = remainingSeconds % 60;",
|
|
866
|
+
// ONE end state: data-gem-countdown-state="complete" (the runtime's word, same as the event
|
|
867
|
+
// gem:countdown:complete and the duration timer). gemsite's port wrote "ended"; its meaning travels as the
|
|
868
|
+
// attribute data-gem-countdown-ended="true" on the timer, the same mark a duration timer gets at zero. A timer
|
|
869
|
+
// 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.
|
|
723
870
|
" timer.dataset.gemCountdownState = ms > 0 ? 'running' : 'complete';",
|
|
724
871
|
" timer.dataset.gemCountdownRemainingSeconds = String(remainingSeconds);",
|
|
725
872
|
" write(timer, '[data-gem-countdown-days]', days);",
|
|
726
873
|
" write(timer, '[data-gem-countdown-hours]', hours);",
|
|
727
874
|
" write(timer, '[data-gem-countdown-minutes]', minutes);",
|
|
875
|
+
" write(timer, '[data-gem-countdown-seconds]', seconds);",
|
|
876
|
+
" write(timer, '[data-gem-cd-days]', days);",
|
|
877
|
+
" write(timer, '[data-gem-cd-hours]', hours);",
|
|
878
|
+
" write(timer, '[data-gem-cd-mins]', minutes);",
|
|
879
|
+
" write(timer, '[data-gem-cd-secs]', seconds);",
|
|
728
880
|
" if (ms <= 0) {",
|
|
729
881
|
" if (band) band.setAttribute('data-gem-countdown-ended', 'true');",
|
|
882
|
+
" if (timer.setAttribute && !(timer.hasAttribute && timer.hasAttribute('data-gem-section-id'))) timer.setAttribute('data-gem-countdown-ended', 'true');",
|
|
730
883
|
" if (handle !== null && target.clearInterval) { target.clearInterval(handle); handle = null; }",
|
|
731
884
|
" }",
|
|
732
|
-
|
|
885
|
+
// start: once a minute as before, but a per-second readout announces it once; complete: once.
|
|
886
|
+
" const event = ms > 0 ? 'gem:countdown:start' : 'gem:countdown:complete';",
|
|
887
|
+
" if (event === announced && (event === 'gem:countdown:complete' || hasSecondsCell)) return;",
|
|
888
|
+
" announced = event;",
|
|
889
|
+
" api.emit(event, { source: 'gem-runtime', id: timer.id || undefined, remainingSeconds });",
|
|
890
|
+
" };",
|
|
891
|
+
" const onTheMinute = () => {",
|
|
892
|
+
" const ms = targetTime - Date.now();",
|
|
893
|
+
" if (!(ms > 0) || !target.setTimeout) return;",
|
|
894
|
+
" target.setTimeout(() => { update(); onTheMinute(); }, Math.min((ms % 60000) + 5, ms));",
|
|
733
895
|
" };",
|
|
734
896
|
" update();",
|
|
735
|
-
" if (
|
|
736
|
-
|
|
897
|
+
" if (Number.isFinite(targetTime) && targetTime - Date.now() > 0) {",
|
|
898
|
+
" if (!hasSecondsCell) onTheMinute();",
|
|
899
|
+
" else if (target.setInterval) handle = target.setInterval(update, 1000);",
|
|
900
|
+
" }",
|
|
901
|
+
// A minute readout moves once a minute, but the END is not allowed to wait for the next minute: one timer
|
|
737
902
|
// for the exact instant (while it fits a setTimeout, ~24.8 days; past that the minute tick gets there).
|
|
738
903
|
" const left = targetTime - Date.now();",
|
|
739
904
|
" if (target.setTimeout && Number.isFinite(left) && left > 0 && left < 2147483647) target.setTimeout(update, left);",
|
|
@@ -774,7 +939,9 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
|
774
939
|
"--gem-icon-size",
|
|
775
940
|
"--gem-drawer-offset",
|
|
776
941
|
// The row primitive's variables (primitives.ts GEM_ROW_CSS). Sections SET these on their grid.
|
|
777
|
-
...GEM_ROW_CSS_VARIABLES
|
|
942
|
+
...GEM_ROW_CSS_VARIABLES,
|
|
943
|
+
// The form set's variables (form-set.ts): set from a plan's `tokens.form`, read by fields under [data-gem-form-set].
|
|
944
|
+
...GEM_FORM_CSS_VARIABLES
|
|
778
945
|
]);
|
|
779
946
|
var TOKENIZED_PROPERTIES = [
|
|
780
947
|
"background",
|
|
@@ -1179,6 +1346,67 @@ var PROMISE_VI_PARTS = {
|
|
|
1179
1346
|
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}`
|
|
1180
1347
|
};
|
|
1181
1348
|
var PROMISE_VI = new RegExp(Object.values(PROMISE_VI_PARTS).join("|"), "iu");
|
|
1349
|
+
var SHIP = String.raw`(?:shipping|delivery)`;
|
|
1350
|
+
var MONEY = String.raw`(?:fees?|costs?|charges?)`;
|
|
1351
|
+
var TAIL = String.raw`(?:\s+(?:apply(?:\s+to\s+(?:this|your)\s+order)?|at\s+checkout|on\s+(?:this|your|every)\s+order|for\s+members|ever|will\s+be\s+charged))?`;
|
|
1352
|
+
var LEAD = String.raw`(?:(?:plus|and|also|absolutely|with|yes|so|oh,?\s+and)[,!]?\s+)?`;
|
|
1353
|
+
var VI_SHIP_WORD = String.raw`(?:ship|vận chuyển|giao hàng)`;
|
|
1354
|
+
var SHIPPING_ZERO_SENTENCES = {
|
|
1355
|
+
noExtraShipping: new RegExp(String.raw`^${LEAD}no\s+(?:extra|additional|added)\s+${SHIP}(?:\s+${MONEY})?${TAIL}$`, "i"),
|
|
1356
|
+
noExtraChargeForShipping: new RegExp(String.raw`^${LEAD}no\s+(?:extra|additional|added)\s+${MONEY}\s+for\s+${SHIP}${TAIL}$`, "i"),
|
|
1357
|
+
noShippingFees: new RegExp(String.raw`^${LEAD}no\s+${SHIP}\s+${MONEY}${TAIL}$`, "i"),
|
|
1358
|
+
zeroShippingFees: new RegExp(String.raw`^${LEAD}zero\s+${SHIP}\s+${MONEY}${TAIL}$`, "i"),
|
|
1359
|
+
// "Shipping included", "Shipping costs: included", "Shipping — included". "Total: Shipping included" không khớp: câu bắt
|
|
1360
|
+
// đầu bằng "Total" (nhãn giỏ, cố ý sạch).
|
|
1361
|
+
shippingIncluded: new RegExp(String.raw`^${LEAD}(?:(?:taxes?|duties)\s+(?:and|&)\s+)?${SHIP}(?:\s+(?:and|&)\s+handling)?(?:\s+${MONEY})?(?:['’]s|\s+(?:is|are)|\s*[:—–-])?\s+(?:always\s+)?included(?:\s+in\s+(?:the\s+)?price)?$`, "i"),
|
|
1362
|
+
// Chỉ "Includes" (ngôi thứ ba, một lời mô tả gói hàng). "Include shipping" là nhãn tuỳ chọn / công tắc.
|
|
1363
|
+
includesShipping: new RegExp(String.raw`^includes\s+${SHIP}$`, "i"),
|
|
1364
|
+
// "Không cần trả thêm phí giao hàng", "Không mất phí ship", "Không thu phí vận chuyển". KHÔNG nhận "không trả phí ship…"
|
|
1365
|
+
// trần ("Shop không trả phí ship đổi trả" là lời từ chối) và "không tính phí…" (nhãn giá: CHƯA tính phí ship).
|
|
1366
|
+
viNoFee: new RegExp(String.raw`^không\s+(?:cần\s+(?:trả|mất|tốn)|mất|thu|tốn)\s+(?:thêm\s+)?(?:phí|tiền)\s+${VI_SHIP_WORD}$`, "iu"),
|
|
1367
|
+
viNoFeeBare: new RegExp(String.raw`^không\s+(?:phí|tiền)\s+${VI_SHIP_WORD}$`, "iu"),
|
|
1368
|
+
// "Giá đã bao gồm phí ship", "Đã gồm phí ship". BẮT BUỘC "đã": "Bao gồm phí ship" trần là nhãn tuỳ chọn (như "Include
|
|
1369
|
+
// shipping" bên tiếng Anh). "Chưa/không bao gồm…" không khớp vì câu phải BẮT ĐẦU bằng các chữ này.
|
|
1370
|
+
viIncludedFee: new RegExp(String.raw`^(?:giá(?:\s+(?:sản phẩm|trên))?\s+)?đã\s+(?:bao\s+)?gồm\s+(?:cả\s+)?phí\s+${VI_SHIP_WORD}$`, "iu"),
|
|
1371
|
+
// "Phí ship đã bao gồm" (trong giá). "Phí ship đã bao gồm VAT" không khớp trọn câu: phí đó CÓ, nó gồm thứ khác.
|
|
1372
|
+
viFeeIncluded: new RegExp(String.raw`^phí\s+${VI_SHIP_WORD}\s+đã\s+(?:được\s+)?(?:bao\s+)?gồm(?:\s+trong\s+giá)?$`, "iu")
|
|
1373
|
+
};
|
|
1374
|
+
var DECOR = String.raw`[\s\-*+>•·–—✓✔☑\p{Extended_Pictographic}️]`;
|
|
1375
|
+
var DECOR_START = new RegExp(`^${DECOR}+`, "u");
|
|
1376
|
+
var DECOR_END = new RegExp(String.raw`(?:${DECOR}|[.!,;:)])+$`, "u");
|
|
1377
|
+
var ABBREV = /\b(?:e\.g|i\.e|approx|ex|etc|vs|incl|excl|mr|mrs|ms|dr)\.$/iu;
|
|
1378
|
+
var LABEL_LEAD = /^(?:(?:grand\s+|sub\s*)?total|subtotal|price|unless\s+noted|tổng(?:\s+cộng)?|tạm\s+tính|giá)\b/iu;
|
|
1379
|
+
var QUESTION_END = /[??❓❔][\s\p{Extended_Pictographic}\uFE0F]*$/u;
|
|
1380
|
+
var MENTIONS_SHIPPING = /ship|deliver|vận chuyển|giao hàng/iu;
|
|
1381
|
+
var SHIPPING_ZERO_ENTRIES = Object.entries(SHIPPING_ZERO_SENTENCES);
|
|
1382
|
+
function sentencesOf(text) {
|
|
1383
|
+
const out = [];
|
|
1384
|
+
for (const line of text.split(/\n+|\s[|•·]\s|\s(?=[✓✔☑]\s)/u)) {
|
|
1385
|
+
let cur = "";
|
|
1386
|
+
for (const piece of line.split(/(?<=[.!?;])\s+/u)) {
|
|
1387
|
+
cur = cur ? `${cur} ${piece}` : piece;
|
|
1388
|
+
if (!ABBREV.test(cur)) {
|
|
1389
|
+
out.push(cur);
|
|
1390
|
+
cur = "";
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
if (cur) out.push(cur);
|
|
1394
|
+
}
|
|
1395
|
+
return out.map((x) => x.trim()).filter(Boolean);
|
|
1396
|
+
}
|
|
1397
|
+
function shippingZeroPromiseIn(raw) {
|
|
1398
|
+
if (!MENTIONS_SHIPPING.test(raw)) return null;
|
|
1399
|
+
const text = raw.normalize("NFC").replace(/\\n/g, "\n").replace(/\\(['"’])/g, "$1");
|
|
1400
|
+
for (const sentence of sentencesOf(text)) {
|
|
1401
|
+
if (QUESTION_END.test(sentence)) continue;
|
|
1402
|
+
const whole = sentence.replace(DECOR_START, "").replace(DECOR_END, "").trim();
|
|
1403
|
+
const clauses = LABEL_LEAD.test(whole) ? [whole] : [whole, ...whole.split(/,\s+|\s+[—–]\s+/u)].map((c) => c.replace(DECOR_START, "").replace(DECOR_END, "").trim());
|
|
1404
|
+
for (const clause of clauses) {
|
|
1405
|
+
for (const [name, re] of SHIPPING_ZERO_ENTRIES) if (re.test(clause)) return name;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
return null;
|
|
1409
|
+
}
|
|
1182
1410
|
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;
|
|
1183
1411
|
var PRICE_K_VI = /(?<![\p{L}\p{N}.,])\d{2,3}(?:[.,]\d{3})*\s?k(?![\p{L}\p{N}])/u;
|
|
1184
1412
|
var DISPLAY_QUANTITY = /(_to_show|_per_page|_per_row|_limit|_max|_visible|_columns?)$/;
|
|
@@ -1234,7 +1462,7 @@ function claimIn(raw) {
|
|
|
1234
1462
|
if (INVENTED_PERSON.test(`"${text}"`) || INVENTED_PERSON_VI.test(text)) return "invented person";
|
|
1235
1463
|
if (isQuestion(text)) return PRICE_ANY.test(text) || PRICE_K_VI.test(text) ? "promise the merchant never made" : null;
|
|
1236
1464
|
if (PRICE_K_VI.test(text) && /^\s*\d[\d.,]*\s?k\s*\+?\s*$/.test(text)) return "hardcoded figure";
|
|
1237
|
-
if (PROMISE.test(text) || PROMISE_VI.test(text) || PRICE_K_VI.test(text)) return "promise the merchant never made";
|
|
1465
|
+
if (PROMISE.test(text) || PROMISE_VI.test(text) || shippingZeroPromiseIn(text) || PRICE_K_VI.test(text)) return "promise the merchant never made";
|
|
1238
1466
|
return null;
|
|
1239
1467
|
}
|
|
1240
1468
|
function demoInvents(value) {
|
|
@@ -1476,6 +1704,7 @@ export {
|
|
|
1476
1704
|
requiredArtifactPaths,
|
|
1477
1705
|
sectionClassesIn,
|
|
1478
1706
|
sectionLibraryIdOf,
|
|
1707
|
+
shippingZeroPromiseIn,
|
|
1479
1708
|
slotContractDrift,
|
|
1480
1709
|
slotSourceFromElementTree,
|
|
1481
1710
|
slotsReadBy,
|