@gemboss/gem-framework 0.10.1 → 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
@@ -93,6 +93,40 @@ 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.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).
96
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à
97
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
98
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
package/dist/SOURCE.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "package": "@gemboss/gem-framework",
3
- "version": "0.10.1",
4
- "gemtheme_sha": "cc172eb630a30b0bacf02648deaa097c9d149883",
3
+ "version": "0.12.0",
4
+ "gemtheme_sha": "19bae499182a6b3479f331ae4c4647458d3625c5",
5
5
  "gemtheme_dirty": false,
6
- "framework_version": "0.10.1"
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
+ };
@@ -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.1";
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"]
@@ -473,12 +614,14 @@ function validateGemTokens(tokens) {
473
614
  issues
474
615
  };
475
616
  }
476
- function createGemTokenCss(tokens, selector = ":root") {
617
+ function createGemTokenCss(tokens, selector = ":root", options = {}) {
477
618
  const button = readButtonTokens(tokens);
478
619
  const phone = readPhoneTokens(tokens);
479
620
  const spacing = desktopEntries("spacing", tokens.spacing);
480
621
  const declarations = [
481
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),
482
625
  ...toCssVars("font", Object.fromEntries(Object.entries(button.groups.typography ?? tokens.typography ?? {}).filter(([k, v]) => !isRampKey(k) && !badLineHeight(k, v)))),
483
626
  // Space ramp is scaled by the global density knob --gem-style-space (default 1),
484
627
  // so a style pack can make the WHOLE storefront airier or denser without touching
@@ -521,10 +664,13 @@ function createGemTokenCss(tokens, selector = ":root") {
521
664
  const formBlock = gemFormCssBlock(tokens.form);
522
665
  const buttonCase = gemButtonCaseCss(tokens, selector);
523
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(", ");
524
669
  return [
525
670
  `${selector} {`,
526
671
  ...declarations.map((line) => ` ${line}`),
527
672
  "}",
673
+ ...bandBorders.length ? [`:where(${bandScope}) {`, ...bandBorders.map((line) => ` ${line}`), "}"] : [],
528
674
  ...mobile.length ? [`@media (max-width: ${GEM_TYPE_MOBILE_MAX}) {`, ` ${selector} {`, ...mobile.map((line) => ` ${line}`), " }", "}"] : [],
529
675
  ...formBlock ? [formBlock] : [],
530
676
  ...buttonCase ? [buttonCase] : []
@@ -610,6 +756,44 @@ function tokenIssue(severity, code, message, path) {
610
756
  return { severity, code, message, path };
611
757
  }
612
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
+
613
797
  // ../../packages/gem-framework/src/manifest.ts
614
798
  var gemFrameworkPrimitiveGroups = [
615
799
  {
@@ -1374,7 +1558,7 @@ function createGemThemeCssParts(tokens, selector = ":root", options = {}) {
1374
1558
  const base = createGemBaseCssParts(options.profile ?? "dawn-parity");
1375
1559
  return {
1376
1560
  tokens: [
1377
- createGemTokenCss(tokens, selector),
1561
+ createGemTokenCss(tokens, selector, { bandSchemes: options.bandSchemes }),
1378
1562
  // Set the knobs the space / radius / border / type ramps read (only when they differ from the
1379
1563
  // default 1, so a standard-shaped storefront emits nothing extra).
1380
1564
  ...createGemStyleKnobCss(options, selector),
@@ -1472,7 +1656,8 @@ function createGemBaseCssParts(profile) {
1472
1656
  "}"
1473
1657
  ].join("\n");
1474
1658
  return {
1475
- 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"),
1476
1661
  sections: [createShellCss(), "", createSectionCss(), "", createThemeProfileCss(profile)].join("\n")
1477
1662
  };
1478
1663
  }
@@ -3117,8 +3302,14 @@ function createSectionCss() {
3117
3302
  // rather than re-deriving the swatch in every divider rule.
3118
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; }",
3119
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); }",
3120
- ":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); }",
3121
- ":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
+ ),
3122
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); }",
3123
3314
  // On accent/dark bands, keep muted sub-copy + headings legible against the inverted ground.
3124
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; }",
@@ -5888,12 +6079,13 @@ function gemFrameworkPublicCssVariables() {
5888
6079
  return frameworkPublicVariables;
5889
6080
  }
5890
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*$/;
5891
6083
  function gemPublicCssVariablesFrom(stylesheet) {
5892
6084
  const css = stylesheet.replace(/\/\*[\s\S]*?\*\//g, " ");
5893
6085
  const declaredOnRoot = /* @__PURE__ */ new Set();
5894
6086
  const declaredElsewhere = /* @__PURE__ */ new Set();
5895
6087
  for (const [, selector, body] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
5896
- 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));
5897
6089
  for (const [, name] of body.matchAll(/(--gem-[a-z0-9-]+)\s*:/g)) (onRoot ? declaredOnRoot : declaredElsewhere).add(name);
5898
6090
  }
5899
6091
  const mentioned = new Set([...css.matchAll(/--gem-[a-z0-9-]+/g)].map((match) => match[0]));
@@ -5994,9 +6186,11 @@ function renderGemPreviewDocument(plan, options = {}) {
5994
6186
  const title = options.title ?? `${plan.name} preview`;
5995
6187
  const formAttrs = Object.entries(gemFormAttributes(plan.tokens.form)).map(([k, v]) => ` ${k}="${escapeAttribute(v)}"`).join("");
5996
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}"` : "";
5997
6191
  return [
5998
6192
  "<!doctype html>",
5999
- `<html lang="en" data-gem-theme-profile="${escapeAttribute(profile)}">`,
6193
+ `<html lang="en" data-gem-theme-profile="${escapeAttribute(profile)}"${editionAttr}>`,
6000
6194
  "<head>",
6001
6195
  ' <meta charset="utf-8" />',
6002
6196
  ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
@@ -7052,9 +7246,12 @@ function stringSetting(value) {
7052
7246
  export {
7053
7247
  COUNTDOWN_END_RUNTIME_LINES,
7054
7248
  GEM_AMBIENT_CSS,
7249
+ GEM_BAND_SCHEMES,
7250
+ GEM_BAND_SCHEME_FALLBACK_CSS,
7055
7251
  GEM_BUTTON_TOKEN_KEYS,
7056
7252
  GEM_BUTTON_TOKEN_SHAPES,
7057
7253
  GEM_COUNTDOWN_ENDED_CSS,
7254
+ GEM_EDITION_SUPPORT_MONTHS,
7058
7255
  GEM_FIELD_JOINS,
7059
7256
  GEM_FIELD_SHAPES,
7060
7257
  GEM_FORM_CSS_VARIABLES,
@@ -7063,6 +7260,7 @@ export {
7063
7260
  GEM_FORM_SET_CSS,
7064
7261
  GEM_FORM_TOKEN_KEYS,
7065
7262
  GEM_FORM_VAR_SCOPE,
7263
+ GEM_FRAMEWORK_EDITIONS,
7066
7264
  GEM_GRID_COLUMNS_ROW_KNOB_CSS,
7067
7265
  GEM_HEADING_SIZE_CSS_VARS,
7068
7266
  GEM_LINE_HEIGHT_TOKEN_KEYS,
@@ -7072,11 +7270,15 @@ export {
7072
7270
  GEM_ROW_CSS,
7073
7271
  GEM_ROW_CSS_VARIABLES,
7074
7272
  GEM_ROW_GAP_CSS,
7273
+ LATEST_GEM_FRAMEWORK_EDITION,
7274
+ MIN_TEXT_CONTRAST,
7075
7275
  MODERN_MINERAL_FAMILY_PACK_ID,
7076
7276
  MODERN_MINERAL_HEADER_FOOTER_PRESET_ID,
7077
7277
  MODERN_MINERAL_HOMEPAGE_LINEAGE_SECTIONS,
7078
7278
  MODERN_MINERAL_HOMEPAGE_PACK_ID,
7079
7279
  MODERN_MINERAL_STYLE_PACK_ID,
7280
+ aaLabelColor,
7281
+ aaTextColor,
7080
7282
  auditGemFrameworkClassName,
7081
7283
  createGemBaseCss,
7082
7284
  createGemFrameworkManifest,
@@ -7097,11 +7299,18 @@ export {
7097
7299
  createThemeProfileCss,
7098
7300
  defaultGemTokens,
7099
7301
  defaultSelection,
7302
+ gemBandSchemeColors,
7303
+ gemBandSchemeCssVars,
7304
+ gemBandSchemeDeclarations,
7100
7305
  gemButtonCaseCss,
7101
7306
  gemFormAttributes,
7102
7307
  gemFormCssBlock,
7103
7308
  gemFormCssVars,
7104
7309
  gemFormSetsFor,
7310
+ gemFrameworkEditionCss,
7311
+ gemFrameworkEditionOf,
7312
+ gemFrameworkEditionScope,
7313
+ gemFrameworkEditionSupportedUntil,
7105
7314
  gemFrameworkPrimitiveGroups,
7106
7315
  gemFrameworkRuntimePlugins,
7107
7316
  gemFrameworkValidationRules,
@@ -7113,11 +7322,15 @@ export {
7113
7322
  gemRuntimePluginModuleKeys,
7114
7323
  gemRuntimePluginSources,
7115
7324
  getGemFrameworkValidationRule,
7325
+ hexContrastRatio,
7326
+ hexRelativeLuminance,
7116
7327
  isModernMineralFamilyLineagePlan,
7328
+ mixHex,
7117
7329
  modernMineralFamilyLineage,
7118
7330
  modernMineralHomepageLineageSections,
7119
7331
  motionCssVarAliases,
7120
7332
  normalizeGemTokens,
7333
+ normalizeHex,
7121
7334
  offerEngineRuntimeScript,
7122
7335
  optionalGemTokenKeys,
7123
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"]
@@ -309,12 +450,14 @@ var defaultGemTokens = {
309
450
  policyTone: "standard"
310
451
  }
311
452
  };
312
- function createGemTokenCss(tokens, selector = ":root") {
453
+ function createGemTokenCss(tokens, selector = ":root", options = {}) {
313
454
  const button = readButtonTokens(tokens);
314
455
  const phone = readPhoneTokens(tokens);
315
456
  const spacing = desktopEntries("spacing", tokens.spacing);
316
457
  const declarations = [
317
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),
318
461
  ...toCssVars("font", Object.fromEntries(Object.entries(button.groups.typography ?? tokens.typography ?? {}).filter(([k, v]) => !isRampKey(k) && !badLineHeight(k, v)))),
319
462
  // Space ramp is scaled by the global density knob --gem-style-space (default 1),
320
463
  // so a style pack can make the WHOLE storefront airier or denser without touching
@@ -357,10 +500,13 @@ function createGemTokenCss(tokens, selector = ":root") {
357
500
  const formBlock = gemFormCssBlock(tokens.form);
358
501
  const buttonCase = gemButtonCaseCss(tokens, selector);
359
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(", ");
360
505
  return [
361
506
  `${selector} {`,
362
507
  ...declarations.map((line) => ` ${line}`),
363
508
  "}",
509
+ ...bandBorders.length ? [`:where(${bandScope}) {`, ...bandBorders.map((line) => ` ${line}`), "}"] : [],
364
510
  ...mobile.length ? [`@media (max-width: ${GEM_TYPE_MOBILE_MAX}) {`, ` ${selector} {`, ...mobile.map((line) => ` ${line}`), " }", "}"] : [],
365
511
  ...formBlock ? [formBlock] : [],
366
512
  ...buttonCase ? [buttonCase] : []
@@ -443,6 +589,28 @@ function slugify(value) {
443
589
  return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
444
590
  }
445
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
+
446
614
  // ../../packages/gem-framework/src/manifest.ts
447
615
  var gemFrameworkPrimitiveGroups = [
448
616
  {
@@ -1068,7 +1236,7 @@ function createGemThemeCssParts(tokens, selector = ":root", options = {}) {
1068
1236
  const base = createGemBaseCssParts(options.profile ?? "dawn-parity");
1069
1237
  return {
1070
1238
  tokens: [
1071
- createGemTokenCss(tokens, selector),
1239
+ createGemTokenCss(tokens, selector, { bandSchemes: options.bandSchemes }),
1072
1240
  // Set the knobs the space / radius / border / type ramps read (only when they differ from the
1073
1241
  // default 1, so a standard-shaped storefront emits nothing extra).
1074
1242
  ...createGemStyleKnobCss(options, selector),
@@ -1159,7 +1327,8 @@ function createGemBaseCssParts(profile) {
1159
1327
  "}"
1160
1328
  ].join("\n");
1161
1329
  return {
1162
- 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"),
1163
1332
  sections: [createShellCss(), "", createSectionCss(), "", createThemeProfileCss(profile)].join("\n")
1164
1333
  };
1165
1334
  }
@@ -2804,8 +2973,14 @@ function createSectionCss() {
2804
2973
  // rather than re-deriving the swatch in every divider rule.
2805
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; }",
2806
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); }",
2807
- ":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); }",
2808
- ":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
+ ),
2809
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); }",
2810
2985
  // On accent/dark bands, keep muted sub-copy + headings legible against the inverted ground.
2811
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; }",
@@ -4344,12 +4519,13 @@ function gemFrameworkPublicCssVariables() {
4344
4519
  return frameworkPublicVariables;
4345
4520
  }
4346
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*$/;
4347
4523
  function gemPublicCssVariablesFrom(stylesheet) {
4348
4524
  const css = stylesheet.replace(/\/\*[\s\S]*?\*\//g, " ");
4349
4525
  const declaredOnRoot = /* @__PURE__ */ new Set();
4350
4526
  const declaredElsewhere = /* @__PURE__ */ new Set();
4351
4527
  for (const [, selector, body] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
4352
- 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));
4353
4529
  for (const [, name] of body.matchAll(/(--gem-[a-z0-9-]+)\s*:/g)) (onRoot ? declaredOnRoot : declaredElsewhere).add(name);
4354
4530
  }
4355
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
@@ -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.1",
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",