@gemboss/gem-framework 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -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 +10 -3
- package/dist/index.js +225 -9
- package/dist/primitives.css +2 -0
- package/dist/section-gate.js +186 -7
- package/dist/sections.css +3 -2
- package/dist/tokens.css +5 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -25,6 +25,26 @@ khi CÀI, nên build trên Railway không kéo được, và GemFactory phải c
|
|
|
25
25
|
thì mọi repo cài theo version, và bị tụt thì thấy ngay ở số version. Mã trong gói là CSS và runtime vốn đã tới
|
|
26
26
|
trình duyệt của người mua, cộng bộ kiểm; `license: UNLICENSED` giữ nguyên (xem được, không có quyền dùng lại).
|
|
27
27
|
|
|
28
|
+
## Kênh `next` (thử trước khi phát hành)
|
|
29
|
+
|
|
30
|
+
Mỗi commit lên main của gemtheme mà đổi gói thì có ngay một bản trên GitHub Packages:
|
|
31
|
+
`@chriskoh26/gem-framework@next`, số phiên bản `<x.y.z của main>-next.<10 ký tự băm của bản dựng>`. Workflow:
|
|
32
|
+
`.github/workflows/gem-framework-next.yml`. Kênh `latest` trên npm vẫn chỉ đi qua passkey (mục dưới).
|
|
33
|
+
|
|
34
|
+
Thử ở một repo dùng gói, KHÔNG commit hai thay đổi này:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
# .npmrc của repo (hoặc ~/.npmrc), token chỉ cần read:packages
|
|
38
|
+
@chriskoh26:registry=https://npm.pkg.github.com
|
|
39
|
+
//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}
|
|
40
|
+
|
|
41
|
+
npm install --no-save "@gemboss/gem-framework@npm:@chriskoh26/gem-framework@next"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Bí danh giữ nguyên tên `@gemboss/gem-framework`, nên mã và `import` của bên dùng không đổi. Bản dựng ghi
|
|
45
|
+
`gemtheme_sha` trong `dist/SOURCE.json`. Luôn ghi `@next` (hoặc đúng số): tag `latest` của gói trên GitHub Packages là
|
|
46
|
+
0.4.0 từ 29/09/2026, không phải bản mới nhất.
|
|
47
|
+
|
|
28
48
|
## Phát hành
|
|
29
49
|
|
|
30
50
|
Một lệnh, chạy trên máy, không dùng GitHub Actions (Chris chốt 30/09/2026: không trả tiền runner):
|
|
@@ -73,6 +93,46 @@ Từ 0.8.2 mỗi module có `.d.ts` cạnh nó (`mount(root?: ParentNode | null,
|
|
|
73
93
|
|
|
74
94
|
## Nhật ký thay đổi
|
|
75
95
|
|
|
96
|
+
- **0.12.0** (10/10/2026): **edition** (Chris chốt 10/10/2026). Mỗi store ghim một edition; CSS làm đổi hình store
|
|
97
|
+
không yêu cầu nó thì phát dưới edition kế tiếp thay vì đổi mọi store cùng lúc. Plan mang `framework_edition` (số nguyên,
|
|
98
|
+
thiếu = 1); storefront đặt nó lên `<html>` thành `data-gem-edition`. Luật của edition N viết dưới
|
|
99
|
+
`:where([data-gem-edition='N'], …edition sau)`: độ ưu tiên 0, nên mọi luật giữ đúng chỗ của nó trong thác CSS; danh sách
|
|
100
|
+
edition trong phạm vi sinh từ sổ, nên thêm edition 3 thì luật của edition 2 tự nhận `"3"`. Edition cũ được hỗ trợ 6 tháng
|
|
101
|
+
kể từ khi có edition mới. Khối luật edition nằm cuối `primitives.css`.
|
|
102
|
+
**Edition 2:** tiêu đề section = H2, tiêu đề hero = H1 của thang chữ ở màn rộng hơn 760px kể cả khi plan không khai
|
|
103
|
+
`typography.base` / `scale`; điện thoại giữ cỡ cũ. Đo Chromium trên `globals.css` của gemsite (tiêu đề section / hero, px):
|
|
104
|
+
1280 64/51 → 43/53, 1366 68/55 → 43/53, 1920 68/77 → 43/53, 768 38/40 → 43/53, 320 và 390 không đổi (32/35, 32/43).
|
|
105
|
+
Export mới: `GEM_FRAMEWORK_EDITIONS`, `LATEST_GEM_FRAMEWORK_EDITION`, `GEM_EDITION_SUPPORT_MONTHS`,
|
|
106
|
+
`gemFrameworkEditionOf` (đọc giá trị của plan: thiếu / sai = 1, mới hơn gói biết = edition mới nhất gói biết),
|
|
107
|
+
`gemFrameworkEditionSupportedUntil`, `gemFrameworkEditionScope`, `gemFrameworkEditionCss`; riêng sổ edition có entry
|
|
108
|
+
nhẹ `@gemboss/gem-framework/editions` (6,4 KB, kèm `editions.d.ts` do tsc sinh) để storefront đọc `framework_edition`
|
|
109
|
+
mà không nạp `index.js` (429 KB). `renderGemPreviewDocument` đặt `data-gem-edition` theo plan như storefront.
|
|
110
|
+
`themePlanSchema` (gói schemas) khai `framework_edition`, đọc dễ dãi như `gemFrameworkEditionOf` (giá trị sai bị bỏ, plan
|
|
111
|
+
vẫn parse được): công cụ vá theme parse plan, khoá không khai thì bị gỡ. Bộ kiểm biến CSS coi phạm
|
|
112
|
+
vi edition là gốc (`<html>`). Store không mang `data-gem-edition` vẽ y như 0.11.0.
|
|
113
|
+
- **0.11.0** (10/10/2026): băng nền `data-gem-section-background="dark"` / `"accent"` là một **bộ màu**: luật băng trỏ
|
|
114
|
+
mọi `--gem-color-*` bắt buộc vào `--gem-scheme-<băng>-<khoá>`. Trước bản này băng chỉ đổi nền và `color` của chính nó:
|
|
115
|
+
thẻ lồng trong băng vẽ nền sáng với chữ sáng, `color: var(--gem-color-text)` ra chữ tối trên nền tối, section tự vẽ nền
|
|
116
|
+
bằng token thì đè mất nền băng. Bộ màu có hai tầng:
|
|
117
|
+
1. `createGemTokenCss` phát giá trị tính sẵn từ màu của plan (`aaTextColor`: chữ, muted, nhấn, trạng thái ≥ 4,5:1 trên
|
|
118
|
+
nền và surface của băng; chữ nút ≥ 4,5:1 trên màu nhấn của băng). Tắt bằng `{ bandSchemes: false }`.
|
|
119
|
+
2. CSS của gói mang tầng dự phòng `:where(:root)` (độ ưu tiên 0) tính bằng `var()` / `color-mix` từ `--gem-color-*`: nơi
|
|
120
|
+
tự đặt `--gem-color-*` bằng tay vẫn ra băng đúng màu brand, chỉ màu nhấn / trạng thái không được bảo đảm AA. Khối
|
|
121
|
+
token mặc định trong `gem-framework.css` / `tokens.css` không mang tầng 1, để tầng này quyết.
|
|
122
|
+
`--gem-border-*` đọc màu được khai lại trong phạm vi băng (viền theo băng). Đo 248 section kho GemFactory × 4 preset
|
|
123
|
+
(18.092 dòng chữ, Chromium): băng tối 3.972 → 211 dòng dưới AA, băng accent 3.980 → 771, không băng giữ 405; chỉ tầng
|
|
124
|
+
dự phòng: 211 / 769. Phần còn lại là section tự pha màu (`color-mix` tỉ lệ cố định, màu ghi cứng). Thêm export
|
|
125
|
+
`gemBandSchemeColors`, `gemBandSchemeCssVars`, `gemBandSchemeDeclarations`, `GEM_BAND_SCHEMES`,
|
|
126
|
+
`GEM_BAND_SCHEME_FALLBACK_CSS`, và phép tính màu dời từ theme-presets: `aaTextColor`, `aaLabelColor`, `hexContrastRatio`,
|
|
127
|
+
`hexRelativeLuminance`, `mixHex`, `normalizeHex`, `MIN_TEXT_CONTRAST`. Bản chép luật băng ở nơi khác (gemsite
|
|
128
|
+
`globals.css`) phải mang cả tầng dự phòng: luật băng không có `--gem-scheme-*` nào thì mọi `--gem-color-*` trong băng
|
|
129
|
+
thành rỗng (nền nút, nền thẻ trong suốt).
|
|
130
|
+
- **0.10.1** (09/10/2026): màu `color.warning` mặc định `#b7791f` → `#9a661a`. Màu cũ chỉ đạt 3,36:1 trên nền mặc định và
|
|
131
|
+
3,64:1 trên surface (dưới AA 4,5:1), trong khi section GemFactory dùng nó làm màu chữ và badge vẽ `accentText` trên nó. Màu
|
|
132
|
+
mới đạt 4,52 / 4,90, giữ tông. Phiên gemfactory phát hiện khi thêm cảnh báo tương phản cho preset. Test mới
|
|
133
|
+
`default-colours-meet-aa`: mọi màu mặc định dùng làm chữ ≥ 4,5:1 trên cả hai nền. Plan khai `color.warning` riêng (mọi
|
|
134
|
+
plan sinh từ style pack) thì không đổi: bảng màu pack sửa ở theme-presets (#722). Preview harness của gem-landing-blocks
|
|
135
|
+
theo màu mới. Vòng tham chiếu chéo nhóm của khoá điện thoại vẫn CHƯA chặn (ghi ở `readPhoneTokens`).
|
|
76
136
|
- **0.10.0** (08/10/2026, gộp 0.9.10 và 0.9.11 chưa phát hành): phần còn lại của yêu cầu #4 GemFactory (Figma CRO, issue
|
|
77
137
|
gemfactory #95). Plan không khai khoá nào dưới đây thì CSS tính ra như cũ (mỗi `var()` lùi về đúng giá trị cũ).
|
|
78
138
|
- Line-height tiêu đề: opt-in `data-gem-heading="section"` (1.1), `.gem-section__heading` nền (1.04) và của Dawn parity
|
package/dist/SOURCE.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "@gemboss/gem-framework",
|
|
3
|
-
"version": "0.
|
|
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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
--gem-color-accent-text: #ffffff;
|
|
8
8
|
--gem-color-border: #d2d5d8;
|
|
9
9
|
--gem-color-success: #008060;
|
|
10
|
-
--gem-color-warning: #
|
|
10
|
+
--gem-color-warning: #9a661a;
|
|
11
11
|
--gem-color-danger: #d72c0d;
|
|
12
12
|
--gem-font-body: Inter, system-ui, sans-serif;
|
|
13
13
|
--gem-font-heading: Inter, system-ui, sans-serif;
|
|
@@ -64,6 +64,10 @@
|
|
|
64
64
|
--gem-tap-min: 44px;
|
|
65
65
|
--gem-focus-offset: 3px;
|
|
66
66
|
}
|
|
67
|
+
:where(:root [data-gem-section-background='dark'], :root [data-gem-section-background='accent']) {
|
|
68
|
+
--gem-border-subtle: calc(1px * var(--gem-style-border, 1)) solid var(--gem-color-border);
|
|
69
|
+
--gem-border-strong: calc(1px * var(--gem-style-border, 1)) solid color-mix(in srgb, var(--gem-color-text) 28%, transparent);
|
|
70
|
+
}
|
|
67
71
|
|
|
68
72
|
*, *::before, *::after {
|
|
69
73
|
box-sizing: border-box;
|
|
@@ -1481,6 +1485,8 @@ a {
|
|
|
1481
1485
|
.gem-collection-filter-toolbar__active-filter, .gem-collection-filter-toolbar__clear { min-height: 2.25rem; align-items: center; }
|
|
1482
1486
|
}
|
|
1483
1487
|
|
|
1488
|
+
:where([data-gem-edition='2']) { --gem-section-heading-size: calc(var(--gem-h2) * var(--gem-typography-heading-scale, 1)); --gem-hero-heading-size: calc(var(--gem-h1) * var(--gem-typography-heading-scale, 1)); }
|
|
1489
|
+
|
|
1484
1490
|
.gem-popup-shell { padding: 0; }
|
|
1485
1491
|
.gem-popup-shell__dialog {
|
|
1486
1492
|
padding: 0; border: 0; margin: auto; color: var(--gem-color-text); background: var(--gem-color-background);
|
|
@@ -2184,8 +2190,9 @@ sticky-header.header-wrapper {
|
|
|
2184
2190
|
:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-grid-columns='4'] .gem-row:is(.gem-section__grid, [class*='__grid']):not(.gem-row .gem-row) { --gem-row-cols: 4; --gem-row-cols-md: 4; --gem-row-tracks: initial; --gem-row-tracks-md: initial; }
|
|
2185
2191
|
:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='default'] { --gem-section-bg: var(--gem-color-background); background: transparent; }
|
|
2186
2192
|
:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='surface'] { --gem-section-bg: var(--gem-color-surface); background: var(--gem-section-bg); padding-block: var(--gem-space-xl); }
|
|
2187
|
-
:
|
|
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"]
|
|
@@ -363,7 +504,10 @@ var defaultGemTokens = {
|
|
|
363
504
|
accentText: "#ffffff",
|
|
364
505
|
border: "#d2d5d8",
|
|
365
506
|
success: "#008060",
|
|
366
|
-
|
|
507
|
+
// AA as TEXT on both defaults (gem-framework 0.10.1): #b7791f measured 3.36:1 on the background and 3.64:1 on white, and
|
|
508
|
+
// GemFactory sections set `color: var(--gem-color-warning)` on words; white text on a warning badge was 3.64:1 too.
|
|
509
|
+
// #9a661a keeps the hue at 4.52:1 / 4.90:1. default-colours-meet-aa.test.ts holds every foreground default to 4.5:1.
|
|
510
|
+
warning: "#9a661a",
|
|
367
511
|
danger: "#d72c0d"
|
|
368
512
|
},
|
|
369
513
|
typography: {
|
|
@@ -470,12 +614,14 @@ function validateGemTokens(tokens) {
|
|
|
470
614
|
issues
|
|
471
615
|
};
|
|
472
616
|
}
|
|
473
|
-
function createGemTokenCss(tokens, selector = ":root") {
|
|
617
|
+
function createGemTokenCss(tokens, selector = ":root", options = {}) {
|
|
474
618
|
const button = readButtonTokens(tokens);
|
|
475
619
|
const phone = readPhoneTokens(tokens);
|
|
476
620
|
const spacing = desktopEntries("spacing", tokens.spacing);
|
|
477
621
|
const declarations = [
|
|
478
622
|
...toCssVars("color", tokens.color),
|
|
623
|
+
// Bộ màu của băng dark / accent (gemBandSchemeColors): luật băng trỏ --gem-color-* vào đây.
|
|
624
|
+
...options.bandSchemes === false ? [] : gemBandSchemeCssVars(tokens.color),
|
|
479
625
|
...toCssVars("font", Object.fromEntries(Object.entries(button.groups.typography ?? tokens.typography ?? {}).filter(([k, v]) => !isRampKey(k) && !badLineHeight(k, v)))),
|
|
480
626
|
// Space ramp is scaled by the global density knob --gem-style-space (default 1),
|
|
481
627
|
// so a style pack can make the WHOLE storefront airier or denser without touching
|
|
@@ -518,10 +664,13 @@ function createGemTokenCss(tokens, selector = ":root") {
|
|
|
518
664
|
const formBlock = gemFormCssBlock(tokens.form);
|
|
519
665
|
const buttonCase = gemButtonCaseCss(tokens, selector);
|
|
520
666
|
const mobile = gemMobileTokenCssVars(tokens, phone);
|
|
667
|
+
const bandBorders = declarations.filter((line) => line.startsWith("--gem-border-") && line.includes("var(--gem-color-"));
|
|
668
|
+
const bandScope = splitSelectorList(selector).flatMap((sel) => GEM_BAND_SCHEMES.map((band) => `${sel} [data-gem-section-background='${band}']`)).join(", ");
|
|
521
669
|
return [
|
|
522
670
|
`${selector} {`,
|
|
523
671
|
...declarations.map((line) => ` ${line}`),
|
|
524
672
|
"}",
|
|
673
|
+
...bandBorders.length ? [`:where(${bandScope}) {`, ...bandBorders.map((line) => ` ${line}`), "}"] : [],
|
|
525
674
|
...mobile.length ? [`@media (max-width: ${GEM_TYPE_MOBILE_MAX}) {`, ` ${selector} {`, ...mobile.map((line) => ` ${line}`), " }", "}"] : [],
|
|
526
675
|
...formBlock ? [formBlock] : [],
|
|
527
676
|
...buttonCase ? [buttonCase] : []
|
|
@@ -607,6 +756,44 @@ function tokenIssue(severity, code, message, path) {
|
|
|
607
756
|
return { severity, code, message, path };
|
|
608
757
|
}
|
|
609
758
|
|
|
759
|
+
// ../../packages/gem-framework/src/editions.ts
|
|
760
|
+
var GEM_EDITION_SUPPORT_MONTHS = 6;
|
|
761
|
+
var EDITION_2_CSS = [
|
|
762
|
+
`:where(&) { ${GEM_HEADING_SIZE_CSS_VARS.filter((decl) => !decl.startsWith("--gem-section-heading-size-mobile") && !decl.startsWith("--gem-hero-heading-size-mobile")).join(" ")} }`
|
|
763
|
+
];
|
|
764
|
+
var GEM_FRAMEWORK_EDITIONS = [
|
|
765
|
+
{ edition: 1, since: "0.1.0", released: "2026-07-04", changes: [], css: [] },
|
|
766
|
+
{
|
|
767
|
+
edition: 2,
|
|
768
|
+
since: "0.12.0",
|
|
769
|
+
released: "2026-10-10",
|
|
770
|
+
changes: ["Section headings and hero titles follow your type scale on tablets and computers. Phones keep their current size."],
|
|
771
|
+
css: EDITION_2_CSS
|
|
772
|
+
}
|
|
773
|
+
];
|
|
774
|
+
var LATEST_GEM_FRAMEWORK_EDITION = GEM_FRAMEWORK_EDITIONS[GEM_FRAMEWORK_EDITIONS.length - 1].edition;
|
|
775
|
+
function gemFrameworkEditionOf(value) {
|
|
776
|
+
const n = typeof value === "string" && /^\d+$/.test(value.trim()) ? Number(value.trim()) : value;
|
|
777
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 1) return 1;
|
|
778
|
+
return Math.min(n, LATEST_GEM_FRAMEWORK_EDITION);
|
|
779
|
+
}
|
|
780
|
+
function gemFrameworkEditionSupportedUntil(edition) {
|
|
781
|
+
const next = GEM_FRAMEWORK_EDITIONS.find((e) => e.edition === gemFrameworkEditionOf(edition) + 1);
|
|
782
|
+
return next ? addEditionSupportMonths(next.released) : null;
|
|
783
|
+
}
|
|
784
|
+
function addEditionSupportMonths(released) {
|
|
785
|
+
const [y, m, d] = released.split("-").map(Number);
|
|
786
|
+
const month = m - 1 + GEM_EDITION_SUPPORT_MONTHS;
|
|
787
|
+
const lastDay = new Date(Date.UTC(y, month + 1, 0)).getUTCDate();
|
|
788
|
+
return new Date(Date.UTC(y, month, Math.min(d, lastDay))).toISOString().slice(0, 10);
|
|
789
|
+
}
|
|
790
|
+
function gemFrameworkEditionScope(edition) {
|
|
791
|
+
return GEM_FRAMEWORK_EDITIONS.filter((e) => e.edition >= edition).map((e) => `[data-gem-edition='${e.edition}']`).join(", ");
|
|
792
|
+
}
|
|
793
|
+
function gemFrameworkEditionCss() {
|
|
794
|
+
return GEM_FRAMEWORK_EDITIONS.flatMap((e) => e.css.map((rule) => rule.replaceAll("&", gemFrameworkEditionScope(e.edition)))).join("\n");
|
|
795
|
+
}
|
|
796
|
+
|
|
610
797
|
// ../../packages/gem-framework/src/manifest.ts
|
|
611
798
|
var gemFrameworkPrimitiveGroups = [
|
|
612
799
|
{
|
|
@@ -1371,7 +1558,7 @@ function createGemThemeCssParts(tokens, selector = ":root", options = {}) {
|
|
|
1371
1558
|
const base = createGemBaseCssParts(options.profile ?? "dawn-parity");
|
|
1372
1559
|
return {
|
|
1373
1560
|
tokens: [
|
|
1374
|
-
createGemTokenCss(tokens, selector),
|
|
1561
|
+
createGemTokenCss(tokens, selector, { bandSchemes: options.bandSchemes }),
|
|
1375
1562
|
// Set the knobs the space / radius / border / type ramps read (only when they differ from the
|
|
1376
1563
|
// default 1, so a standard-shaped storefront emits nothing extra).
|
|
1377
1564
|
...createGemStyleKnobCss(options, selector),
|
|
@@ -1469,7 +1656,8 @@ function createGemBaseCssParts(profile) {
|
|
|
1469
1656
|
"}"
|
|
1470
1657
|
].join("\n");
|
|
1471
1658
|
return {
|
|
1472
|
-
|
|
1659
|
+
// Edition rules close the primitives (editions.ts): custom properties on <html>, scoped :where([data-gem-edition]).
|
|
1660
|
+
primitives: [reset, "", createPrimitiveCss(), "", gemFrameworkEditionCss(), "", ""].join("\n"),
|
|
1473
1661
|
sections: [createShellCss(), "", createSectionCss(), "", createThemeProfileCss(profile)].join("\n")
|
|
1474
1662
|
};
|
|
1475
1663
|
}
|
|
@@ -3114,8 +3302,14 @@ function createSectionCss() {
|
|
|
3114
3302
|
// rather than re-deriving the swatch in every divider rule.
|
|
3115
3303
|
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='default'] { --gem-section-bg: var(--gem-color-background); background: transparent; }",
|
|
3116
3304
|
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='surface'] { --gem-section-bg: var(--gem-color-surface); background: var(--gem-section-bg); padding-block: var(--gem-space-xl); }",
|
|
3117
|
-
|
|
3118
|
-
|
|
3305
|
+
// accent / dark are COLOUR SCHEMES (tokens.ts gemBandSchemeColors): the band re-points every --gem-color-* to its own
|
|
3306
|
+
// AA set, so a card, a button or a price nested in the band reads the band's colours instead of the page's. Before,
|
|
3307
|
+
// only the band's own background and `color` changed: 988 / 991 lines of text under AA across 248 GemFactory sections.
|
|
3308
|
+
// The CSS fallback tier sits next to the rules, so a page that sets --gem-color-* by hand still gets a band in its brand.
|
|
3309
|
+
GEM_BAND_SCHEME_FALLBACK_CSS,
|
|
3310
|
+
...GEM_BAND_SCHEMES.map(
|
|
3311
|
+
(band) => `:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='${band}'] { ${gemBandSchemeDeclarations(band)} --gem-section-bg: var(--gem-color-background); background: var(--gem-section-bg); color: var(--gem-color-text); --gem-band-bg: var(--gem-color-background); --gem-focus-ring: 2px solid var(--gem-color-text); padding-block: var(--gem-space-xl); }`
|
|
3312
|
+
),
|
|
3119
3313
|
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='light'] { --gem-section-bg: var(--gem-color-background); background: var(--gem-section-bg); color: var(--gem-color-text); padding-block: var(--gem-space-xl); }",
|
|
3120
3314
|
// On accent/dark bands, keep muted sub-copy + headings legible against the inverted ground.
|
|
3121
3315
|
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='accent'] :is(.gem-section__heading, .gem-section__body, .gem-section__eyebrow), :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='dark'] :is(.gem-section__heading, .gem-section__body, .gem-section__eyebrow) { color: inherit; }",
|
|
@@ -5885,12 +6079,13 @@ function gemFrameworkPublicCssVariables() {
|
|
|
5885
6079
|
return frameworkPublicVariables;
|
|
5886
6080
|
}
|
|
5887
6081
|
var ROOT_SELECTOR = /^\s*(?::root|html|:host)(?:\[[^\]]*\]|:[a-z-]+(?:\([^()]*\))?)*\s*$/;
|
|
6082
|
+
var EDITION_ROOT_SELECTOR = /^\s*:where\(\s*\[data-gem-edition=(["'])\d+\1\](?:\s*,\s*\[data-gem-edition=(["'])\d+\2\])*\s*\)\s*$/;
|
|
5888
6083
|
function gemPublicCssVariablesFrom(stylesheet) {
|
|
5889
6084
|
const css = stylesheet.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
5890
6085
|
const declaredOnRoot = /* @__PURE__ */ new Set();
|
|
5891
6086
|
const declaredElsewhere = /* @__PURE__ */ new Set();
|
|
5892
6087
|
for (const [, selector, body] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
5893
|
-
const onRoot = selector.split(",").every((part) => ROOT_SELECTOR.test(part));
|
|
6088
|
+
const onRoot = EDITION_ROOT_SELECTOR.test(selector) || selector.split(",").every((part) => ROOT_SELECTOR.test(part));
|
|
5894
6089
|
for (const [, name] of body.matchAll(/(--gem-[a-z0-9-]+)\s*:/g)) (onRoot ? declaredOnRoot : declaredElsewhere).add(name);
|
|
5895
6090
|
}
|
|
5896
6091
|
const mentioned = new Set([...css.matchAll(/--gem-[a-z0-9-]+/g)].map((match) => match[0]));
|
|
@@ -5991,9 +6186,11 @@ function renderGemPreviewDocument(plan, options = {}) {
|
|
|
5991
6186
|
const title = options.title ?? `${plan.name} preview`;
|
|
5992
6187
|
const formAttrs = Object.entries(gemFormAttributes(plan.tokens.form)).map(([k, v]) => ` ${k}="${escapeAttribute(v)}"`).join("");
|
|
5993
6188
|
const bodyClass = ["gem-theme", `gem-theme--${slugify(profile)}`, options.bodyClassName].filter(Boolean).join(" ");
|
|
6189
|
+
const edition = gemFrameworkEditionOf(plan.framework_edition);
|
|
6190
|
+
const editionAttr = edition > 1 ? ` data-gem-edition="${edition}"` : "";
|
|
5994
6191
|
return [
|
|
5995
6192
|
"<!doctype html>",
|
|
5996
|
-
`<html lang="en" data-gem-theme-profile="${escapeAttribute(profile)}">`,
|
|
6193
|
+
`<html lang="en" data-gem-theme-profile="${escapeAttribute(profile)}"${editionAttr}>`,
|
|
5997
6194
|
"<head>",
|
|
5998
6195
|
' <meta charset="utf-8" />',
|
|
5999
6196
|
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
@@ -7049,9 +7246,12 @@ function stringSetting(value) {
|
|
|
7049
7246
|
export {
|
|
7050
7247
|
COUNTDOWN_END_RUNTIME_LINES,
|
|
7051
7248
|
GEM_AMBIENT_CSS,
|
|
7249
|
+
GEM_BAND_SCHEMES,
|
|
7250
|
+
GEM_BAND_SCHEME_FALLBACK_CSS,
|
|
7052
7251
|
GEM_BUTTON_TOKEN_KEYS,
|
|
7053
7252
|
GEM_BUTTON_TOKEN_SHAPES,
|
|
7054
7253
|
GEM_COUNTDOWN_ENDED_CSS,
|
|
7254
|
+
GEM_EDITION_SUPPORT_MONTHS,
|
|
7055
7255
|
GEM_FIELD_JOINS,
|
|
7056
7256
|
GEM_FIELD_SHAPES,
|
|
7057
7257
|
GEM_FORM_CSS_VARIABLES,
|
|
@@ -7060,6 +7260,7 @@ export {
|
|
|
7060
7260
|
GEM_FORM_SET_CSS,
|
|
7061
7261
|
GEM_FORM_TOKEN_KEYS,
|
|
7062
7262
|
GEM_FORM_VAR_SCOPE,
|
|
7263
|
+
GEM_FRAMEWORK_EDITIONS,
|
|
7063
7264
|
GEM_GRID_COLUMNS_ROW_KNOB_CSS,
|
|
7064
7265
|
GEM_HEADING_SIZE_CSS_VARS,
|
|
7065
7266
|
GEM_LINE_HEIGHT_TOKEN_KEYS,
|
|
@@ -7069,11 +7270,15 @@ export {
|
|
|
7069
7270
|
GEM_ROW_CSS,
|
|
7070
7271
|
GEM_ROW_CSS_VARIABLES,
|
|
7071
7272
|
GEM_ROW_GAP_CSS,
|
|
7273
|
+
LATEST_GEM_FRAMEWORK_EDITION,
|
|
7274
|
+
MIN_TEXT_CONTRAST,
|
|
7072
7275
|
MODERN_MINERAL_FAMILY_PACK_ID,
|
|
7073
7276
|
MODERN_MINERAL_HEADER_FOOTER_PRESET_ID,
|
|
7074
7277
|
MODERN_MINERAL_HOMEPAGE_LINEAGE_SECTIONS,
|
|
7075
7278
|
MODERN_MINERAL_HOMEPAGE_PACK_ID,
|
|
7076
7279
|
MODERN_MINERAL_STYLE_PACK_ID,
|
|
7280
|
+
aaLabelColor,
|
|
7281
|
+
aaTextColor,
|
|
7077
7282
|
auditGemFrameworkClassName,
|
|
7078
7283
|
createGemBaseCss,
|
|
7079
7284
|
createGemFrameworkManifest,
|
|
@@ -7094,11 +7299,18 @@ export {
|
|
|
7094
7299
|
createThemeProfileCss,
|
|
7095
7300
|
defaultGemTokens,
|
|
7096
7301
|
defaultSelection,
|
|
7302
|
+
gemBandSchemeColors,
|
|
7303
|
+
gemBandSchemeCssVars,
|
|
7304
|
+
gemBandSchemeDeclarations,
|
|
7097
7305
|
gemButtonCaseCss,
|
|
7098
7306
|
gemFormAttributes,
|
|
7099
7307
|
gemFormCssBlock,
|
|
7100
7308
|
gemFormCssVars,
|
|
7101
7309
|
gemFormSetsFor,
|
|
7310
|
+
gemFrameworkEditionCss,
|
|
7311
|
+
gemFrameworkEditionOf,
|
|
7312
|
+
gemFrameworkEditionScope,
|
|
7313
|
+
gemFrameworkEditionSupportedUntil,
|
|
7102
7314
|
gemFrameworkPrimitiveGroups,
|
|
7103
7315
|
gemFrameworkRuntimePlugins,
|
|
7104
7316
|
gemFrameworkValidationRules,
|
|
@@ -7110,11 +7322,15 @@ export {
|
|
|
7110
7322
|
gemRuntimePluginModuleKeys,
|
|
7111
7323
|
gemRuntimePluginSources,
|
|
7112
7324
|
getGemFrameworkValidationRule,
|
|
7325
|
+
hexContrastRatio,
|
|
7326
|
+
hexRelativeLuminance,
|
|
7113
7327
|
isModernMineralFamilyLineagePlan,
|
|
7328
|
+
mixHex,
|
|
7114
7329
|
modernMineralFamilyLineage,
|
|
7115
7330
|
modernMineralHomepageLineageSections,
|
|
7116
7331
|
motionCssVarAliases,
|
|
7117
7332
|
normalizeGemTokens,
|
|
7333
|
+
normalizeHex,
|
|
7118
7334
|
offerEngineRuntimeScript,
|
|
7119
7335
|
optionalGemTokenKeys,
|
|
7120
7336
|
planToRuntimeBootstrapConfig,
|
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"]
|
|
@@ -242,7 +383,10 @@ var defaultGemTokens = {
|
|
|
242
383
|
accentText: "#ffffff",
|
|
243
384
|
border: "#d2d5d8",
|
|
244
385
|
success: "#008060",
|
|
245
|
-
|
|
386
|
+
// AA as TEXT on both defaults (gem-framework 0.10.1): #b7791f measured 3.36:1 on the background and 3.64:1 on white, and
|
|
387
|
+
// GemFactory sections set `color: var(--gem-color-warning)` on words; white text on a warning badge was 3.64:1 too.
|
|
388
|
+
// #9a661a keeps the hue at 4.52:1 / 4.90:1. default-colours-meet-aa.test.ts holds every foreground default to 4.5:1.
|
|
389
|
+
warning: "#9a661a",
|
|
246
390
|
danger: "#d72c0d"
|
|
247
391
|
},
|
|
248
392
|
typography: {
|
|
@@ -306,12 +450,14 @@ var defaultGemTokens = {
|
|
|
306
450
|
policyTone: "standard"
|
|
307
451
|
}
|
|
308
452
|
};
|
|
309
|
-
function createGemTokenCss(tokens, selector = ":root") {
|
|
453
|
+
function createGemTokenCss(tokens, selector = ":root", options = {}) {
|
|
310
454
|
const button = readButtonTokens(tokens);
|
|
311
455
|
const phone = readPhoneTokens(tokens);
|
|
312
456
|
const spacing = desktopEntries("spacing", tokens.spacing);
|
|
313
457
|
const declarations = [
|
|
314
458
|
...toCssVars("color", tokens.color),
|
|
459
|
+
// Bộ màu của băng dark / accent (gemBandSchemeColors): luật băng trỏ --gem-color-* vào đây.
|
|
460
|
+
...options.bandSchemes === false ? [] : gemBandSchemeCssVars(tokens.color),
|
|
315
461
|
...toCssVars("font", Object.fromEntries(Object.entries(button.groups.typography ?? tokens.typography ?? {}).filter(([k, v]) => !isRampKey(k) && !badLineHeight(k, v)))),
|
|
316
462
|
// Space ramp is scaled by the global density knob --gem-style-space (default 1),
|
|
317
463
|
// so a style pack can make the WHOLE storefront airier or denser without touching
|
|
@@ -354,10 +500,13 @@ function createGemTokenCss(tokens, selector = ":root") {
|
|
|
354
500
|
const formBlock = gemFormCssBlock(tokens.form);
|
|
355
501
|
const buttonCase = gemButtonCaseCss(tokens, selector);
|
|
356
502
|
const mobile = gemMobileTokenCssVars(tokens, phone);
|
|
503
|
+
const bandBorders = declarations.filter((line) => line.startsWith("--gem-border-") && line.includes("var(--gem-color-"));
|
|
504
|
+
const bandScope = splitSelectorList(selector).flatMap((sel) => GEM_BAND_SCHEMES.map((band) => `${sel} [data-gem-section-background='${band}']`)).join(", ");
|
|
357
505
|
return [
|
|
358
506
|
`${selector} {`,
|
|
359
507
|
...declarations.map((line) => ` ${line}`),
|
|
360
508
|
"}",
|
|
509
|
+
...bandBorders.length ? [`:where(${bandScope}) {`, ...bandBorders.map((line) => ` ${line}`), "}"] : [],
|
|
361
510
|
...mobile.length ? [`@media (max-width: ${GEM_TYPE_MOBILE_MAX}) {`, ` ${selector} {`, ...mobile.map((line) => ` ${line}`), " }", "}"] : [],
|
|
362
511
|
...formBlock ? [formBlock] : [],
|
|
363
512
|
...buttonCase ? [buttonCase] : []
|
|
@@ -440,6 +589,28 @@ function slugify(value) {
|
|
|
440
589
|
return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
441
590
|
}
|
|
442
591
|
|
|
592
|
+
// ../../packages/gem-framework/src/editions.ts
|
|
593
|
+
var EDITION_2_CSS = [
|
|
594
|
+
`:where(&) { ${GEM_HEADING_SIZE_CSS_VARS.filter((decl) => !decl.startsWith("--gem-section-heading-size-mobile") && !decl.startsWith("--gem-hero-heading-size-mobile")).join(" ")} }`
|
|
595
|
+
];
|
|
596
|
+
var GEM_FRAMEWORK_EDITIONS = [
|
|
597
|
+
{ edition: 1, since: "0.1.0", released: "2026-07-04", changes: [], css: [] },
|
|
598
|
+
{
|
|
599
|
+
edition: 2,
|
|
600
|
+
since: "0.12.0",
|
|
601
|
+
released: "2026-10-10",
|
|
602
|
+
changes: ["Section headings and hero titles follow your type scale on tablets and computers. Phones keep their current size."],
|
|
603
|
+
css: EDITION_2_CSS
|
|
604
|
+
}
|
|
605
|
+
];
|
|
606
|
+
var LATEST_GEM_FRAMEWORK_EDITION = GEM_FRAMEWORK_EDITIONS[GEM_FRAMEWORK_EDITIONS.length - 1].edition;
|
|
607
|
+
function gemFrameworkEditionScope(edition) {
|
|
608
|
+
return GEM_FRAMEWORK_EDITIONS.filter((e) => e.edition >= edition).map((e) => `[data-gem-edition='${e.edition}']`).join(", ");
|
|
609
|
+
}
|
|
610
|
+
function gemFrameworkEditionCss() {
|
|
611
|
+
return GEM_FRAMEWORK_EDITIONS.flatMap((e) => e.css.map((rule) => rule.replaceAll("&", gemFrameworkEditionScope(e.edition)))).join("\n");
|
|
612
|
+
}
|
|
613
|
+
|
|
443
614
|
// ../../packages/gem-framework/src/manifest.ts
|
|
444
615
|
var gemFrameworkPrimitiveGroups = [
|
|
445
616
|
{
|
|
@@ -1065,7 +1236,7 @@ function createGemThemeCssParts(tokens, selector = ":root", options = {}) {
|
|
|
1065
1236
|
const base = createGemBaseCssParts(options.profile ?? "dawn-parity");
|
|
1066
1237
|
return {
|
|
1067
1238
|
tokens: [
|
|
1068
|
-
createGemTokenCss(tokens, selector),
|
|
1239
|
+
createGemTokenCss(tokens, selector, { bandSchemes: options.bandSchemes }),
|
|
1069
1240
|
// Set the knobs the space / radius / border / type ramps read (only when they differ from the
|
|
1070
1241
|
// default 1, so a standard-shaped storefront emits nothing extra).
|
|
1071
1242
|
...createGemStyleKnobCss(options, selector),
|
|
@@ -1156,7 +1327,8 @@ function createGemBaseCssParts(profile) {
|
|
|
1156
1327
|
"}"
|
|
1157
1328
|
].join("\n");
|
|
1158
1329
|
return {
|
|
1159
|
-
|
|
1330
|
+
// Edition rules close the primitives (editions.ts): custom properties on <html>, scoped :where([data-gem-edition]).
|
|
1331
|
+
primitives: [reset, "", createPrimitiveCss(), "", gemFrameworkEditionCss(), "", ""].join("\n"),
|
|
1160
1332
|
sections: [createShellCss(), "", createSectionCss(), "", createThemeProfileCss(profile)].join("\n")
|
|
1161
1333
|
};
|
|
1162
1334
|
}
|
|
@@ -2801,8 +2973,14 @@ function createSectionCss() {
|
|
|
2801
2973
|
// rather than re-deriving the swatch in every divider rule.
|
|
2802
2974
|
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='default'] { --gem-section-bg: var(--gem-color-background); background: transparent; }",
|
|
2803
2975
|
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='surface'] { --gem-section-bg: var(--gem-color-surface); background: var(--gem-section-bg); padding-block: var(--gem-space-xl); }",
|
|
2804
|
-
|
|
2805
|
-
|
|
2976
|
+
// accent / dark are COLOUR SCHEMES (tokens.ts gemBandSchemeColors): the band re-points every --gem-color-* to its own
|
|
2977
|
+
// AA set, so a card, a button or a price nested in the band reads the band's colours instead of the page's. Before,
|
|
2978
|
+
// only the band's own background and `color` changed: 988 / 991 lines of text under AA across 248 GemFactory sections.
|
|
2979
|
+
// The CSS fallback tier sits next to the rules, so a page that sets --gem-color-* by hand still gets a band in its brand.
|
|
2980
|
+
GEM_BAND_SCHEME_FALLBACK_CSS,
|
|
2981
|
+
...GEM_BAND_SCHEMES.map(
|
|
2982
|
+
(band) => `:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='${band}'] { ${gemBandSchemeDeclarations(band)} --gem-section-bg: var(--gem-color-background); background: var(--gem-section-bg); color: var(--gem-color-text); --gem-band-bg: var(--gem-color-background); --gem-focus-ring: 2px solid var(--gem-color-text); padding-block: var(--gem-space-xl); }`
|
|
2983
|
+
),
|
|
2806
2984
|
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='light'] { --gem-section-bg: var(--gem-color-background); background: var(--gem-section-bg); color: var(--gem-color-text); padding-block: var(--gem-space-xl); }",
|
|
2807
2985
|
// On accent/dark bands, keep muted sub-copy + headings legible against the inverted ground.
|
|
2808
2986
|
":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='accent'] :is(.gem-section__heading, .gem-section__body, .gem-section__eyebrow), :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='dark'] :is(.gem-section__heading, .gem-section__body, .gem-section__eyebrow) { color: inherit; }",
|
|
@@ -4341,12 +4519,13 @@ function gemFrameworkPublicCssVariables() {
|
|
|
4341
4519
|
return frameworkPublicVariables;
|
|
4342
4520
|
}
|
|
4343
4521
|
var ROOT_SELECTOR = /^\s*(?::root|html|:host)(?:\[[^\]]*\]|:[a-z-]+(?:\([^()]*\))?)*\s*$/;
|
|
4522
|
+
var EDITION_ROOT_SELECTOR = /^\s*:where\(\s*\[data-gem-edition=(["'])\d+\1\](?:\s*,\s*\[data-gem-edition=(["'])\d+\2\])*\s*\)\s*$/;
|
|
4344
4523
|
function gemPublicCssVariablesFrom(stylesheet) {
|
|
4345
4524
|
const css = stylesheet.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
4346
4525
|
const declaredOnRoot = /* @__PURE__ */ new Set();
|
|
4347
4526
|
const declaredElsewhere = /* @__PURE__ */ new Set();
|
|
4348
4527
|
for (const [, selector, body] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
4349
|
-
const onRoot = selector.split(",").every((part) => ROOT_SELECTOR.test(part));
|
|
4528
|
+
const onRoot = EDITION_ROOT_SELECTOR.test(selector) || selector.split(",").every((part) => ROOT_SELECTOR.test(part));
|
|
4350
4529
|
for (const [, name] of body.matchAll(/(--gem-[a-z0-9-]+)\s*:/g)) (onRoot ? declaredOnRoot : declaredElsewhere).add(name);
|
|
4351
4530
|
}
|
|
4352
4531
|
const mentioned = new Set([...css.matchAll(/--gem-[a-z0-9-]+/g)].map((match) => match[0]));
|
package/dist/sections.css
CHANGED
|
@@ -610,8 +610,9 @@ sticky-header.header-wrapper {
|
|
|
610
610
|
:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-grid-columns='4'] .gem-row:is(.gem-section__grid, [class*='__grid']):not(.gem-row .gem-row) { --gem-row-cols: 4; --gem-row-cols-md: 4; --gem-row-tracks: initial; --gem-row-tracks-md: initial; }
|
|
611
611
|
:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='default'] { --gem-section-bg: var(--gem-color-background); background: transparent; }
|
|
612
612
|
:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-section-background='surface'] { --gem-section-bg: var(--gem-color-surface); background: var(--gem-section-bg); padding-block: var(--gem-space-xl); }
|
|
613
|
-
:
|
|
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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
--gem-color-accent-text: #ffffff;
|
|
8
8
|
--gem-color-border: #d2d5d8;
|
|
9
9
|
--gem-color-success: #008060;
|
|
10
|
-
--gem-color-warning: #
|
|
10
|
+
--gem-color-warning: #9a661a;
|
|
11
11
|
--gem-color-danger: #d72c0d;
|
|
12
12
|
--gem-font-body: Inter, system-ui, sans-serif;
|
|
13
13
|
--gem-font-heading: Inter, system-ui, sans-serif;
|
|
@@ -64,4 +64,8 @@
|
|
|
64
64
|
--gem-tap-min: 44px;
|
|
65
65
|
--gem-focus-offset: 3px;
|
|
66
66
|
}
|
|
67
|
+
:where(:root [data-gem-section-background='dark'], :root [data-gem-section-background='accent']) {
|
|
68
|
+
--gem-border-subtle: calc(1px * var(--gem-style-border, 1)) solid var(--gem-color-border);
|
|
69
|
+
--gem-border-strong: calc(1px * var(--gem-style-border, 1)) solid color-mix(in srgb, var(--gem-color-text) 28%, transparent);
|
|
70
|
+
}
|
|
67
71
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemboss/gem-framework",
|
|
3
|
-
"version": "0.
|
|
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",
|