@gemboss/gem-framework 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -93,6 +93,17 @@ Từ 0.8.2 mỗi module có `.d.ts` cạnh nó (`mount(root?: ParentNode | null,
93
93
 
94
94
  ## Nhật ký thay đổi
95
95
 
96
+ - **0.13.0** (11/10/2026): kiểu nút **`gradient`** (`plan.global_settings.button_style`, `<body data-gem-button-style="gradient">`).
97
+ GemFactory Global Style đo 339 trang store hàng đầu: nút mua chính là gradient ở 8,4% (10,6% trang landing), 16/21 cái đo
98
+ được là MỘT tông đi từ sáng sang sẫm, và 14/21 có chữ dưới 4,5:1. Đầu kia của gradient là màu nhấn dời 25%
99
+ (`GEM_BUTTON_GRADIENT_SHIFT`) RA XA chữ nút: về đen khi màu nhấn tối hơn chữ, về trắng khi sáng hơn, nên chữ không bao giờ
100
+ kém tương phản hơn trên nền đặc. Viết bằng CSS (`color-mix` + `rgb(from …)`), đọc `--gem-color-accent` / `-accent-text`,
101
+ nên nút trong băng tối / băng accent theo bộ màu của băng; nằm trong `@supports`, trình duyệt không có cú pháp màu tương đối
102
+ giữ nút đặc như cũ. Bỏ qua nút link và secondary. Export mới: `gemButtonGradientEnd` (cùng phép tính bằng JS, cho phép đo),
103
+ `gemButtonGradientEndCss`, `GEM_BUTTON_GRADIENT_SHIFT`, `GEM_BUTTON_GRADIENT_PIVOT`. Schema `@gemtheme/schemas`: một
104
+ `buttonStyleSchema` cho cả ba chỗ trước đây tự liệt kê (cài đặt theme, design system plan, preset morph). Store không chọn
105
+ gradient không đổi gì, nên không cần edition mới. Biết mà chưa sửa: luật outline / soft cũng chạm nút link (chờ edition sau).
106
+
96
107
  - **0.12.1** (10/10/2026): hai ca biên của token nút, review gemsite #839 bắt khi gemsite chép phép đọc này để phát token
97
108
  cho store. (1) Bo trỏ một bậc bo khác (`radius.button: var(--gem-radius-pill)`) không bị nhân núm `--gem-style-radius`
98
109
  lần hai: bậc được trỏ tới đã nhân rồi (pill 1,4 thành 1,96×). Áp chung cho mọi thang qua `toScaledCssVars`; giá trị số
package/dist/SOURCE.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "package": "@gemboss/gem-framework",
3
- "version": "0.12.1",
4
- "gemtheme_sha": "4ceacbe0c2ace7da410851fc5fe15589c1d87b77",
3
+ "version": "0.13.0",
4
+ "gemtheme_sha": "3e6e73a7466bf54609b5e81abfc3c4d4a06b42d6",
5
5
  "gemtheme_dirty": false,
6
- "framework_version": "0.12.1"
6
+ "framework_version": "0.13.0"
7
7
  }
@@ -214,6 +214,16 @@ a {
214
214
  border-color: transparent;
215
215
  color: var(--gem-color-text);
216
216
  }
217
+ @supports (color: rgb(from color-mix(in srgb, red 50%, rgb(from blue calc(255 - r) g b)) clamp(0, (r - 127.5) * 1000, 255) g b)) {
218
+ [data-gem-button-style='gradient'] .gem-button:not(.gem-button--secondary, .gem-button--link),
219
+ [data-gem-button-style='gradient'] .gem-button--primary {
220
+ background-color: var(--gem-color-accent);
221
+ background-image: linear-gradient(135deg, var(--gem-color-accent), color-mix(in srgb, var(--gem-color-accent) 75%, rgb(from color-mix(in srgb, var(--gem-color-accent) 50%, rgb(from var(--gem-color-accent-text, var(--gem-color-surface)) calc(255 - r) calc(255 - g) calc(255 - b))) clamp(0, (0.2126 * r + 0.7152 * g + 0.0722 * b - 127.5) * 1000, 255) clamp(0, (0.2126 * r + 0.7152 * g + 0.0722 * b - 127.5) * 1000, 255) clamp(0, (0.2126 * r + 0.7152 * g + 0.0722 * b - 127.5) * 1000, 255) / 1)));
222
+ background-origin: border-box;
223
+ border-color: transparent;
224
+ color: var(--gem-color-accent-text, var(--gem-color-surface));
225
+ }
226
+ }
217
227
  .gem-card {
218
228
  border: var(--gem-border-subtle, 1px solid var(--gem-color-border));
219
229
  border-radius: var(--gem-card-radius, var(--gem-radius-md));
package/dist/index.js CHANGED
@@ -259,9 +259,28 @@ function aaLabelColor(preferred, fill, fallbacks = []) {
259
259
  if (pass) return pass;
260
260
  return candidates.reduce((a, b) => hexContrastRatio(b, ground) > hexContrastRatio(a, ground) ? b : a, preferred);
261
261
  }
262
+ var GEM_BUTTON_GRADIENT_SHIFT = 0.25;
263
+ var GEM_BUTTON_GRADIENT_PIVOT = 127.5;
264
+ var weighted = (r, g, b) => 0.2126 * r + 0.7152 * g + 0.0722 * b;
265
+ var target = (w) => Math.min(255, Math.max(0, (w - GEM_BUTTON_GRADIENT_PIVOT) * 1e3));
266
+ function gemButtonGradientEnd(accent, accentText) {
267
+ const a = normalizeHex(accent);
268
+ const t = normalizeHex(accentText);
269
+ if (!a || !t) return void 0;
270
+ const ch = (h) => [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16));
271
+ const [ar, ag, ab] = ch(a);
272
+ const [tr, tg, tb] = ch(t);
273
+ const gray = Math.round(target(weighted((ar + 255 - tr) / 2, (ag + 255 - tg) / 2, (ab + 255 - tb) / 2))).toString(16).padStart(2, "0");
274
+ return mixHex(a, `#${gray}${gray}${gray}`, GEM_BUTTON_GRADIENT_SHIFT);
275
+ }
276
+ function gemButtonGradientEndCss(accent = "var(--gem-color-accent)", accentText = "var(--gem-color-accent-text, var(--gem-color-surface))") {
277
+ const t = `clamp(0, (0.2126 * r + 0.7152 * g + 0.0722 * b - ${GEM_BUTTON_GRADIENT_PIVOT}) * 1000, 255)`;
278
+ const probe = `color-mix(in srgb, ${accent} 50%, rgb(from ${accentText} calc(255 - r) calc(255 - g) calc(255 - b)))`;
279
+ return `color-mix(in srgb, ${accent} ${100 - GEM_BUTTON_GRADIENT_SHIFT * 100}%, rgb(from ${probe} ${t} ${t} ${t} / 1))`;
280
+ }
262
281
 
263
282
  // ../../packages/gem-framework/src/tokens.ts
264
- var gemFrameworkVersion = "0.12.1";
283
+ var gemFrameworkVersion = "0.13.0";
265
284
  var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
266
285
  var GEM_TAP_MIN_CSS_VAR = "--gem-tap-min: 44px;";
267
286
  var GEM_FOCUS_OFFSET_CSS_VAR = "--gem-focus-offset: 3px;";
@@ -464,13 +483,13 @@ function readPhoneTokens(tokens) {
464
483
  for (const group of ["spacing", "layout"]) {
465
484
  const ownGroup = group === "spacing" ? /var\(\s*--gem-space-/i : /var\(\s*--gem-layout-/i;
466
485
  for (const [key, raw] of Object.entries(tokens?.[group] ?? {})) {
467
- const target = gemPhoneTokenTarget(group, key);
468
- if (target == null) continue;
486
+ const target2 = gemPhoneTokenTarget(group, key);
487
+ if (target2 == null) continue;
469
488
  const v = stripImportant(String(raw ?? "")).trim();
470
489
  const kind = gemSpaceMdKind(v);
471
490
  if (kind === "absent") continue;
472
- if (kind === "zero") out[group][target] = "0px";
473
- else if (kind === "length" && !ownGroup.test(v)) out[group][target] = v;
491
+ if (kind === "zero") out[group][target2] = "0px";
492
+ else if (kind === "length" && !ownGroup.test(v)) out[group][target2] = v;
474
493
  else out.invalid.push({ path: `${group}.${key}`, value: raw, why: kind === "length" ? `it reads a ${group} variable, which re-declared on :root is a cycle` : "not a length (a number needs a unit, e.g. 70px)" });
475
494
  }
476
495
  }
@@ -1767,6 +1786,9 @@ function createPrimitiveCss() {
1767
1786
  // and `.gem-button--soft` following button_style on the published storefront but NOT in the
1768
1787
  // preview, which is the drift this rule exists to remove. `--primary` is named too so the
1769
1788
  // modifier keeps winning where both could apply.
1789
+ // KNOWN (11/10/2026): these two also reach `.gem-button--link` (soft paints its tint behind a link-styled button,
1790
+ // outline recolours it). The gradient rule below leaves the link out; fixing these changes stores that did not ask,
1791
+ // so it waits for the next framework edition (editions.ts).
1770
1792
  "[data-gem-button-style='outline'] .gem-button:not(.gem-button--secondary),",
1771
1793
  "[data-gem-button-style='outline'] .gem-button--primary {",
1772
1794
  " background: transparent;",
@@ -1778,6 +1800,28 @@ function createPrimitiveCss() {
1778
1800
  " border-color: transparent;",
1779
1801
  " color: var(--gem-color-text);",
1780
1802
  "}",
1803
+ // `gradient` (0.13.0). GemFactory Global Style, 339 top-store pages: the main CTA is a gradient on 8.4% (10.6% of
1804
+ // landing pages), and 16 of the 21 measured are ONE hue going lighter → darker; 14 of the 21 drew their text under
1805
+ // 4.5:1. The far stop is the accent moved GEM_BUTTON_GRADIENT_SHIFT AWAY from the button text: toward black when the
1806
+ // accent is the darker of the two, toward white when it is the lighter (gemButtonGradientEnd is the same sum in JS;
1807
+ // contrast.ts has the two versions that failed), so the text only gains contrast across the button. It reads
1808
+ // --gem-color-accent / -accent-text, which a dark or accent band points at its own scheme, so a gradient button inside
1809
+ // a band follows the band. The text colour is set here because a bare `.gem-button` sets none. The link variant is
1810
+ // left out (it is text). Behind @supports, and in LONGHANDS: @supports only proves the syntax parses, and a value that
1811
+ // still fails at computed time (a var() that does not resolve) would take the whole `background` shorthand to
1812
+ // transparent under white text (review of #737, reproduced). As longhands only the image drops, over the accent.
1813
+ // Near black or white the two stops all but meet (#121212 → #0e0e0e): moving toward the text would break the AA
1814
+ // promise, so such a palette draws a flat button, and GemFactory's preset gate says so.
1815
+ "@supports (color: rgb(from color-mix(in srgb, red 50%, rgb(from blue calc(255 - r) g b)) clamp(0, (r - 127.5) * 1000, 255) g b)) {",
1816
+ " [data-gem-button-style='gradient'] .gem-button:not(.gem-button--secondary, .gem-button--link),",
1817
+ " [data-gem-button-style='gradient'] .gem-button--primary {",
1818
+ " background-color: var(--gem-color-accent);",
1819
+ ` background-image: linear-gradient(135deg, var(--gem-color-accent), ${gemButtonGradientEndCss()});`,
1820
+ " background-origin: border-box;",
1821
+ " border-color: transparent;",
1822
+ " color: var(--gem-color-accent-text, var(--gem-color-surface));",
1823
+ " }",
1824
+ "}",
1781
1825
  ".gem-card {",
1782
1826
  " border: var(--gem-border-subtle, 1px solid var(--gem-color-border));",
1783
1827
  " border-radius: var(--gem-card-radius, var(--gem-radius-md));",
@@ -6389,9 +6433,9 @@ function renderHeroElementBlocks(section) {
6389
6433
  const s = b?.settings ?? {};
6390
6434
  if (b?.type === "countdown") {
6391
6435
  const label = str(s.label) ? `<p class="gem-hero__countdown-label">${escapeHtml(str(s.label))}</p>` : "";
6392
- const target = escapeAttribute(str(s.end_date));
6436
+ const target2 = escapeAttribute(str(s.end_date));
6393
6437
  const unit = (n, l) => `<div class="gem-countdown__unit"><span class="gem-countdown__num" data-gem-cd-${n}>00</span><span class="gem-countdown__label">${l}</span></div>`;
6394
- return `<div class="gem-hero__countdown">${label}<div class="gem-countdown" data-gem-countdown data-gem-countdown-target="${target}" role="timer" aria-label="Offer countdown">${unit("days", "Days")}<div class="gem-countdown__sep">:</div>${unit("hours", "Hrs")}<div class="gem-countdown__sep">:</div>${unit("mins", "Min")}<div class="gem-countdown__sep">:</div>${unit("secs", "Sec")}</div></div>`;
6438
+ return `<div class="gem-hero__countdown">${label}<div class="gem-countdown" data-gem-countdown data-gem-countdown-target="${target2}" role="timer" aria-label="Offer countdown">${unit("days", "Days")}<div class="gem-countdown__sep">:</div>${unit("hours", "Hrs")}<div class="gem-countdown__sep">:</div>${unit("mins", "Min")}<div class="gem-countdown__sep">:</div>${unit("secs", "Sec")}</div></div>`;
6395
6439
  }
6396
6440
  if (b?.type === "trust_badges") {
6397
6441
  const items = ["badge_1", "badge_2", "badge_3"].map((k) => str(s[k])).filter(Boolean).map((t) => `<li class="gem-hero__trust-item"><svg class="gem-hero__trust-check" viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>${escapeHtml(t)}</li>`).join("");
@@ -7255,6 +7299,8 @@ export {
7255
7299
  GEM_AMBIENT_CSS,
7256
7300
  GEM_BAND_SCHEMES,
7257
7301
  GEM_BAND_SCHEME_FALLBACK_CSS,
7302
+ GEM_BUTTON_GRADIENT_PIVOT,
7303
+ GEM_BUTTON_GRADIENT_SHIFT,
7258
7304
  GEM_BUTTON_TOKEN_KEYS,
7259
7305
  GEM_BUTTON_TOKEN_SHAPES,
7260
7306
  GEM_COUNTDOWN_ENDED_CSS,
@@ -7310,6 +7356,8 @@ export {
7310
7356
  gemBandSchemeCssVars,
7311
7357
  gemBandSchemeDeclarations,
7312
7358
  gemButtonCaseCss,
7359
+ gemButtonGradientEnd,
7360
+ gemButtonGradientEndCss,
7313
7361
  gemFormAttributes,
7314
7362
  gemFormCssBlock,
7315
7363
  gemFormCssVars,
@@ -143,6 +143,16 @@ a {
143
143
  border-color: transparent;
144
144
  color: var(--gem-color-text);
145
145
  }
146
+ @supports (color: rgb(from color-mix(in srgb, red 50%, rgb(from blue calc(255 - r) g b)) clamp(0, (r - 127.5) * 1000, 255) g b)) {
147
+ [data-gem-button-style='gradient'] .gem-button:not(.gem-button--secondary, .gem-button--link),
148
+ [data-gem-button-style='gradient'] .gem-button--primary {
149
+ background-color: var(--gem-color-accent);
150
+ background-image: linear-gradient(135deg, var(--gem-color-accent), color-mix(in srgb, var(--gem-color-accent) 75%, rgb(from color-mix(in srgb, var(--gem-color-accent) 50%, rgb(from var(--gem-color-accent-text, var(--gem-color-surface)) calc(255 - r) calc(255 - g) calc(255 - b))) clamp(0, (0.2126 * r + 0.7152 * g + 0.0722 * b - 127.5) * 1000, 255) clamp(0, (0.2126 * r + 0.7152 * g + 0.0722 * b - 127.5) * 1000, 255) clamp(0, (0.2126 * r + 0.7152 * g + 0.0722 * b - 127.5) * 1000, 255) / 1)));
151
+ background-origin: border-box;
152
+ border-color: transparent;
153
+ color: var(--gem-color-accent-text, var(--gem-color-surface));
154
+ }
155
+ }
146
156
  .gem-card {
147
157
  border: var(--gem-border-subtle, 1px solid var(--gem-color-border));
148
158
  border-radius: var(--gem-card-radius, var(--gem-radius-md));
@@ -139,6 +139,13 @@ function aaLabelColor(preferred, fill, fallbacks = []) {
139
139
  if (pass) return pass;
140
140
  return candidates.reduce((a, b) => hexContrastRatio(b, ground) > hexContrastRatio(a, ground) ? b : a, preferred);
141
141
  }
142
+ var GEM_BUTTON_GRADIENT_SHIFT = 0.25;
143
+ var GEM_BUTTON_GRADIENT_PIVOT = 127.5;
144
+ function gemButtonGradientEndCss(accent = "var(--gem-color-accent)", accentText = "var(--gem-color-accent-text, var(--gem-color-surface))") {
145
+ const t = `clamp(0, (0.2126 * r + 0.7152 * g + 0.0722 * b - ${GEM_BUTTON_GRADIENT_PIVOT}) * 1000, 255)`;
146
+ const probe = `color-mix(in srgb, ${accent} 50%, rgb(from ${accentText} calc(255 - r) calc(255 - g) calc(255 - b)))`;
147
+ return `color-mix(in srgb, ${accent} ${100 - GEM_BUTTON_GRADIENT_SHIFT * 100}%, rgb(from ${probe} ${t} ${t} ${t} / 1))`;
148
+ }
142
149
 
143
150
  // ../../packages/gem-framework/src/tokens.ts
144
151
  var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
@@ -1438,6 +1445,9 @@ function createPrimitiveCss() {
1438
1445
  // and `.gem-button--soft` following button_style on the published storefront but NOT in the
1439
1446
  // preview, which is the drift this rule exists to remove. `--primary` is named too so the
1440
1447
  // modifier keeps winning where both could apply.
1448
+ // KNOWN (11/10/2026): these two also reach `.gem-button--link` (soft paints its tint behind a link-styled button,
1449
+ // outline recolours it). The gradient rule below leaves the link out; fixing these changes stores that did not ask,
1450
+ // so it waits for the next framework edition (editions.ts).
1441
1451
  "[data-gem-button-style='outline'] .gem-button:not(.gem-button--secondary),",
1442
1452
  "[data-gem-button-style='outline'] .gem-button--primary {",
1443
1453
  " background: transparent;",
@@ -1449,6 +1459,28 @@ function createPrimitiveCss() {
1449
1459
  " border-color: transparent;",
1450
1460
  " color: var(--gem-color-text);",
1451
1461
  "}",
1462
+ // `gradient` (0.13.0). GemFactory Global Style, 339 top-store pages: the main CTA is a gradient on 8.4% (10.6% of
1463
+ // landing pages), and 16 of the 21 measured are ONE hue going lighter → darker; 14 of the 21 drew their text under
1464
+ // 4.5:1. The far stop is the accent moved GEM_BUTTON_GRADIENT_SHIFT AWAY from the button text: toward black when the
1465
+ // accent is the darker of the two, toward white when it is the lighter (gemButtonGradientEnd is the same sum in JS;
1466
+ // contrast.ts has the two versions that failed), so the text only gains contrast across the button. It reads
1467
+ // --gem-color-accent / -accent-text, which a dark or accent band points at its own scheme, so a gradient button inside
1468
+ // a band follows the band. The text colour is set here because a bare `.gem-button` sets none. The link variant is
1469
+ // left out (it is text). Behind @supports, and in LONGHANDS: @supports only proves the syntax parses, and a value that
1470
+ // still fails at computed time (a var() that does not resolve) would take the whole `background` shorthand to
1471
+ // transparent under white text (review of #737, reproduced). As longhands only the image drops, over the accent.
1472
+ // Near black or white the two stops all but meet (#121212 → #0e0e0e): moving toward the text would break the AA
1473
+ // promise, so such a palette draws a flat button, and GemFactory's preset gate says so.
1474
+ "@supports (color: rgb(from color-mix(in srgb, red 50%, rgb(from blue calc(255 - r) g b)) clamp(0, (r - 127.5) * 1000, 255) g b)) {",
1475
+ " [data-gem-button-style='gradient'] .gem-button:not(.gem-button--secondary, .gem-button--link),",
1476
+ " [data-gem-button-style='gradient'] .gem-button--primary {",
1477
+ " background-color: var(--gem-color-accent);",
1478
+ ` background-image: linear-gradient(135deg, var(--gem-color-accent), ${gemButtonGradientEndCss()});`,
1479
+ " background-origin: border-box;",
1480
+ " border-color: transparent;",
1481
+ " color: var(--gem-color-accent-text, var(--gem-color-surface));",
1482
+ " }",
1483
+ "}",
1452
1484
  ".gem-card {",
1453
1485
  " border: var(--gem-border-subtle, 1px solid var(--gem-color-border));",
1454
1486
  " border-radius: var(--gem-card-radius, var(--gem-radius-md));",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemboss/gem-framework",
3
- "version": "0.12.1",
3
+ "version": "0.13.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",