@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 +34 -0
- package/dist/SOURCE.json +3 -3
- package/dist/editions.d.ts +55 -0
- package/dist/editions.js +136 -0
- package/dist/gem-framework.css +9 -2
- package/dist/index.js +221 -8
- package/dist/primitives.css +2 -0
- package/dist/section-gate.js +182 -6
- package/dist/sections.css +3 -2
- package/dist/tokens.css +4 -0
- package/package.json +5 -1
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.
|
|
4
|
-
"gemtheme_sha": "
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"gemtheme_sha": "19bae499182a6b3479f331ae4c4647458d3625c5",
|
|
5
5
|
"gemtheme_dirty": false,
|
|
6
|
-
"framework_version": "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;
|
package/dist/editions.js
ADDED
|
@@ -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
|
+
};
|
package/dist/gem-framework.css
CHANGED
|
@@ -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
|
-
:
|
|
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-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
3121
|
-
|
|
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,
|
package/dist/primitives.css
CHANGED
|
@@ -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
|
+
|
package/dist/section-gate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
2808
|
-
|
|
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
|
-
:
|
|
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-
|
|
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.
|
|
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",
|