@gemboss/gem-framework 0.10.0 → 0.12.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
@@ -25,6 +25,26 @@ khi CÀI, nên build trên Railway không kéo được, và GemFactory phải c
25
25
  thì mọi repo cài theo version, và bị tụt thì thấy ngay ở số version. Mã trong gói là CSS và runtime vốn đã tới
26
26
  trình duyệt của người mua, cộng bộ kiểm; `license: UNLICENSED` giữ nguyên (xem được, không có quyền dùng lại).
27
27
 
28
+ ## Kênh `next` (thử trước khi phát hành)
29
+
30
+ Mỗi commit lên main của gemtheme mà đổi gói thì có ngay một bản trên GitHub Packages:
31
+ `@chriskoh26/gem-framework@next`, số phiên bản `<x.y.z của main>-next.<10 ký tự băm của bản dựng>`. Workflow:
32
+ `.github/workflows/gem-framework-next.yml`. Kênh `latest` trên npm vẫn chỉ đi qua passkey (mục dưới).
33
+
34
+ Thử ở một repo dùng gói, KHÔNG commit hai thay đổi này:
35
+
36
+ ```
37
+ # .npmrc của repo (hoặc ~/.npmrc), token chỉ cần read:packages
38
+ @chriskoh26:registry=https://npm.pkg.github.com
39
+ //npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}
40
+
41
+ npm install --no-save "@gemboss/gem-framework@npm:@chriskoh26/gem-framework@next"
42
+ ```
43
+
44
+ Bí danh giữ nguyên tên `@gemboss/gem-framework`, nên mã và `import` của bên dùng không đổi. Bản dựng ghi
45
+ `gemtheme_sha` trong `dist/SOURCE.json`. Luôn ghi `@next` (hoặc đúng số): tag `latest` của gói trên GitHub Packages là
46
+ 0.4.0 từ 29/09/2026, không phải bản mới nhất.
47
+
28
48
  ## Phát hành
29
49
 
30
50
  Một lệnh, chạy trên máy, không dùng GitHub Actions (Chris chốt 30/09/2026: không trả tiền runner):
@@ -73,6 +93,46 @@ Từ 0.8.2 mỗi module có `.d.ts` cạnh nó (`mount(root?: ParentNode | null,
73
93
 
74
94
  ## Nhật ký thay đổi
75
95
 
96
+ - **0.12.0** (10/10/2026): **edition** (Chris chốt 10/10/2026). Mỗi store ghim một edition; CSS làm đổi hình store
97
+ không yêu cầu nó thì phát dưới edition kế tiếp thay vì đổi mọi store cùng lúc. Plan mang `framework_edition` (số nguyên,
98
+ thiếu = 1); storefront đặt nó lên `<html>` thành `data-gem-edition`. Luật của edition N viết dưới
99
+ `:where([data-gem-edition='N'], …edition sau)`: độ ưu tiên 0, nên mọi luật giữ đúng chỗ của nó trong thác CSS; danh sách
100
+ edition trong phạm vi sinh từ sổ, nên thêm edition 3 thì luật của edition 2 tự nhận `"3"`. Edition cũ được hỗ trợ 6 tháng
101
+ kể từ khi có edition mới. Khối luật edition nằm cuối `primitives.css`.
102
+ **Edition 2:** tiêu đề section = H2, tiêu đề hero = H1 của thang chữ ở màn rộng hơn 760px kể cả khi plan không khai
103
+ `typography.base` / `scale`; điện thoại giữ cỡ cũ. Đo Chromium trên `globals.css` của gemsite (tiêu đề section / hero, px):
104
+ 1280 64/51 → 43/53, 1366 68/55 → 43/53, 1920 68/77 → 43/53, 768 38/40 → 43/53, 320 và 390 không đổi (32/35, 32/43).
105
+ Export mới: `GEM_FRAMEWORK_EDITIONS`, `LATEST_GEM_FRAMEWORK_EDITION`, `GEM_EDITION_SUPPORT_MONTHS`,
106
+ `gemFrameworkEditionOf` (đọc giá trị của plan: thiếu / sai = 1, mới hơn gói biết = edition mới nhất gói biết),
107
+ `gemFrameworkEditionSupportedUntil`, `gemFrameworkEditionScope`, `gemFrameworkEditionCss`; riêng sổ edition có entry
108
+ nhẹ `@gemboss/gem-framework/editions` (6,4 KB, kèm `editions.d.ts` do tsc sinh) để storefront đọc `framework_edition`
109
+ mà không nạp `index.js` (429 KB). `renderGemPreviewDocument` đặt `data-gem-edition` theo plan như storefront.
110
+ `themePlanSchema` (gói schemas) khai `framework_edition`, đọc dễ dãi như `gemFrameworkEditionOf` (giá trị sai bị bỏ, plan
111
+ vẫn parse được): công cụ vá theme parse plan, khoá không khai thì bị gỡ. Bộ kiểm biến CSS coi phạm
112
+ vi edition là gốc (`<html>`). Store không mang `data-gem-edition` vẽ y như 0.11.0.
113
+ - **0.11.0** (10/10/2026): băng nền `data-gem-section-background="dark"` / `"accent"` là một **bộ màu**: luật băng trỏ
114
+ mọi `--gem-color-*` bắt buộc vào `--gem-scheme-<băng>-<khoá>`. Trước bản này băng chỉ đổi nền và `color` của chính nó:
115
+ thẻ lồng trong băng vẽ nền sáng với chữ sáng, `color: var(--gem-color-text)` ra chữ tối trên nền tối, section tự vẽ nền
116
+ bằng token thì đè mất nền băng. Bộ màu có hai tầng:
117
+ 1. `createGemTokenCss` phát giá trị tính sẵn từ màu của plan (`aaTextColor`: chữ, muted, nhấn, trạng thái ≥ 4,5:1 trên
118
+ nền và surface của băng; chữ nút ≥ 4,5:1 trên màu nhấn của băng). Tắt bằng `{ bandSchemes: false }`.
119
+ 2. CSS của gói mang tầng dự phòng `:where(:root)` (độ ưu tiên 0) tính bằng `var()` / `color-mix` từ `--gem-color-*`: nơi
120
+ tự đặt `--gem-color-*` bằng tay vẫn ra băng đúng màu brand, chỉ màu nhấn / trạng thái không được bảo đảm AA. Khối
121
+ token mặc định trong `gem-framework.css` / `tokens.css` không mang tầng 1, để tầng này quyết.
122
+ `--gem-border-*` đọc màu được khai lại trong phạm vi băng (viền theo băng). Đo 248 section kho GemFactory × 4 preset
123
+ (18.092 dòng chữ, Chromium): băng tối 3.972 → 211 dòng dưới AA, băng accent 3.980 → 771, không băng giữ 405; chỉ tầng
124
+ dự phòng: 211 / 769. Phần còn lại là section tự pha màu (`color-mix` tỉ lệ cố định, màu ghi cứng). Thêm export
125
+ `gemBandSchemeColors`, `gemBandSchemeCssVars`, `gemBandSchemeDeclarations`, `GEM_BAND_SCHEMES`,
126
+ `GEM_BAND_SCHEME_FALLBACK_CSS`, và phép tính màu dời từ theme-presets: `aaTextColor`, `aaLabelColor`, `hexContrastRatio`,
127
+ `hexRelativeLuminance`, `mixHex`, `normalizeHex`, `MIN_TEXT_CONTRAST`. Bản chép luật băng ở nơi khác (gemsite
128
+ `globals.css`) phải mang cả tầng dự phòng: luật băng không có `--gem-scheme-*` nào thì mọi `--gem-color-*` trong băng
129
+ thành rỗng (nền nút, nền thẻ trong suốt).
130
+ - **0.10.1** (09/10/2026): màu `color.warning` mặc định `#b7791f` → `#9a661a`. Màu cũ chỉ đạt 3,36:1 trên nền mặc định và
131
+ 3,64:1 trên surface (dưới AA 4,5:1), trong khi section GemFactory dùng nó làm màu chữ và badge vẽ `accentText` trên nó. Màu
132
+ mới đạt 4,52 / 4,90, giữ tông. Phiên gemfactory phát hiện khi thêm cảnh báo tương phản cho preset. Test mới
133
+ `default-colours-meet-aa`: mọi màu mặc định dùng làm chữ ≥ 4,5:1 trên cả hai nền. Plan khai `color.warning` riêng (mọi
134
+ plan sinh từ style pack) thì không đổi: bảng màu pack sửa ở theme-presets (#722). Preview harness của gem-landing-blocks
135
+ theo màu mới. Vòng tham chiếu chéo nhóm của khoá điện thoại vẫn CHƯA chặn (ghi ở `readPhoneTokens`).
76
136
  - **0.10.0** (08/10/2026, gộp 0.9.10 và 0.9.11 chưa phát hành): phần còn lại của yêu cầu #4 GemFactory (Figma CRO, issue
77
137
  gemfactory #95). Plan không khai khoá nào dưới đây thì CSS tính ra như cũ (mỗi `var()` lùi về đúng giá trị cũ).
78
138
  - Line-height tiêu đề: opt-in `data-gem-heading="section"` (1.1), `.gem-section__heading` nền (1.04) và của Dawn parity
package/dist/SOURCE.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "package": "@gemboss/gem-framework",
3
- "version": "0.10.0",
4
- "gemtheme_sha": "15d7875982c5db5dd12446e59108221eabe273ee",
3
+ "version": "0.12.0",
4
+ "gemtheme_sha": "19bae499182a6b3479f331ae4c4647458d3625c5",
5
5
  "gemtheme_dirty": false,
6
- "framework_version": "0.10.0"
6
+ "framework_version": "0.12.0"
7
7
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Framework EDITIONS (P5, Chris 10/10/2026). Every store pins one; CSS that would change the shape of a store that did
3
+ * not ask for it ships under the next edition instead of changing every store at once.
4
+ *
5
+ * - The plan carries `framework_edition` (an integer; absent = edition 1, every store before editions existed). It goes on
6
+ * <html> as `data-gem-edition`: the storefront (gemsite lib/framework-edition.ts) and this package's own preview document
7
+ * (preview-document.ts) put it there, edition 1 puts nothing.
8
+ * - An edition's rules are scoped `:where([data-gem-edition='N'], …later editions)`: zero specificity, and a store on
9
+ * edition 3 also gets what edition 2 changed. The list of editions in the scope is generated from this registry, so adding
10
+ * edition 3 widens edition 2's rules by itself (and moves the anchors of gemsite's synced block: its notice says BREAKS).
11
+ * - The registry's CSS closes primitives.css and may only set custom properties on <html> (editions.test.ts enforces it):
12
+ * at that position an ELEMENT rule would lose to every later rule of the same specificity in sections.css. An edition that
13
+ * restyles an element writes its :where()-scoped rule beside the rule it changes, where the later position makes it win.
14
+ * - New stores get LATEST_GEM_FRAMEWORK_EDITION. Existing stores move when the seller upgrades (gemboss admin compares
15
+ * both before the switch).
16
+ * - An edition is supported for GEM_EDITION_SUPPORT_MONTHS after the next one is released (Chris 10/10/2026). Retiring
17
+ * it is a later release that drops the scope from the next edition's rules, which moves every remaining store.
18
+ *
19
+ * A consumer that wants the newest look without a plan (a lab, a preview) sets data-gem-edition to the latest edition.
20
+ */
21
+ export interface GemFrameworkEdition {
22
+ edition: number;
23
+ /** The package version that introduced it. */
24
+ since: string;
25
+ /** YYYY-MM-DD the edition was released. Its predecessor's support ends GEM_EDITION_SUPPORT_MONTHS later. */
26
+ released: string;
27
+ /** What a store sees change when it moves to this edition, in the words the admin shows the seller. */
28
+ changes: readonly string[];
29
+ /** The CSS this edition adds, unscoped; gemFrameworkEditionCss scopes it. Empty for edition 1. */
30
+ css: readonly string[];
31
+ }
32
+ export declare const GEM_EDITION_SUPPORT_MONTHS = 6;
33
+ export declare const GEM_FRAMEWORK_EDITIONS: readonly GemFrameworkEdition[];
34
+ export declare const LATEST_GEM_FRAMEWORK_EDITION: number;
35
+ /**
36
+ * The edition a plan's `framework_edition` means. Absent, not a whole number or below 1: edition 1. Above the newest edition
37
+ * this package knows (a plan written by a newer admin, read by an older storefront): the newest it knows, so the store
38
+ * still gets every change this package can draw rather than falling back to edition 1.
39
+ */
40
+ export declare function gemFrameworkEditionOf(value: unknown): number;
41
+ /**
42
+ * YYYY-MM-DD the edition stops being supported, or null while it is the newest. Takes what a plan holds (read through
43
+ * gemFrameworkEditionOf, so an old plan without the key is edition 1 and gets its date, not "no end"). Six calendar months
44
+ * after the next edition's release, kept inside the month: released 31 August ends on the last day of February.
45
+ */
46
+ export declare function gemFrameworkEditionSupportedUntil(edition: unknown): string | null;
47
+ /** GEM_EDITION_SUPPORT_MONTHS after a YYYY-MM-DD date, clamped to the end of the month it lands in. */
48
+ export declare function addEditionSupportMonths(released: string): string;
49
+ /** `:where([data-gem-edition='N'], [data-gem-edition='N+1'], …)`: the attribute selectors of edition N and every later one. */
50
+ export declare function gemFrameworkEditionScope(edition: number): string;
51
+ /**
52
+ * Every edition's CSS, scoped, oldest first. It closes primitives.css (primitives.ts createGemThemeCssParts): the rules set
53
+ * custom properties on <html>, so their place in the sheet does not decide anything against element rules.
54
+ */
55
+ export declare function gemFrameworkEditionCss(): string;
@@ -0,0 +1,136 @@
1
+ // ../../packages/gem-framework/src/form-set.ts
2
+ var FORM_CSS_VAR = {
3
+ radius: "--gem-input-radius",
4
+ borderWidth: "--gem-input-border-width",
5
+ borderColor: "--gem-input-border-color",
6
+ fill: "--gem-input-bg",
7
+ height: "--gem-input-height",
8
+ paddingInline: "--gem-input-padding-inline",
9
+ fontSize: "--gem-input-font-size",
10
+ focusRing: "--gem-input-focus-ring",
11
+ checkboxAccent: "--gem-checkbox-accent",
12
+ checkboxSize: "--gem-checkbox-size"
13
+ };
14
+ var GEM_FORM_CSS_VARIABLES = Object.values(FORM_CSS_VAR);
15
+
16
+ // ../../packages/gem-framework/src/tokens.ts
17
+ var GEM_DERIVED_SPACE_STEPS = { "2xs": 0.125, ms: 0.75, "4xl": 7 };
18
+ var GEM_DERIVED_SPACE_CSS_VARIABLES = Object.keys(GEM_DERIVED_SPACE_STEPS).map((key) => `--gem-space-${slugify(key)}`);
19
+ var GEM_BUTTON_TOKEN_KEYS = {
20
+ radius: ["button"],
21
+ typography: ["buttonWeight", "buttonTransform", "buttonLetterSpacing"]
22
+ };
23
+ var requiredGemTokenKeys = {
24
+ color: ["background", "surface", "text", "muted", "accent", "accentText", "border", "success", "warning", "danger"],
25
+ typography: ["body", "heading", "mono"],
26
+ spacing: ["xs", "sm", "md", "lg", "xl", "2xl", "3xl"],
27
+ radius: ["sm", "md", "lg", "pill"],
28
+ border: ["subtle", "strong"],
29
+ shadow: ["sm", "md"],
30
+ motion: ["duration", "easing"],
31
+ layout: ["page", "gutter", "section"],
32
+ component: ["headerHeight", "cardPadding", "buttonPaddingInline", "buttonPaddingBlock", "inputHeight"],
33
+ morphology: ["campaignDensity", "mediaRhythm", "proofIntensity", "commerceEmphasis", "policyTone"]
34
+ };
35
+ var GEM_BAND_SCHEMES = ["dark", "accent"];
36
+ var GEM_BAND_GROUND = {
37
+ dark: { ground: "text", ink: "background", surface: 8, muted: 72, status: 55 },
38
+ accent: { ground: "accent", ink: "accentText", surface: 0, muted: 100, status: 0 }
39
+ };
40
+ function gemBandSchemeExpressions(band, ground, ink, status) {
41
+ const mix = GEM_BAND_GROUND[band];
42
+ const blend = (a, b, toward) => toward <= 0 ? a : toward >= 100 ? b : `color-mix(in srgb, ${a} ${100 - toward}%, ${b})`;
43
+ return {
44
+ background: ground,
45
+ surface: blend(ground, ink, mix.surface),
46
+ text: ink,
47
+ muted: blend(ground, ink, mix.muted),
48
+ // Màu nhấn trong băng = màu chữ của băng (đọc được ở mọi bảng màu); nút thành nút đảo.
49
+ accent: ink,
50
+ accentText: ground,
51
+ border: blend(ground, ink, 22),
52
+ // Trạng thái giữ tông (băng tối: kéo về phía chữ cho sáng hơn), băng accent dùng chữ. Không bảo đảm AA.
53
+ success: blend(ink, status("success"), mix.status),
54
+ warning: blend(ink, status("warning"), mix.status),
55
+ danger: blend(ink, status("danger"), mix.status)
56
+ };
57
+ }
58
+ var GEM_BAND_SCHEME_FALLBACK_CSS = `:where(:root) { ${GEM_BAND_SCHEMES.flatMap((band) => {
59
+ const { ground, ink } = GEM_BAND_GROUND[band];
60
+ const v = (key) => `var(--gem-color-${slugify(key)})`;
61
+ const expr = gemBandSchemeExpressions(band, v(ground), v(ink), (key) => v(key));
62
+ return requiredGemTokenKeys.color.map((key) => `--gem-scheme-${band}-${slugify(key)}: ${expr[key]};`);
63
+ }).join(" ")} }`;
64
+ var GEM_PHONE_TOKEN_TARGETS = {
65
+ spacing: [...requiredGemTokenKeys.spacing, ...Object.keys(GEM_DERIVED_SPACE_STEPS)],
66
+ layout: ["section"]
67
+ };
68
+ var tokenKeyNorm = (k) => k.toLowerCase().replace(/[^a-z0-9]/g, "");
69
+ var PHONE_TARGET_BY_KEY = Object.fromEntries(
70
+ Object.entries(GEM_PHONE_TOKEN_TARGETS).map(([group, targets]) => [group, new Map(targets.map((t) => [`${tokenKeyNorm(t)}mobile`, t]))])
71
+ );
72
+ var GEM_LINE_HEIGHT_TOKEN_KEYS = ["headingLineHeight", "bodyLineHeight"];
73
+ var GEM_TYPE_RAMP_KEYS = ["base", "scale", "baseMobile"];
74
+ var optionalGemTokenKeys = {
75
+ typography: [...GEM_TYPE_RAMP_KEYS, ...GEM_LINE_HEIGHT_TOKEN_KEYS, ...GEM_BUTTON_TOKEN_KEYS.typography],
76
+ spacing: [...Object.keys(GEM_DERIVED_SPACE_STEPS), ...GEM_PHONE_TOKEN_TARGETS.spacing.map((k) => `${k}Mobile`)],
77
+ layout: GEM_PHONE_TOKEN_TARGETS.layout.map((k) => `${k}Mobile`),
78
+ radius: [...GEM_BUTTON_TOKEN_KEYS.radius]
79
+ };
80
+ var GEM_HEADING_SIZE_CSS_VARS = [
81
+ "--gem-section-heading-size: calc(var(--gem-h2) * var(--gem-typography-heading-scale, 1));",
82
+ "--gem-hero-heading-size: calc(var(--gem-h1) * var(--gem-typography-heading-scale, 1));",
83
+ "--gem-section-heading-size-mobile: min(var(--gem-section-heading-size), var(--gem-heading-phone-cap, clamp(1.75rem, 9vw, 2.75rem)));",
84
+ "--gem-hero-heading-size-mobile: min(var(--gem-hero-heading-size), var(--gem-heading-phone-cap, clamp(1.75rem, 9vw, 2.75rem)));"
85
+ ];
86
+ function slugify(value) {
87
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
88
+ }
89
+
90
+ // ../../packages/gem-framework/src/editions.ts
91
+ var GEM_EDITION_SUPPORT_MONTHS = 6;
92
+ var EDITION_2_CSS = [
93
+ `:where(&) { ${GEM_HEADING_SIZE_CSS_VARS.filter((decl) => !decl.startsWith("--gem-section-heading-size-mobile") && !decl.startsWith("--gem-hero-heading-size-mobile")).join(" ")} }`
94
+ ];
95
+ var GEM_FRAMEWORK_EDITIONS = [
96
+ { edition: 1, since: "0.1.0", released: "2026-07-04", changes: [], css: [] },
97
+ {
98
+ edition: 2,
99
+ since: "0.12.0",
100
+ released: "2026-10-10",
101
+ changes: ["Section headings and hero titles follow your type scale on tablets and computers. Phones keep their current size."],
102
+ css: EDITION_2_CSS
103
+ }
104
+ ];
105
+ var LATEST_GEM_FRAMEWORK_EDITION = GEM_FRAMEWORK_EDITIONS[GEM_FRAMEWORK_EDITIONS.length - 1].edition;
106
+ function gemFrameworkEditionOf(value) {
107
+ const n = typeof value === "string" && /^\d+$/.test(value.trim()) ? Number(value.trim()) : value;
108
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1) return 1;
109
+ return Math.min(n, LATEST_GEM_FRAMEWORK_EDITION);
110
+ }
111
+ function gemFrameworkEditionSupportedUntil(edition) {
112
+ const next = GEM_FRAMEWORK_EDITIONS.find((e) => e.edition === gemFrameworkEditionOf(edition) + 1);
113
+ return next ? addEditionSupportMonths(next.released) : null;
114
+ }
115
+ function addEditionSupportMonths(released) {
116
+ const [y, m, d] = released.split("-").map(Number);
117
+ const month = m - 1 + GEM_EDITION_SUPPORT_MONTHS;
118
+ const lastDay = new Date(Date.UTC(y, month + 1, 0)).getUTCDate();
119
+ return new Date(Date.UTC(y, month, Math.min(d, lastDay))).toISOString().slice(0, 10);
120
+ }
121
+ function gemFrameworkEditionScope(edition) {
122
+ return GEM_FRAMEWORK_EDITIONS.filter((e) => e.edition >= edition).map((e) => `[data-gem-edition='${e.edition}']`).join(", ");
123
+ }
124
+ function gemFrameworkEditionCss() {
125
+ return GEM_FRAMEWORK_EDITIONS.flatMap((e) => e.css.map((rule) => rule.replaceAll("&", gemFrameworkEditionScope(e.edition)))).join("\n");
126
+ }
127
+ export {
128
+ GEM_EDITION_SUPPORT_MONTHS,
129
+ GEM_FRAMEWORK_EDITIONS,
130
+ LATEST_GEM_FRAMEWORK_EDITION,
131
+ addEditionSupportMonths,
132
+ gemFrameworkEditionCss,
133
+ gemFrameworkEditionOf,
134
+ gemFrameworkEditionScope,
135
+ gemFrameworkEditionSupportedUntil
136
+ };
@@ -7,7 +7,7 @@
7
7
  --gem-color-accent-text: #ffffff;
8
8
  --gem-color-border: #d2d5d8;
9
9
  --gem-color-success: #008060;
10
- --gem-color-warning: #b7791f;
10
+ --gem-color-warning: #9a661a;
11
11
  --gem-color-danger: #d72c0d;
12
12
  --gem-font-body: Inter, system-ui, sans-serif;
13
13
  --gem-font-heading: Inter, system-ui, sans-serif;
@@ -64,6 +64,10 @@
64
64
  --gem-tap-min: 44px;
65
65
  --gem-focus-offset: 3px;
66
66
  }
67
+ :where(:root [data-gem-section-background='dark'], :root [data-gem-section-background='accent']) {
68
+ --gem-border-subtle: calc(1px * var(--gem-style-border, 1)) solid var(--gem-color-border);
69
+ --gem-border-strong: calc(1px * var(--gem-style-border, 1)) solid color-mix(in srgb, var(--gem-color-text) 28%, transparent);
70
+ }
67
71
 
68
72
  *, *::before, *::after {
69
73
  box-sizing: border-box;
@@ -1481,6 +1485,8 @@ a {
1481
1485
  .gem-collection-filter-toolbar__active-filter, .gem-collection-filter-toolbar__clear { min-height: 2.25rem; align-items: center; }
1482
1486
  }
1483
1487
 
1488
+ :where([data-gem-edition='2']) { --gem-section-heading-size: calc(var(--gem-h2) * var(--gem-typography-heading-scale, 1)); --gem-hero-heading-size: calc(var(--gem-h1) * var(--gem-typography-heading-scale, 1)); }
1489
+
1484
1490
  .gem-popup-shell { padding: 0; }
1485
1491
  .gem-popup-shell__dialog {
1486
1492
  padding: 0; border: 0; margin: auto; color: var(--gem-color-text); background: var(--gem-color-background);
@@ -2184,8 +2190,9 @@ sticky-header.header-wrapper {
2184
2190
  :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-grid-columns='4'] .gem-row:is(.gem-section__grid, [class*='__grid']):not(.gem-row .gem-row) { --gem-row-cols: 4; --gem-row-cols-md: 4; --gem-row-tracks: initial; --gem-row-tracks-md: initial; }
2185
2191
  :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='default'] { --gem-section-bg: var(--gem-color-background); background: transparent; }
2186
2192
  :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='surface'] { --gem-section-bg: var(--gem-color-surface); background: var(--gem-section-bg); padding-block: var(--gem-space-xl); }
2187
- :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='accent'] { --gem-section-bg: var(--gem-color-accent); background: var(--gem-section-bg); color: var(--gem-color-accent-text); --gem-band-bg: var(--gem-color-accent); --gem-focus-ring: 2px solid var(--gem-color-accent-text); padding-block: var(--gem-space-xl); }
2188
- :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='dark'] { --gem-section-bg: var(--gem-color-text); background: var(--gem-section-bg); color: var(--gem-color-background); --gem-band-bg: var(--gem-color-text); --gem-focus-ring: 2px solid var(--gem-color-background); padding-block: var(--gem-space-xl); }
2193
+ :where(:root) { --gem-scheme-dark-background: var(--gem-color-text); --gem-scheme-dark-surface: color-mix(in srgb, var(--gem-color-text) 92%, var(--gem-color-background)); --gem-scheme-dark-text: var(--gem-color-background); --gem-scheme-dark-muted: color-mix(in srgb, var(--gem-color-text) 28%, var(--gem-color-background)); --gem-scheme-dark-accent: var(--gem-color-background); --gem-scheme-dark-accent-text: var(--gem-color-text); --gem-scheme-dark-border: color-mix(in srgb, var(--gem-color-text) 78%, var(--gem-color-background)); --gem-scheme-dark-success: color-mix(in srgb, var(--gem-color-background) 45%, var(--gem-color-success)); --gem-scheme-dark-warning: color-mix(in srgb, var(--gem-color-background) 45%, var(--gem-color-warning)); --gem-scheme-dark-danger: color-mix(in srgb, var(--gem-color-background) 45%, var(--gem-color-danger)); --gem-scheme-accent-background: var(--gem-color-accent); --gem-scheme-accent-surface: var(--gem-color-accent); --gem-scheme-accent-text: var(--gem-color-accent-text); --gem-scheme-accent-muted: var(--gem-color-accent-text); --gem-scheme-accent-accent: var(--gem-color-accent-text); --gem-scheme-accent-accent-text: var(--gem-color-accent); --gem-scheme-accent-border: color-mix(in srgb, var(--gem-color-accent) 78%, var(--gem-color-accent-text)); --gem-scheme-accent-success: var(--gem-color-accent-text); --gem-scheme-accent-warning: var(--gem-color-accent-text); --gem-scheme-accent-danger: var(--gem-color-accent-text); }
2194
+ :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='dark'] { --gem-color-background: var(--gem-scheme-dark-background); --gem-color-surface: var(--gem-scheme-dark-surface); --gem-color-text: var(--gem-scheme-dark-text); --gem-color-muted: var(--gem-scheme-dark-muted); --gem-color-accent: var(--gem-scheme-dark-accent); --gem-color-accent-text: var(--gem-scheme-dark-accent-text); --gem-color-border: var(--gem-scheme-dark-border); --gem-color-success: var(--gem-scheme-dark-success); --gem-color-warning: var(--gem-scheme-dark-warning); --gem-color-danger: var(--gem-scheme-dark-danger); --gem-section-bg: var(--gem-color-background); background: var(--gem-section-bg); color: var(--gem-color-text); --gem-band-bg: var(--gem-color-background); --gem-focus-ring: 2px solid var(--gem-color-text); padding-block: var(--gem-space-xl); }
2195
+ :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='accent'] { --gem-color-background: var(--gem-scheme-accent-background); --gem-color-surface: var(--gem-scheme-accent-surface); --gem-color-text: var(--gem-scheme-accent-text); --gem-color-muted: var(--gem-scheme-accent-muted); --gem-color-accent: var(--gem-scheme-accent-accent); --gem-color-accent-text: var(--gem-scheme-accent-accent-text); --gem-color-border: var(--gem-scheme-accent-border); --gem-color-success: var(--gem-scheme-accent-success); --gem-color-warning: var(--gem-scheme-accent-warning); --gem-color-danger: var(--gem-scheme-accent-danger); --gem-section-bg: var(--gem-color-background); background: var(--gem-section-bg); color: var(--gem-color-text); --gem-band-bg: var(--gem-color-background); --gem-focus-ring: 2px solid var(--gem-color-text); padding-block: var(--gem-space-xl); }
2189
2196
  :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='light'] { --gem-section-bg: var(--gem-color-background); background: var(--gem-section-bg); color: var(--gem-color-text); padding-block: var(--gem-space-xl); }
2190
2197
  :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='accent'] :is(.gem-section__heading, .gem-section__body, .gem-section__eyebrow), :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='dark'] :is(.gem-section__heading, .gem-section__body, .gem-section__eyebrow) { color: inherit; }
2191
2198
  :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-price-color='accent'] :is(.gem-price, .gem-price__regular, [class*='__price']) { color: var(--gem-color-accent); }
package/dist/index.js CHANGED
@@ -190,8 +190,78 @@ function gemPlanShapeKnobs(plan) {
190
190
  };
191
191
  }
192
192
 
193
+ // ../../packages/gem-framework/src/contrast.ts
194
+ var HEX_COLOR_PATTERN = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
195
+ var MIN_TEXT_CONTRAST = 4.5;
196
+ function normalizeHex(value) {
197
+ if (typeof value !== "string") return void 0;
198
+ const trimmed = value.trim().toLowerCase();
199
+ if (!HEX_COLOR_PATTERN.test(trimmed)) return void 0;
200
+ if (trimmed.length === 4) {
201
+ return `#${trimmed[1]}${trimmed[1]}${trimmed[2]}${trimmed[2]}${trimmed[3]}${trimmed[3]}`;
202
+ }
203
+ return trimmed;
204
+ }
205
+ function hexToRgb(hex) {
206
+ const normalized = normalizeHex(hex) ?? "#000000";
207
+ return {
208
+ r: parseInt(normalized.slice(1, 3), 16),
209
+ g: parseInt(normalized.slice(3, 5), 16),
210
+ b: parseInt(normalized.slice(5, 7), 16)
211
+ };
212
+ }
213
+ function hexRelativeLuminance(hex) {
214
+ const { r, g, b } = hexToRgb(hex);
215
+ const channel = (c) => {
216
+ const s = c / 255;
217
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
218
+ };
219
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
220
+ }
221
+ function hexContrastRatio(a, b) {
222
+ const la = hexRelativeLuminance(a);
223
+ const lb = hexRelativeLuminance(b);
224
+ const lighter = Math.max(la, lb);
225
+ const darker = Math.min(la, lb);
226
+ return (lighter + 0.05) / (darker + 0.05);
227
+ }
228
+ function mixHex(from, to, t) {
229
+ const [a, b] = [hexToRgb(from), hexToRgb(to)];
230
+ const channel = (x, y) => Math.round(x * (1 - t) + y * t).toString(16).padStart(2, "0");
231
+ return `#${channel(a.r, b.r)}${channel(a.g, b.g)}${channel(a.b, b.b)}`;
232
+ }
233
+ function aaTextColor(color, backgrounds, min = MIN_TEXT_CONTRAST) {
234
+ const from = normalizeHex(color);
235
+ const bgs = backgrounds.map(normalizeHex).filter((bg) => Boolean(bg));
236
+ if (!from || !bgs.length) return color;
237
+ const worst = (c) => Math.min(...bgs.map((bg) => hexContrastRatio(c, bg)));
238
+ let best = { color, score: worst(from) };
239
+ if (best.score >= min) return color;
240
+ let pass;
241
+ for (const toward of ["#000000", "#ffffff"]) {
242
+ for (let step = 1; step <= 50; step++) {
243
+ const candidate = mixHex(from, toward, step / 50);
244
+ const score = worst(candidate);
245
+ if (score >= min) {
246
+ if (!pass || step < pass.step) pass = { color: candidate, step };
247
+ break;
248
+ }
249
+ if (score > best.score) best = { color: candidate, score };
250
+ }
251
+ }
252
+ return pass?.color ?? best.color;
253
+ }
254
+ function aaLabelColor(preferred, fill, fallbacks = []) {
255
+ const ground = normalizeHex(fill);
256
+ if (!ground) return preferred;
257
+ const candidates = [preferred, ...fallbacks, "#ffffff", "#111111", "#000000"].filter((c) => normalizeHex(c));
258
+ const pass = candidates.find((c) => hexContrastRatio(c, ground) >= MIN_TEXT_CONTRAST);
259
+ if (pass) return pass;
260
+ return candidates.reduce((a, b) => hexContrastRatio(b, ground) > hexContrastRatio(a, ground) ? b : a, preferred);
261
+ }
262
+
193
263
  // ../../packages/gem-framework/src/tokens.ts
194
- var gemFrameworkVersion = "0.10.0";
264
+ var gemFrameworkVersion = "0.12.0";
195
265
  var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
196
266
  var GEM_TAP_MIN_CSS_VAR = "--gem-tap-min: 44px;";
197
267
  var GEM_FOCUS_OFFSET_CSS_VAR = "--gem-focus-offset: 3px;";
@@ -304,6 +374,77 @@ var requiredGemTokenKeys = {
304
374
  component: ["headerHeight", "cardPadding", "buttonPaddingInline", "buttonPaddingBlock", "inputHeight"],
305
375
  morphology: ["campaignDensity", "mediaRhythm", "proofIntensity", "commerceEmphasis", "policyTone"]
306
376
  };
377
+ var GEM_BAND_SCHEMES = ["dark", "accent"];
378
+ var GEM_BAND_GROUND = {
379
+ dark: { ground: "text", ink: "background", surface: 8, muted: 72, status: 55 },
380
+ accent: { ground: "accent", ink: "accentText", surface: 0, muted: 100, status: 0 }
381
+ };
382
+ function gemBandSchemeExpressions(band, ground, ink, status) {
383
+ const mix = GEM_BAND_GROUND[band];
384
+ const blend = (a, b, toward) => toward <= 0 ? a : toward >= 100 ? b : `color-mix(in srgb, ${a} ${100 - toward}%, ${b})`;
385
+ return {
386
+ background: ground,
387
+ surface: blend(ground, ink, mix.surface),
388
+ text: ink,
389
+ muted: blend(ground, ink, mix.muted),
390
+ // Màu nhấn trong băng = màu chữ của băng (đọc được ở mọi bảng màu); nút thành nút đảo.
391
+ accent: ink,
392
+ accentText: ground,
393
+ border: blend(ground, ink, 22),
394
+ // Trạng thái giữ tông (băng tối: kéo về phía chữ cho sáng hơn), băng accent dùng chữ. Không bảo đảm AA.
395
+ success: blend(ink, status("success"), mix.status),
396
+ warning: blend(ink, status("warning"), mix.status),
397
+ danger: blend(ink, status("danger"), mix.status)
398
+ };
399
+ }
400
+ function gemBandScheme(c, band) {
401
+ const { ground, ink } = GEM_BAND_GROUND[band];
402
+ const g = normalizeHex(c[ground]);
403
+ const i = normalizeHex(c[ink]);
404
+ if (!g || !i) return gemBandSchemeExpressions(band, c[ground], c[ink], (key) => c[key]);
405
+ const toward = mixHex(g, i, 0.08);
406
+ const away = mixHex(g, hexRelativeLuminance(i) > hexRelativeLuminance(g) ? "#000000" : "#ffffff", 0.08);
407
+ const surface = hexContrastRatio(i, toward) >= MIN_TEXT_CONTRAST ? toward : away;
408
+ const grounds = [g, surface];
409
+ const text = aaTextColor(i, grounds);
410
+ const accent = aaTextColor(c.accent, grounds);
411
+ return {
412
+ background: g,
413
+ surface,
414
+ text,
415
+ muted: aaTextColor(mixHex(i, g, 0.3), grounds),
416
+ accent,
417
+ accentText: aaLabelColor(c.accentText, accent, [g, text]),
418
+ border: mixHex(g, i, 0.22),
419
+ success: aaTextColor(c.success, grounds),
420
+ warning: aaTextColor(c.warning, grounds),
421
+ danger: aaTextColor(c.danger, grounds)
422
+ };
423
+ }
424
+ function gemBandSchemeColors(color = {}) {
425
+ const c = Object.fromEntries(
426
+ requiredGemTokenKeys.color.map((key) => {
427
+ const value = color?.[key];
428
+ return [key, typeof value === "string" && value.trim() ? stripImportant(value) : defaultGemTokens.color[key]];
429
+ })
430
+ );
431
+ return { dark: gemBandScheme(c, "dark"), accent: gemBandScheme(c, "accent") };
432
+ }
433
+ function gemBandSchemeCssVars(color = {}) {
434
+ const schemes = gemBandSchemeColors(color);
435
+ return GEM_BAND_SCHEMES.flatMap(
436
+ (band) => requiredGemTokenKeys.color.map((key) => `--gem-scheme-${band}-${slugify(key)}: ${schemes[band][key]};`)
437
+ );
438
+ }
439
+ var GEM_BAND_SCHEME_FALLBACK_CSS = `:where(:root) { ${GEM_BAND_SCHEMES.flatMap((band) => {
440
+ const { ground, ink } = GEM_BAND_GROUND[band];
441
+ const v = (key) => `var(--gem-color-${slugify(key)})`;
442
+ const expr = gemBandSchemeExpressions(band, v(ground), v(ink), (key) => v(key));
443
+ return requiredGemTokenKeys.color.map((key) => `--gem-scheme-${band}-${slugify(key)}: ${expr[key]};`);
444
+ }).join(" ")} }`;
445
+ function gemBandSchemeDeclarations(band) {
446
+ return requiredGemTokenKeys.color.map((key) => `--gem-color-${slugify(key)}: var(--gem-scheme-${band}-${slugify(key)});`).join(" ");
447
+ }
307
448
  var GEM_PHONE_TOKEN_TARGETS = {
308
449
  spacing: [...requiredGemTokenKeys.spacing, ...Object.keys(GEM_DERIVED_SPACE_STEPS)],
309
450
  layout: ["section"]
@@ -363,7 +504,10 @@ var defaultGemTokens = {
363
504
  accentText: "#ffffff",
364
505
  border: "#d2d5d8",
365
506
  success: "#008060",
366
- warning: "#b7791f",
507
+ // AA as TEXT on both defaults (gem-framework 0.10.1): #b7791f measured 3.36:1 on the background and 3.64:1 on white, and
508
+ // GemFactory sections set `color: var(--gem-color-warning)` on words; white text on a warning badge was 3.64:1 too.
509
+ // #9a661a keeps the hue at 4.52:1 / 4.90:1. default-colours-meet-aa.test.ts holds every foreground default to 4.5:1.
510
+ warning: "#9a661a",
367
511
  danger: "#d72c0d"
368
512
  },
369
513
  typography: {
@@ -470,12 +614,14 @@ function validateGemTokens(tokens) {
470
614
  issues
471
615
  };
472
616
  }
473
- function createGemTokenCss(tokens, selector = ":root") {
617
+ function createGemTokenCss(tokens, selector = ":root", options = {}) {
474
618
  const button = readButtonTokens(tokens);
475
619
  const phone = readPhoneTokens(tokens);
476
620
  const spacing = desktopEntries("spacing", tokens.spacing);
477
621
  const declarations = [
478
622
  ...toCssVars("color", tokens.color),
623
+ // Bộ màu của băng dark / accent (gemBandSchemeColors): luật băng trỏ --gem-color-* vào đây.
624
+ ...options.bandSchemes === false ? [] : gemBandSchemeCssVars(tokens.color),
479
625
  ...toCssVars("font", Object.fromEntries(Object.entries(button.groups.typography ?? tokens.typography ?? {}).filter(([k, v]) => !isRampKey(k) && !badLineHeight(k, v)))),
480
626
  // Space ramp is scaled by the global density knob --gem-style-space (default 1),
481
627
  // so a style pack can make the WHOLE storefront airier or denser without touching
@@ -518,10 +664,13 @@ function createGemTokenCss(tokens, selector = ":root") {
518
664
  const formBlock = gemFormCssBlock(tokens.form);
519
665
  const buttonCase = gemButtonCaseCss(tokens, selector);
520
666
  const mobile = gemMobileTokenCssVars(tokens, phone);
667
+ const bandBorders = declarations.filter((line) => line.startsWith("--gem-border-") && line.includes("var(--gem-color-"));
668
+ const bandScope = splitSelectorList(selector).flatMap((sel) => GEM_BAND_SCHEMES.map((band) => `${sel} [data-gem-section-background='${band}']`)).join(", ");
521
669
  return [
522
670
  `${selector} {`,
523
671
  ...declarations.map((line) => ` ${line}`),
524
672
  "}",
673
+ ...bandBorders.length ? [`:where(${bandScope}) {`, ...bandBorders.map((line) => ` ${line}`), "}"] : [],
525
674
  ...mobile.length ? [`@media (max-width: ${GEM_TYPE_MOBILE_MAX}) {`, ` ${selector} {`, ...mobile.map((line) => ` ${line}`), " }", "}"] : [],
526
675
  ...formBlock ? [formBlock] : [],
527
676
  ...buttonCase ? [buttonCase] : []
@@ -607,6 +756,44 @@ function tokenIssue(severity, code, message, path) {
607
756
  return { severity, code, message, path };
608
757
  }
609
758
 
759
+ // ../../packages/gem-framework/src/editions.ts
760
+ var GEM_EDITION_SUPPORT_MONTHS = 6;
761
+ var EDITION_2_CSS = [
762
+ `:where(&) { ${GEM_HEADING_SIZE_CSS_VARS.filter((decl) => !decl.startsWith("--gem-section-heading-size-mobile") && !decl.startsWith("--gem-hero-heading-size-mobile")).join(" ")} }`
763
+ ];
764
+ var GEM_FRAMEWORK_EDITIONS = [
765
+ { edition: 1, since: "0.1.0", released: "2026-07-04", changes: [], css: [] },
766
+ {
767
+ edition: 2,
768
+ since: "0.12.0",
769
+ released: "2026-10-10",
770
+ changes: ["Section headings and hero titles follow your type scale on tablets and computers. Phones keep their current size."],
771
+ css: EDITION_2_CSS
772
+ }
773
+ ];
774
+ var LATEST_GEM_FRAMEWORK_EDITION = GEM_FRAMEWORK_EDITIONS[GEM_FRAMEWORK_EDITIONS.length - 1].edition;
775
+ function gemFrameworkEditionOf(value) {
776
+ const n = typeof value === "string" && /^\d+$/.test(value.trim()) ? Number(value.trim()) : value;
777
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1) return 1;
778
+ return Math.min(n, LATEST_GEM_FRAMEWORK_EDITION);
779
+ }
780
+ function gemFrameworkEditionSupportedUntil(edition) {
781
+ const next = GEM_FRAMEWORK_EDITIONS.find((e) => e.edition === gemFrameworkEditionOf(edition) + 1);
782
+ return next ? addEditionSupportMonths(next.released) : null;
783
+ }
784
+ function addEditionSupportMonths(released) {
785
+ const [y, m, d] = released.split("-").map(Number);
786
+ const month = m - 1 + GEM_EDITION_SUPPORT_MONTHS;
787
+ const lastDay = new Date(Date.UTC(y, month + 1, 0)).getUTCDate();
788
+ return new Date(Date.UTC(y, month, Math.min(d, lastDay))).toISOString().slice(0, 10);
789
+ }
790
+ function gemFrameworkEditionScope(edition) {
791
+ return GEM_FRAMEWORK_EDITIONS.filter((e) => e.edition >= edition).map((e) => `[data-gem-edition='${e.edition}']`).join(", ");
792
+ }
793
+ function gemFrameworkEditionCss() {
794
+ return GEM_FRAMEWORK_EDITIONS.flatMap((e) => e.css.map((rule) => rule.replaceAll("&", gemFrameworkEditionScope(e.edition)))).join("\n");
795
+ }
796
+
610
797
  // ../../packages/gem-framework/src/manifest.ts
611
798
  var gemFrameworkPrimitiveGroups = [
612
799
  {
@@ -1371,7 +1558,7 @@ function createGemThemeCssParts(tokens, selector = ":root", options = {}) {
1371
1558
  const base = createGemBaseCssParts(options.profile ?? "dawn-parity");
1372
1559
  return {
1373
1560
  tokens: [
1374
- createGemTokenCss(tokens, selector),
1561
+ createGemTokenCss(tokens, selector, { bandSchemes: options.bandSchemes }),
1375
1562
  // Set the knobs the space / radius / border / type ramps read (only when they differ from the
1376
1563
  // default 1, so a standard-shaped storefront emits nothing extra).
1377
1564
  ...createGemStyleKnobCss(options, selector),
@@ -1469,7 +1656,8 @@ function createGemBaseCssParts(profile) {
1469
1656
  "}"
1470
1657
  ].join("\n");
1471
1658
  return {
1472
- primitives: [reset, "", createPrimitiveCss(), "", ""].join("\n"),
1659
+ // Edition rules close the primitives (editions.ts): custom properties on <html>, scoped :where([data-gem-edition]).
1660
+ primitives: [reset, "", createPrimitiveCss(), "", gemFrameworkEditionCss(), "", ""].join("\n"),
1473
1661
  sections: [createShellCss(), "", createSectionCss(), "", createThemeProfileCss(profile)].join("\n")
1474
1662
  };
1475
1663
  }
@@ -3114,8 +3302,14 @@ function createSectionCss() {
3114
3302
  // rather than re-deriving the swatch in every divider rule.
3115
3303
  ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='default'] { --gem-section-bg: var(--gem-color-background); background: transparent; }",
3116
3304
  ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='surface'] { --gem-section-bg: var(--gem-color-surface); background: var(--gem-section-bg); padding-block: var(--gem-space-xl); }",
3117
- ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='accent'] { --gem-section-bg: var(--gem-color-accent); background: var(--gem-section-bg); color: var(--gem-color-accent-text); --gem-band-bg: var(--gem-color-accent); --gem-focus-ring: 2px solid var(--gem-color-accent-text); padding-block: var(--gem-space-xl); }",
3118
- ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='dark'] { --gem-section-bg: var(--gem-color-text); background: var(--gem-section-bg); color: var(--gem-color-background); --gem-band-bg: var(--gem-color-text); --gem-focus-ring: 2px solid var(--gem-color-background); padding-block: var(--gem-space-xl); }",
3305
+ // accent / dark are COLOUR SCHEMES (tokens.ts gemBandSchemeColors): the band re-points every --gem-color-* to its own
3306
+ // AA set, so a card, a button or a price nested in the band reads the band's colours instead of the page's. Before,
3307
+ // only the band's own background and `color` changed: 988 / 991 lines of text under AA across 248 GemFactory sections.
3308
+ // The CSS fallback tier sits next to the rules, so a page that sets --gem-color-* by hand still gets a band in its brand.
3309
+ GEM_BAND_SCHEME_FALLBACK_CSS,
3310
+ ...GEM_BAND_SCHEMES.map(
3311
+ (band) => `:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='${band}'] { ${gemBandSchemeDeclarations(band)} --gem-section-bg: var(--gem-color-background); background: var(--gem-section-bg); color: var(--gem-color-text); --gem-band-bg: var(--gem-color-background); --gem-focus-ring: 2px solid var(--gem-color-text); padding-block: var(--gem-space-xl); }`
3312
+ ),
3119
3313
  ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='light'] { --gem-section-bg: var(--gem-color-background); background: var(--gem-section-bg); color: var(--gem-color-text); padding-block: var(--gem-space-xl); }",
3120
3314
  // On accent/dark bands, keep muted sub-copy + headings legible against the inverted ground.
3121
3315
  ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='accent'] :is(.gem-section__heading, .gem-section__body, .gem-section__eyebrow), :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='dark'] :is(.gem-section__heading, .gem-section__body, .gem-section__eyebrow) { color: inherit; }",
@@ -5885,12 +6079,13 @@ function gemFrameworkPublicCssVariables() {
5885
6079
  return frameworkPublicVariables;
5886
6080
  }
5887
6081
  var ROOT_SELECTOR = /^\s*(?::root|html|:host)(?:\[[^\]]*\]|:[a-z-]+(?:\([^()]*\))?)*\s*$/;
6082
+ var EDITION_ROOT_SELECTOR = /^\s*:where\(\s*\[data-gem-edition=(["'])\d+\1\](?:\s*,\s*\[data-gem-edition=(["'])\d+\2\])*\s*\)\s*$/;
5888
6083
  function gemPublicCssVariablesFrom(stylesheet) {
5889
6084
  const css = stylesheet.replace(/\/\*[\s\S]*?\*\//g, " ");
5890
6085
  const declaredOnRoot = /* @__PURE__ */ new Set();
5891
6086
  const declaredElsewhere = /* @__PURE__ */ new Set();
5892
6087
  for (const [, selector, body] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
5893
- const onRoot = selector.split(",").every((part) => ROOT_SELECTOR.test(part));
6088
+ const onRoot = EDITION_ROOT_SELECTOR.test(selector) || selector.split(",").every((part) => ROOT_SELECTOR.test(part));
5894
6089
  for (const [, name] of body.matchAll(/(--gem-[a-z0-9-]+)\s*:/g)) (onRoot ? declaredOnRoot : declaredElsewhere).add(name);
5895
6090
  }
5896
6091
  const mentioned = new Set([...css.matchAll(/--gem-[a-z0-9-]+/g)].map((match) => match[0]));
@@ -5991,9 +6186,11 @@ function renderGemPreviewDocument(plan, options = {}) {
5991
6186
  const title = options.title ?? `${plan.name} preview`;
5992
6187
  const formAttrs = Object.entries(gemFormAttributes(plan.tokens.form)).map(([k, v]) => ` ${k}="${escapeAttribute(v)}"`).join("");
5993
6188
  const bodyClass = ["gem-theme", `gem-theme--${slugify(profile)}`, options.bodyClassName].filter(Boolean).join(" ");
6189
+ const edition = gemFrameworkEditionOf(plan.framework_edition);
6190
+ const editionAttr = edition > 1 ? ` data-gem-edition="${edition}"` : "";
5994
6191
  return [
5995
6192
  "<!doctype html>",
5996
- `<html lang="en" data-gem-theme-profile="${escapeAttribute(profile)}">`,
6193
+ `<html lang="en" data-gem-theme-profile="${escapeAttribute(profile)}"${editionAttr}>`,
5997
6194
  "<head>",
5998
6195
  ' <meta charset="utf-8" />',
5999
6196
  ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
@@ -7049,9 +7246,12 @@ function stringSetting(value) {
7049
7246
  export {
7050
7247
  COUNTDOWN_END_RUNTIME_LINES,
7051
7248
  GEM_AMBIENT_CSS,
7249
+ GEM_BAND_SCHEMES,
7250
+ GEM_BAND_SCHEME_FALLBACK_CSS,
7052
7251
  GEM_BUTTON_TOKEN_KEYS,
7053
7252
  GEM_BUTTON_TOKEN_SHAPES,
7054
7253
  GEM_COUNTDOWN_ENDED_CSS,
7254
+ GEM_EDITION_SUPPORT_MONTHS,
7055
7255
  GEM_FIELD_JOINS,
7056
7256
  GEM_FIELD_SHAPES,
7057
7257
  GEM_FORM_CSS_VARIABLES,
@@ -7060,6 +7260,7 @@ export {
7060
7260
  GEM_FORM_SET_CSS,
7061
7261
  GEM_FORM_TOKEN_KEYS,
7062
7262
  GEM_FORM_VAR_SCOPE,
7263
+ GEM_FRAMEWORK_EDITIONS,
7063
7264
  GEM_GRID_COLUMNS_ROW_KNOB_CSS,
7064
7265
  GEM_HEADING_SIZE_CSS_VARS,
7065
7266
  GEM_LINE_HEIGHT_TOKEN_KEYS,
@@ -7069,11 +7270,15 @@ export {
7069
7270
  GEM_ROW_CSS,
7070
7271
  GEM_ROW_CSS_VARIABLES,
7071
7272
  GEM_ROW_GAP_CSS,
7273
+ LATEST_GEM_FRAMEWORK_EDITION,
7274
+ MIN_TEXT_CONTRAST,
7072
7275
  MODERN_MINERAL_FAMILY_PACK_ID,
7073
7276
  MODERN_MINERAL_HEADER_FOOTER_PRESET_ID,
7074
7277
  MODERN_MINERAL_HOMEPAGE_LINEAGE_SECTIONS,
7075
7278
  MODERN_MINERAL_HOMEPAGE_PACK_ID,
7076
7279
  MODERN_MINERAL_STYLE_PACK_ID,
7280
+ aaLabelColor,
7281
+ aaTextColor,
7077
7282
  auditGemFrameworkClassName,
7078
7283
  createGemBaseCss,
7079
7284
  createGemFrameworkManifest,
@@ -7094,11 +7299,18 @@ export {
7094
7299
  createThemeProfileCss,
7095
7300
  defaultGemTokens,
7096
7301
  defaultSelection,
7302
+ gemBandSchemeColors,
7303
+ gemBandSchemeCssVars,
7304
+ gemBandSchemeDeclarations,
7097
7305
  gemButtonCaseCss,
7098
7306
  gemFormAttributes,
7099
7307
  gemFormCssBlock,
7100
7308
  gemFormCssVars,
7101
7309
  gemFormSetsFor,
7310
+ gemFrameworkEditionCss,
7311
+ gemFrameworkEditionOf,
7312
+ gemFrameworkEditionScope,
7313
+ gemFrameworkEditionSupportedUntil,
7102
7314
  gemFrameworkPrimitiveGroups,
7103
7315
  gemFrameworkRuntimePlugins,
7104
7316
  gemFrameworkValidationRules,
@@ -7110,11 +7322,15 @@ export {
7110
7322
  gemRuntimePluginModuleKeys,
7111
7323
  gemRuntimePluginSources,
7112
7324
  getGemFrameworkValidationRule,
7325
+ hexContrastRatio,
7326
+ hexRelativeLuminance,
7113
7327
  isModernMineralFamilyLineagePlan,
7328
+ mixHex,
7114
7329
  modernMineralFamilyLineage,
7115
7330
  modernMineralHomepageLineageSections,
7116
7331
  motionCssVarAliases,
7117
7332
  normalizeGemTokens,
7333
+ normalizeHex,
7118
7334
  offerEngineRuntimeScript,
7119
7335
  optionalGemTokenKeys,
7120
7336
  planToRuntimeBootstrapConfig,
@@ -1414,3 +1414,5 @@ a {
1414
1414
  .gem-collection-filter-toolbar__active-filter, .gem-collection-filter-toolbar__clear { min-height: 2.25rem; align-items: center; }
1415
1415
  }
1416
1416
 
1417
+ :where([data-gem-edition='2']) { --gem-section-heading-size: calc(var(--gem-h2) * var(--gem-typography-heading-scale, 1)); --gem-hero-heading-size: calc(var(--gem-h1) * var(--gem-typography-heading-scale, 1)); }
1418
+
@@ -70,6 +70,76 @@ function gemFormCssVars(form) {
70
70
  return out;
71
71
  }
72
72
 
73
+ // ../../packages/gem-framework/src/contrast.ts
74
+ var HEX_COLOR_PATTERN = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
75
+ var MIN_TEXT_CONTRAST = 4.5;
76
+ function normalizeHex(value) {
77
+ if (typeof value !== "string") return void 0;
78
+ const trimmed = value.trim().toLowerCase();
79
+ if (!HEX_COLOR_PATTERN.test(trimmed)) return void 0;
80
+ if (trimmed.length === 4) {
81
+ return `#${trimmed[1]}${trimmed[1]}${trimmed[2]}${trimmed[2]}${trimmed[3]}${trimmed[3]}`;
82
+ }
83
+ return trimmed;
84
+ }
85
+ function hexToRgb(hex) {
86
+ const normalized = normalizeHex(hex) ?? "#000000";
87
+ return {
88
+ r: parseInt(normalized.slice(1, 3), 16),
89
+ g: parseInt(normalized.slice(3, 5), 16),
90
+ b: parseInt(normalized.slice(5, 7), 16)
91
+ };
92
+ }
93
+ function hexRelativeLuminance(hex) {
94
+ const { r, g, b } = hexToRgb(hex);
95
+ const channel = (c) => {
96
+ const s = c / 255;
97
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
98
+ };
99
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
100
+ }
101
+ function hexContrastRatio(a, b) {
102
+ const la = hexRelativeLuminance(a);
103
+ const lb = hexRelativeLuminance(b);
104
+ const lighter = Math.max(la, lb);
105
+ const darker = Math.min(la, lb);
106
+ return (lighter + 0.05) / (darker + 0.05);
107
+ }
108
+ function mixHex(from, to, t) {
109
+ const [a, b] = [hexToRgb(from), hexToRgb(to)];
110
+ const channel = (x, y) => Math.round(x * (1 - t) + y * t).toString(16).padStart(2, "0");
111
+ return `#${channel(a.r, b.r)}${channel(a.g, b.g)}${channel(a.b, b.b)}`;
112
+ }
113
+ function aaTextColor(color, backgrounds, min = MIN_TEXT_CONTRAST) {
114
+ const from = normalizeHex(color);
115
+ const bgs = backgrounds.map(normalizeHex).filter((bg) => Boolean(bg));
116
+ if (!from || !bgs.length) return color;
117
+ const worst = (c) => Math.min(...bgs.map((bg) => hexContrastRatio(c, bg)));
118
+ let best = { color, score: worst(from) };
119
+ if (best.score >= min) return color;
120
+ let pass;
121
+ for (const toward of ["#000000", "#ffffff"]) {
122
+ for (let step = 1; step <= 50; step++) {
123
+ const candidate = mixHex(from, toward, step / 50);
124
+ const score = worst(candidate);
125
+ if (score >= min) {
126
+ if (!pass || step < pass.step) pass = { color: candidate, step };
127
+ break;
128
+ }
129
+ if (score > best.score) best = { color: candidate, score };
130
+ }
131
+ }
132
+ return pass?.color ?? best.color;
133
+ }
134
+ function aaLabelColor(preferred, fill, fallbacks = []) {
135
+ const ground = normalizeHex(fill);
136
+ if (!ground) return preferred;
137
+ const candidates = [preferred, ...fallbacks, "#ffffff", "#111111", "#000000"].filter((c) => normalizeHex(c));
138
+ const pass = candidates.find((c) => hexContrastRatio(c, ground) >= MIN_TEXT_CONTRAST);
139
+ if (pass) return pass;
140
+ return candidates.reduce((a, b) => hexContrastRatio(b, ground) > hexContrastRatio(a, ground) ? b : a, preferred);
141
+ }
142
+
73
143
  // ../../packages/gem-framework/src/tokens.ts
74
144
  var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
75
145
  var GEM_TAP_MIN_CSS_VAR = "--gem-tap-min: 44px;";
@@ -183,6 +253,77 @@ var requiredGemTokenKeys = {
183
253
  component: ["headerHeight", "cardPadding", "buttonPaddingInline", "buttonPaddingBlock", "inputHeight"],
184
254
  morphology: ["campaignDensity", "mediaRhythm", "proofIntensity", "commerceEmphasis", "policyTone"]
185
255
  };
256
+ var GEM_BAND_SCHEMES = ["dark", "accent"];
257
+ var GEM_BAND_GROUND = {
258
+ dark: { ground: "text", ink: "background", surface: 8, muted: 72, status: 55 },
259
+ accent: { ground: "accent", ink: "accentText", surface: 0, muted: 100, status: 0 }
260
+ };
261
+ function gemBandSchemeExpressions(band, ground, ink, status) {
262
+ const mix = GEM_BAND_GROUND[band];
263
+ const blend = (a, b, toward) => toward <= 0 ? a : toward >= 100 ? b : `color-mix(in srgb, ${a} ${100 - toward}%, ${b})`;
264
+ return {
265
+ background: ground,
266
+ surface: blend(ground, ink, mix.surface),
267
+ text: ink,
268
+ muted: blend(ground, ink, mix.muted),
269
+ // Màu nhấn trong băng = màu chữ của băng (đọc được ở mọi bảng màu); nút thành nút đảo.
270
+ accent: ink,
271
+ accentText: ground,
272
+ border: blend(ground, ink, 22),
273
+ // Trạng thái giữ tông (băng tối: kéo về phía chữ cho sáng hơn), băng accent dùng chữ. Không bảo đảm AA.
274
+ success: blend(ink, status("success"), mix.status),
275
+ warning: blend(ink, status("warning"), mix.status),
276
+ danger: blend(ink, status("danger"), mix.status)
277
+ };
278
+ }
279
+ function gemBandScheme(c, band) {
280
+ const { ground, ink } = GEM_BAND_GROUND[band];
281
+ const g = normalizeHex(c[ground]);
282
+ const i = normalizeHex(c[ink]);
283
+ if (!g || !i) return gemBandSchemeExpressions(band, c[ground], c[ink], (key) => c[key]);
284
+ const toward = mixHex(g, i, 0.08);
285
+ const away = mixHex(g, hexRelativeLuminance(i) > hexRelativeLuminance(g) ? "#000000" : "#ffffff", 0.08);
286
+ const surface = hexContrastRatio(i, toward) >= MIN_TEXT_CONTRAST ? toward : away;
287
+ const grounds = [g, surface];
288
+ const text = aaTextColor(i, grounds);
289
+ const accent = aaTextColor(c.accent, grounds);
290
+ return {
291
+ background: g,
292
+ surface,
293
+ text,
294
+ muted: aaTextColor(mixHex(i, g, 0.3), grounds),
295
+ accent,
296
+ accentText: aaLabelColor(c.accentText, accent, [g, text]),
297
+ border: mixHex(g, i, 0.22),
298
+ success: aaTextColor(c.success, grounds),
299
+ warning: aaTextColor(c.warning, grounds),
300
+ danger: aaTextColor(c.danger, grounds)
301
+ };
302
+ }
303
+ function gemBandSchemeColors(color = {}) {
304
+ const c = Object.fromEntries(
305
+ requiredGemTokenKeys.color.map((key) => {
306
+ const value = color?.[key];
307
+ return [key, typeof value === "string" && value.trim() ? stripImportant(value) : defaultGemTokens.color[key]];
308
+ })
309
+ );
310
+ return { dark: gemBandScheme(c, "dark"), accent: gemBandScheme(c, "accent") };
311
+ }
312
+ function gemBandSchemeCssVars(color = {}) {
313
+ const schemes = gemBandSchemeColors(color);
314
+ return GEM_BAND_SCHEMES.flatMap(
315
+ (band) => requiredGemTokenKeys.color.map((key) => `--gem-scheme-${band}-${slugify(key)}: ${schemes[band][key]};`)
316
+ );
317
+ }
318
+ var GEM_BAND_SCHEME_FALLBACK_CSS = `:where(:root) { ${GEM_BAND_SCHEMES.flatMap((band) => {
319
+ const { ground, ink } = GEM_BAND_GROUND[band];
320
+ const v = (key) => `var(--gem-color-${slugify(key)})`;
321
+ const expr = gemBandSchemeExpressions(band, v(ground), v(ink), (key) => v(key));
322
+ return requiredGemTokenKeys.color.map((key) => `--gem-scheme-${band}-${slugify(key)}: ${expr[key]};`);
323
+ }).join(" ")} }`;
324
+ function gemBandSchemeDeclarations(band) {
325
+ return requiredGemTokenKeys.color.map((key) => `--gem-color-${slugify(key)}: var(--gem-scheme-${band}-${slugify(key)});`).join(" ");
326
+ }
186
327
  var GEM_PHONE_TOKEN_TARGETS = {
187
328
  spacing: [...requiredGemTokenKeys.spacing, ...Object.keys(GEM_DERIVED_SPACE_STEPS)],
188
329
  layout: ["section"]
@@ -242,7 +383,10 @@ var defaultGemTokens = {
242
383
  accentText: "#ffffff",
243
384
  border: "#d2d5d8",
244
385
  success: "#008060",
245
- warning: "#b7791f",
386
+ // AA as TEXT on both defaults (gem-framework 0.10.1): #b7791f measured 3.36:1 on the background and 3.64:1 on white, and
387
+ // GemFactory sections set `color: var(--gem-color-warning)` on words; white text on a warning badge was 3.64:1 too.
388
+ // #9a661a keeps the hue at 4.52:1 / 4.90:1. default-colours-meet-aa.test.ts holds every foreground default to 4.5:1.
389
+ warning: "#9a661a",
246
390
  danger: "#d72c0d"
247
391
  },
248
392
  typography: {
@@ -306,12 +450,14 @@ var defaultGemTokens = {
306
450
  policyTone: "standard"
307
451
  }
308
452
  };
309
- function createGemTokenCss(tokens, selector = ":root") {
453
+ function createGemTokenCss(tokens, selector = ":root", options = {}) {
310
454
  const button = readButtonTokens(tokens);
311
455
  const phone = readPhoneTokens(tokens);
312
456
  const spacing = desktopEntries("spacing", tokens.spacing);
313
457
  const declarations = [
314
458
  ...toCssVars("color", tokens.color),
459
+ // Bộ màu của băng dark / accent (gemBandSchemeColors): luật băng trỏ --gem-color-* vào đây.
460
+ ...options.bandSchemes === false ? [] : gemBandSchemeCssVars(tokens.color),
315
461
  ...toCssVars("font", Object.fromEntries(Object.entries(button.groups.typography ?? tokens.typography ?? {}).filter(([k, v]) => !isRampKey(k) && !badLineHeight(k, v)))),
316
462
  // Space ramp is scaled by the global density knob --gem-style-space (default 1),
317
463
  // so a style pack can make the WHOLE storefront airier or denser without touching
@@ -354,10 +500,13 @@ function createGemTokenCss(tokens, selector = ":root") {
354
500
  const formBlock = gemFormCssBlock(tokens.form);
355
501
  const buttonCase = gemButtonCaseCss(tokens, selector);
356
502
  const mobile = gemMobileTokenCssVars(tokens, phone);
503
+ const bandBorders = declarations.filter((line) => line.startsWith("--gem-border-") && line.includes("var(--gem-color-"));
504
+ const bandScope = splitSelectorList(selector).flatMap((sel) => GEM_BAND_SCHEMES.map((band) => `${sel} [data-gem-section-background='${band}']`)).join(", ");
357
505
  return [
358
506
  `${selector} {`,
359
507
  ...declarations.map((line) => ` ${line}`),
360
508
  "}",
509
+ ...bandBorders.length ? [`:where(${bandScope}) {`, ...bandBorders.map((line) => ` ${line}`), "}"] : [],
361
510
  ...mobile.length ? [`@media (max-width: ${GEM_TYPE_MOBILE_MAX}) {`, ` ${selector} {`, ...mobile.map((line) => ` ${line}`), " }", "}"] : [],
362
511
  ...formBlock ? [formBlock] : [],
363
512
  ...buttonCase ? [buttonCase] : []
@@ -440,6 +589,28 @@ function slugify(value) {
440
589
  return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
441
590
  }
442
591
 
592
+ // ../../packages/gem-framework/src/editions.ts
593
+ var EDITION_2_CSS = [
594
+ `:where(&) { ${GEM_HEADING_SIZE_CSS_VARS.filter((decl) => !decl.startsWith("--gem-section-heading-size-mobile") && !decl.startsWith("--gem-hero-heading-size-mobile")).join(" ")} }`
595
+ ];
596
+ var GEM_FRAMEWORK_EDITIONS = [
597
+ { edition: 1, since: "0.1.0", released: "2026-07-04", changes: [], css: [] },
598
+ {
599
+ edition: 2,
600
+ since: "0.12.0",
601
+ released: "2026-10-10",
602
+ changes: ["Section headings and hero titles follow your type scale on tablets and computers. Phones keep their current size."],
603
+ css: EDITION_2_CSS
604
+ }
605
+ ];
606
+ var LATEST_GEM_FRAMEWORK_EDITION = GEM_FRAMEWORK_EDITIONS[GEM_FRAMEWORK_EDITIONS.length - 1].edition;
607
+ function gemFrameworkEditionScope(edition) {
608
+ return GEM_FRAMEWORK_EDITIONS.filter((e) => e.edition >= edition).map((e) => `[data-gem-edition='${e.edition}']`).join(", ");
609
+ }
610
+ function gemFrameworkEditionCss() {
611
+ return GEM_FRAMEWORK_EDITIONS.flatMap((e) => e.css.map((rule) => rule.replaceAll("&", gemFrameworkEditionScope(e.edition)))).join("\n");
612
+ }
613
+
443
614
  // ../../packages/gem-framework/src/manifest.ts
444
615
  var gemFrameworkPrimitiveGroups = [
445
616
  {
@@ -1065,7 +1236,7 @@ function createGemThemeCssParts(tokens, selector = ":root", options = {}) {
1065
1236
  const base = createGemBaseCssParts(options.profile ?? "dawn-parity");
1066
1237
  return {
1067
1238
  tokens: [
1068
- createGemTokenCss(tokens, selector),
1239
+ createGemTokenCss(tokens, selector, { bandSchemes: options.bandSchemes }),
1069
1240
  // Set the knobs the space / radius / border / type ramps read (only when they differ from the
1070
1241
  // default 1, so a standard-shaped storefront emits nothing extra).
1071
1242
  ...createGemStyleKnobCss(options, selector),
@@ -1156,7 +1327,8 @@ function createGemBaseCssParts(profile) {
1156
1327
  "}"
1157
1328
  ].join("\n");
1158
1329
  return {
1159
- primitives: [reset, "", createPrimitiveCss(), "", ""].join("\n"),
1330
+ // Edition rules close the primitives (editions.ts): custom properties on <html>, scoped :where([data-gem-edition]).
1331
+ primitives: [reset, "", createPrimitiveCss(), "", gemFrameworkEditionCss(), "", ""].join("\n"),
1160
1332
  sections: [createShellCss(), "", createSectionCss(), "", createThemeProfileCss(profile)].join("\n")
1161
1333
  };
1162
1334
  }
@@ -2801,8 +2973,14 @@ function createSectionCss() {
2801
2973
  // rather than re-deriving the swatch in every divider rule.
2802
2974
  ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='default'] { --gem-section-bg: var(--gem-color-background); background: transparent; }",
2803
2975
  ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='surface'] { --gem-section-bg: var(--gem-color-surface); background: var(--gem-section-bg); padding-block: var(--gem-space-xl); }",
2804
- ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='accent'] { --gem-section-bg: var(--gem-color-accent); background: var(--gem-section-bg); color: var(--gem-color-accent-text); --gem-band-bg: var(--gem-color-accent); --gem-focus-ring: 2px solid var(--gem-color-accent-text); padding-block: var(--gem-space-xl); }",
2805
- ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='dark'] { --gem-section-bg: var(--gem-color-text); background: var(--gem-section-bg); color: var(--gem-color-background); --gem-band-bg: var(--gem-color-text); --gem-focus-ring: 2px solid var(--gem-color-background); padding-block: var(--gem-space-xl); }",
2976
+ // accent / dark are COLOUR SCHEMES (tokens.ts gemBandSchemeColors): the band re-points every --gem-color-* to its own
2977
+ // AA set, so a card, a button or a price nested in the band reads the band's colours instead of the page's. Before,
2978
+ // only the band's own background and `color` changed: 988 / 991 lines of text under AA across 248 GemFactory sections.
2979
+ // The CSS fallback tier sits next to the rules, so a page that sets --gem-color-* by hand still gets a band in its brand.
2980
+ GEM_BAND_SCHEME_FALLBACK_CSS,
2981
+ ...GEM_BAND_SCHEMES.map(
2982
+ (band) => `:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='${band}'] { ${gemBandSchemeDeclarations(band)} --gem-section-bg: var(--gem-color-background); background: var(--gem-section-bg); color: var(--gem-color-text); --gem-band-bg: var(--gem-color-background); --gem-focus-ring: 2px solid var(--gem-color-text); padding-block: var(--gem-space-xl); }`
2983
+ ),
2806
2984
  ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='light'] { --gem-section-bg: var(--gem-color-background); background: var(--gem-section-bg); color: var(--gem-color-text); padding-block: var(--gem-space-xl); }",
2807
2985
  // On accent/dark bands, keep muted sub-copy + headings legible against the inverted ground.
2808
2986
  ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='accent'] :is(.gem-section__heading, .gem-section__body, .gem-section__eyebrow), :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='dark'] :is(.gem-section__heading, .gem-section__body, .gem-section__eyebrow) { color: inherit; }",
@@ -4341,12 +4519,13 @@ function gemFrameworkPublicCssVariables() {
4341
4519
  return frameworkPublicVariables;
4342
4520
  }
4343
4521
  var ROOT_SELECTOR = /^\s*(?::root|html|:host)(?:\[[^\]]*\]|:[a-z-]+(?:\([^()]*\))?)*\s*$/;
4522
+ var EDITION_ROOT_SELECTOR = /^\s*:where\(\s*\[data-gem-edition=(["'])\d+\1\](?:\s*,\s*\[data-gem-edition=(["'])\d+\2\])*\s*\)\s*$/;
4344
4523
  function gemPublicCssVariablesFrom(stylesheet) {
4345
4524
  const css = stylesheet.replace(/\/\*[\s\S]*?\*\//g, " ");
4346
4525
  const declaredOnRoot = /* @__PURE__ */ new Set();
4347
4526
  const declaredElsewhere = /* @__PURE__ */ new Set();
4348
4527
  for (const [, selector, body] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
4349
- const onRoot = selector.split(",").every((part) => ROOT_SELECTOR.test(part));
4528
+ const onRoot = EDITION_ROOT_SELECTOR.test(selector) || selector.split(",").every((part) => ROOT_SELECTOR.test(part));
4350
4529
  for (const [, name] of body.matchAll(/(--gem-[a-z0-9-]+)\s*:/g)) (onRoot ? declaredOnRoot : declaredElsewhere).add(name);
4351
4530
  }
4352
4531
  const mentioned = new Set([...css.matchAll(/--gem-[a-z0-9-]+/g)].map((match) => match[0]));
package/dist/sections.css CHANGED
@@ -610,8 +610,9 @@ sticky-header.header-wrapper {
610
610
  :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-grid-columns='4'] .gem-row:is(.gem-section__grid, [class*='__grid']):not(.gem-row .gem-row) { --gem-row-cols: 4; --gem-row-cols-md: 4; --gem-row-tracks: initial; --gem-row-tracks-md: initial; }
611
611
  :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='default'] { --gem-section-bg: var(--gem-color-background); background: transparent; }
612
612
  :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='surface'] { --gem-section-bg: var(--gem-color-surface); background: var(--gem-section-bg); padding-block: var(--gem-space-xl); }
613
- :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='accent'] { --gem-section-bg: var(--gem-color-accent); background: var(--gem-section-bg); color: var(--gem-color-accent-text); --gem-band-bg: var(--gem-color-accent); --gem-focus-ring: 2px solid var(--gem-color-accent-text); padding-block: var(--gem-space-xl); }
614
- :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='dark'] { --gem-section-bg: var(--gem-color-text); background: var(--gem-section-bg); color: var(--gem-color-background); --gem-band-bg: var(--gem-color-text); --gem-focus-ring: 2px solid var(--gem-color-background); padding-block: var(--gem-space-xl); }
613
+ :where(:root) { --gem-scheme-dark-background: var(--gem-color-text); --gem-scheme-dark-surface: color-mix(in srgb, var(--gem-color-text) 92%, var(--gem-color-background)); --gem-scheme-dark-text: var(--gem-color-background); --gem-scheme-dark-muted: color-mix(in srgb, var(--gem-color-text) 28%, var(--gem-color-background)); --gem-scheme-dark-accent: var(--gem-color-background); --gem-scheme-dark-accent-text: var(--gem-color-text); --gem-scheme-dark-border: color-mix(in srgb, var(--gem-color-text) 78%, var(--gem-color-background)); --gem-scheme-dark-success: color-mix(in srgb, var(--gem-color-background) 45%, var(--gem-color-success)); --gem-scheme-dark-warning: color-mix(in srgb, var(--gem-color-background) 45%, var(--gem-color-warning)); --gem-scheme-dark-danger: color-mix(in srgb, var(--gem-color-background) 45%, var(--gem-color-danger)); --gem-scheme-accent-background: var(--gem-color-accent); --gem-scheme-accent-surface: var(--gem-color-accent); --gem-scheme-accent-text: var(--gem-color-accent-text); --gem-scheme-accent-muted: var(--gem-color-accent-text); --gem-scheme-accent-accent: var(--gem-color-accent-text); --gem-scheme-accent-accent-text: var(--gem-color-accent); --gem-scheme-accent-border: color-mix(in srgb, var(--gem-color-accent) 78%, var(--gem-color-accent-text)); --gem-scheme-accent-success: var(--gem-color-accent-text); --gem-scheme-accent-warning: var(--gem-color-accent-text); --gem-scheme-accent-danger: var(--gem-color-accent-text); }
614
+ :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='dark'] { --gem-color-background: var(--gem-scheme-dark-background); --gem-color-surface: var(--gem-scheme-dark-surface); --gem-color-text: var(--gem-scheme-dark-text); --gem-color-muted: var(--gem-scheme-dark-muted); --gem-color-accent: var(--gem-scheme-dark-accent); --gem-color-accent-text: var(--gem-scheme-dark-accent-text); --gem-color-border: var(--gem-scheme-dark-border); --gem-color-success: var(--gem-scheme-dark-success); --gem-color-warning: var(--gem-scheme-dark-warning); --gem-color-danger: var(--gem-scheme-dark-danger); --gem-section-bg: var(--gem-color-background); background: var(--gem-section-bg); color: var(--gem-color-text); --gem-band-bg: var(--gem-color-background); --gem-focus-ring: 2px solid var(--gem-color-text); padding-block: var(--gem-space-xl); }
615
+ :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='accent'] { --gem-color-background: var(--gem-scheme-accent-background); --gem-color-surface: var(--gem-scheme-accent-surface); --gem-color-text: var(--gem-scheme-accent-text); --gem-color-muted: var(--gem-scheme-accent-muted); --gem-color-accent: var(--gem-scheme-accent-accent); --gem-color-accent-text: var(--gem-scheme-accent-accent-text); --gem-color-border: var(--gem-scheme-accent-border); --gem-color-success: var(--gem-scheme-accent-success); --gem-color-warning: var(--gem-scheme-accent-warning); --gem-color-danger: var(--gem-scheme-accent-danger); --gem-section-bg: var(--gem-color-background); background: var(--gem-section-bg); color: var(--gem-color-text); --gem-band-bg: var(--gem-color-background); --gem-focus-ring: 2px solid var(--gem-color-text); padding-block: var(--gem-space-xl); }
615
616
  :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='light'] { --gem-section-bg: var(--gem-color-background); background: var(--gem-section-bg); color: var(--gem-color-text); padding-block: var(--gem-space-xl); }
616
617
  :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='accent'] :is(.gem-section__heading, .gem-section__body, .gem-section__eyebrow), :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='dark'] :is(.gem-section__heading, .gem-section__body, .gem-section__eyebrow) { color: inherit; }
617
618
  :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-price-color='accent'] :is(.gem-price, .gem-price__regular, [class*='__price']) { color: var(--gem-color-accent); }
package/dist/tokens.css CHANGED
@@ -7,7 +7,7 @@
7
7
  --gem-color-accent-text: #ffffff;
8
8
  --gem-color-border: #d2d5d8;
9
9
  --gem-color-success: #008060;
10
- --gem-color-warning: #b7791f;
10
+ --gem-color-warning: #9a661a;
11
11
  --gem-color-danger: #d72c0d;
12
12
  --gem-font-body: Inter, system-ui, sans-serif;
13
13
  --gem-font-heading: Inter, system-ui, sans-serif;
@@ -64,4 +64,8 @@
64
64
  --gem-tap-min: 44px;
65
65
  --gem-focus-offset: 3px;
66
66
  }
67
+ :where(:root [data-gem-section-background='dark'], :root [data-gem-section-background='accent']) {
68
+ --gem-border-subtle: calc(1px * var(--gem-style-border, 1)) solid var(--gem-color-border);
69
+ --gem-border-strong: calc(1px * var(--gem-style-border, 1)) solid color-mix(in srgb, var(--gem-color-text) 28%, transparent);
70
+ }
67
71
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemboss/gem-framework",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",
@@ -9,6 +9,10 @@
9
9
  "./sections": "./dist/sections.js",
10
10
  "./section-gate": "./dist/section-gate.js",
11
11
  "./settings-schema": "./dist/settings-schema.js",
12
+ "./editions": {
13
+ "types": "./dist/editions.d.ts",
14
+ "default": "./dist/editions.js"
15
+ },
12
16
  "./gem-framework.css": "./dist/gem-framework.css",
13
17
  "./tokens.css": "./dist/tokens.css",
14
18
  "./primitives.css": "./dist/primitives.css",