@gemboss/gem-framework 0.4.0 → 0.5.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 +66 -3
- package/dist/gem-runtime.js +28 -6
- package/dist/index.js +176 -18
- package/dist/section-gate.js +108 -5
- 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.5.0",
|
|
4
|
+
"gemtheme_sha": "02d4924e5c672fd4ae6f21f4ead858dcfa08e4e3",
|
|
5
5
|
"gemtheme_dirty": false,
|
|
6
6
|
"framework_version": "0.1.0"
|
|
7
7
|
}
|
package/dist/gem-framework.css
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
--gem-space-xl: calc(32px * var(--gem-style-space, 1));
|
|
20
20
|
--gem-space-2xl: calc(48px * var(--gem-style-space, 1));
|
|
21
21
|
--gem-space-3xl: calc(64px * var(--gem-style-space, 1));
|
|
22
|
+
--gem-space-2xs: calc(var(--gem-space-md) * 0.125);
|
|
23
|
+
--gem-space-ms: calc(var(--gem-space-md) * 0.75);
|
|
24
|
+
--gem-space-4xl: calc(var(--gem-space-md) * 7);
|
|
22
25
|
--gem-radius-sm: calc(4px * var(--gem-style-radius, 1));
|
|
23
26
|
--gem-radius-md: calc(8px * var(--gem-style-radius, 1));
|
|
24
27
|
--gem-radius-lg: calc(16px * var(--gem-style-radius, 1));
|
|
@@ -59,6 +62,7 @@
|
|
|
59
62
|
--gem-h1: calc(var(--gem-h2) * var(--gem-type-scale));
|
|
60
63
|
--gem-focus-ring: 2px solid var(--gem-color-accent);
|
|
61
64
|
--gem-tap-min: 44px;
|
|
65
|
+
--gem-focus-offset: 3px;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
*, *::before, *::after {
|
|
@@ -95,7 +99,7 @@ a {
|
|
|
95
99
|
}
|
|
96
100
|
:focus-visible {
|
|
97
101
|
outline: var(--gem-focus-ring, 2px solid var(--gem-color-accent));
|
|
98
|
-
outline-offset: 3px;
|
|
102
|
+
outline-offset: var(--gem-focus-offset, 3px);
|
|
99
103
|
}
|
|
100
104
|
.gem-skip-to-content-link:focus {
|
|
101
105
|
position: fixed;
|
|
@@ -267,6 +271,46 @@ a {
|
|
|
267
271
|
color: var(--gem-color-text);
|
|
268
272
|
padding: 0 var(--gem-space-md);
|
|
269
273
|
}
|
|
274
|
+
.gem-field-join {
|
|
275
|
+
display: flex;
|
|
276
|
+
align-items: stretch;
|
|
277
|
+
min-width: 0;
|
|
278
|
+
}
|
|
279
|
+
.gem-field-join > :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden) {
|
|
280
|
+
min-height: var(--gem-component-input-height, 44px);
|
|
281
|
+
margin: 0;
|
|
282
|
+
}
|
|
283
|
+
.gem-field-join > :is(.gem-input, .gem-select, .gem-form-field) {
|
|
284
|
+
flex: 1 1 10rem;
|
|
285
|
+
width: auto;
|
|
286
|
+
min-width: min(10rem, 50%);
|
|
287
|
+
}
|
|
288
|
+
.gem-field-join > .gem-form-field > :is(.gem-input, .gem-select) {
|
|
289
|
+
width: 100%;
|
|
290
|
+
height: 100%;
|
|
291
|
+
min-height: var(--gem-component-input-height, 44px);
|
|
292
|
+
}
|
|
293
|
+
.gem-field-join > :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden):not(.gem-input, .gem-select, .gem-form-field) {
|
|
294
|
+
flex: 0 1 auto;
|
|
295
|
+
min-width: var(--gem-component-input-height, 44px);
|
|
296
|
+
white-space: normal;
|
|
297
|
+
overflow-wrap: anywhere;
|
|
298
|
+
}
|
|
299
|
+
.gem-field-join > :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden):not(:nth-child(1 of :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden))), .gem-field-join > .gem-form-field:not(:nth-child(1 of :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden))) > :is(.gem-input, .gem-select) {
|
|
300
|
+
border-start-start-radius: 0;
|
|
301
|
+
border-end-start-radius: 0;
|
|
302
|
+
}
|
|
303
|
+
.gem-field-join > :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden):not(:nth-child(1 of :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden))) {
|
|
304
|
+
margin-inline-start: calc(-1 * var(--gem-input-border-width, 1px));
|
|
305
|
+
}
|
|
306
|
+
.gem-field-join > :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden):not(:nth-last-child(1 of :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden))), .gem-field-join > .gem-form-field:not(:nth-last-child(1 of :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden))) > :is(.gem-input, .gem-select) {
|
|
307
|
+
border-start-end-radius: 0;
|
|
308
|
+
border-end-end-radius: 0;
|
|
309
|
+
}
|
|
310
|
+
.gem-field-join > :focus-within {
|
|
311
|
+
position: relative;
|
|
312
|
+
z-index: 1;
|
|
313
|
+
}
|
|
270
314
|
.gem-textarea {
|
|
271
315
|
min-height: calc(var(--gem-component-input-height, 44px) * 2);
|
|
272
316
|
padding-block: var(--gem-space-sm);
|
|
@@ -307,10 +351,17 @@ a {
|
|
|
307
351
|
display: inline-flex;
|
|
308
352
|
align-items: center;
|
|
309
353
|
justify-content: center;
|
|
310
|
-
inline-size: var(--gem-space-lg);
|
|
311
|
-
block-size: var(--gem-space-lg);
|
|
354
|
+
inline-size: var(--gem-icon-size, var(--gem-space-lg));
|
|
355
|
+
block-size: var(--gem-icon-size, var(--gem-space-lg));
|
|
312
356
|
color: var(--gem-color-accent);
|
|
313
357
|
}
|
|
358
|
+
.gem-icon--sm { --gem-icon-size: 16px; }
|
|
359
|
+
.gem-icon--md { --gem-icon-size: 20px; }
|
|
360
|
+
.gem-icon--lg { --gem-icon-size: 24px; }
|
|
361
|
+
.gem-icon--current { color: currentColor; }
|
|
362
|
+
.gem-icon--chevron { transition: rotate var(--gem-motion-duration, 160ms) var(--gem-motion-easing, ease); }
|
|
363
|
+
:is(button, summary, [role='button'])[aria-expanded='true'] > .gem-icon--chevron, details[open] > summary > .gem-icon--chevron { rotate: 180deg; }
|
|
364
|
+
@media (prefers-reduced-motion: reduce) { .gem-icon--chevron { transition: none; } }
|
|
314
365
|
.gem-icon-list {
|
|
315
366
|
display: grid;
|
|
316
367
|
gap: var(--gem-space-sm);
|
|
@@ -1021,6 +1072,10 @@ a {
|
|
|
1021
1072
|
:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-card='outline'] :is(.gem-section__inner, [class*='__inner']) { border: 1px solid var(--gem-color-border); }
|
|
1022
1073
|
:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-card='shadow'] :is(.gem-section__inner, [class*='__inner']) { background: var(--gem-color-surface); box-shadow: var(--gem-shadow-md); }
|
|
1023
1074
|
: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); }
|
|
1075
|
+
:root:not(:has([data-gem-canvas])) [data-gem-section-id][data-gem-countdown-ended='true'] { display: none; }
|
|
1076
|
+
:root:has([data-gem-canvas]) [data-gem-section-id][data-gem-countdown-ended='true'] { opacity: .55; }
|
|
1077
|
+
.gem-countdown-ended-note { display: none; }
|
|
1078
|
+
: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; }
|
|
1024
1079
|
.gem-promo-banner, .gem-promo-tile {
|
|
1025
1080
|
border: var(--gem-border-subtle, 1px solid var(--gem-color-border));
|
|
1026
1081
|
border-radius: var(--gem-radius-md);
|
|
@@ -1124,6 +1179,11 @@ a {
|
|
|
1124
1179
|
inset: 0;
|
|
1125
1180
|
background: rgba(0, 0, 0, 0.36);
|
|
1126
1181
|
}
|
|
1182
|
+
@keyframes gem-cart-drawer-panel-in { from { translate: calc(var(--gem-drawer-offset, 24px) * var(--gem-drawer-dir, 1)) 0; opacity: 0; } }
|
|
1183
|
+
@keyframes gem-cart-drawer-overlay-in { from { opacity: 0; } }
|
|
1184
|
+
.gem-cart-drawer__panel:dir(rtl) { --gem-drawer-dir: -1; }
|
|
1185
|
+
.gem-cart-drawer[data-gem-drawer-motion='slide'] .gem-cart-drawer__overlay { animation: gem-cart-drawer-overlay-in var(--gem-motion-duration, 160ms) var(--gem-motion-easing, ease); }
|
|
1186
|
+
.gem-cart-drawer[data-gem-drawer-motion='slide'] .gem-cart-drawer__panel { animation: gem-cart-drawer-panel-in var(--gem-motion-duration, 160ms) var(--gem-motion-easing, ease); }
|
|
1127
1187
|
.gem-cart-drawer__panel {
|
|
1128
1188
|
position: fixed;
|
|
1129
1189
|
inset-block: 0;
|
|
@@ -1136,6 +1196,7 @@ a {
|
|
|
1136
1196
|
box-shadow: var(--gem-shadow-md, none);
|
|
1137
1197
|
padding: var(--gem-space-lg);
|
|
1138
1198
|
}
|
|
1199
|
+
@media (prefers-reduced-motion: reduce) { .gem-cart-drawer[data-gem-drawer-motion] :is(.gem-cart-drawer__panel, .gem-cart-drawer__overlay) { animation: none; } }
|
|
1139
1200
|
.gem-cart-drawer__header, .gem-cart-drawer__footer {
|
|
1140
1201
|
display: flex;
|
|
1141
1202
|
flex-wrap: wrap;
|
|
@@ -2151,6 +2212,7 @@ sticky-header.header-wrapper {
|
|
|
2151
2212
|
}
|
|
2152
2213
|
:where(.gem-section[data-gem-eyebrow='show']) .gem-section__eyebrow { display: block; letter-spacing: 0.12em; }
|
|
2153
2214
|
:where(.gem-section[data-gem-heading='section']) .gem-section__heading { margin: 0; font-family: var(--gem-font-heading); font-size: var(--gem-h2); line-height: 1.1; letter-spacing: -0.01em; text-wrap: balance; }
|
|
2215
|
+
:where(.gem-section[data-gem-body='section']) .gem-section__body { margin: 0; color: var(--gem-color-muted); font-size: var(--gem-body); line-height: var(--gem-font-body-line-height, 1.5); }
|
|
2154
2216
|
@media (max-width: 760px) {
|
|
2155
2217
|
:where(.gem-section[data-gem-heading='section']) .gem-section__heading { font-size: min(var(--gem-h2), clamp(1.75rem, 9vw, 2.75rem)); }
|
|
2156
2218
|
}
|
|
@@ -2369,6 +2431,7 @@ main > .gem-section:first-child { padding-top: 0; }
|
|
|
2369
2431
|
.gem-shell-footer__localization .gem-select, .gem-shell-footer__localization > .gem-select { display: inline-flex; align-items: center; min-width: 210px; min-height: 45px; border: 1px solid rgb(18 18 18 / 55%); border-radius: 0; background: transparent; padding: 10px 36px 10px 12px; color: rgb(18 18 18); font-size: 13px; line-height: 18px; }
|
|
2370
2432
|
:where(.gem-section[data-gem-eyebrow='show']) .gem-section__eyebrow { display: block; letter-spacing: 0.12em; }
|
|
2371
2433
|
:where(.gem-section[data-gem-heading='section']) .gem-section__heading { margin: 0; font-family: var(--gem-font-heading); font-size: var(--gem-h2); line-height: 1.1; letter-spacing: -0.01em; text-wrap: balance; }
|
|
2434
|
+
:where(.gem-section[data-gem-body='section']) .gem-section__body { margin: 0; color: var(--gem-color-muted); font-size: var(--gem-body); line-height: var(--gem-font-body-line-height, 1.5); }
|
|
2372
2435
|
@media (max-width: 760px) {
|
|
2373
2436
|
:root { --gem-layout-gutter: 24px; --gem-layout-section: 36px; }
|
|
2374
2437
|
.gem-shell-header, .gem-shell-header__inner, .header { min-height: 64px; }
|
package/dist/gem-runtime.js
CHANGED
|
@@ -645,32 +645,54 @@
|
|
|
645
645
|
const timers = document.querySelectorAll('[data-gem-countdown], .gem-countdown');
|
|
646
646
|
let bound = 0;
|
|
647
647
|
const pad = (value) => String(Math.max(0, value)).padStart(2, '0');
|
|
648
|
+
const COUNTDOWN_ISO = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/i;
|
|
649
|
+
const countdownEnd = (value) => {
|
|
650
|
+
if (typeof value !== 'string') return NaN;
|
|
651
|
+
const raw = value.trim();
|
|
652
|
+
const m = COUNTDOWN_ISO.exec(raw);
|
|
653
|
+
if (!m) return NaN;
|
|
654
|
+
const y = Number(m[1]), mo = Number(m[2]), d = Number(m[3]);
|
|
655
|
+
const day = new Date(Date.UTC(y, mo - 1, d));
|
|
656
|
+
if (day.getUTCFullYear() !== y || day.getUTCMonth() !== mo - 1 || day.getUTCDate() !== d) return NaN;
|
|
657
|
+
if (m[4] === undefined) return Date.parse(m[1] + '-' + m[2] + '-' + m[3] + 'T23:59:59Z');
|
|
658
|
+
const iso = m[1] + '-' + m[2] + '-' + m[3] + 'T' + raw.slice(11);
|
|
659
|
+
return Date.parse(m[7] ? iso : iso + 'Z');
|
|
660
|
+
};
|
|
648
661
|
const write = (timer, selector, value) => {
|
|
649
662
|
const targetNode = timer.querySelector && timer.querySelector(selector);
|
|
650
663
|
if (targetNode) targetNode.textContent = pad(value);
|
|
651
664
|
};
|
|
652
665
|
timers.forEach((timer) => {
|
|
653
666
|
if (!mark(timer, 'countdown')) return;
|
|
654
|
-
const
|
|
655
|
-
const
|
|
667
|
+
const targetTime = countdownEnd(timer.getAttribute && timer.getAttribute('data-gem-countdown-target'));
|
|
668
|
+
const section = timer.closest ? timer.closest('[data-gem-section-id]') : null;
|
|
669
|
+
const band = section && section.hasAttribute && section.hasAttribute('data-gem-countdown-end') ? section : null;
|
|
670
|
+
let handle = null;
|
|
656
671
|
const update = () => {
|
|
657
672
|
if (!Number.isFinite(targetTime)) {
|
|
658
673
|
timer.dataset.gemCountdownState = 'invalid';
|
|
659
674
|
return;
|
|
660
675
|
}
|
|
661
|
-
const
|
|
676
|
+
const ms = targetTime - Date.now();
|
|
677
|
+
const remainingSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
662
678
|
const days = Math.floor(remainingSeconds / 86400);
|
|
663
679
|
const hours = Math.floor((remainingSeconds % 86400) / 3600);
|
|
664
680
|
const minutes = Math.floor((remainingSeconds % 3600) / 60);
|
|
665
|
-
timer.dataset.gemCountdownState =
|
|
681
|
+
timer.dataset.gemCountdownState = ms > 0 ? 'running' : 'complete';
|
|
666
682
|
timer.dataset.gemCountdownRemainingSeconds = String(remainingSeconds);
|
|
667
683
|
write(timer, '[data-gem-countdown-days]', days);
|
|
668
684
|
write(timer, '[data-gem-countdown-hours]', hours);
|
|
669
685
|
write(timer, '[data-gem-countdown-minutes]', minutes);
|
|
670
|
-
|
|
686
|
+
if (ms <= 0) {
|
|
687
|
+
if (band) band.setAttribute('data-gem-countdown-ended', 'true');
|
|
688
|
+
if (handle !== null && target.clearInterval) { target.clearInterval(handle); handle = null; }
|
|
689
|
+
}
|
|
690
|
+
api.emit(ms > 0 ? 'gem:countdown:start' : 'gem:countdown:complete', { source: 'gem-runtime', id: timer.id || undefined, remainingSeconds });
|
|
671
691
|
};
|
|
672
692
|
update();
|
|
673
|
-
if (target.setInterval && Number.isFinite(targetTime)) target.setInterval(update, 60000);
|
|
693
|
+
if (target.setInterval && Number.isFinite(targetTime) && targetTime - Date.now() > 0) handle = target.setInterval(update, 60000);
|
|
694
|
+
const left = targetTime - Date.now();
|
|
695
|
+
if (target.setTimeout && Number.isFinite(left) && left > 0 && left < 2147483647) target.setTimeout(update, left);
|
|
674
696
|
bound += 1;
|
|
675
697
|
});
|
|
676
698
|
api.emit('gem:countdown:ready', { source: 'gem-runtime', count: bound });
|
package/dist/index.js
CHANGED
|
@@ -82,6 +82,15 @@ function gemPlanShapeKnobs(plan) {
|
|
|
82
82
|
var gemFrameworkVersion = "0.1.0";
|
|
83
83
|
var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
|
|
84
84
|
var GEM_TAP_MIN_CSS_VAR = "--gem-tap-min: 44px;";
|
|
85
|
+
var GEM_FOCUS_OFFSET_CSS_VAR = "--gem-focus-offset: 3px;";
|
|
86
|
+
var GEM_DERIVED_SPACE_STEPS = { "2xs": 0.125, ms: 0.75, "4xl": 7 };
|
|
87
|
+
var GEM_DERIVED_SPACE_CSS_VARIABLES = Object.keys(GEM_DERIVED_SPACE_STEPS).map((key) => `--gem-space-${slugify(key)}`);
|
|
88
|
+
function gemDerivedSpaceCssVars(spacing) {
|
|
89
|
+
const named = new Set(Object.entries(spacing ?? {}).filter(([, v]) => String(v ?? "").trim()).map(([k]) => slugify(k)));
|
|
90
|
+
const md = String(spacing?.md ?? "").trim();
|
|
91
|
+
const mdUnitless = md !== "" && !/\d\s*[a-z%]/i.test(stripImportant(md));
|
|
92
|
+
return Object.entries(GEM_DERIVED_SPACE_STEPS).filter(([key]) => !named.has(slugify(key))).map(([key, factor]) => `--gem-space-${slugify(key)}: ${mdUnitless ? "0" : `calc(var(--gem-space-md) * ${factor})`};`);
|
|
93
|
+
}
|
|
85
94
|
var GEM_ROW_CSS_VARIABLES = [
|
|
86
95
|
"--gem-row-cols",
|
|
87
96
|
"--gem-row-cols-md",
|
|
@@ -214,11 +223,12 @@ function validateGemTokens(tokens) {
|
|
|
214
223
|
function createGemTokenCss(tokens, selector = ":root") {
|
|
215
224
|
const declarations = [
|
|
216
225
|
...toCssVars("color", tokens.color),
|
|
217
|
-
...toCssVars("font", tokens.typography),
|
|
226
|
+
...toCssVars("font", Object.fromEntries(Object.entries(tokens.typography ?? {}).filter(([k]) => !isRampKey(k)))),
|
|
218
227
|
// Space ramp is scaled by the global density knob --gem-style-space (default 1),
|
|
219
228
|
// so a style pack can make the WHOLE storefront airier or denser without touching
|
|
220
229
|
// any section — every section already consumes --gem-space-*. Knob unset → identical.
|
|
221
230
|
...toScaledCssVars("space", tokens.spacing, "--gem-style-space"),
|
|
231
|
+
...gemDerivedSpaceCssVars(tokens.spacing),
|
|
222
232
|
// Radius ramp scaled by --gem-style-radius (default 1): a 'sharp' style sets it to
|
|
223
233
|
// 0 (square everything), 'pill' rounds further — across every section, no edits.
|
|
224
234
|
...toScaledCssVars("radius", tokens.radius, "--gem-style-radius"),
|
|
@@ -239,24 +249,39 @@ function createGemTokenCss(tokens, selector = ":root") {
|
|
|
239
249
|
// live framework defined them: a rebuild that followed the contract styled its headings with
|
|
240
250
|
// variables that resolved to nothing. One modular scale, retuned by overriding --gem-type-base
|
|
241
251
|
// or --gem-type-scale (see gemTypeRampCssVars).
|
|
242
|
-
...gemTypeRampCssVars(),
|
|
252
|
+
...gemTypeRampCssVars(tokens.typography),
|
|
243
253
|
// Focus ring (gemfactory request #3, 28/09/2026): 11 sections wrote `outline: 2px solid
|
|
244
254
|
// var(--gem-color-accent)` by hand. Not a token GROUP (a group would name it --gem-border-focus-ring
|
|
245
255
|
// and a required key would fail every stored plan/preset without it), so it is emitted here like the
|
|
246
256
|
// type ramp and registered in validation.ts. Follows the accent, so presets retint it for free.
|
|
247
257
|
GEM_FOCUS_RING_CSS_VAR,
|
|
248
|
-
GEM_TAP_MIN_CSS_VAR
|
|
258
|
+
GEM_TAP_MIN_CSS_VAR,
|
|
259
|
+
GEM_FOCUS_OFFSET_CSS_VAR
|
|
249
260
|
];
|
|
261
|
+
const baseMobile = typeRampValue(tokens.typography, "baseMobile");
|
|
250
262
|
return [
|
|
251
263
|
`${selector} {`,
|
|
252
264
|
...declarations.map((line) => ` ${line}`),
|
|
253
|
-
"}"
|
|
265
|
+
"}",
|
|
266
|
+
...baseMobile ? [`@media (max-width: ${GEM_TYPE_MOBILE_MAX}) {`, ` ${selector} {`, ` --gem-type-base: ${baseMobile};`, " }", "}"] : []
|
|
254
267
|
].join("\n");
|
|
255
268
|
}
|
|
256
|
-
|
|
269
|
+
var GEM_TYPE_RAMP_KEYS = ["base", "scale", "baseMobile"];
|
|
270
|
+
var rampKey = (k) => k.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
271
|
+
var isRampKey = (k) => GEM_TYPE_RAMP_KEYS.some((r) => rampKey(r) === rampKey(k));
|
|
272
|
+
var LENGTH = /^\d*\.?\d+(px|rem|em)$/;
|
|
273
|
+
var NUMBER = /^\d*\.?\d+$/;
|
|
274
|
+
var typeRampValue = (typography, key) => {
|
|
275
|
+
const hit = Object.entries(typography ?? {}).find(([k]) => rampKey(k) === rampKey(key));
|
|
276
|
+
const v = hit == null || hit[1] == null ? "" : stripImportant(hit[1]).trim();
|
|
277
|
+
if (!v) return null;
|
|
278
|
+
return (key === "scale" ? NUMBER.test(v) && Number(v) > 0 : LENGTH.test(v)) ? v : null;
|
|
279
|
+
};
|
|
280
|
+
var GEM_TYPE_MOBILE_MAX = "749px";
|
|
281
|
+
function gemTypeRampCssVars(typography) {
|
|
257
282
|
return [
|
|
258
|
-
|
|
259
|
-
|
|
283
|
+
`--gem-type-base: ${typeRampValue(typography, "base") ?? "1rem"};`,
|
|
284
|
+
`--gem-type-scale: ${typeRampValue(typography, "scale") ?? "1.22"};`,
|
|
260
285
|
"--gem-text-sm: calc(var(--gem-type-base) / var(--gem-type-scale));",
|
|
261
286
|
"--gem-body: var(--gem-type-base);",
|
|
262
287
|
"--gem-h6: calc(var(--gem-type-base) * var(--gem-type-scale));",
|
|
@@ -333,7 +358,9 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
333
358
|
},
|
|
334
359
|
{
|
|
335
360
|
group: "surfaces",
|
|
336
|
-
|
|
361
|
+
// `gem-countdown-ended-note`: the editor-canvas line every countdown band carries, styled once in
|
|
362
|
+
// primitives.ts GEM_COUNTDOWN_ENDED_CSS (hidden on the store, shown on an ended band in the canvas).
|
|
363
|
+
classes: ["gem-card", "gem-media", "gem-badge", "gem-accordion", "gem-divider", "gem-countdown-ended-note"]
|
|
337
364
|
},
|
|
338
365
|
{
|
|
339
366
|
group: "forms",
|
|
@@ -344,7 +371,9 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
344
371
|
"gem-select",
|
|
345
372
|
"gem-textarea",
|
|
346
373
|
"gem-submit",
|
|
347
|
-
"gem-newsletter-form"
|
|
374
|
+
"gem-newsletter-form",
|
|
375
|
+
// Input + button as one control (primitives.ts, gemfactory request #8).
|
|
376
|
+
"gem-field-join"
|
|
348
377
|
]
|
|
349
378
|
},
|
|
350
379
|
{
|
|
@@ -356,6 +385,12 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
356
385
|
// storefront alongside gem-section__ctas.
|
|
357
386
|
"gem-prose",
|
|
358
387
|
"gem-icon",
|
|
388
|
+
// Size / colour / rotate hooks (gemfactory request #11); the bare class renders as before.
|
|
389
|
+
"gem-icon--sm",
|
|
390
|
+
"gem-icon--md",
|
|
391
|
+
"gem-icon--lg",
|
|
392
|
+
"gem-icon--current",
|
|
393
|
+
"gem-icon--chevron",
|
|
359
394
|
"gem-icon-list",
|
|
360
395
|
"gem-icon-list__item",
|
|
361
396
|
"gem-video",
|
|
@@ -768,6 +803,13 @@ var GEM_CARD_CSS_LINES = [
|
|
|
768
803
|
":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); }"
|
|
769
804
|
];
|
|
770
805
|
var GEM_CARD_CSS = GEM_CARD_CSS_LINES.join("\n");
|
|
806
|
+
var GEM_COUNTDOWN_ENDED_CSS_LINES = [
|
|
807
|
+
":root:not(:has([data-gem-canvas])) [data-gem-section-id][data-gem-countdown-ended='true'] { display: none; }",
|
|
808
|
+
":root:has([data-gem-canvas]) [data-gem-section-id][data-gem-countdown-ended='true'] { opacity: .55; }",
|
|
809
|
+
".gem-countdown-ended-note { display: none; }",
|
|
810
|
+
":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; }"
|
|
811
|
+
];
|
|
812
|
+
var GEM_COUNTDOWN_ENDED_CSS = GEM_COUNTDOWN_ENDED_CSS_LINES.join("\n");
|
|
771
813
|
var GEM_REVEAL_CSS_LINES = [
|
|
772
814
|
// Longhand, so the per-element duration/delay knobs can reach it. The shorthand hard-coded 600ms,
|
|
773
815
|
// while `gem_animation_duration` and `gem_animation_delay` are declared on ALL FIFTY kinds in the
|
|
@@ -893,7 +935,7 @@ function createGemBaseCss(profile = "dawn-parity") {
|
|
|
893
935
|
"}",
|
|
894
936
|
":focus-visible {",
|
|
895
937
|
" outline: var(--gem-focus-ring, 2px solid var(--gem-color-accent));",
|
|
896
|
-
" outline-offset: 3px;",
|
|
938
|
+
" outline-offset: var(--gem-focus-offset, 3px);",
|
|
897
939
|
"}",
|
|
898
940
|
".gem-skip-to-content-link:focus {",
|
|
899
941
|
" position: fixed;",
|
|
@@ -1105,6 +1147,56 @@ function createPrimitiveCss() {
|
|
|
1105
1147
|
" color: var(--gem-color-text);",
|
|
1106
1148
|
" padding: 0 var(--gem-space-md);",
|
|
1107
1149
|
"}",
|
|
1150
|
+
// .gem-field-join (gemfactory request #8, 01/10/2026; 16 stored footers trimmed the touching corners by hand): an
|
|
1151
|
+
// input/select and its button as one block (newsletter, discount code). Only VISIBLE children count: Shopify forms carry hidden inputs (form_type, utf8) and a sr-only
|
|
1152
|
+
// label, and with plain :first-child those would square the visible outer corners and take a 44px slot. A field
|
|
1153
|
+
// may be wrapped in .gem-form-field. The field grows with a floor of min(10rem, 50%); the button sizes to its
|
|
1154
|
+
// label (dropping dawn-parity's 12rem minimum, which at 320px left the field 80px wide) and wraps a long label
|
|
1155
|
+
// instead of squeezing the field or overflowing. Only the corners where two children meet are squared, in logical
|
|
1156
|
+
// properties so it mirrors under RTL, and the seam overlaps by one border width so it is not drawn twice. A child
|
|
1157
|
+
// holding focus is lifted so its ring is not painted under the neighbour. Stays one row at every width.
|
|
1158
|
+
".gem-field-join {",
|
|
1159
|
+
" display: flex;",
|
|
1160
|
+
" align-items: stretch;",
|
|
1161
|
+
" min-width: 0;",
|
|
1162
|
+
"}",
|
|
1163
|
+
'.gem-field-join > :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden) {',
|
|
1164
|
+
" min-height: var(--gem-component-input-height, 44px);",
|
|
1165
|
+
" margin: 0;",
|
|
1166
|
+
"}",
|
|
1167
|
+
".gem-field-join > :is(.gem-input, .gem-select, .gem-form-field) {",
|
|
1168
|
+
" flex: 1 1 10rem;",
|
|
1169
|
+
" width: auto;",
|
|
1170
|
+
" min-width: min(10rem, 50%);",
|
|
1171
|
+
"}",
|
|
1172
|
+
// A wrapped field gets the join's height too: the dawn-parity profile sets .gem-input { min-height: 4.5rem }, and
|
|
1173
|
+
// only the direct-child rule above outranked it, so a wrapped input stood 72px next to a 44px one (02/10/2026).
|
|
1174
|
+
".gem-field-join > .gem-form-field > :is(.gem-input, .gem-select) {",
|
|
1175
|
+
" width: 100%;",
|
|
1176
|
+
" height: 100%;",
|
|
1177
|
+
" min-height: var(--gem-component-input-height, 44px);",
|
|
1178
|
+
"}",
|
|
1179
|
+
'.gem-field-join > :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden):not(.gem-input, .gem-select, .gem-form-field) {',
|
|
1180
|
+
" flex: 0 1 auto;",
|
|
1181
|
+
" min-width: var(--gem-component-input-height, 44px);",
|
|
1182
|
+
" white-space: normal;",
|
|
1183
|
+
" overflow-wrap: anywhere;",
|
|
1184
|
+
"}",
|
|
1185
|
+
'.gem-field-join > :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden):not(:nth-child(1 of :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden))), .gem-field-join > .gem-form-field:not(:nth-child(1 of :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden))) > :is(.gem-input, .gem-select) {',
|
|
1186
|
+
" border-start-start-radius: 0;",
|
|
1187
|
+
" border-end-start-radius: 0;",
|
|
1188
|
+
"}",
|
|
1189
|
+
'.gem-field-join > :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden):not(:nth-child(1 of :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden))) {',
|
|
1190
|
+
" margin-inline-start: calc(-1 * var(--gem-input-border-width, 1px));",
|
|
1191
|
+
"}",
|
|
1192
|
+
'.gem-field-join > :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden):not(:nth-last-child(1 of :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden))), .gem-field-join > .gem-form-field:not(:nth-last-child(1 of :not([type="hidden"]):not([hidden]):not(.gem-u-visually-hidden))) > :is(.gem-input, .gem-select) {',
|
|
1193
|
+
" border-start-end-radius: 0;",
|
|
1194
|
+
" border-end-end-radius: 0;",
|
|
1195
|
+
"}",
|
|
1196
|
+
".gem-field-join > :focus-within {",
|
|
1197
|
+
" position: relative;",
|
|
1198
|
+
" z-index: 1;",
|
|
1199
|
+
"}",
|
|
1108
1200
|
".gem-textarea {",
|
|
1109
1201
|
" min-height: calc(var(--gem-component-input-height, 44px) * 2);",
|
|
1110
1202
|
" padding-block: var(--gem-space-sm);",
|
|
@@ -1141,14 +1233,26 @@ function createPrimitiveCss() {
|
|
|
1141
1233
|
" color: var(--gem-color-muted, color-mix(in srgb, var(--gem-color-text) 72%, transparent));",
|
|
1142
1234
|
" line-height: 1.6;",
|
|
1143
1235
|
"}",
|
|
1236
|
+
// Icon size and colour hooks (gemfactory request #11, 01/10/2026: 33 sections reset colour to inherit and wrote
|
|
1237
|
+
// 22/14/16/20px by hand). Additive only: with no modifier and no --gem-icon-size an icon renders exactly as before
|
|
1238
|
+
// (the spacing step, the accent). Changing those DEFAULTS is a design decision left open on #11.
|
|
1144
1239
|
".gem-icon {",
|
|
1145
1240
|
" display: inline-flex;",
|
|
1146
1241
|
" align-items: center;",
|
|
1147
1242
|
" justify-content: center;",
|
|
1148
|
-
" inline-size: var(--gem-space-lg);",
|
|
1149
|
-
" block-size: var(--gem-space-lg);",
|
|
1243
|
+
" inline-size: var(--gem-icon-size, var(--gem-space-lg));",
|
|
1244
|
+
" block-size: var(--gem-icon-size, var(--gem-space-lg));",
|
|
1150
1245
|
" color: var(--gem-color-accent);",
|
|
1151
1246
|
"}",
|
|
1247
|
+
".gem-icon--sm { --gem-icon-size: 16px; }",
|
|
1248
|
+
".gem-icon--md { --gem-icon-size: 20px; }",
|
|
1249
|
+
".gem-icon--lg { --gem-icon-size: 24px; }",
|
|
1250
|
+
".gem-icon--current { color: currentColor; }",
|
|
1251
|
+
".gem-icon--chevron { transition: rotate var(--gem-motion-duration, 160ms) var(--gem-motion-easing, ease); }",
|
|
1252
|
+
// Only the TOGGLE's own state: the runtime also sets aria-expanded on containers (the open cart drawer, the
|
|
1253
|
+
// header drawer), and a descendant match would flip every chevron inside them (review #650).
|
|
1254
|
+
":is(button, summary, [role='button'])[aria-expanded='true'] > .gem-icon--chevron, details[open] > summary > .gem-icon--chevron { rotate: 180deg; }",
|
|
1255
|
+
"@media (prefers-reduced-motion: reduce) { .gem-icon--chevron { transition: none; } }",
|
|
1152
1256
|
".gem-icon-list {",
|
|
1153
1257
|
" display: grid;",
|
|
1154
1258
|
" gap: var(--gem-space-sm);",
|
|
@@ -1856,6 +1960,7 @@ function createPrimitiveCss() {
|
|
|
1856
1960
|
...GEM_AMBIENT_CSS_LINES,
|
|
1857
1961
|
...GEM_DIVIDER_CSS_LINES,
|
|
1858
1962
|
...GEM_CARD_CSS_LINES,
|
|
1963
|
+
...GEM_COUNTDOWN_ENDED_CSS_LINES,
|
|
1859
1964
|
".gem-promo-banner, .gem-promo-tile {",
|
|
1860
1965
|
" border: var(--gem-border-subtle, 1px solid var(--gem-color-border));",
|
|
1861
1966
|
" border-radius: var(--gem-radius-md);",
|
|
@@ -1959,6 +2064,18 @@ function createPrimitiveCss() {
|
|
|
1959
2064
|
" inset: 0;",
|
|
1960
2065
|
" background: rgba(0, 0, 0, 0.36);",
|
|
1961
2066
|
"}",
|
|
2067
|
+
// Opening motion (gemfactory request #13, 01/10/2026): 28 stored drawers each declared their own
|
|
2068
|
+
// `@keyframes …-in { from { transform: translateX(24px); opacity: 0 } }`. OPT-IN on the drawer root,
|
|
2069
|
+
// data-gem-drawer-motion="slide", so no existing drawer changes (review #650: as a default the overlay fade
|
|
2070
|
+
// reached all 28): the panel slides in by --gem-drawer-offset (24px) and the overlay fades. A drawer drops its own
|
|
2071
|
+
// keyframes by adding the attribute. The panel sits at inline-end, so under RTL it comes from the left.
|
|
2072
|
+
// :dir(rtl) follows the HTML dir attribute through inheritance, so a dir="ltr" island inside an rtl page stays
|
|
2073
|
+
// ltr (a `[dir='rtl'] .x` descendant rule got that wrong). CSS `direction` alone is invisible to selectors. Off under prefers-reduced-motion.
|
|
2074
|
+
"@keyframes gem-cart-drawer-panel-in { from { translate: calc(var(--gem-drawer-offset, 24px) * var(--gem-drawer-dir, 1)) 0; opacity: 0; } }",
|
|
2075
|
+
"@keyframes gem-cart-drawer-overlay-in { from { opacity: 0; } }",
|
|
2076
|
+
".gem-cart-drawer__panel:dir(rtl) { --gem-drawer-dir: -1; }",
|
|
2077
|
+
".gem-cart-drawer[data-gem-drawer-motion='slide'] .gem-cart-drawer__overlay { animation: gem-cart-drawer-overlay-in var(--gem-motion-duration, 160ms) var(--gem-motion-easing, ease); }",
|
|
2078
|
+
".gem-cart-drawer[data-gem-drawer-motion='slide'] .gem-cart-drawer__panel { animation: gem-cart-drawer-panel-in var(--gem-motion-duration, 160ms) var(--gem-motion-easing, ease); }",
|
|
1962
2079
|
".gem-cart-drawer__panel {",
|
|
1963
2080
|
" position: fixed;",
|
|
1964
2081
|
" inset-block: 0;",
|
|
@@ -1971,6 +2088,9 @@ function createPrimitiveCss() {
|
|
|
1971
2088
|
" box-shadow: var(--gem-shadow-md, none);",
|
|
1972
2089
|
" padding: var(--gem-space-lg);",
|
|
1973
2090
|
"}",
|
|
2091
|
+
// AFTER the opt-in rules and at THEIR specificity: measured 02/10/2026, a reduced-motion rule placed before the
|
|
2092
|
+
// animation rule (or weaker than it) left the panel animating.
|
|
2093
|
+
"@media (prefers-reduced-motion: reduce) { .gem-cart-drawer[data-gem-drawer-motion] :is(.gem-cart-drawer__panel, .gem-cart-drawer__overlay) { animation: none; } }",
|
|
1974
2094
|
".gem-cart-drawer__header, .gem-cart-drawer__footer {",
|
|
1975
2095
|
" display: flex;",
|
|
1976
2096
|
" flex-wrap: wrap;",
|
|
@@ -2346,6 +2466,7 @@ function createGemSectionRuntimeContractCss(selector = "[data-gem-section-librar
|
|
|
2346
2466
|
}
|
|
2347
2467
|
var OPT_IN_EYEBROW = ":where(.gem-section[data-gem-eyebrow='show']) .gem-section__eyebrow { display: block; letter-spacing: 0.12em; }";
|
|
2348
2468
|
var OPT_IN_HEADING = ":where(.gem-section[data-gem-heading='section']) .gem-section__heading { margin: 0; font-family: var(--gem-font-heading); font-size: var(--gem-h2); line-height: 1.1; letter-spacing: -0.01em; text-wrap: balance; }";
|
|
2469
|
+
var OPT_IN_BODY = ":where(.gem-section[data-gem-body='section']) .gem-section__body { margin: 0; color: var(--gem-color-muted); font-size: var(--gem-body); line-height: var(--gem-font-body-line-height, 1.5); }";
|
|
2349
2470
|
var OPT_IN_HEADING_MOBILE = " :where(.gem-section[data-gem-heading='section']) .gem-section__heading { font-size: min(var(--gem-h2), clamp(1.75rem, 9vw, 2.75rem)); }";
|
|
2350
2471
|
function createSectionCss() {
|
|
2351
2472
|
return [
|
|
@@ -2667,6 +2788,7 @@ function createSectionCss() {
|
|
|
2667
2788
|
"}",
|
|
2668
2789
|
OPT_IN_EYEBROW,
|
|
2669
2790
|
OPT_IN_HEADING,
|
|
2791
|
+
OPT_IN_BODY,
|
|
2670
2792
|
"@media (max-width: 760px) {",
|
|
2671
2793
|
OPT_IN_HEADING_MOBILE,
|
|
2672
2794
|
"}",
|
|
@@ -2917,6 +3039,7 @@ function createDawnParityCss() {
|
|
|
2917
3039
|
// Opt-ins again, AFTER the Dawn rules above (see OPT_IN_EYEBROW).
|
|
2918
3040
|
OPT_IN_EYEBROW,
|
|
2919
3041
|
OPT_IN_HEADING,
|
|
3042
|
+
OPT_IN_BODY,
|
|
2920
3043
|
createDawnParityMobileCss(),
|
|
2921
3044
|
// The row again, after every Dawn rule including its phone block (see GEM_ROW_CSS_LINES).
|
|
2922
3045
|
...GEM_ROW_CSS_LINES
|
|
@@ -4335,37 +4458,64 @@ var beforeAfterPluginRegistrationLines = [
|
|
|
4335
4458
|
" api.emit('gem:before-after:ready', { source: 'gem-runtime', count: bound });",
|
|
4336
4459
|
" });"
|
|
4337
4460
|
];
|
|
4461
|
+
var COUNTDOWN_END_RUNTIME_LINES = [
|
|
4462
|
+
" const COUNTDOWN_ISO = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d+)?)?(Z|[+-]\\d{2}:?\\d{2})?)?$/i;",
|
|
4463
|
+
" const countdownEnd = (value) => {",
|
|
4464
|
+
" if (typeof value !== 'string') return NaN;",
|
|
4465
|
+
" const raw = value.trim();",
|
|
4466
|
+
" const m = COUNTDOWN_ISO.exec(raw);",
|
|
4467
|
+
" if (!m) return NaN;",
|
|
4468
|
+
" const y = Number(m[1]), mo = Number(m[2]), d = Number(m[3]);",
|
|
4469
|
+
" const day = new Date(Date.UTC(y, mo - 1, d));",
|
|
4470
|
+
" if (day.getUTCFullYear() !== y || day.getUTCMonth() !== mo - 1 || day.getUTCDate() !== d) return NaN;",
|
|
4471
|
+
" if (m[4] === undefined) return Date.parse(m[1] + '-' + m[2] + '-' + m[3] + 'T23:59:59Z');",
|
|
4472
|
+
" const iso = m[1] + '-' + m[2] + '-' + m[3] + 'T' + raw.slice(11);",
|
|
4473
|
+
" return Date.parse(m[7] ? iso : iso + 'Z');",
|
|
4474
|
+
" };"
|
|
4475
|
+
];
|
|
4338
4476
|
var countdownPluginRegistrationLines = [
|
|
4339
4477
|
" registerPlugin('countdown', ({ api }) => {",
|
|
4340
4478
|
" const timers = document.querySelectorAll('[data-gem-countdown], .gem-countdown');",
|
|
4341
4479
|
" let bound = 0;",
|
|
4342
4480
|
" const pad = (value) => String(Math.max(0, value)).padStart(2, '0');",
|
|
4481
|
+
...COUNTDOWN_END_RUNTIME_LINES.map((line) => " " + line),
|
|
4343
4482
|
" const write = (timer, selector, value) => {",
|
|
4344
4483
|
" const targetNode = timer.querySelector && timer.querySelector(selector);",
|
|
4345
4484
|
" if (targetNode) targetNode.textContent = pad(value);",
|
|
4346
4485
|
" };",
|
|
4347
4486
|
" timers.forEach((timer) => {",
|
|
4348
4487
|
" if (!mark(timer, 'countdown')) return;",
|
|
4349
|
-
" const
|
|
4350
|
-
" const
|
|
4488
|
+
" const targetTime = countdownEnd(timer.getAttribute && timer.getAttribute('data-gem-countdown-target'));",
|
|
4489
|
+
" const section = timer.closest ? timer.closest('[data-gem-section-id]') : null;",
|
|
4490
|
+
" const band = section && section.hasAttribute && section.hasAttribute('data-gem-countdown-end') ? section : null;",
|
|
4491
|
+
" let handle = null;",
|
|
4351
4492
|
" const update = () => {",
|
|
4352
4493
|
" if (!Number.isFinite(targetTime)) {",
|
|
4353
4494
|
" timer.dataset.gemCountdownState = 'invalid';",
|
|
4354
4495
|
" return;",
|
|
4355
4496
|
" }",
|
|
4356
|
-
" const
|
|
4497
|
+
" const ms = targetTime - Date.now();",
|
|
4498
|
+
" const remainingSeconds = Math.max(0, Math.floor(ms / 1000));",
|
|
4357
4499
|
" const days = Math.floor(remainingSeconds / 86400);",
|
|
4358
4500
|
" const hours = Math.floor((remainingSeconds % 86400) / 3600);",
|
|
4359
4501
|
" const minutes = Math.floor((remainingSeconds % 3600) / 60);",
|
|
4360
|
-
" timer.dataset.gemCountdownState =
|
|
4502
|
+
" timer.dataset.gemCountdownState = ms > 0 ? 'running' : 'complete';",
|
|
4361
4503
|
" timer.dataset.gemCountdownRemainingSeconds = String(remainingSeconds);",
|
|
4362
4504
|
" write(timer, '[data-gem-countdown-days]', days);",
|
|
4363
4505
|
" write(timer, '[data-gem-countdown-hours]', hours);",
|
|
4364
4506
|
" write(timer, '[data-gem-countdown-minutes]', minutes);",
|
|
4365
|
-
"
|
|
4507
|
+
" if (ms <= 0) {",
|
|
4508
|
+
" if (band) band.setAttribute('data-gem-countdown-ended', 'true');",
|
|
4509
|
+
" if (handle !== null && target.clearInterval) { target.clearInterval(handle); handle = null; }",
|
|
4510
|
+
" }",
|
|
4511
|
+
" api.emit(ms > 0 ? 'gem:countdown:start' : 'gem:countdown:complete', { source: 'gem-runtime', id: timer.id || undefined, remainingSeconds });",
|
|
4366
4512
|
" };",
|
|
4367
4513
|
" update();",
|
|
4368
|
-
" if (target.setInterval && Number.isFinite(targetTime)) target.setInterval(update, 60000);",
|
|
4514
|
+
" if (target.setInterval && Number.isFinite(targetTime) && targetTime - Date.now() > 0) handle = target.setInterval(update, 60000);",
|
|
4515
|
+
// The readout moves once a minute, but the END is not allowed to wait for the next minute: one timer
|
|
4516
|
+
// for the exact instant (while it fits a setTimeout, ~24.8 days; past that the minute tick gets there).
|
|
4517
|
+
" const left = targetTime - Date.now();",
|
|
4518
|
+
" if (target.setTimeout && Number.isFinite(left) && left > 0 && left < 2147483647) target.setTimeout(update, left);",
|
|
4369
4519
|
" bound += 1;",
|
|
4370
4520
|
" });",
|
|
4371
4521
|
" api.emit('gem:countdown:ready', { source: 'gem-runtime', count: bound });",
|
|
@@ -4717,6 +4867,12 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
|
4717
4867
|
// --gem-focus-ring: emitted with the tokens, not a group (tokens.ts GEM_FOCUS_RING_CSS_VAR).
|
|
4718
4868
|
GEM_FOCUS_RING_CSS_VAR.split(":")[0].trim(),
|
|
4719
4869
|
GEM_TAP_MIN_CSS_VAR.split(":")[0].trim(),
|
|
4870
|
+
GEM_FOCUS_OFFSET_CSS_VAR.split(":")[0].trim(),
|
|
4871
|
+
// The derived spacing steps (tokens.ts GEM_DERIVED_SPACE_STEPS): always emitted, never required.
|
|
4872
|
+
...GEM_DERIVED_SPACE_CSS_VARIABLES,
|
|
4873
|
+
// Extension points a section SETS (gemtheme #650): icon size, drawer slide distance.
|
|
4874
|
+
"--gem-icon-size",
|
|
4875
|
+
"--gem-drawer-offset",
|
|
4720
4876
|
// The row primitive's variables (primitives.ts GEM_ROW_CSS). Sections SET these on their grid.
|
|
4721
4877
|
...GEM_ROW_CSS_VARIABLES
|
|
4722
4878
|
]);
|
|
@@ -5997,7 +6153,9 @@ function stringSetting(value) {
|
|
|
5997
6153
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
5998
6154
|
}
|
|
5999
6155
|
export {
|
|
6156
|
+
COUNTDOWN_END_RUNTIME_LINES,
|
|
6000
6157
|
GEM_AMBIENT_CSS,
|
|
6158
|
+
GEM_COUNTDOWN_ENDED_CSS,
|
|
6001
6159
|
GEM_GRID_COLUMNS_ROW_KNOB_CSS,
|
|
6002
6160
|
GEM_PART_CLASS,
|
|
6003
6161
|
GEM_REVEAL_CSS,
|
package/dist/section-gate.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// ../../packages/gem-framework/src/tokens.ts
|
|
2
2
|
var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
|
|
3
3
|
var GEM_TAP_MIN_CSS_VAR = "--gem-tap-min: 44px;";
|
|
4
|
+
var GEM_FOCUS_OFFSET_CSS_VAR = "--gem-focus-offset: 3px;";
|
|
5
|
+
var GEM_DERIVED_SPACE_STEPS = { "2xs": 0.125, ms: 0.75, "4xl": 7 };
|
|
6
|
+
var GEM_DERIVED_SPACE_CSS_VARIABLES = Object.keys(GEM_DERIVED_SPACE_STEPS).map((key) => `--gem-space-${slugify(key)}`);
|
|
4
7
|
var GEM_ROW_CSS_VARIABLES = [
|
|
5
8
|
"--gem-row-cols",
|
|
6
9
|
"--gem-row-cols-md",
|
|
@@ -23,10 +26,19 @@ var requiredGemTokenKeys = {
|
|
|
23
26
|
component: ["headerHeight", "cardPadding", "buttonPaddingInline", "buttonPaddingBlock", "inputHeight"],
|
|
24
27
|
morphology: ["campaignDensity", "mediaRhythm", "proofIntensity", "commerceEmphasis", "policyTone"]
|
|
25
28
|
};
|
|
26
|
-
|
|
29
|
+
var rampKey = (k) => k.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
30
|
+
var LENGTH = /^\d*\.?\d+(px|rem|em)$/;
|
|
31
|
+
var NUMBER = /^\d*\.?\d+$/;
|
|
32
|
+
var typeRampValue = (typography, key) => {
|
|
33
|
+
const hit = Object.entries(typography ?? {}).find(([k]) => rampKey(k) === rampKey(key));
|
|
34
|
+
const v = hit == null || hit[1] == null ? "" : stripImportant(hit[1]).trim();
|
|
35
|
+
if (!v) return null;
|
|
36
|
+
return (key === "scale" ? NUMBER.test(v) && Number(v) > 0 : LENGTH.test(v)) ? v : null;
|
|
37
|
+
};
|
|
38
|
+
function gemTypeRampCssVars(typography) {
|
|
27
39
|
return [
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
`--gem-type-base: ${typeRampValue(typography, "base") ?? "1rem"};`,
|
|
41
|
+
`--gem-type-scale: ${typeRampValue(typography, "scale") ?? "1.22"};`,
|
|
30
42
|
"--gem-text-sm: calc(var(--gem-type-base) / var(--gem-type-scale));",
|
|
31
43
|
"--gem-body: var(--gem-type-base);",
|
|
32
44
|
"--gem-h6: calc(var(--gem-type-base) * var(--gem-type-scale));",
|
|
@@ -37,6 +49,11 @@ function gemTypeRampCssVars() {
|
|
|
37
49
|
"--gem-h1: calc(var(--gem-h2) * var(--gem-type-scale));"
|
|
38
50
|
];
|
|
39
51
|
}
|
|
52
|
+
function stripImportant(value) {
|
|
53
|
+
const s = String(value);
|
|
54
|
+
if (!/!\s*important/i.test(s)) return s;
|
|
55
|
+
return s.replace(/!\s*important/gi, " ").replace(/\s+/g, " ").replace(/\s+,/g, ",").trim();
|
|
56
|
+
}
|
|
40
57
|
function slugify(value) {
|
|
41
58
|
return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
42
59
|
}
|
|
@@ -60,7 +77,9 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
60
77
|
},
|
|
61
78
|
{
|
|
62
79
|
group: "surfaces",
|
|
63
|
-
|
|
80
|
+
// `gem-countdown-ended-note`: the editor-canvas line every countdown band carries, styled once in
|
|
81
|
+
// primitives.ts GEM_COUNTDOWN_ENDED_CSS (hidden on the store, shown on an ended band in the canvas).
|
|
82
|
+
classes: ["gem-card", "gem-media", "gem-badge", "gem-accordion", "gem-divider", "gem-countdown-ended-note"]
|
|
64
83
|
},
|
|
65
84
|
{
|
|
66
85
|
group: "forms",
|
|
@@ -71,7 +90,9 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
71
90
|
"gem-select",
|
|
72
91
|
"gem-textarea",
|
|
73
92
|
"gem-submit",
|
|
74
|
-
"gem-newsletter-form"
|
|
93
|
+
"gem-newsletter-form",
|
|
94
|
+
// Input + button as one control (primitives.ts, gemfactory request #8).
|
|
95
|
+
"gem-field-join"
|
|
75
96
|
]
|
|
76
97
|
},
|
|
77
98
|
{
|
|
@@ -83,6 +104,12 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
83
104
|
// storefront alongside gem-section__ctas.
|
|
84
105
|
"gem-prose",
|
|
85
106
|
"gem-icon",
|
|
107
|
+
// Size / colour / rotate hooks (gemfactory request #11); the bare class renders as before.
|
|
108
|
+
"gem-icon--sm",
|
|
109
|
+
"gem-icon--md",
|
|
110
|
+
"gem-icon--lg",
|
|
111
|
+
"gem-icon--current",
|
|
112
|
+
"gem-icon--chevron",
|
|
86
113
|
"gem-icon-list",
|
|
87
114
|
"gem-icon-list__item",
|
|
88
115
|
"gem-video",
|
|
@@ -356,6 +383,13 @@ var GEM_CARD_CSS_LINES = [
|
|
|
356
383
|
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-card='filled'] :is(.gem-section__inner, [class*='__inner']) { background: color-mix(in srgb, currentColor 6%, transparent); }"
|
|
357
384
|
];
|
|
358
385
|
var GEM_CARD_CSS = GEM_CARD_CSS_LINES.join("\n");
|
|
386
|
+
var GEM_COUNTDOWN_ENDED_CSS_LINES = [
|
|
387
|
+
":root:not(:has([data-gem-canvas])) [data-gem-section-id][data-gem-countdown-ended='true'] { display: none; }",
|
|
388
|
+
":root:has([data-gem-canvas]) [data-gem-section-id][data-gem-countdown-ended='true'] { opacity: .55; }",
|
|
389
|
+
".gem-countdown-ended-note { display: none; }",
|
|
390
|
+
":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; }"
|
|
391
|
+
];
|
|
392
|
+
var GEM_COUNTDOWN_ENDED_CSS = GEM_COUNTDOWN_ENDED_CSS_LINES.join("\n");
|
|
359
393
|
var GEM_REVEAL_CSS_LINES = [
|
|
360
394
|
// Longhand, so the per-element duration/delay knobs can reach it. The shorthand hard-coded 600ms,
|
|
361
395
|
// while `gem_animation_duration` and `gem_animation_delay` are declared on ALL FIFTY kinds in the
|
|
@@ -463,6 +497,69 @@ function offerEngineRuntimeScript() {
|
|
|
463
497
|
}
|
|
464
498
|
|
|
465
499
|
// ../../packages/gem-framework/src/runtime.ts
|
|
500
|
+
var COUNTDOWN_END_RUNTIME_LINES = [
|
|
501
|
+
" const COUNTDOWN_ISO = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d+)?)?(Z|[+-]\\d{2}:?\\d{2})?)?$/i;",
|
|
502
|
+
" const countdownEnd = (value) => {",
|
|
503
|
+
" if (typeof value !== 'string') return NaN;",
|
|
504
|
+
" const raw = value.trim();",
|
|
505
|
+
" const m = COUNTDOWN_ISO.exec(raw);",
|
|
506
|
+
" if (!m) return NaN;",
|
|
507
|
+
" const y = Number(m[1]), mo = Number(m[2]), d = Number(m[3]);",
|
|
508
|
+
" const day = new Date(Date.UTC(y, mo - 1, d));",
|
|
509
|
+
" if (day.getUTCFullYear() !== y || day.getUTCMonth() !== mo - 1 || day.getUTCDate() !== d) return NaN;",
|
|
510
|
+
" if (m[4] === undefined) return Date.parse(m[1] + '-' + m[2] + '-' + m[3] + 'T23:59:59Z');",
|
|
511
|
+
" const iso = m[1] + '-' + m[2] + '-' + m[3] + 'T' + raw.slice(11);",
|
|
512
|
+
" return Date.parse(m[7] ? iso : iso + 'Z');",
|
|
513
|
+
" };"
|
|
514
|
+
];
|
|
515
|
+
var countdownPluginRegistrationLines = [
|
|
516
|
+
" registerPlugin('countdown', ({ api }) => {",
|
|
517
|
+
" const timers = document.querySelectorAll('[data-gem-countdown], .gem-countdown');",
|
|
518
|
+
" let bound = 0;",
|
|
519
|
+
" const pad = (value) => String(Math.max(0, value)).padStart(2, '0');",
|
|
520
|
+
...COUNTDOWN_END_RUNTIME_LINES.map((line) => " " + line),
|
|
521
|
+
" const write = (timer, selector, value) => {",
|
|
522
|
+
" const targetNode = timer.querySelector && timer.querySelector(selector);",
|
|
523
|
+
" if (targetNode) targetNode.textContent = pad(value);",
|
|
524
|
+
" };",
|
|
525
|
+
" timers.forEach((timer) => {",
|
|
526
|
+
" if (!mark(timer, 'countdown')) return;",
|
|
527
|
+
" const targetTime = countdownEnd(timer.getAttribute && timer.getAttribute('data-gem-countdown-target'));",
|
|
528
|
+
" const section = timer.closest ? timer.closest('[data-gem-section-id]') : null;",
|
|
529
|
+
" const band = section && section.hasAttribute && section.hasAttribute('data-gem-countdown-end') ? section : null;",
|
|
530
|
+
" let handle = null;",
|
|
531
|
+
" const update = () => {",
|
|
532
|
+
" if (!Number.isFinite(targetTime)) {",
|
|
533
|
+
" timer.dataset.gemCountdownState = 'invalid';",
|
|
534
|
+
" return;",
|
|
535
|
+
" }",
|
|
536
|
+
" const ms = targetTime - Date.now();",
|
|
537
|
+
" const remainingSeconds = Math.max(0, Math.floor(ms / 1000));",
|
|
538
|
+
" const days = Math.floor(remainingSeconds / 86400);",
|
|
539
|
+
" const hours = Math.floor((remainingSeconds % 86400) / 3600);",
|
|
540
|
+
" const minutes = Math.floor((remainingSeconds % 3600) / 60);",
|
|
541
|
+
" timer.dataset.gemCountdownState = ms > 0 ? 'running' : 'complete';",
|
|
542
|
+
" timer.dataset.gemCountdownRemainingSeconds = String(remainingSeconds);",
|
|
543
|
+
" write(timer, '[data-gem-countdown-days]', days);",
|
|
544
|
+
" write(timer, '[data-gem-countdown-hours]', hours);",
|
|
545
|
+
" write(timer, '[data-gem-countdown-minutes]', minutes);",
|
|
546
|
+
" if (ms <= 0) {",
|
|
547
|
+
" if (band) band.setAttribute('data-gem-countdown-ended', 'true');",
|
|
548
|
+
" if (handle !== null && target.clearInterval) { target.clearInterval(handle); handle = null; }",
|
|
549
|
+
" }",
|
|
550
|
+
" api.emit(ms > 0 ? 'gem:countdown:start' : 'gem:countdown:complete', { source: 'gem-runtime', id: timer.id || undefined, remainingSeconds });",
|
|
551
|
+
" };",
|
|
552
|
+
" update();",
|
|
553
|
+
" if (target.setInterval && Number.isFinite(targetTime) && targetTime - Date.now() > 0) handle = target.setInterval(update, 60000);",
|
|
554
|
+
// The readout moves once a minute, but the END is not allowed to wait for the next minute: one timer
|
|
555
|
+
// for the exact instant (while it fits a setTimeout, ~24.8 days; past that the minute tick gets there).
|
|
556
|
+
" const left = targetTime - Date.now();",
|
|
557
|
+
" if (target.setTimeout && Number.isFinite(left) && left > 0 && left < 2147483647) target.setTimeout(update, left);",
|
|
558
|
+
" bound += 1;",
|
|
559
|
+
" });",
|
|
560
|
+
" api.emit('gem:countdown:ready', { source: 'gem-runtime', count: bound });",
|
|
561
|
+
" });"
|
|
562
|
+
];
|
|
466
563
|
var offerEnginePluginRegistrationLines = [
|
|
467
564
|
" registerPlugin('offer-engine', ({ api }) => {",
|
|
468
565
|
" " + offerEngineRuntimeScript(),
|
|
@@ -488,6 +585,12 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
|
488
585
|
// --gem-focus-ring: emitted with the tokens, not a group (tokens.ts GEM_FOCUS_RING_CSS_VAR).
|
|
489
586
|
GEM_FOCUS_RING_CSS_VAR.split(":")[0].trim(),
|
|
490
587
|
GEM_TAP_MIN_CSS_VAR.split(":")[0].trim(),
|
|
588
|
+
GEM_FOCUS_OFFSET_CSS_VAR.split(":")[0].trim(),
|
|
589
|
+
// The derived spacing steps (tokens.ts GEM_DERIVED_SPACE_STEPS): always emitted, never required.
|
|
590
|
+
...GEM_DERIVED_SPACE_CSS_VARIABLES,
|
|
591
|
+
// Extension points a section SETS (gemtheme #650): icon size, drawer slide distance.
|
|
592
|
+
"--gem-icon-size",
|
|
593
|
+
"--gem-drawer-offset",
|
|
491
594
|
// The row primitive's variables (primitives.ts GEM_ROW_CSS). Sections SET these on their grid.
|
|
492
595
|
...GEM_ROW_CSS_VARIABLES
|
|
493
596
|
]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemboss/gem-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|