@gemboss/gem-framework 0.5.2 → 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 +68 -0
- package/dist/index.js +184 -0
- 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
|
@@ -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);",
|
|
@@ -4514,8 +4617,88 @@ var countdownPluginRegistrationLines = [
|
|
|
4514
4617
|
" const targetNode = timer.querySelector && timer.querySelector(selector);",
|
|
4515
4618
|
" if (targetNode) targetNode.textContent = pad(value);",
|
|
4516
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
|
+
" };",
|
|
4517
4694
|
" timers.forEach((timer) => {",
|
|
4518
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
|
+
" }",
|
|
4519
4702
|
" const targetTime = countdownEnd(timer.getAttribute && timer.getAttribute('data-gem-countdown-target'));",
|
|
4520
4703
|
" const section = timer.closest ? timer.closest('[data-gem-section-id]') : null;",
|
|
4521
4704
|
" const band = section && section.hasAttribute && section.hasAttribute('data-gem-countdown-end') ? section : null;",
|
|
@@ -6189,6 +6372,7 @@ export {
|
|
|
6189
6372
|
GEM_COUNTDOWN_ENDED_CSS,
|
|
6190
6373
|
GEM_GRID_COLUMNS_ROW_KNOB_CSS,
|
|
6191
6374
|
GEM_PART_CLASS,
|
|
6375
|
+
GEM_POPUP_SHELL_CSS,
|
|
6192
6376
|
GEM_REVEAL_CSS,
|
|
6193
6377
|
GEM_ROW_CSS,
|
|
6194
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",
|