@gemboss/gem-framework 0.10.1 → 0.12.1
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 +40 -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 +230 -10
- package/dist/primitives.css +2 -0
- package/dist/section-gate.js +191 -8
- package/dist/sections.css +3 -2
- package/dist/tokens.css +4 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -93,6 +93,46 @@ 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.1** (10/10/2026): hai ca biên của token nút, review gemsite #839 bắt khi gemsite chép phép đọc này để phát token
|
|
97
|
+
cho store. (1) Bo trỏ một bậc bo khác (`radius.button: var(--gem-radius-pill)`) không bị nhân núm `--gem-style-radius`
|
|
98
|
+
lần hai: bậc được trỏ tới đã nhân rồi (pill 1,4 thành 1,96×). Áp chung cho mọi thang qua `toScaledCssVars`; giá trị số
|
|
99
|
+
và biến ngoài thang (`var(--brand-radius)`) vẫn nhân như cũ. (2) Khai trùng khoá (`buttonTransform` + `button_transform`)
|
|
100
|
+
giữ giá trị HỢP LỆ cuối cùng; bản cũ xoá mất biến trong khi luật in hoa vẫn trỏ tới nó. Không plan, preset hay giá trị mặc
|
|
101
|
+
định nào đang dùng hai dạng này, nên không cần edition mới.
|
|
102
|
+
- **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
|
|
103
|
+
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,
|
|
104
|
+
thiếu = 1); storefront đặt nó lên `<html>` thành `data-gem-edition`. Luật của edition N viết dưới
|
|
105
|
+
`: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
|
|
106
|
+
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
|
|
107
|
+
kể từ khi có edition mới. Khối luật edition nằm cuối `primitives.css`.
|
|
108
|
+
**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
|
|
109
|
+
`typography.base` / `scale`; điện thoại giữ cỡ cũ. Đo Chromium trên `globals.css` của gemsite (tiêu đề section / hero, px):
|
|
110
|
+
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).
|
|
111
|
+
Export mới: `GEM_FRAMEWORK_EDITIONS`, `LATEST_GEM_FRAMEWORK_EDITION`, `GEM_EDITION_SUPPORT_MONTHS`,
|
|
112
|
+
`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),
|
|
113
|
+
`gemFrameworkEditionSupportedUntil`, `gemFrameworkEditionScope`, `gemFrameworkEditionCss`; riêng sổ edition có entry
|
|
114
|
+
nhẹ `@gemboss/gem-framework/editions` (6,4 KB, kèm `editions.d.ts` do tsc sinh) để storefront đọc `framework_edition`
|
|
115
|
+
mà không nạp `index.js` (429 KB). `renderGemPreviewDocument` đặt `data-gem-edition` theo plan như storefront.
|
|
116
|
+
`themePlanSchema` (gói schemas) khai `framework_edition`, đọc dễ dãi như `gemFrameworkEditionOf` (giá trị sai bị bỏ, plan
|
|
117
|
+
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
|
|
118
|
+
vi edition là gốc (`<html>`). Store không mang `data-gem-edition` vẽ y như 0.11.0.
|
|
119
|
+
- **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ỏ
|
|
120
|
+
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ó:
|
|
121
|
+
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
|
|
122
|
+
bằng token thì đè mất nền băng. Bộ màu có hai tầng:
|
|
123
|
+
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
|
|
124
|
+
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 }`.
|
|
125
|
+
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
|
|
126
|
+
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
|
|
127
|
+
token mặc định trong `gem-framework.css` / `tokens.css` không mang tầng 1, để tầng này quyết.
|
|
128
|
+
`--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
|
|
129
|
+
(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
|
|
130
|
+
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
|
|
131
|
+
`gemBandSchemeColors`, `gemBandSchemeCssVars`, `gemBandSchemeDeclarations`, `GEM_BAND_SCHEMES`,
|
|
132
|
+
`GEM_BAND_SCHEME_FALLBACK_CSS`, và phép tính màu dời từ theme-presets: `aaTextColor`, `aaLabelColor`, `hexContrastRatio`,
|
|
133
|
+
`hexRelativeLuminance`, `mixHex`, `normalizeHex`, `MIN_TEXT_CONTRAST`. Bản chép luật băng ở nơi khác (gemsite
|
|
134
|
+
`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
|
|
135
|
+
thành rỗng (nền nút, nền thẻ trong suốt).
|
|
96
136
|
- **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
137
|
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
138
|
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.1",
|
|
4
|
+
"gemtheme_sha": "4ceacbe0c2ace7da410851fc5fe15589c1d87b77",
|
|
5
5
|
"gemtheme_dirty": false,
|
|
6
|
-
"framework_version": "0.
|
|
6
|
+
"framework_version": "0.12.1"
|
|
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.1";
|
|
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;";
|
|
@@ -256,7 +326,7 @@ function readButtonTokens(tokens) {
|
|
|
256
326
|
const key = byNorm.get(buttonKeyNorm(rawKey));
|
|
257
327
|
if (!key) continue;
|
|
258
328
|
copy ??= { ...values };
|
|
259
|
-
delete copy[rawKey];
|
|
329
|
+
if (rawKey !== key) delete copy[rawKey];
|
|
260
330
|
const v = buttonTokenValue(group, key, raw);
|
|
261
331
|
if (v === null) out.invalid.push({ path: `${group}.${rawKey}`, value: raw });
|
|
262
332
|
else {
|
|
@@ -264,6 +334,9 @@ function readButtonTokens(tokens) {
|
|
|
264
334
|
out.values[key] = v;
|
|
265
335
|
}
|
|
266
336
|
}
|
|
337
|
+
if (copy) {
|
|
338
|
+
for (const key of keys) if (!(key in out.values)) delete copy[key];
|
|
339
|
+
}
|
|
267
340
|
if (copy) out.groups[group] = copy;
|
|
268
341
|
}
|
|
269
342
|
return out;
|
|
@@ -304,6 +377,77 @@ var requiredGemTokenKeys = {
|
|
|
304
377
|
component: ["headerHeight", "cardPadding", "buttonPaddingInline", "buttonPaddingBlock", "inputHeight"],
|
|
305
378
|
morphology: ["campaignDensity", "mediaRhythm", "proofIntensity", "commerceEmphasis", "policyTone"]
|
|
306
379
|
};
|
|
380
|
+
var GEM_BAND_SCHEMES = ["dark", "accent"];
|
|
381
|
+
var GEM_BAND_GROUND = {
|
|
382
|
+
dark: { ground: "text", ink: "background", surface: 8, muted: 72, status: 55 },
|
|
383
|
+
accent: { ground: "accent", ink: "accentText", surface: 0, muted: 100, status: 0 }
|
|
384
|
+
};
|
|
385
|
+
function gemBandSchemeExpressions(band, ground, ink, status) {
|
|
386
|
+
const mix = GEM_BAND_GROUND[band];
|
|
387
|
+
const blend = (a, b, toward) => toward <= 0 ? a : toward >= 100 ? b : `color-mix(in srgb, ${a} ${100 - toward}%, ${b})`;
|
|
388
|
+
return {
|
|
389
|
+
background: ground,
|
|
390
|
+
surface: blend(ground, ink, mix.surface),
|
|
391
|
+
text: ink,
|
|
392
|
+
muted: blend(ground, ink, mix.muted),
|
|
393
|
+
// 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.
|
|
394
|
+
accent: ink,
|
|
395
|
+
accentText: ground,
|
|
396
|
+
border: blend(ground, ink, 22),
|
|
397
|
+
// 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.
|
|
398
|
+
success: blend(ink, status("success"), mix.status),
|
|
399
|
+
warning: blend(ink, status("warning"), mix.status),
|
|
400
|
+
danger: blend(ink, status("danger"), mix.status)
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function gemBandScheme(c, band) {
|
|
404
|
+
const { ground, ink } = GEM_BAND_GROUND[band];
|
|
405
|
+
const g = normalizeHex(c[ground]);
|
|
406
|
+
const i = normalizeHex(c[ink]);
|
|
407
|
+
if (!g || !i) return gemBandSchemeExpressions(band, c[ground], c[ink], (key) => c[key]);
|
|
408
|
+
const toward = mixHex(g, i, 0.08);
|
|
409
|
+
const away = mixHex(g, hexRelativeLuminance(i) > hexRelativeLuminance(g) ? "#000000" : "#ffffff", 0.08);
|
|
410
|
+
const surface = hexContrastRatio(i, toward) >= MIN_TEXT_CONTRAST ? toward : away;
|
|
411
|
+
const grounds = [g, surface];
|
|
412
|
+
const text = aaTextColor(i, grounds);
|
|
413
|
+
const accent = aaTextColor(c.accent, grounds);
|
|
414
|
+
return {
|
|
415
|
+
background: g,
|
|
416
|
+
surface,
|
|
417
|
+
text,
|
|
418
|
+
muted: aaTextColor(mixHex(i, g, 0.3), grounds),
|
|
419
|
+
accent,
|
|
420
|
+
accentText: aaLabelColor(c.accentText, accent, [g, text]),
|
|
421
|
+
border: mixHex(g, i, 0.22),
|
|
422
|
+
success: aaTextColor(c.success, grounds),
|
|
423
|
+
warning: aaTextColor(c.warning, grounds),
|
|
424
|
+
danger: aaTextColor(c.danger, grounds)
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function gemBandSchemeColors(color = {}) {
|
|
428
|
+
const c = Object.fromEntries(
|
|
429
|
+
requiredGemTokenKeys.color.map((key) => {
|
|
430
|
+
const value = color?.[key];
|
|
431
|
+
return [key, typeof value === "string" && value.trim() ? stripImportant(value) : defaultGemTokens.color[key]];
|
|
432
|
+
})
|
|
433
|
+
);
|
|
434
|
+
return { dark: gemBandScheme(c, "dark"), accent: gemBandScheme(c, "accent") };
|
|
435
|
+
}
|
|
436
|
+
function gemBandSchemeCssVars(color = {}) {
|
|
437
|
+
const schemes = gemBandSchemeColors(color);
|
|
438
|
+
return GEM_BAND_SCHEMES.flatMap(
|
|
439
|
+
(band) => requiredGemTokenKeys.color.map((key) => `--gem-scheme-${band}-${slugify(key)}: ${schemes[band][key]};`)
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
var GEM_BAND_SCHEME_FALLBACK_CSS = `:where(:root) { ${GEM_BAND_SCHEMES.flatMap((band) => {
|
|
443
|
+
const { ground, ink } = GEM_BAND_GROUND[band];
|
|
444
|
+
const v = (key) => `var(--gem-color-${slugify(key)})`;
|
|
445
|
+
const expr = gemBandSchemeExpressions(band, v(ground), v(ink), (key) => v(key));
|
|
446
|
+
return requiredGemTokenKeys.color.map((key) => `--gem-scheme-${band}-${slugify(key)}: ${expr[key]};`);
|
|
447
|
+
}).join(" ")} }`;
|
|
448
|
+
function gemBandSchemeDeclarations(band) {
|
|
449
|
+
return requiredGemTokenKeys.color.map((key) => `--gem-color-${slugify(key)}: var(--gem-scheme-${band}-${slugify(key)});`).join(" ");
|
|
450
|
+
}
|
|
307
451
|
var GEM_PHONE_TOKEN_TARGETS = {
|
|
308
452
|
spacing: [...requiredGemTokenKeys.spacing, ...Object.keys(GEM_DERIVED_SPACE_STEPS)],
|
|
309
453
|
layout: ["section"]
|
|
@@ -473,12 +617,14 @@ function validateGemTokens(tokens) {
|
|
|
473
617
|
issues
|
|
474
618
|
};
|
|
475
619
|
}
|
|
476
|
-
function createGemTokenCss(tokens, selector = ":root") {
|
|
620
|
+
function createGemTokenCss(tokens, selector = ":root", options = {}) {
|
|
477
621
|
const button = readButtonTokens(tokens);
|
|
478
622
|
const phone = readPhoneTokens(tokens);
|
|
479
623
|
const spacing = desktopEntries("spacing", tokens.spacing);
|
|
480
624
|
const declarations = [
|
|
481
625
|
...toCssVars("color", tokens.color),
|
|
626
|
+
// Bộ màu của băng dark / accent (gemBandSchemeColors): luật băng trỏ --gem-color-* vào đây.
|
|
627
|
+
...options.bandSchemes === false ? [] : gemBandSchemeCssVars(tokens.color),
|
|
482
628
|
...toCssVars("font", Object.fromEntries(Object.entries(button.groups.typography ?? tokens.typography ?? {}).filter(([k, v]) => !isRampKey(k) && !badLineHeight(k, v)))),
|
|
483
629
|
// Space ramp is scaled by the global density knob --gem-style-space (default 1),
|
|
484
630
|
// so a style pack can make the WHOLE storefront airier or denser without touching
|
|
@@ -521,10 +667,13 @@ function createGemTokenCss(tokens, selector = ":root") {
|
|
|
521
667
|
const formBlock = gemFormCssBlock(tokens.form);
|
|
522
668
|
const buttonCase = gemButtonCaseCss(tokens, selector);
|
|
523
669
|
const mobile = gemMobileTokenCssVars(tokens, phone);
|
|
670
|
+
const bandBorders = declarations.filter((line) => line.startsWith("--gem-border-") && line.includes("var(--gem-color-"));
|
|
671
|
+
const bandScope = splitSelectorList(selector).flatMap((sel) => GEM_BAND_SCHEMES.map((band) => `${sel} [data-gem-section-background='${band}']`)).join(", ");
|
|
524
672
|
return [
|
|
525
673
|
`${selector} {`,
|
|
526
674
|
...declarations.map((line) => ` ${line}`),
|
|
527
675
|
"}",
|
|
676
|
+
...bandBorders.length ? [`:where(${bandScope}) {`, ...bandBorders.map((line) => ` ${line}`), "}"] : [],
|
|
528
677
|
...mobile.length ? [`@media (max-width: ${GEM_TYPE_MOBILE_MAX}) {`, ` ${selector} {`, ...mobile.map((line) => ` ${line}`), " }", "}"] : [],
|
|
529
678
|
...formBlock ? [formBlock] : [],
|
|
530
679
|
...buttonCase ? [buttonCase] : []
|
|
@@ -593,7 +742,11 @@ function toScaledCssVars(namespace, values, knob) {
|
|
|
593
742
|
// bọc nó vào GIỮA giá trị — `calc(16px !important * var(...))` — nên custom property lưu
|
|
594
743
|
// nguyên xi, rồi mọi `padding: var(--gem-space-md)` thành invalid ở computed-value và SẬP VỀ 0
|
|
595
744
|
// trên toàn bộ .gem-* section.
|
|
596
|
-
([key, value]) =>
|
|
745
|
+
([key, value]) => {
|
|
746
|
+
const clean = stripImportant(value);
|
|
747
|
+
if (new RegExp(`^var\\(--gem-${namespace}-[a-z0-9-]+\\)$`, "i").test(clean.trim())) return `--gem-${namespace}-${slugify(key)}: ${clean.trim()};`;
|
|
748
|
+
return `--gem-${namespace}-${slugify(key)}: calc(${clean} * var(${knob}, 1));`;
|
|
749
|
+
}
|
|
597
750
|
);
|
|
598
751
|
}
|
|
599
752
|
function toBorderCssVars(namespace, values, knob) {
|
|
@@ -610,6 +763,44 @@ function tokenIssue(severity, code, message, path) {
|
|
|
610
763
|
return { severity, code, message, path };
|
|
611
764
|
}
|
|
612
765
|
|
|
766
|
+
// ../../packages/gem-framework/src/editions.ts
|
|
767
|
+
var GEM_EDITION_SUPPORT_MONTHS = 6;
|
|
768
|
+
var EDITION_2_CSS = [
|
|
769
|
+
`:where(&) { ${GEM_HEADING_SIZE_CSS_VARS.filter((decl) => !decl.startsWith("--gem-section-heading-size-mobile") && !decl.startsWith("--gem-hero-heading-size-mobile")).join(" ")} }`
|
|
770
|
+
];
|
|
771
|
+
var GEM_FRAMEWORK_EDITIONS = [
|
|
772
|
+
{ edition: 1, since: "0.1.0", released: "2026-07-04", changes: [], css: [] },
|
|
773
|
+
{
|
|
774
|
+
edition: 2,
|
|
775
|
+
since: "0.12.0",
|
|
776
|
+
released: "2026-10-10",
|
|
777
|
+
changes: ["Section headings and hero titles follow your type scale on tablets and computers. Phones keep their current size."],
|
|
778
|
+
css: EDITION_2_CSS
|
|
779
|
+
}
|
|
780
|
+
];
|
|
781
|
+
var LATEST_GEM_FRAMEWORK_EDITION = GEM_FRAMEWORK_EDITIONS[GEM_FRAMEWORK_EDITIONS.length - 1].edition;
|
|
782
|
+
function gemFrameworkEditionOf(value) {
|
|
783
|
+
const n = typeof value === "string" && /^\d+$/.test(value.trim()) ? Number(value.trim()) : value;
|
|
784
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 1) return 1;
|
|
785
|
+
return Math.min(n, LATEST_GEM_FRAMEWORK_EDITION);
|
|
786
|
+
}
|
|
787
|
+
function gemFrameworkEditionSupportedUntil(edition) {
|
|
788
|
+
const next = GEM_FRAMEWORK_EDITIONS.find((e) => e.edition === gemFrameworkEditionOf(edition) + 1);
|
|
789
|
+
return next ? addEditionSupportMonths(next.released) : null;
|
|
790
|
+
}
|
|
791
|
+
function addEditionSupportMonths(released) {
|
|
792
|
+
const [y, m, d] = released.split("-").map(Number);
|
|
793
|
+
const month = m - 1 + GEM_EDITION_SUPPORT_MONTHS;
|
|
794
|
+
const lastDay = new Date(Date.UTC(y, month + 1, 0)).getUTCDate();
|
|
795
|
+
return new Date(Date.UTC(y, month, Math.min(d, lastDay))).toISOString().slice(0, 10);
|
|
796
|
+
}
|
|
797
|
+
function gemFrameworkEditionScope(edition) {
|
|
798
|
+
return GEM_FRAMEWORK_EDITIONS.filter((e) => e.edition >= edition).map((e) => `[data-gem-edition='${e.edition}']`).join(", ");
|
|
799
|
+
}
|
|
800
|
+
function gemFrameworkEditionCss() {
|
|
801
|
+
return GEM_FRAMEWORK_EDITIONS.flatMap((e) => e.css.map((rule) => rule.replaceAll("&", gemFrameworkEditionScope(e.edition)))).join("\n");
|
|
802
|
+
}
|
|
803
|
+
|
|
613
804
|
// ../../packages/gem-framework/src/manifest.ts
|
|
614
805
|
var gemFrameworkPrimitiveGroups = [
|
|
615
806
|
{
|
|
@@ -1374,7 +1565,7 @@ function createGemThemeCssParts(tokens, selector = ":root", options = {}) {
|
|
|
1374
1565
|
const base = createGemBaseCssParts(options.profile ?? "dawn-parity");
|
|
1375
1566
|
return {
|
|
1376
1567
|
tokens: [
|
|
1377
|
-
createGemTokenCss(tokens, selector),
|
|
1568
|
+
createGemTokenCss(tokens, selector, { bandSchemes: options.bandSchemes }),
|
|
1378
1569
|
// Set the knobs the space / radius / border / type ramps read (only when they differ from the
|
|
1379
1570
|
// default 1, so a standard-shaped storefront emits nothing extra).
|
|
1380
1571
|
...createGemStyleKnobCss(options, selector),
|
|
@@ -1472,7 +1663,8 @@ function createGemBaseCssParts(profile) {
|
|
|
1472
1663
|
"}"
|
|
1473
1664
|
].join("\n");
|
|
1474
1665
|
return {
|
|
1475
|
-
|
|
1666
|
+
// Edition rules close the primitives (editions.ts): custom properties on <html>, scoped :where([data-gem-edition]).
|
|
1667
|
+
primitives: [reset, "", createPrimitiveCss(), "", gemFrameworkEditionCss(), "", ""].join("\n"),
|
|
1476
1668
|
sections: [createShellCss(), "", createSectionCss(), "", createThemeProfileCss(profile)].join("\n")
|
|
1477
1669
|
};
|
|
1478
1670
|
}
|
|
@@ -3117,8 +3309,14 @@ function createSectionCss() {
|
|
|
3117
3309
|
// rather than re-deriving the swatch in every divider rule.
|
|
3118
3310
|
":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
3311
|
":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
|
-
|
|
3312
|
+
// accent / dark are COLOUR SCHEMES (tokens.ts gemBandSchemeColors): the band re-points every --gem-color-* to its own
|
|
3313
|
+
// 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,
|
|
3314
|
+
// only the band's own background and `color` changed: 988 / 991 lines of text under AA across 248 GemFactory sections.
|
|
3315
|
+
// 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.
|
|
3316
|
+
GEM_BAND_SCHEME_FALLBACK_CSS,
|
|
3317
|
+
...GEM_BAND_SCHEMES.map(
|
|
3318
|
+
(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); }`
|
|
3319
|
+
),
|
|
3122
3320
|
":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
3321
|
// On accent/dark bands, keep muted sub-copy + headings legible against the inverted ground.
|
|
3124
3322
|
":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 +6086,13 @@ function gemFrameworkPublicCssVariables() {
|
|
|
5888
6086
|
return frameworkPublicVariables;
|
|
5889
6087
|
}
|
|
5890
6088
|
var ROOT_SELECTOR = /^\s*(?::root|html|:host)(?:\[[^\]]*\]|:[a-z-]+(?:\([^()]*\))?)*\s*$/;
|
|
6089
|
+
var EDITION_ROOT_SELECTOR = /^\s*:where\(\s*\[data-gem-edition=(["'])\d+\1\](?:\s*,\s*\[data-gem-edition=(["'])\d+\2\])*\s*\)\s*$/;
|
|
5891
6090
|
function gemPublicCssVariablesFrom(stylesheet) {
|
|
5892
6091
|
const css = stylesheet.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
5893
6092
|
const declaredOnRoot = /* @__PURE__ */ new Set();
|
|
5894
6093
|
const declaredElsewhere = /* @__PURE__ */ new Set();
|
|
5895
6094
|
for (const [, selector, body] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
5896
|
-
const onRoot = selector.split(",").every((part) => ROOT_SELECTOR.test(part));
|
|
6095
|
+
const onRoot = EDITION_ROOT_SELECTOR.test(selector) || selector.split(",").every((part) => ROOT_SELECTOR.test(part));
|
|
5897
6096
|
for (const [, name] of body.matchAll(/(--gem-[a-z0-9-]+)\s*:/g)) (onRoot ? declaredOnRoot : declaredElsewhere).add(name);
|
|
5898
6097
|
}
|
|
5899
6098
|
const mentioned = new Set([...css.matchAll(/--gem-[a-z0-9-]+/g)].map((match) => match[0]));
|
|
@@ -5994,9 +6193,11 @@ function renderGemPreviewDocument(plan, options = {}) {
|
|
|
5994
6193
|
const title = options.title ?? `${plan.name} preview`;
|
|
5995
6194
|
const formAttrs = Object.entries(gemFormAttributes(plan.tokens.form)).map(([k, v]) => ` ${k}="${escapeAttribute(v)}"`).join("");
|
|
5996
6195
|
const bodyClass = ["gem-theme", `gem-theme--${slugify(profile)}`, options.bodyClassName].filter(Boolean).join(" ");
|
|
6196
|
+
const edition = gemFrameworkEditionOf(plan.framework_edition);
|
|
6197
|
+
const editionAttr = edition > 1 ? ` data-gem-edition="${edition}"` : "";
|
|
5997
6198
|
return [
|
|
5998
6199
|
"<!doctype html>",
|
|
5999
|
-
`<html lang="en" data-gem-theme-profile="${escapeAttribute(profile)}">`,
|
|
6200
|
+
`<html lang="en" data-gem-theme-profile="${escapeAttribute(profile)}"${editionAttr}>`,
|
|
6000
6201
|
"<head>",
|
|
6001
6202
|
' <meta charset="utf-8" />',
|
|
6002
6203
|
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
@@ -7052,9 +7253,12 @@ function stringSetting(value) {
|
|
|
7052
7253
|
export {
|
|
7053
7254
|
COUNTDOWN_END_RUNTIME_LINES,
|
|
7054
7255
|
GEM_AMBIENT_CSS,
|
|
7256
|
+
GEM_BAND_SCHEMES,
|
|
7257
|
+
GEM_BAND_SCHEME_FALLBACK_CSS,
|
|
7055
7258
|
GEM_BUTTON_TOKEN_KEYS,
|
|
7056
7259
|
GEM_BUTTON_TOKEN_SHAPES,
|
|
7057
7260
|
GEM_COUNTDOWN_ENDED_CSS,
|
|
7261
|
+
GEM_EDITION_SUPPORT_MONTHS,
|
|
7058
7262
|
GEM_FIELD_JOINS,
|
|
7059
7263
|
GEM_FIELD_SHAPES,
|
|
7060
7264
|
GEM_FORM_CSS_VARIABLES,
|
|
@@ -7063,6 +7267,7 @@ export {
|
|
|
7063
7267
|
GEM_FORM_SET_CSS,
|
|
7064
7268
|
GEM_FORM_TOKEN_KEYS,
|
|
7065
7269
|
GEM_FORM_VAR_SCOPE,
|
|
7270
|
+
GEM_FRAMEWORK_EDITIONS,
|
|
7066
7271
|
GEM_GRID_COLUMNS_ROW_KNOB_CSS,
|
|
7067
7272
|
GEM_HEADING_SIZE_CSS_VARS,
|
|
7068
7273
|
GEM_LINE_HEIGHT_TOKEN_KEYS,
|
|
@@ -7072,11 +7277,15 @@ export {
|
|
|
7072
7277
|
GEM_ROW_CSS,
|
|
7073
7278
|
GEM_ROW_CSS_VARIABLES,
|
|
7074
7279
|
GEM_ROW_GAP_CSS,
|
|
7280
|
+
LATEST_GEM_FRAMEWORK_EDITION,
|
|
7281
|
+
MIN_TEXT_CONTRAST,
|
|
7075
7282
|
MODERN_MINERAL_FAMILY_PACK_ID,
|
|
7076
7283
|
MODERN_MINERAL_HEADER_FOOTER_PRESET_ID,
|
|
7077
7284
|
MODERN_MINERAL_HOMEPAGE_LINEAGE_SECTIONS,
|
|
7078
7285
|
MODERN_MINERAL_HOMEPAGE_PACK_ID,
|
|
7079
7286
|
MODERN_MINERAL_STYLE_PACK_ID,
|
|
7287
|
+
aaLabelColor,
|
|
7288
|
+
aaTextColor,
|
|
7080
7289
|
auditGemFrameworkClassName,
|
|
7081
7290
|
createGemBaseCss,
|
|
7082
7291
|
createGemFrameworkManifest,
|
|
@@ -7097,11 +7306,18 @@ export {
|
|
|
7097
7306
|
createThemeProfileCss,
|
|
7098
7307
|
defaultGemTokens,
|
|
7099
7308
|
defaultSelection,
|
|
7309
|
+
gemBandSchemeColors,
|
|
7310
|
+
gemBandSchemeCssVars,
|
|
7311
|
+
gemBandSchemeDeclarations,
|
|
7100
7312
|
gemButtonCaseCss,
|
|
7101
7313
|
gemFormAttributes,
|
|
7102
7314
|
gemFormCssBlock,
|
|
7103
7315
|
gemFormCssVars,
|
|
7104
7316
|
gemFormSetsFor,
|
|
7317
|
+
gemFrameworkEditionCss,
|
|
7318
|
+
gemFrameworkEditionOf,
|
|
7319
|
+
gemFrameworkEditionScope,
|
|
7320
|
+
gemFrameworkEditionSupportedUntil,
|
|
7105
7321
|
gemFrameworkPrimitiveGroups,
|
|
7106
7322
|
gemFrameworkRuntimePlugins,
|
|
7107
7323
|
gemFrameworkValidationRules,
|
|
@@ -7113,11 +7329,15 @@ export {
|
|
|
7113
7329
|
gemRuntimePluginModuleKeys,
|
|
7114
7330
|
gemRuntimePluginSources,
|
|
7115
7331
|
getGemFrameworkValidationRule,
|
|
7332
|
+
hexContrastRatio,
|
|
7333
|
+
hexRelativeLuminance,
|
|
7116
7334
|
isModernMineralFamilyLineagePlan,
|
|
7335
|
+
mixHex,
|
|
7117
7336
|
modernMineralFamilyLineage,
|
|
7118
7337
|
modernMineralHomepageLineageSections,
|
|
7119
7338
|
motionCssVarAliases,
|
|
7120
7339
|
normalizeGemTokens,
|
|
7340
|
+
normalizeHex,
|
|
7121
7341
|
offerEngineRuntimeScript,
|
|
7122
7342
|
optionalGemTokenKeys,
|
|
7123
7343
|
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;";
|
|
@@ -135,7 +205,7 @@ function readButtonTokens(tokens) {
|
|
|
135
205
|
const key = byNorm.get(buttonKeyNorm(rawKey));
|
|
136
206
|
if (!key) continue;
|
|
137
207
|
copy ??= { ...values };
|
|
138
|
-
delete copy[rawKey];
|
|
208
|
+
if (rawKey !== key) delete copy[rawKey];
|
|
139
209
|
const v = buttonTokenValue(group, key, raw);
|
|
140
210
|
if (v === null) out.invalid.push({ path: `${group}.${rawKey}`, value: raw });
|
|
141
211
|
else {
|
|
@@ -143,6 +213,9 @@ function readButtonTokens(tokens) {
|
|
|
143
213
|
out.values[key] = v;
|
|
144
214
|
}
|
|
145
215
|
}
|
|
216
|
+
if (copy) {
|
|
217
|
+
for (const key of keys) if (!(key in out.values)) delete copy[key];
|
|
218
|
+
}
|
|
146
219
|
if (copy) out.groups[group] = copy;
|
|
147
220
|
}
|
|
148
221
|
return out;
|
|
@@ -183,6 +256,77 @@ var requiredGemTokenKeys = {
|
|
|
183
256
|
component: ["headerHeight", "cardPadding", "buttonPaddingInline", "buttonPaddingBlock", "inputHeight"],
|
|
184
257
|
morphology: ["campaignDensity", "mediaRhythm", "proofIntensity", "commerceEmphasis", "policyTone"]
|
|
185
258
|
};
|
|
259
|
+
var GEM_BAND_SCHEMES = ["dark", "accent"];
|
|
260
|
+
var GEM_BAND_GROUND = {
|
|
261
|
+
dark: { ground: "text", ink: "background", surface: 8, muted: 72, status: 55 },
|
|
262
|
+
accent: { ground: "accent", ink: "accentText", surface: 0, muted: 100, status: 0 }
|
|
263
|
+
};
|
|
264
|
+
function gemBandSchemeExpressions(band, ground, ink, status) {
|
|
265
|
+
const mix = GEM_BAND_GROUND[band];
|
|
266
|
+
const blend = (a, b, toward) => toward <= 0 ? a : toward >= 100 ? b : `color-mix(in srgb, ${a} ${100 - toward}%, ${b})`;
|
|
267
|
+
return {
|
|
268
|
+
background: ground,
|
|
269
|
+
surface: blend(ground, ink, mix.surface),
|
|
270
|
+
text: ink,
|
|
271
|
+
muted: blend(ground, ink, mix.muted),
|
|
272
|
+
// 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.
|
|
273
|
+
accent: ink,
|
|
274
|
+
accentText: ground,
|
|
275
|
+
border: blend(ground, ink, 22),
|
|
276
|
+
// 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.
|
|
277
|
+
success: blend(ink, status("success"), mix.status),
|
|
278
|
+
warning: blend(ink, status("warning"), mix.status),
|
|
279
|
+
danger: blend(ink, status("danger"), mix.status)
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function gemBandScheme(c, band) {
|
|
283
|
+
const { ground, ink } = GEM_BAND_GROUND[band];
|
|
284
|
+
const g = normalizeHex(c[ground]);
|
|
285
|
+
const i = normalizeHex(c[ink]);
|
|
286
|
+
if (!g || !i) return gemBandSchemeExpressions(band, c[ground], c[ink], (key) => c[key]);
|
|
287
|
+
const toward = mixHex(g, i, 0.08);
|
|
288
|
+
const away = mixHex(g, hexRelativeLuminance(i) > hexRelativeLuminance(g) ? "#000000" : "#ffffff", 0.08);
|
|
289
|
+
const surface = hexContrastRatio(i, toward) >= MIN_TEXT_CONTRAST ? toward : away;
|
|
290
|
+
const grounds = [g, surface];
|
|
291
|
+
const text = aaTextColor(i, grounds);
|
|
292
|
+
const accent = aaTextColor(c.accent, grounds);
|
|
293
|
+
return {
|
|
294
|
+
background: g,
|
|
295
|
+
surface,
|
|
296
|
+
text,
|
|
297
|
+
muted: aaTextColor(mixHex(i, g, 0.3), grounds),
|
|
298
|
+
accent,
|
|
299
|
+
accentText: aaLabelColor(c.accentText, accent, [g, text]),
|
|
300
|
+
border: mixHex(g, i, 0.22),
|
|
301
|
+
success: aaTextColor(c.success, grounds),
|
|
302
|
+
warning: aaTextColor(c.warning, grounds),
|
|
303
|
+
danger: aaTextColor(c.danger, grounds)
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function gemBandSchemeColors(color = {}) {
|
|
307
|
+
const c = Object.fromEntries(
|
|
308
|
+
requiredGemTokenKeys.color.map((key) => {
|
|
309
|
+
const value = color?.[key];
|
|
310
|
+
return [key, typeof value === "string" && value.trim() ? stripImportant(value) : defaultGemTokens.color[key]];
|
|
311
|
+
})
|
|
312
|
+
);
|
|
313
|
+
return { dark: gemBandScheme(c, "dark"), accent: gemBandScheme(c, "accent") };
|
|
314
|
+
}
|
|
315
|
+
function gemBandSchemeCssVars(color = {}) {
|
|
316
|
+
const schemes = gemBandSchemeColors(color);
|
|
317
|
+
return GEM_BAND_SCHEMES.flatMap(
|
|
318
|
+
(band) => requiredGemTokenKeys.color.map((key) => `--gem-scheme-${band}-${slugify(key)}: ${schemes[band][key]};`)
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
var GEM_BAND_SCHEME_FALLBACK_CSS = `:where(:root) { ${GEM_BAND_SCHEMES.flatMap((band) => {
|
|
322
|
+
const { ground, ink } = GEM_BAND_GROUND[band];
|
|
323
|
+
const v = (key) => `var(--gem-color-${slugify(key)})`;
|
|
324
|
+
const expr = gemBandSchemeExpressions(band, v(ground), v(ink), (key) => v(key));
|
|
325
|
+
return requiredGemTokenKeys.color.map((key) => `--gem-scheme-${band}-${slugify(key)}: ${expr[key]};`);
|
|
326
|
+
}).join(" ")} }`;
|
|
327
|
+
function gemBandSchemeDeclarations(band) {
|
|
328
|
+
return requiredGemTokenKeys.color.map((key) => `--gem-color-${slugify(key)}: var(--gem-scheme-${band}-${slugify(key)});`).join(" ");
|
|
329
|
+
}
|
|
186
330
|
var GEM_PHONE_TOKEN_TARGETS = {
|
|
187
331
|
spacing: [...requiredGemTokenKeys.spacing, ...Object.keys(GEM_DERIVED_SPACE_STEPS)],
|
|
188
332
|
layout: ["section"]
|
|
@@ -309,12 +453,14 @@ var defaultGemTokens = {
|
|
|
309
453
|
policyTone: "standard"
|
|
310
454
|
}
|
|
311
455
|
};
|
|
312
|
-
function createGemTokenCss(tokens, selector = ":root") {
|
|
456
|
+
function createGemTokenCss(tokens, selector = ":root", options = {}) {
|
|
313
457
|
const button = readButtonTokens(tokens);
|
|
314
458
|
const phone = readPhoneTokens(tokens);
|
|
315
459
|
const spacing = desktopEntries("spacing", tokens.spacing);
|
|
316
460
|
const declarations = [
|
|
317
461
|
...toCssVars("color", tokens.color),
|
|
462
|
+
// Bộ màu của băng dark / accent (gemBandSchemeColors): luật băng trỏ --gem-color-* vào đây.
|
|
463
|
+
...options.bandSchemes === false ? [] : gemBandSchemeCssVars(tokens.color),
|
|
318
464
|
...toCssVars("font", Object.fromEntries(Object.entries(button.groups.typography ?? tokens.typography ?? {}).filter(([k, v]) => !isRampKey(k) && !badLineHeight(k, v)))),
|
|
319
465
|
// Space ramp is scaled by the global density knob --gem-style-space (default 1),
|
|
320
466
|
// so a style pack can make the WHOLE storefront airier or denser without touching
|
|
@@ -357,10 +503,13 @@ function createGemTokenCss(tokens, selector = ":root") {
|
|
|
357
503
|
const formBlock = gemFormCssBlock(tokens.form);
|
|
358
504
|
const buttonCase = gemButtonCaseCss(tokens, selector);
|
|
359
505
|
const mobile = gemMobileTokenCssVars(tokens, phone);
|
|
506
|
+
const bandBorders = declarations.filter((line) => line.startsWith("--gem-border-") && line.includes("var(--gem-color-"));
|
|
507
|
+
const bandScope = splitSelectorList(selector).flatMap((sel) => GEM_BAND_SCHEMES.map((band) => `${sel} [data-gem-section-background='${band}']`)).join(", ");
|
|
360
508
|
return [
|
|
361
509
|
`${selector} {`,
|
|
362
510
|
...declarations.map((line) => ` ${line}`),
|
|
363
511
|
"}",
|
|
512
|
+
...bandBorders.length ? [`:where(${bandScope}) {`, ...bandBorders.map((line) => ` ${line}`), "}"] : [],
|
|
364
513
|
...mobile.length ? [`@media (max-width: ${GEM_TYPE_MOBILE_MAX}) {`, ` ${selector} {`, ...mobile.map((line) => ` ${line}`), " }", "}"] : [],
|
|
365
514
|
...formBlock ? [formBlock] : [],
|
|
366
515
|
...buttonCase ? [buttonCase] : []
|
|
@@ -429,7 +578,11 @@ function toScaledCssVars(namespace, values, knob) {
|
|
|
429
578
|
// bọc nó vào GIỮA giá trị — `calc(16px !important * var(...))` — nên custom property lưu
|
|
430
579
|
// nguyên xi, rồi mọi `padding: var(--gem-space-md)` thành invalid ở computed-value và SẬP VỀ 0
|
|
431
580
|
// trên toàn bộ .gem-* section.
|
|
432
|
-
([key, value]) =>
|
|
581
|
+
([key, value]) => {
|
|
582
|
+
const clean = stripImportant(value);
|
|
583
|
+
if (new RegExp(`^var\\(--gem-${namespace}-[a-z0-9-]+\\)$`, "i").test(clean.trim())) return `--gem-${namespace}-${slugify(key)}: ${clean.trim()};`;
|
|
584
|
+
return `--gem-${namespace}-${slugify(key)}: calc(${clean} * var(${knob}, 1));`;
|
|
585
|
+
}
|
|
433
586
|
);
|
|
434
587
|
}
|
|
435
588
|
function toBorderCssVars(namespace, values, knob) {
|
|
@@ -443,6 +596,28 @@ function slugify(value) {
|
|
|
443
596
|
return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
444
597
|
}
|
|
445
598
|
|
|
599
|
+
// ../../packages/gem-framework/src/editions.ts
|
|
600
|
+
var EDITION_2_CSS = [
|
|
601
|
+
`:where(&) { ${GEM_HEADING_SIZE_CSS_VARS.filter((decl) => !decl.startsWith("--gem-section-heading-size-mobile") && !decl.startsWith("--gem-hero-heading-size-mobile")).join(" ")} }`
|
|
602
|
+
];
|
|
603
|
+
var GEM_FRAMEWORK_EDITIONS = [
|
|
604
|
+
{ edition: 1, since: "0.1.0", released: "2026-07-04", changes: [], css: [] },
|
|
605
|
+
{
|
|
606
|
+
edition: 2,
|
|
607
|
+
since: "0.12.0",
|
|
608
|
+
released: "2026-10-10",
|
|
609
|
+
changes: ["Section headings and hero titles follow your type scale on tablets and computers. Phones keep their current size."],
|
|
610
|
+
css: EDITION_2_CSS
|
|
611
|
+
}
|
|
612
|
+
];
|
|
613
|
+
var LATEST_GEM_FRAMEWORK_EDITION = GEM_FRAMEWORK_EDITIONS[GEM_FRAMEWORK_EDITIONS.length - 1].edition;
|
|
614
|
+
function gemFrameworkEditionScope(edition) {
|
|
615
|
+
return GEM_FRAMEWORK_EDITIONS.filter((e) => e.edition >= edition).map((e) => `[data-gem-edition='${e.edition}']`).join(", ");
|
|
616
|
+
}
|
|
617
|
+
function gemFrameworkEditionCss() {
|
|
618
|
+
return GEM_FRAMEWORK_EDITIONS.flatMap((e) => e.css.map((rule) => rule.replaceAll("&", gemFrameworkEditionScope(e.edition)))).join("\n");
|
|
619
|
+
}
|
|
620
|
+
|
|
446
621
|
// ../../packages/gem-framework/src/manifest.ts
|
|
447
622
|
var gemFrameworkPrimitiveGroups = [
|
|
448
623
|
{
|
|
@@ -1068,7 +1243,7 @@ function createGemThemeCssParts(tokens, selector = ":root", options = {}) {
|
|
|
1068
1243
|
const base = createGemBaseCssParts(options.profile ?? "dawn-parity");
|
|
1069
1244
|
return {
|
|
1070
1245
|
tokens: [
|
|
1071
|
-
createGemTokenCss(tokens, selector),
|
|
1246
|
+
createGemTokenCss(tokens, selector, { bandSchemes: options.bandSchemes }),
|
|
1072
1247
|
// Set the knobs the space / radius / border / type ramps read (only when they differ from the
|
|
1073
1248
|
// default 1, so a standard-shaped storefront emits nothing extra).
|
|
1074
1249
|
...createGemStyleKnobCss(options, selector),
|
|
@@ -1159,7 +1334,8 @@ function createGemBaseCssParts(profile) {
|
|
|
1159
1334
|
"}"
|
|
1160
1335
|
].join("\n");
|
|
1161
1336
|
return {
|
|
1162
|
-
|
|
1337
|
+
// Edition rules close the primitives (editions.ts): custom properties on <html>, scoped :where([data-gem-edition]).
|
|
1338
|
+
primitives: [reset, "", createPrimitiveCss(), "", gemFrameworkEditionCss(), "", ""].join("\n"),
|
|
1163
1339
|
sections: [createShellCss(), "", createSectionCss(), "", createThemeProfileCss(profile)].join("\n")
|
|
1164
1340
|
};
|
|
1165
1341
|
}
|
|
@@ -2804,8 +2980,14 @@ function createSectionCss() {
|
|
|
2804
2980
|
// rather than re-deriving the swatch in every divider rule.
|
|
2805
2981
|
":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
2982
|
":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
|
-
|
|
2983
|
+
// accent / dark are COLOUR SCHEMES (tokens.ts gemBandSchemeColors): the band re-points every --gem-color-* to its own
|
|
2984
|
+
// 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,
|
|
2985
|
+
// only the band's own background and `color` changed: 988 / 991 lines of text under AA across 248 GemFactory sections.
|
|
2986
|
+
// 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.
|
|
2987
|
+
GEM_BAND_SCHEME_FALLBACK_CSS,
|
|
2988
|
+
...GEM_BAND_SCHEMES.map(
|
|
2989
|
+
(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); }`
|
|
2990
|
+
),
|
|
2809
2991
|
":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
2992
|
// On accent/dark bands, keep muted sub-copy + headings legible against the inverted ground.
|
|
2811
2993
|
":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 +4526,13 @@ function gemFrameworkPublicCssVariables() {
|
|
|
4344
4526
|
return frameworkPublicVariables;
|
|
4345
4527
|
}
|
|
4346
4528
|
var ROOT_SELECTOR = /^\s*(?::root|html|:host)(?:\[[^\]]*\]|:[a-z-]+(?:\([^()]*\))?)*\s*$/;
|
|
4529
|
+
var EDITION_ROOT_SELECTOR = /^\s*:where\(\s*\[data-gem-edition=(["'])\d+\1\](?:\s*,\s*\[data-gem-edition=(["'])\d+\2\])*\s*\)\s*$/;
|
|
4347
4530
|
function gemPublicCssVariablesFrom(stylesheet) {
|
|
4348
4531
|
const css = stylesheet.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
4349
4532
|
const declaredOnRoot = /* @__PURE__ */ new Set();
|
|
4350
4533
|
const declaredElsewhere = /* @__PURE__ */ new Set();
|
|
4351
4534
|
for (const [, selector, body] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
4352
|
-
const onRoot = selector.split(",").every((part) => ROOT_SELECTOR.test(part));
|
|
4535
|
+
const onRoot = EDITION_ROOT_SELECTOR.test(selector) || selector.split(",").every((part) => ROOT_SELECTOR.test(part));
|
|
4353
4536
|
for (const [, name] of body.matchAll(/(--gem-[a-z0-9-]+)\s*:/g)) (onRoot ? declaredOnRoot : declaredElsewhere).add(name);
|
|
4354
4537
|
}
|
|
4355
4538
|
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.1",
|
|
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",
|