@gemboss/gem-framework 0.5.1 → 0.6.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/dist/SOURCE.json +2 -2
- package/dist/gem-framework.css +83 -0
- package/dist/gem-runtime.js +69 -1
- package/dist/index.js +187 -1
- package/dist/section-gate.js +182 -0
- package/package.json +1 -1
package/dist/SOURCE.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "@gemboss/gem-framework",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"gemtheme_sha": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"gemtheme_sha": "114a3fa4717f4e14e1f624433c069c13112d84b3",
|
|
5
5
|
"gemtheme_dirty": false,
|
|
6
6
|
"framework_version": "0.1.0"
|
|
7
7
|
}
|
package/dist/gem-framework.css
CHANGED
|
@@ -1082,6 +1082,89 @@ a {
|
|
|
1082
1082
|
:root:has([data-gem-canvas]) [data-gem-section-id][data-gem-countdown-ended='true'] { opacity: .55; }
|
|
1083
1083
|
.gem-countdown-ended-note { display: none; }
|
|
1084
1084
|
:root:has([data-gem-canvas]) [data-gem-countdown-ended='true'] .gem-countdown-ended-note { display: block; margin: 0; padding: .25rem 0; font-size: .75rem; font-weight: 700; text-align: center; }
|
|
1085
|
+
.gem-popup-shell { padding: 0; }
|
|
1086
|
+
.gem-popup-shell__dialog {
|
|
1087
|
+
padding: 0; border: 0; margin: auto; color: var(--gem-color-text); background: var(--gem-color-background);
|
|
1088
|
+
width: min(880px, 92vw); max-width: none; max-height: min(640px, 92vh); overflow: hidden;
|
|
1089
|
+
border-radius: var(--gem-radius-lg); box-shadow: var(--gem-shadow-md);
|
|
1090
|
+
}
|
|
1091
|
+
.gem-popup-shell__dialog::backdrop { background: rgb(0 0 0 / 0.55); }
|
|
1092
|
+
.gem-popup-shell__frame { position: relative; display: grid; grid-template-columns: 1fr 1fr; min-height: 520px; height: 100%; }
|
|
1093
|
+
.gem-popup-shell__media { position: relative; min-width: 0; overflow: hidden; background: var(--gem-color-surface); }
|
|
1094
|
+
.gem-popup-shell__image { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
1095
|
+
.gem-popup-shell__panel {
|
|
1096
|
+
position: relative; min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: var(--gem-space-md);
|
|
1097
|
+
padding: var(--gem-space-2xl) var(--gem-space-xl); overflow-y: auto; align-items: center; text-align: center;
|
|
1098
|
+
}
|
|
1099
|
+
.gem-popup-shell[data-gem-popup-align='start'] .gem-popup-shell__panel { align-items: stretch; text-align: start; }
|
|
1100
|
+
.gem-popup-shell[data-gem-popup-desktop='split-right'] .gem-popup-shell__media { order: 2; }
|
|
1101
|
+
.gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__frame,
|
|
1102
|
+
.gem-popup-shell[data-gem-popup-desktop='card'] .gem-popup-shell__frame { grid-template-columns: 1fr; }
|
|
1103
|
+
.gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__dialog,
|
|
1104
|
+
.gem-popup-shell[data-gem-popup-desktop='card'] .gem-popup-shell__dialog { width: min(480px, 92vw); }
|
|
1105
|
+
.gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__media { display: none; }
|
|
1106
|
+
.gem-popup-shell[data-gem-popup-desktop='card'] .gem-popup-shell__media { max-height: 200px; }
|
|
1107
|
+
.gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__frame { grid-template-columns: 1fr; place-items: center; }
|
|
1108
|
+
.gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__media { position: absolute; inset: 0; }
|
|
1109
|
+
.gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__panel {
|
|
1110
|
+
width: min(420px, 86%); margin: var(--gem-space-xl) 0; background: var(--gem-color-background); border-radius: var(--gem-radius-lg); box-shadow: var(--gem-shadow-sm);
|
|
1111
|
+
}
|
|
1112
|
+
.gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__media::after { content: ''; position: absolute; inset: 0; background: linear-gradient(rgb(0 0 0 / 0.35), rgb(0 0 0 / 0.6)); }
|
|
1113
|
+
.gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__panel,
|
|
1114
|
+
.gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__close { color: #fff; }
|
|
1115
|
+
.gem-popup-shell__close {
|
|
1116
|
+
position: absolute; inset-block-start: var(--gem-space-xs); inset-inline-end: var(--gem-space-xs); z-index: 2;
|
|
1117
|
+
display: inline-grid; place-items: center; width: var(--gem-tap-min); height: var(--gem-tap-min); padding: 0;
|
|
1118
|
+
border: 0; border-radius: var(--gem-radius-pill); color: inherit; background: transparent; cursor: pointer;
|
|
1119
|
+
}
|
|
1120
|
+
.gem-popup-shell__close:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); }
|
|
1121
|
+
.gem-popup-shell__teaser {
|
|
1122
|
+
position: fixed; inset-block-end: var(--gem-space-md); inset-inline-end: var(--gem-space-md); z-index: 40;
|
|
1123
|
+
min-height: var(--gem-tap-min); padding: 0 var(--gem-space-md); font: inherit; font-weight: 600; cursor: pointer;
|
|
1124
|
+
color: var(--gem-color-accent-text); background: var(--gem-color-accent); border: 0; border-radius: var(--gem-radius-pill); box-shadow: var(--gem-shadow-md);
|
|
1125
|
+
}
|
|
1126
|
+
.gem-popup-shell__teaser[data-position='left'] { inset-inline-end: auto; inset-inline-start: var(--gem-space-md); }
|
|
1127
|
+
.gem-popup-shell__teaser[hidden] { display: none; }
|
|
1128
|
+
@media (min-width: 750px) {
|
|
1129
|
+
.gem-popup-shell[data-gem-popup-desktop='fullscreen'] .gem-popup-shell__dialog { width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh; margin: 0; border-radius: 0; box-shadow: none; }
|
|
1130
|
+
.gem-popup-shell[data-gem-popup-desktop='fullscreen'] .gem-popup-shell__frame { min-height: 100%; }
|
|
1131
|
+
.gem-popup-shell[data-gem-popup-desktop='fullscreen'] .gem-popup-shell__media { order: 2; }
|
|
1132
|
+
.gem-popup-shell[data-gem-popup-desktop='fullscreen'] .gem-popup-shell__panel { padding: var(--gem-space-3xl); }
|
|
1133
|
+
.gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-align='start'] .gem-popup-shell__panel { padding-inline-start: max(var(--gem-space-3xl), 8vw); }
|
|
1134
|
+
.gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-media='background'] .gem-popup-shell__frame { grid-template-columns: 1fr; }
|
|
1135
|
+
.gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-media='background'][data-gem-popup-align='start'] .gem-popup-shell__frame { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
|
|
1136
|
+
.gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-media='background'] .gem-popup-shell__media { position: absolute; inset: 0; }
|
|
1137
|
+
.gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-media='background'] .gem-popup-shell__panel { position: relative; z-index: 1; }
|
|
1138
|
+
.gem-popup-shell[data-gem-popup-desktop='card-over-image'][data-gem-popup-media='background'] .gem-popup-shell__panel { color: var(--gem-color-text); }
|
|
1139
|
+
}
|
|
1140
|
+
@media (max-width: 749px) {
|
|
1141
|
+
.gem-popup-shell__dialog { width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh; margin: 0; border-radius: 0; }
|
|
1142
|
+
.gem-popup-shell[data-gem-popup-mobile='sheet'] .gem-popup-shell__dialog { height: auto; max-height: 88dvh; margin: auto 0 0; border-radius: var(--gem-radius-lg) var(--gem-radius-lg) 0 0; }
|
|
1143
|
+
.gem-popup-shell__frame { grid-template-columns: 1fr; grid-template-rows: auto 1fr; min-height: 100%; }
|
|
1144
|
+
.gem-popup-shell__panel { justify-content: flex-start; padding: var(--gem-space-3xl) var(--gem-space-lg) var(--gem-space-xl); }
|
|
1145
|
+
.gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__panel,
|
|
1146
|
+
.gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__panel { justify-content: center; }
|
|
1147
|
+
.gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__panel { width: 100%; margin: 0; border-radius: 0; box-shadow: none; }
|
|
1148
|
+
.gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__media { position: relative; }
|
|
1149
|
+
.gem-popup-shell__media { order: 2; max-height: 34dvh; }
|
|
1150
|
+
.gem-popup-shell[data-gem-popup-media='top'] .gem-popup-shell__media { order: 0; }
|
|
1151
|
+
.gem-popup-shell[data-gem-popup-media='top'] .gem-popup-shell__panel { padding-top: var(--gem-space-lg); }
|
|
1152
|
+
.gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__frame,
|
|
1153
|
+
.gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__frame { grid-template-rows: 1fr; }
|
|
1154
|
+
.gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__media { position: absolute; inset: 0; max-height: none; }
|
|
1155
|
+
}
|
|
1156
|
+
.gem-popup-shell[data-gem-popup-preview='true'] { padding: var(--gem-space-xl) var(--gem-layout-gutter); background: rgb(0 0 0 / 0.55); }
|
|
1157
|
+
.gem-popup-shell[data-gem-popup-preview='true'] .gem-popup-shell__dialog { position: relative; display: block; inset: auto; }
|
|
1158
|
+
@media (min-width: 750px) {
|
|
1159
|
+
.gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] { padding: 0; }
|
|
1160
|
+
.gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] .gem-popup-shell__dialog { height: 720px; max-height: none; }
|
|
1161
|
+
}
|
|
1162
|
+
@media (max-width: 749px) {
|
|
1163
|
+
.gem-popup-shell[data-gem-popup-preview='true'] { padding: 0; }
|
|
1164
|
+
.gem-popup-shell[data-gem-popup-preview='true'] .gem-popup-shell__dialog { height: 844px; max-height: none; width: 100%; max-width: 100%; }
|
|
1165
|
+
.gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-mobile='sheet'] { padding-top: 160px; }
|
|
1166
|
+
.gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-mobile='sheet'] .gem-popup-shell__dialog { min-height: 0; }
|
|
1167
|
+
}
|
|
1085
1168
|
.gem-promo-banner, .gem-promo-tile {
|
|
1086
1169
|
border: var(--gem-border-subtle, 1px solid var(--gem-color-border));
|
|
1087
1170
|
border-radius: var(--gem-radius-md);
|
package/dist/gem-runtime.js
CHANGED
|
@@ -547,7 +547,7 @@
|
|
|
547
547
|
api.emit('gem:quick-view:ready', { source: 'gem-runtime', count: bound, hasSurface: Boolean(surface) });
|
|
548
548
|
});
|
|
549
549
|
registerPlugin('promo-popup', ({ api }) => {
|
|
550
|
-
const popups = document.querySelectorAll('[data-gem-promo-popup]
|
|
550
|
+
const popups = document.querySelectorAll('[data-gem-promo-popup]');
|
|
551
551
|
let bound = 0;
|
|
552
552
|
const storagePrefix = 'gem:promo-popup-dismissed:';
|
|
553
553
|
const now = () => Date.now ? Date.now() : new Date().getTime();
|
|
@@ -662,8 +662,76 @@
|
|
|
662
662
|
const targetNode = timer.querySelector && timer.querySelector(selector);
|
|
663
663
|
if (targetNode) targetNode.textContent = pad(value);
|
|
664
664
|
};
|
|
665
|
+
const clockText = (s) => {
|
|
666
|
+
const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
|
|
667
|
+
return (h ? h + ':' + pad(m) : String(m)) + ':' + pad(sec);
|
|
668
|
+
};
|
|
669
|
+
const bindDuration = (timer, total) => {
|
|
670
|
+
const scope = (timer.closest && timer.closest('[data-gem-section-id]')) || document;
|
|
671
|
+
const mode = String(timer.getAttribute('data-gem-countdown-start') || 'mount').trim() || 'mount';
|
|
672
|
+
const stopEvent = String(timer.getAttribute('data-gem-countdown-stop') || '').trim();
|
|
673
|
+
let left = total * 1000;
|
|
674
|
+
let endAt = 0;
|
|
675
|
+
let handle = null;
|
|
676
|
+
let state = 'idle';
|
|
677
|
+
const detail = () => ({ source: 'gem-runtime', id: timer.id || undefined, mode: 'duration', duration: total, remainingSeconds: Math.max(0, Math.ceil(left / 1000)) });
|
|
678
|
+
const render = () => {
|
|
679
|
+
const s = Math.max(0, Math.ceil(left / 1000));
|
|
680
|
+
timer.dataset.gemCountdownState = state;
|
|
681
|
+
timer.dataset.gemCountdownRemainingSeconds = String(s);
|
|
682
|
+
write(timer, '[data-gem-countdown-days]', Math.floor(s / 86400));
|
|
683
|
+
write(timer, '[data-gem-countdown-hours]', Math.floor((s % 86400) / 3600));
|
|
684
|
+
write(timer, '[data-gem-countdown-minutes]', Math.floor((s % 3600) / 60));
|
|
685
|
+
write(timer, '[data-gem-countdown-seconds]', s % 60);
|
|
686
|
+
const readout = timer.querySelector && timer.querySelector('[data-gem-countdown-clock]');
|
|
687
|
+
if (readout) readout.textContent = clockText(s);
|
|
688
|
+
};
|
|
689
|
+
const halt = () => { if (handle !== null && target.clearInterval) target.clearInterval(handle); handle = null; };
|
|
690
|
+
const finish = () => {
|
|
691
|
+
halt();
|
|
692
|
+
left = 0;
|
|
693
|
+
state = 'complete';
|
|
694
|
+
render();
|
|
695
|
+
timer.setAttribute('data-gem-countdown-ended', 'true');
|
|
696
|
+
if (timer.dispatchEvent && timer.isConnected !== false) {
|
|
697
|
+
timer.dispatchEvent(new CustomEvent('gem:countdown:complete', { bubbles: true, detail: detail() }));
|
|
698
|
+
for (const callback of api.listeners['gem:countdown:complete'] || []) callback(detail());
|
|
699
|
+
} else api.emit('gem:countdown:complete', detail());
|
|
700
|
+
};
|
|
701
|
+
const tick = () => { left = endAt - Date.now(); if (left <= 0) finish(); else render(); };
|
|
702
|
+
const run = () => {
|
|
703
|
+
if (state === 'running' || state === 'complete' || state === 'stopped') return;
|
|
704
|
+
state = 'running';
|
|
705
|
+
endAt = Date.now() + left;
|
|
706
|
+
render();
|
|
707
|
+
if (target.setInterval) handle = target.setInterval(tick, 250);
|
|
708
|
+
api.emit('gem:countdown:start', detail());
|
|
709
|
+
};
|
|
710
|
+
const pause = (next) => {
|
|
711
|
+
if (state === 'running') { left = Math.max(0, endAt - Date.now()); halt(); }
|
|
712
|
+
if (state === 'complete' || state === 'stopped') return;
|
|
713
|
+
state = next;
|
|
714
|
+
render();
|
|
715
|
+
};
|
|
716
|
+
render();
|
|
717
|
+
if (stopEvent && canListen(scope)) scope.addEventListener(stopEvent, () => pause('stopped'));
|
|
718
|
+
if (mode === 'visible' && target.IntersectionObserver) {
|
|
719
|
+
const observer = new target.IntersectionObserver((entries) => {
|
|
720
|
+
for (const entry of entries) entry.isIntersecting ? run() : pause('paused');
|
|
721
|
+
if (state === 'complete' || state === 'stopped') observer.disconnect();
|
|
722
|
+
});
|
|
723
|
+
observer.observe(timer);
|
|
724
|
+
} else if (mode === 'mount' || mode === 'visible') run();
|
|
725
|
+
else if (canListen(scope)) scope.addEventListener(mode, run);
|
|
726
|
+
};
|
|
665
727
|
timers.forEach((timer) => {
|
|
666
728
|
if (!mark(timer, 'countdown')) return;
|
|
729
|
+
const duration = Number(timer.getAttribute && timer.getAttribute('data-gem-countdown-duration'));
|
|
730
|
+
if (Number.isFinite(duration) && duration > 0) {
|
|
731
|
+
bindDuration(timer, Math.min(Math.round(duration), 86400));
|
|
732
|
+
bound += 1;
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
667
735
|
const targetTime = countdownEnd(timer.getAttribute && timer.getAttribute('data-gem-countdown-target'));
|
|
668
736
|
const section = timer.closest ? timer.closest('[data-gem-section-id]') : null;
|
|
669
737
|
const band = section && section.hasAttribute && section.hasAttribute('data-gem-countdown-end') ? section : null;
|
package/dist/index.js
CHANGED
|
@@ -496,6 +496,15 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
496
496
|
"gem-delivery-date",
|
|
497
497
|
"gem-stock-indicator",
|
|
498
498
|
"gem-popup",
|
|
499
|
+
// Popup shell (primitives.ts GEM_POPUP_SHELL_CSS): the dialog frame popup sections draw their content in.
|
|
500
|
+
"gem-popup-shell",
|
|
501
|
+
"gem-popup-shell__dialog",
|
|
502
|
+
"gem-popup-shell__frame",
|
|
503
|
+
"gem-popup-shell__media",
|
|
504
|
+
"gem-popup-shell__image",
|
|
505
|
+
"gem-popup-shell__panel",
|
|
506
|
+
"gem-popup-shell__close",
|
|
507
|
+
"gem-popup-shell__teaser",
|
|
499
508
|
"gem-back-to-top",
|
|
500
509
|
// measured commerce atoms mapped by parts.ts (GEM_PART_CLASS) alongside gem-price/gem-badge.
|
|
501
510
|
"gem-discount",
|
|
@@ -815,6 +824,99 @@ var GEM_CARD_CSS_LINES = [
|
|
|
815
824
|
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-card='filled'] :is(.gem-section__inner, [class*='__inner']) { background: color-mix(in srgb, currentColor 6%, transparent); }"
|
|
816
825
|
];
|
|
817
826
|
var GEM_CARD_CSS = GEM_CARD_CSS_LINES.join("\n");
|
|
827
|
+
var GEM_POPUP_SHELL_CSS_LINES = [
|
|
828
|
+
".gem-popup-shell { padding: 0; }",
|
|
829
|
+
".gem-popup-shell__dialog {",
|
|
830
|
+
" padding: 0; border: 0; margin: auto; color: var(--gem-color-text); background: var(--gem-color-background);",
|
|
831
|
+
" width: min(880px, 92vw); max-width: none; max-height: min(640px, 92vh); overflow: hidden;",
|
|
832
|
+
" border-radius: var(--gem-radius-lg); box-shadow: var(--gem-shadow-md);",
|
|
833
|
+
"}",
|
|
834
|
+
".gem-popup-shell__dialog::backdrop { background: rgb(0 0 0 / 0.55); }",
|
|
835
|
+
".gem-popup-shell__frame { position: relative; display: grid; grid-template-columns: 1fr 1fr; min-height: 520px; height: 100%; }",
|
|
836
|
+
".gem-popup-shell__media { position: relative; min-width: 0; overflow: hidden; background: var(--gem-color-surface); }",
|
|
837
|
+
".gem-popup-shell__image { display: block; width: 100%; height: 100%; object-fit: cover; }",
|
|
838
|
+
".gem-popup-shell__panel {",
|
|
839
|
+
" position: relative; min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: var(--gem-space-md);",
|
|
840
|
+
" padding: var(--gem-space-2xl) var(--gem-space-xl); overflow-y: auto; align-items: center; text-align: center;",
|
|
841
|
+
"}",
|
|
842
|
+
".gem-popup-shell[data-gem-popup-align='start'] .gem-popup-shell__panel { align-items: stretch; text-align: start; }",
|
|
843
|
+
".gem-popup-shell[data-gem-popup-desktop='split-right'] .gem-popup-shell__media { order: 2; }",
|
|
844
|
+
".gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__frame,",
|
|
845
|
+
".gem-popup-shell[data-gem-popup-desktop='card'] .gem-popup-shell__frame { grid-template-columns: 1fr; }",
|
|
846
|
+
".gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__dialog,",
|
|
847
|
+
".gem-popup-shell[data-gem-popup-desktop='card'] .gem-popup-shell__dialog { width: min(480px, 92vw); }",
|
|
848
|
+
".gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__media { display: none; }",
|
|
849
|
+
".gem-popup-shell[data-gem-popup-desktop='card'] .gem-popup-shell__media { max-height: 200px; }",
|
|
850
|
+
".gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__frame { grid-template-columns: 1fr; place-items: center; }",
|
|
851
|
+
".gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__media { position: absolute; inset: 0; }",
|
|
852
|
+
".gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__panel {",
|
|
853
|
+
" width: min(420px, 86%); margin: var(--gem-space-xl) 0; background: var(--gem-color-background); border-radius: var(--gem-radius-lg); box-shadow: var(--gem-shadow-sm);",
|
|
854
|
+
"}",
|
|
855
|
+
// Image behind the text: light text over a dark wash, every size. A card over the image has its own
|
|
856
|
+
// background, so on desktop it keeps the preset's text colour.
|
|
857
|
+
".gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__media::after { content: ''; position: absolute; inset: 0; background: linear-gradient(rgb(0 0 0 / 0.35), rgb(0 0 0 / 0.6)); }",
|
|
858
|
+
".gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__panel,",
|
|
859
|
+
".gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__close { color: #fff; }",
|
|
860
|
+
".gem-popup-shell__close {",
|
|
861
|
+
" position: absolute; inset-block-start: var(--gem-space-xs); inset-inline-end: var(--gem-space-xs); z-index: 2;",
|
|
862
|
+
" display: inline-grid; place-items: center; width: var(--gem-tap-min); height: var(--gem-tap-min); padding: 0;",
|
|
863
|
+
" border: 0; border-radius: var(--gem-radius-pill); color: inherit; background: transparent; cursor: pointer;",
|
|
864
|
+
"}",
|
|
865
|
+
".gem-popup-shell__close:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); }",
|
|
866
|
+
".gem-popup-shell__teaser {",
|
|
867
|
+
" position: fixed; inset-block-end: var(--gem-space-md); inset-inline-end: var(--gem-space-md); z-index: 40;",
|
|
868
|
+
" min-height: var(--gem-tap-min); padding: 0 var(--gem-space-md); font: inherit; font-weight: 600; cursor: pointer;",
|
|
869
|
+
" color: var(--gem-color-accent-text); background: var(--gem-color-accent); border: 0; border-radius: var(--gem-radius-pill); box-shadow: var(--gem-shadow-md);",
|
|
870
|
+
"}",
|
|
871
|
+
".gem-popup-shell__teaser[data-position='left'] { inset-inline-end: auto; inset-inline-start: var(--gem-space-md); }",
|
|
872
|
+
".gem-popup-shell__teaser[hidden] { display: none; }",
|
|
873
|
+
"@media (min-width: 750px) {",
|
|
874
|
+
// Fullscreen: the dialog covers the viewport. The image goes right; behind the text when media=background,
|
|
875
|
+
// and then text aligned to the start keeps the left half so it never sits half on the image.
|
|
876
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'] .gem-popup-shell__dialog { width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh; margin: 0; border-radius: 0; box-shadow: none; }",
|
|
877
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'] .gem-popup-shell__frame { min-height: 100%; }",
|
|
878
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'] .gem-popup-shell__media { order: 2; }",
|
|
879
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'] .gem-popup-shell__panel { padding: var(--gem-space-3xl); }",
|
|
880
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-align='start'] .gem-popup-shell__panel { padding-inline-start: max(var(--gem-space-3xl), 8vw); }",
|
|
881
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-media='background'] .gem-popup-shell__frame { grid-template-columns: 1fr; }",
|
|
882
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-media='background'][data-gem-popup-align='start'] .gem-popup-shell__frame { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }",
|
|
883
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-media='background'] .gem-popup-shell__media { position: absolute; inset: 0; }",
|
|
884
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-media='background'] .gem-popup-shell__panel { position: relative; z-index: 1; }",
|
|
885
|
+
" .gem-popup-shell[data-gem-popup-desktop='card-over-image'][data-gem-popup-media='background'] .gem-popup-shell__panel { color: var(--gem-color-text); }",
|
|
886
|
+
"}",
|
|
887
|
+
"@media (max-width: 749px) {",
|
|
888
|
+
// Mobile: takeover is the whole screen; a sheet rises from the bottom to at most 88% of it. The image
|
|
889
|
+
// sits under the text (top when media=top), capped at a third of the screen; with no image or the image
|
|
890
|
+
// behind, the text is centred vertically like the measured takeovers.
|
|
891
|
+
" .gem-popup-shell__dialog { width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh; margin: 0; border-radius: 0; }",
|
|
892
|
+
" .gem-popup-shell[data-gem-popup-mobile='sheet'] .gem-popup-shell__dialog { height: auto; max-height: 88dvh; margin: auto 0 0; border-radius: var(--gem-radius-lg) var(--gem-radius-lg) 0 0; }",
|
|
893
|
+
" .gem-popup-shell__frame { grid-template-columns: 1fr; grid-template-rows: auto 1fr; min-height: 100%; }",
|
|
894
|
+
" .gem-popup-shell__panel { justify-content: flex-start; padding: var(--gem-space-3xl) var(--gem-space-lg) var(--gem-space-xl); }",
|
|
895
|
+
" .gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__panel,",
|
|
896
|
+
" .gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__panel { justify-content: center; }",
|
|
897
|
+
" .gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__panel { width: 100%; margin: 0; border-radius: 0; box-shadow: none; }",
|
|
898
|
+
" .gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__media { position: relative; }",
|
|
899
|
+
" .gem-popup-shell__media { order: 2; max-height: 34dvh; }",
|
|
900
|
+
" .gem-popup-shell[data-gem-popup-media='top'] .gem-popup-shell__media { order: 0; }",
|
|
901
|
+
" .gem-popup-shell[data-gem-popup-media='top'] .gem-popup-shell__panel { padding-top: var(--gem-space-lg); }",
|
|
902
|
+
" .gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__frame,",
|
|
903
|
+
" .gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__frame { grid-template-rows: 1fr; }",
|
|
904
|
+
" .gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__media { position: absolute; inset: 0; max-height: none; }",
|
|
905
|
+
"}",
|
|
906
|
+
".gem-popup-shell[data-gem-popup-preview='true'] { padding: var(--gem-space-xl) var(--gem-layout-gutter); background: rgb(0 0 0 / 0.55); }",
|
|
907
|
+
".gem-popup-shell[data-gem-popup-preview='true'] .gem-popup-shell__dialog { position: relative; display: block; inset: auto; }",
|
|
908
|
+
"@media (min-width: 750px) {",
|
|
909
|
+
" .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] { padding: 0; }",
|
|
910
|
+
" .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] .gem-popup-shell__dialog { height: 720px; max-height: none; }",
|
|
911
|
+
"}",
|
|
912
|
+
"@media (max-width: 749px) {",
|
|
913
|
+
" .gem-popup-shell[data-gem-popup-preview='true'] { padding: 0; }",
|
|
914
|
+
" .gem-popup-shell[data-gem-popup-preview='true'] .gem-popup-shell__dialog { height: 844px; max-height: none; width: 100%; max-width: 100%; }",
|
|
915
|
+
" .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-mobile='sheet'] { padding-top: 160px; }",
|
|
916
|
+
" .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-mobile='sheet'] .gem-popup-shell__dialog { min-height: 0; }",
|
|
917
|
+
"}"
|
|
918
|
+
];
|
|
919
|
+
var GEM_POPUP_SHELL_CSS = GEM_POPUP_SHELL_CSS_LINES.join("\n");
|
|
818
920
|
var GEM_COUNTDOWN_ENDED_CSS_LINES = [
|
|
819
921
|
":root:not(:has([data-gem-canvas])) [data-gem-section-id][data-gem-countdown-ended='true'] { display: none; }",
|
|
820
922
|
":root:has([data-gem-canvas]) [data-gem-section-id][data-gem-countdown-ended='true'] { opacity: .55; }",
|
|
@@ -1990,6 +2092,7 @@ function createPrimitiveCss() {
|
|
|
1990
2092
|
...GEM_DIVIDER_CSS_LINES,
|
|
1991
2093
|
...GEM_CARD_CSS_LINES,
|
|
1992
2094
|
...GEM_COUNTDOWN_ENDED_CSS_LINES,
|
|
2095
|
+
...GEM_POPUP_SHELL_CSS_LINES,
|
|
1993
2096
|
".gem-promo-banner, .gem-promo-tile {",
|
|
1994
2097
|
" border: var(--gem-border-subtle, 1px solid var(--gem-color-border));",
|
|
1995
2098
|
" border-radius: var(--gem-radius-md);",
|
|
@@ -4388,7 +4491,9 @@ var quickViewPluginRegistrationLines = [
|
|
|
4388
4491
|
];
|
|
4389
4492
|
var promoPopupPluginRegistrationLines = [
|
|
4390
4493
|
" registerPlugin('promo-popup', ({ api }) => {",
|
|
4391
|
-
|
|
4494
|
+
// Only the explicit hook opts in. Every promo-popup section carries the archetype class .gem-promo-popup, and
|
|
4495
|
+
// binding by class took over variants that run their own behavior.js (GemFactory gap #16, 03/10/2026).
|
|
4496
|
+
" const popups = document.querySelectorAll('[data-gem-promo-popup]');",
|
|
4392
4497
|
" let bound = 0;",
|
|
4393
4498
|
" const storagePrefix = 'gem:promo-popup-dismissed:';",
|
|
4394
4499
|
" const now = () => Date.now ? Date.now() : new Date().getTime();",
|
|
@@ -4512,8 +4617,88 @@ var countdownPluginRegistrationLines = [
|
|
|
4512
4617
|
" const targetNode = timer.querySelector && timer.querySelector(selector);",
|
|
4513
4618
|
" if (targetNode) targetNode.textContent = pad(value);",
|
|
4514
4619
|
" };",
|
|
4620
|
+
// Relative mode: `data-gem-countdown-duration="N"` counts N seconds for THIS shopper instead of running to a
|
|
4621
|
+
// calendar deadline (a popup offer: "this code is yours for 1:00"). `data-gem-countdown-start` says when the N
|
|
4622
|
+
// seconds begin: "mount" (default) at bind; "visible" only while the timer is on screen, which is what a popup
|
|
4623
|
+
// wants: hidden until the popup opens, paused while the shopper has it closed or has moved past the step that
|
|
4624
|
+
// holds it, and no race with the open event (this plugin loads on `viewport`, so it can bind after a popup
|
|
4625
|
+
// that opened at once); any other value is an event name heard on the timer's section. Readouts:
|
|
4626
|
+
// [data-gem-countdown-days|hours|minutes|seconds] (two digits) and [data-gem-countdown-clock] ("1:00", "1:02:05").
|
|
4627
|
+
// At zero the TIMER gets data-gem-countdown-ended="true" and a bubbling gem:countdown:complete leaves it, so
|
|
4628
|
+
// the section that owns the offer can end it. The band is NOT marked: this ending belongs to one shopper, and
|
|
4629
|
+
// the band rule above (data-gem-countdown-end) removes a band from the store for everyone.
|
|
4630
|
+
" const clockText = (s) => {",
|
|
4631
|
+
" const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;",
|
|
4632
|
+
" return (h ? h + ':' + pad(m) : String(m)) + ':' + pad(sec);",
|
|
4633
|
+
" };",
|
|
4634
|
+
" const bindDuration = (timer, total) => {",
|
|
4635
|
+
" const scope = (timer.closest && timer.closest('[data-gem-section-id]')) || document;",
|
|
4636
|
+
" const mode = String(timer.getAttribute('data-gem-countdown-start') || 'mount').trim() || 'mount';",
|
|
4637
|
+
" const stopEvent = String(timer.getAttribute('data-gem-countdown-stop') || '').trim();",
|
|
4638
|
+
" let left = total * 1000;",
|
|
4639
|
+
" let endAt = 0;",
|
|
4640
|
+
" let handle = null;",
|
|
4641
|
+
" let state = 'idle';",
|
|
4642
|
+
" const detail = () => ({ source: 'gem-runtime', id: timer.id || undefined, mode: 'duration', duration: total, remainingSeconds: Math.max(0, Math.ceil(left / 1000)) });",
|
|
4643
|
+
" const render = () => {",
|
|
4644
|
+
" const s = Math.max(0, Math.ceil(left / 1000));",
|
|
4645
|
+
" timer.dataset.gemCountdownState = state;",
|
|
4646
|
+
" timer.dataset.gemCountdownRemainingSeconds = String(s);",
|
|
4647
|
+
" write(timer, '[data-gem-countdown-days]', Math.floor(s / 86400));",
|
|
4648
|
+
" write(timer, '[data-gem-countdown-hours]', Math.floor((s % 86400) / 3600));",
|
|
4649
|
+
" write(timer, '[data-gem-countdown-minutes]', Math.floor((s % 3600) / 60));",
|
|
4650
|
+
" write(timer, '[data-gem-countdown-seconds]', s % 60);",
|
|
4651
|
+
" const readout = timer.querySelector && timer.querySelector('[data-gem-countdown-clock]');",
|
|
4652
|
+
" if (readout) readout.textContent = clockText(s);",
|
|
4653
|
+
" };",
|
|
4654
|
+
" const halt = () => { if (handle !== null && target.clearInterval) target.clearInterval(handle); handle = null; };",
|
|
4655
|
+
" const finish = () => {",
|
|
4656
|
+
" halt();",
|
|
4657
|
+
" left = 0;",
|
|
4658
|
+
" state = 'complete';",
|
|
4659
|
+
" render();",
|
|
4660
|
+
" timer.setAttribute('data-gem-countdown-ended', 'true');",
|
|
4661
|
+
// One event, not two: dispatched from the timer it bubbles to the document; api.emit would add a second
|
|
4662
|
+
// document event. Without a dispatchable timer (tests, detached nodes) fall back to api.emit.
|
|
4663
|
+
" if (timer.dispatchEvent && timer.isConnected !== false) {",
|
|
4664
|
+
" timer.dispatchEvent(new CustomEvent('gem:countdown:complete', { bubbles: true, detail: detail() }));",
|
|
4665
|
+
" for (const callback of api.listeners['gem:countdown:complete'] || []) callback(detail());",
|
|
4666
|
+
" } else api.emit('gem:countdown:complete', detail());",
|
|
4667
|
+
" };",
|
|
4668
|
+
" const tick = () => { left = endAt - Date.now(); if (left <= 0) finish(); else render(); };",
|
|
4669
|
+
" const run = () => {",
|
|
4670
|
+
" if (state === 'running' || state === 'complete' || state === 'stopped') return;",
|
|
4671
|
+
" state = 'running';",
|
|
4672
|
+
" endAt = Date.now() + left;",
|
|
4673
|
+
" render();",
|
|
4674
|
+
" if (target.setInterval) handle = target.setInterval(tick, 250);",
|
|
4675
|
+
" api.emit('gem:countdown:start', detail());",
|
|
4676
|
+
" };",
|
|
4677
|
+
" const pause = (next) => {",
|
|
4678
|
+
" if (state === 'running') { left = Math.max(0, endAt - Date.now()); halt(); }",
|
|
4679
|
+
" if (state === 'complete' || state === 'stopped') return;",
|
|
4680
|
+
" state = next;",
|
|
4681
|
+
" render();",
|
|
4682
|
+
" };",
|
|
4683
|
+
" render();",
|
|
4684
|
+
" if (stopEvent && canListen(scope)) scope.addEventListener(stopEvent, () => pause('stopped'));",
|
|
4685
|
+
" if (mode === 'visible' && target.IntersectionObserver) {",
|
|
4686
|
+
" const observer = new target.IntersectionObserver((entries) => {",
|
|
4687
|
+
" for (const entry of entries) entry.isIntersecting ? run() : pause('paused');",
|
|
4688
|
+
" if (state === 'complete' || state === 'stopped') observer.disconnect();",
|
|
4689
|
+
" });",
|
|
4690
|
+
" observer.observe(timer);",
|
|
4691
|
+
" } else if (mode === 'mount' || mode === 'visible') run();",
|
|
4692
|
+
" else if (canListen(scope)) scope.addEventListener(mode, run);",
|
|
4693
|
+
" };",
|
|
4515
4694
|
" timers.forEach((timer) => {",
|
|
4516
4695
|
" if (!mark(timer, 'countdown')) return;",
|
|
4696
|
+
" const duration = Number(timer.getAttribute && timer.getAttribute('data-gem-countdown-duration'));",
|
|
4697
|
+
" if (Number.isFinite(duration) && duration > 0) {",
|
|
4698
|
+
" bindDuration(timer, Math.min(Math.round(duration), 86400));",
|
|
4699
|
+
" bound += 1;",
|
|
4700
|
+
" return;",
|
|
4701
|
+
" }",
|
|
4517
4702
|
" const targetTime = countdownEnd(timer.getAttribute && timer.getAttribute('data-gem-countdown-target'));",
|
|
4518
4703
|
" const section = timer.closest ? timer.closest('[data-gem-section-id]') : null;",
|
|
4519
4704
|
" const band = section && section.hasAttribute && section.hasAttribute('data-gem-countdown-end') ? section : null;",
|
|
@@ -6187,6 +6372,7 @@ export {
|
|
|
6187
6372
|
GEM_COUNTDOWN_ENDED_CSS,
|
|
6188
6373
|
GEM_GRID_COLUMNS_ROW_KNOB_CSS,
|
|
6189
6374
|
GEM_PART_CLASS,
|
|
6375
|
+
GEM_POPUP_SHELL_CSS,
|
|
6190
6376
|
GEM_REVEAL_CSS,
|
|
6191
6377
|
GEM_ROW_CSS,
|
|
6192
6378
|
GEM_ROW_CSS_VARIABLES,
|
package/dist/section-gate.js
CHANGED
|
@@ -203,6 +203,15 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
203
203
|
"gem-delivery-date",
|
|
204
204
|
"gem-stock-indicator",
|
|
205
205
|
"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",
|
|
206
215
|
"gem-back-to-top",
|
|
207
216
|
// measured commerce atoms mapped by parts.ts (GEM_PART_CLASS) alongside gem-price/gem-badge.
|
|
208
217
|
"gem-discount",
|
|
@@ -383,6 +392,99 @@ var GEM_CARD_CSS_LINES = [
|
|
|
383
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); }"
|
|
384
393
|
];
|
|
385
394
|
var GEM_CARD_CSS = GEM_CARD_CSS_LINES.join("\n");
|
|
395
|
+
var GEM_POPUP_SHELL_CSS_LINES = [
|
|
396
|
+
".gem-popup-shell { padding: 0; }",
|
|
397
|
+
".gem-popup-shell__dialog {",
|
|
398
|
+
" padding: 0; border: 0; margin: auto; color: var(--gem-color-text); background: var(--gem-color-background);",
|
|
399
|
+
" width: min(880px, 92vw); max-width: none; max-height: min(640px, 92vh); overflow: hidden;",
|
|
400
|
+
" border-radius: var(--gem-radius-lg); box-shadow: var(--gem-shadow-md);",
|
|
401
|
+
"}",
|
|
402
|
+
".gem-popup-shell__dialog::backdrop { background: rgb(0 0 0 / 0.55); }",
|
|
403
|
+
".gem-popup-shell__frame { position: relative; display: grid; grid-template-columns: 1fr 1fr; min-height: 520px; height: 100%; }",
|
|
404
|
+
".gem-popup-shell__media { position: relative; min-width: 0; overflow: hidden; background: var(--gem-color-surface); }",
|
|
405
|
+
".gem-popup-shell__image { display: block; width: 100%; height: 100%; object-fit: cover; }",
|
|
406
|
+
".gem-popup-shell__panel {",
|
|
407
|
+
" position: relative; min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: var(--gem-space-md);",
|
|
408
|
+
" padding: var(--gem-space-2xl) var(--gem-space-xl); overflow-y: auto; align-items: center; text-align: center;",
|
|
409
|
+
"}",
|
|
410
|
+
".gem-popup-shell[data-gem-popup-align='start'] .gem-popup-shell__panel { align-items: stretch; text-align: start; }",
|
|
411
|
+
".gem-popup-shell[data-gem-popup-desktop='split-right'] .gem-popup-shell__media { order: 2; }",
|
|
412
|
+
".gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__frame,",
|
|
413
|
+
".gem-popup-shell[data-gem-popup-desktop='card'] .gem-popup-shell__frame { grid-template-columns: 1fr; }",
|
|
414
|
+
".gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__dialog,",
|
|
415
|
+
".gem-popup-shell[data-gem-popup-desktop='card'] .gem-popup-shell__dialog { width: min(480px, 92vw); }",
|
|
416
|
+
".gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__media { display: none; }",
|
|
417
|
+
".gem-popup-shell[data-gem-popup-desktop='card'] .gem-popup-shell__media { max-height: 200px; }",
|
|
418
|
+
".gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__frame { grid-template-columns: 1fr; place-items: center; }",
|
|
419
|
+
".gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__media { position: absolute; inset: 0; }",
|
|
420
|
+
".gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__panel {",
|
|
421
|
+
" width: min(420px, 86%); margin: var(--gem-space-xl) 0; background: var(--gem-color-background); border-radius: var(--gem-radius-lg); box-shadow: var(--gem-shadow-sm);",
|
|
422
|
+
"}",
|
|
423
|
+
// Image behind the text: light text over a dark wash, every size. A card over the image has its own
|
|
424
|
+
// background, so on desktop it keeps the preset's text colour.
|
|
425
|
+
".gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__media::after { content: ''; position: absolute; inset: 0; background: linear-gradient(rgb(0 0 0 / 0.35), rgb(0 0 0 / 0.6)); }",
|
|
426
|
+
".gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__panel,",
|
|
427
|
+
".gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__close { color: #fff; }",
|
|
428
|
+
".gem-popup-shell__close {",
|
|
429
|
+
" position: absolute; inset-block-start: var(--gem-space-xs); inset-inline-end: var(--gem-space-xs); z-index: 2;",
|
|
430
|
+
" display: inline-grid; place-items: center; width: var(--gem-tap-min); height: var(--gem-tap-min); padding: 0;",
|
|
431
|
+
" border: 0; border-radius: var(--gem-radius-pill); color: inherit; background: transparent; cursor: pointer;",
|
|
432
|
+
"}",
|
|
433
|
+
".gem-popup-shell__close:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); }",
|
|
434
|
+
".gem-popup-shell__teaser {",
|
|
435
|
+
" position: fixed; inset-block-end: var(--gem-space-md); inset-inline-end: var(--gem-space-md); z-index: 40;",
|
|
436
|
+
" min-height: var(--gem-tap-min); padding: 0 var(--gem-space-md); font: inherit; font-weight: 600; cursor: pointer;",
|
|
437
|
+
" color: var(--gem-color-accent-text); background: var(--gem-color-accent); border: 0; border-radius: var(--gem-radius-pill); box-shadow: var(--gem-shadow-md);",
|
|
438
|
+
"}",
|
|
439
|
+
".gem-popup-shell__teaser[data-position='left'] { inset-inline-end: auto; inset-inline-start: var(--gem-space-md); }",
|
|
440
|
+
".gem-popup-shell__teaser[hidden] { display: none; }",
|
|
441
|
+
"@media (min-width: 750px) {",
|
|
442
|
+
// Fullscreen: the dialog covers the viewport. The image goes right; behind the text when media=background,
|
|
443
|
+
// and then text aligned to the start keeps the left half so it never sits half on the image.
|
|
444
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'] .gem-popup-shell__dialog { width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh; margin: 0; border-radius: 0; box-shadow: none; }",
|
|
445
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'] .gem-popup-shell__frame { min-height: 100%; }",
|
|
446
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'] .gem-popup-shell__media { order: 2; }",
|
|
447
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'] .gem-popup-shell__panel { padding: var(--gem-space-3xl); }",
|
|
448
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-align='start'] .gem-popup-shell__panel { padding-inline-start: max(var(--gem-space-3xl), 8vw); }",
|
|
449
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-media='background'] .gem-popup-shell__frame { grid-template-columns: 1fr; }",
|
|
450
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-media='background'][data-gem-popup-align='start'] .gem-popup-shell__frame { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }",
|
|
451
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-media='background'] .gem-popup-shell__media { position: absolute; inset: 0; }",
|
|
452
|
+
" .gem-popup-shell[data-gem-popup-desktop='fullscreen'][data-gem-popup-media='background'] .gem-popup-shell__panel { position: relative; z-index: 1; }",
|
|
453
|
+
" .gem-popup-shell[data-gem-popup-desktop='card-over-image'][data-gem-popup-media='background'] .gem-popup-shell__panel { color: var(--gem-color-text); }",
|
|
454
|
+
"}",
|
|
455
|
+
"@media (max-width: 749px) {",
|
|
456
|
+
// Mobile: takeover is the whole screen; a sheet rises from the bottom to at most 88% of it. The image
|
|
457
|
+
// sits under the text (top when media=top), capped at a third of the screen; with no image or the image
|
|
458
|
+
// behind, the text is centred vertically like the measured takeovers.
|
|
459
|
+
" .gem-popup-shell__dialog { width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh; margin: 0; border-radius: 0; }",
|
|
460
|
+
" .gem-popup-shell[data-gem-popup-mobile='sheet'] .gem-popup-shell__dialog { height: auto; max-height: 88dvh; margin: auto 0 0; border-radius: var(--gem-radius-lg) var(--gem-radius-lg) 0 0; }",
|
|
461
|
+
" .gem-popup-shell__frame { grid-template-columns: 1fr; grid-template-rows: auto 1fr; min-height: 100%; }",
|
|
462
|
+
" .gem-popup-shell__panel { justify-content: flex-start; padding: var(--gem-space-3xl) var(--gem-space-lg) var(--gem-space-xl); }",
|
|
463
|
+
" .gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__panel,",
|
|
464
|
+
" .gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__panel { justify-content: center; }",
|
|
465
|
+
" .gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__panel { width: 100%; margin: 0; border-radius: 0; box-shadow: none; }",
|
|
466
|
+
" .gem-popup-shell[data-gem-popup-desktop='card-over-image'] .gem-popup-shell__media { position: relative; }",
|
|
467
|
+
" .gem-popup-shell__media { order: 2; max-height: 34dvh; }",
|
|
468
|
+
" .gem-popup-shell[data-gem-popup-media='top'] .gem-popup-shell__media { order: 0; }",
|
|
469
|
+
" .gem-popup-shell[data-gem-popup-media='top'] .gem-popup-shell__panel { padding-top: var(--gem-space-lg); }",
|
|
470
|
+
" .gem-popup-shell[data-gem-popup-media='none'] .gem-popup-shell__frame,",
|
|
471
|
+
" .gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__frame { grid-template-rows: 1fr; }",
|
|
472
|
+
" .gem-popup-shell[data-gem-popup-media='background'] .gem-popup-shell__media { position: absolute; inset: 0; max-height: none; }",
|
|
473
|
+
"}",
|
|
474
|
+
".gem-popup-shell[data-gem-popup-preview='true'] { padding: var(--gem-space-xl) var(--gem-layout-gutter); background: rgb(0 0 0 / 0.55); }",
|
|
475
|
+
".gem-popup-shell[data-gem-popup-preview='true'] .gem-popup-shell__dialog { position: relative; display: block; inset: auto; }",
|
|
476
|
+
"@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; }",
|
|
479
|
+
"}",
|
|
480
|
+
"@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; }",
|
|
485
|
+
"}"
|
|
486
|
+
];
|
|
487
|
+
var GEM_POPUP_SHELL_CSS = GEM_POPUP_SHELL_CSS_LINES.join("\n");
|
|
386
488
|
var GEM_COUNTDOWN_ENDED_CSS_LINES = [
|
|
387
489
|
":root:not(:has([data-gem-canvas])) [data-gem-section-id][data-gem-countdown-ended='true'] { display: none; }",
|
|
388
490
|
":root:has([data-gem-canvas]) [data-gem-section-id][data-gem-countdown-ended='true'] { opacity: .55; }",
|
|
@@ -522,8 +624,88 @@ var countdownPluginRegistrationLines = [
|
|
|
522
624
|
" const targetNode = timer.querySelector && timer.querySelector(selector);",
|
|
523
625
|
" if (targetNode) targetNode.textContent = pad(value);",
|
|
524
626
|
" };",
|
|
627
|
+
// Relative mode: `data-gem-countdown-duration="N"` counts N seconds for THIS shopper instead of running to a
|
|
628
|
+
// calendar deadline (a popup offer: "this code is yours for 1:00"). `data-gem-countdown-start` says when the N
|
|
629
|
+
// seconds begin: "mount" (default) at bind; "visible" only while the timer is on screen, which is what a popup
|
|
630
|
+
// wants: hidden until the popup opens, paused while the shopper has it closed or has moved past the step that
|
|
631
|
+
// holds it, and no race with the open event (this plugin loads on `viewport`, so it can bind after a popup
|
|
632
|
+
// that opened at once); any other value is an event name heard on the timer's section. Readouts:
|
|
633
|
+
// [data-gem-countdown-days|hours|minutes|seconds] (two digits) and [data-gem-countdown-clock] ("1:00", "1:02:05").
|
|
634
|
+
// At zero the TIMER gets data-gem-countdown-ended="true" and a bubbling gem:countdown:complete leaves it, so
|
|
635
|
+
// the section that owns the offer can end it. The band is NOT marked: this ending belongs to one shopper, and
|
|
636
|
+
// the band rule above (data-gem-countdown-end) removes a band from the store for everyone.
|
|
637
|
+
" const clockText = (s) => {",
|
|
638
|
+
" const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;",
|
|
639
|
+
" return (h ? h + ':' + pad(m) : String(m)) + ':' + pad(sec);",
|
|
640
|
+
" };",
|
|
641
|
+
" const bindDuration = (timer, total) => {",
|
|
642
|
+
" const scope = (timer.closest && timer.closest('[data-gem-section-id]')) || document;",
|
|
643
|
+
" const mode = String(timer.getAttribute('data-gem-countdown-start') || 'mount').trim() || 'mount';",
|
|
644
|
+
" const stopEvent = String(timer.getAttribute('data-gem-countdown-stop') || '').trim();",
|
|
645
|
+
" let left = total * 1000;",
|
|
646
|
+
" let endAt = 0;",
|
|
647
|
+
" let handle = null;",
|
|
648
|
+
" let state = 'idle';",
|
|
649
|
+
" const detail = () => ({ source: 'gem-runtime', id: timer.id || undefined, mode: 'duration', duration: total, remainingSeconds: Math.max(0, Math.ceil(left / 1000)) });",
|
|
650
|
+
" const render = () => {",
|
|
651
|
+
" const s = Math.max(0, Math.ceil(left / 1000));",
|
|
652
|
+
" timer.dataset.gemCountdownState = state;",
|
|
653
|
+
" timer.dataset.gemCountdownRemainingSeconds = String(s);",
|
|
654
|
+
" write(timer, '[data-gem-countdown-days]', Math.floor(s / 86400));",
|
|
655
|
+
" write(timer, '[data-gem-countdown-hours]', Math.floor((s % 86400) / 3600));",
|
|
656
|
+
" write(timer, '[data-gem-countdown-minutes]', Math.floor((s % 3600) / 60));",
|
|
657
|
+
" write(timer, '[data-gem-countdown-seconds]', s % 60);",
|
|
658
|
+
" const readout = timer.querySelector && timer.querySelector('[data-gem-countdown-clock]');",
|
|
659
|
+
" if (readout) readout.textContent = clockText(s);",
|
|
660
|
+
" };",
|
|
661
|
+
" const halt = () => { if (handle !== null && target.clearInterval) target.clearInterval(handle); handle = null; };",
|
|
662
|
+
" const finish = () => {",
|
|
663
|
+
" halt();",
|
|
664
|
+
" left = 0;",
|
|
665
|
+
" state = 'complete';",
|
|
666
|
+
" render();",
|
|
667
|
+
" timer.setAttribute('data-gem-countdown-ended', 'true');",
|
|
668
|
+
// One event, not two: dispatched from the timer it bubbles to the document; api.emit would add a second
|
|
669
|
+
// document event. Without a dispatchable timer (tests, detached nodes) fall back to api.emit.
|
|
670
|
+
" if (timer.dispatchEvent && timer.isConnected !== false) {",
|
|
671
|
+
" timer.dispatchEvent(new CustomEvent('gem:countdown:complete', { bubbles: true, detail: detail() }));",
|
|
672
|
+
" for (const callback of api.listeners['gem:countdown:complete'] || []) callback(detail());",
|
|
673
|
+
" } else api.emit('gem:countdown:complete', detail());",
|
|
674
|
+
" };",
|
|
675
|
+
" const tick = () => { left = endAt - Date.now(); if (left <= 0) finish(); else render(); };",
|
|
676
|
+
" const run = () => {",
|
|
677
|
+
" if (state === 'running' || state === 'complete' || state === 'stopped') return;",
|
|
678
|
+
" state = 'running';",
|
|
679
|
+
" endAt = Date.now() + left;",
|
|
680
|
+
" render();",
|
|
681
|
+
" if (target.setInterval) handle = target.setInterval(tick, 250);",
|
|
682
|
+
" api.emit('gem:countdown:start', detail());",
|
|
683
|
+
" };",
|
|
684
|
+
" const pause = (next) => {",
|
|
685
|
+
" if (state === 'running') { left = Math.max(0, endAt - Date.now()); halt(); }",
|
|
686
|
+
" if (state === 'complete' || state === 'stopped') return;",
|
|
687
|
+
" state = next;",
|
|
688
|
+
" render();",
|
|
689
|
+
" };",
|
|
690
|
+
" render();",
|
|
691
|
+
" if (stopEvent && canListen(scope)) scope.addEventListener(stopEvent, () => pause('stopped'));",
|
|
692
|
+
" if (mode === 'visible' && target.IntersectionObserver) {",
|
|
693
|
+
" const observer = new target.IntersectionObserver((entries) => {",
|
|
694
|
+
" for (const entry of entries) entry.isIntersecting ? run() : pause('paused');",
|
|
695
|
+
" if (state === 'complete' || state === 'stopped') observer.disconnect();",
|
|
696
|
+
" });",
|
|
697
|
+
" observer.observe(timer);",
|
|
698
|
+
" } else if (mode === 'mount' || mode === 'visible') run();",
|
|
699
|
+
" else if (canListen(scope)) scope.addEventListener(mode, run);",
|
|
700
|
+
" };",
|
|
525
701
|
" timers.forEach((timer) => {",
|
|
526
702
|
" if (!mark(timer, 'countdown')) return;",
|
|
703
|
+
" const duration = Number(timer.getAttribute && timer.getAttribute('data-gem-countdown-duration'));",
|
|
704
|
+
" if (Number.isFinite(duration) && duration > 0) {",
|
|
705
|
+
" bindDuration(timer, Math.min(Math.round(duration), 86400));",
|
|
706
|
+
" bound += 1;",
|
|
707
|
+
" return;",
|
|
708
|
+
" }",
|
|
527
709
|
" const targetTime = countdownEnd(timer.getAttribute && timer.getAttribute('data-gem-countdown-target'));",
|
|
528
710
|
" const section = timer.closest ? timer.closest('[data-gem-section-id]') : null;",
|
|
529
711
|
" const band = section && section.hasAttribute && section.hasAttribute('data-gem-countdown-end') ? section : null;",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemboss/gem-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "gem-framework của GemTheme (CSS, runtime, token, bộ kiểm) cộng đúng bộ kiểm section gemtheme dùng khi duyệt. Build từ gemtheme, không sửa ở đây.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|