@gemboss/gem-framework 0.13.0 → 0.15.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 +52 -2
- package/dist/SOURCE.json +3 -3
- package/dist/editions.js +26 -1
- package/dist/gem-framework.css +31 -0
- package/dist/gem-runtime.js +32 -1
- package/dist/index.js +130 -7
- package/dist/plugins/img-in.d.ts +9 -0
- package/dist/plugins/img-in.js +47 -0
- package/dist/primitives.css +11 -0
- package/dist/section-gate.js +83 -6
- package/dist/sections.js +155 -0
- package/dist/tokens.css +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,10 +6,10 @@ gem-framework của GemTheme đóng thành package, cho các repo **ngoài** mon
|
|
|
6
6
|
| Cổng vào | Là gì | Nguồn |
|
|
7
7
|
|---|---|---|
|
|
8
8
|
| `@gemboss/gem-framework` | token, `createGemThemeCss`, runtime, manifest, bộ kiểm framework. Từ 0.3.4 manifest khai họ `gem-cart-line-item` (khối, `__media`, `__details`, `__title`, `__variant`, `__plan`, `__meta`, `__remove`, `__quantity`, `__price`): framework vốn style và adapter Shopify vốn render nó, nên section dùng markup dòng giỏ của framework không còn bị `unknown_gem_class` | `packages/gem-framework/src` |
|
|
9
|
-
| `…/sections` |
|
|
9
|
+
| `…/sections` | Các hàm `headless.tsx` được import: `elementOverrides`, `imageSrcSet`, `subscriptionPlans`, `defaultPlan`; từ 0.15.0 thêm bộ đọc nhấn trong câu `inlineMarkRuns`, `phraseMarkRuns`, `markClassName`, `inlineMarkText`, `safeMarkHref`, `MARK_KINDS` | `packages/gem-sections/src` |
|
|
10
10
|
| `…/section-gate` | `validateSectionArtifacts`: ĐÚNG bộ kiểm section gemtheme dùng khi duyệt. Từ 0.3.0 thêm ba cổng của thư viện cho MỘT section: `layoutKnobCheck` (+ `LAYOUT_ATTRS`), `slotContractDrift` (+ `slotsReadBy`, `slotSourceFromElementTree`), `cssPairingOrphans` (+ `classesStyledBy`, truyền CSS từ `createGemThemeCss`). Từ 0.3.2 thêm hai cổng pháp lý: `fabricatedProofInSource` / `fabricatedProofInSchema` (điểm, số lượt, người, lời hứa bịa) và `inventedTestimonyIn` (hằng DEMO_*_QUOTE/REVIEW… làm giá trị khi thiếu dữ liệu). Từ 0.3.3 hai cổng này đọc được tiếng Việt ("4,8/5", "đã bán", "miễn phí vận chuyển", "199k", cả chữ NFD), tra giá trị của `DEMO.<key>` ở mọi chỗ đọc nó (`??`, `\|\|`, toán tử ba ngôi, dòng bị Prettier ngắt), và bỏ qua số lượng hiển thị (`reviews_to_show`…). Nâng từ 0.3.2 lên có thể làm đỏ section trước đó xanh: đó là lời khẳng định nằm trong DEMO. Từ 0.3.5 bắt thêm chữ ký người Việt (tên kèm nơi chốn như "Chị Lan, Hà Nội", hoặc họ + Thị/Văn như "Nguyễn Thị Hoa"; tiêu đề viết hoa từng chữ thì không), điểm sao ("4,9 ★"), `rating: 4.9` / `reviewCount: "1.284"` trong mảng sản phẩm mẫu, và hằng demo ở nhánh else của toán tử ba ngôi (`bound ? [] : DEMO_REVIEWS`). Mỗi thứ bịa chỉ báo MỘT lần: tại dòng DEMO nếu dòng đó tự lộ, không thì tại chỗ đọc. "đồng hồ" không còn bị đọc là giá. Từ 0.3.6 bắt lời hứa có thời hạn đứng sau từ khoá ("Dùng thử 30 ngày", "Bảo hành 12 tháng", "Nhận trong 2–4 ngày", "Giao 2 giờ") và "hoàn tiền" không kèm số; "Chính sách hoàn tiền" vẫn là nhãn. Từ 0.3.7 câu hỏi (kết thúc bằng "?") không bao giờ là lời hứa, "không hợp lệ"/số bước/nhịp giao không bị báo, và bắt thêm "trong vòng N ngày", "Giao hàng 2–4 ngày", "Bảo hành 1 đổi 1…", "Try it free for 30 days", "Ships in 24h". Từ 0.3.8 xuất thêm `claimIn` (câu hỏi "một chuỗi này có phải lời khẳng định, người bịa, hay lời hứa?"). Từ 0.3.9 bắt "cancel anytime", "guaranteed delivery", "huỷ bất cứ lúc nào" (chính sách huỷ gói và giao hàng). Từ 0.3.10 lời từ chối ("cannot be canceled", "Không thể hủy") không bị báo, "giao dịch/giao diện" không phải giao hàng, "Delivery not guaranteed" là lời miễn trừ, một chuỗi chỉ gồm con số viết tắt ("199k", "12k+") mang nhãn trung tính "hardcoded figure" (không phân biệt được giá với số đếm), và `stringLiteralsIn(line)` thay cho hằng `STRING_LITERAL` (đã gỡ: một regex `/g` dùng chung rò `lastIndex`; 0 chỗ dùng ở gemboss/gemfactory/gemsite). Từ 0.3.11 "Huỷ bỏ lọc…" và "Hủy đơn, gọi chúng tôi bất cứ lúc nào" không bị đọc là chính sách huỷ; "huỷ đăng ký" là bỏ nhận tin (như "Unsubscribe anytime"); `SOURCE.json` ghi `gemtheme_dirty`, và `npm pack` từ chối khi ĐẦU VÀO của gói (`packages/`, gói này trừ `package.json`, `scripts/lib/`) có thay đổi chưa commit hoặc git không trả lời được. Từ 0.3.12 bắt lời hứa hoàn tiền không có chữ "hoàn tiền" liền nhau ("nhận lại toàn bộ tiền", "Hoàn lại 100% tiền", "Trả lại tiền nếu không hài lòng"); lời từ chối ("Không được hoàn lại tiền") và tiền thừa/thối khi thanh toán không bị bắt Từ 0.4.0: `demoOutsidePreview(tsx)` (chữ mẫu DEMO chỉ hiện trong bản xem trước, tức khi `section.demo === true`; plan xuất bản không bao giờ mang cờ đó) và schema `SectionSpec` khai trường `demo` | `packages/gem-sections/src/section-artifacts.ts`, `section-contract.ts` |
|
|
11
11
|
| `…/settings-schema` | `validateSettingsSchema` | `scripts/lib/settings-schema-contract.mjs` |
|
|
12
|
-
| `…/gem-framework.css`, `…/gem-runtime.js` | CSS token mặc định + runtime mọi plugin, dựng sẵn. `gem-runtime.js` tự chạy khi tải: đăng ký và lên lịch đủ
|
|
12
|
+
| `…/gem-framework.css`, `…/gem-runtime.js` | CSS token mặc định + runtime mọi plugin, dựng sẵn. `gem-runtime.js` tự chạy khi tải: đăng ký và lên lịch đủ 23 plugin | build |
|
|
13
13
|
| `…/tokens.css`, `…/primitives.css`, `…/sections.css`, `…/popup-shell.css` | Từ 0.8.0: `gem-framework.css` cắt ba (từ 0.8.3 cắt bốn), nối lại theo đúng thứ tự đó thì bằng `gem-framework.css` từng byte (build dừng nếu không bằng). `tokens.css`: biến `:root` + núm style. `primitives.css`: reset, phần tử nền, tiện ích, mọi primitive `.gem-*`. `sections.css`: khung header/footer, lớp section chung, profile theme. `popup-shell.css` (0.8.3): khung popup `.gem-popup-shell`, chỉ trang có popup mới cần tải, đặt sau `primitives.css` và trước `sections.css` (thứ tự nối: tokens, primitives, popup-shell, sections). CSS riêng của từng section (`section-library/**/gem.css`) KHÔNG nằm trong gói | `createGemThemeCssParts` |
|
|
14
14
|
| `…/plugins/<key>` (`…/plugins/<key>.js`), `…/runtime-core` | Từ 0.8.0: mỗi plugin của runtime là một ES module, `export function mount(root = document, options?)` trả về `unmount()`, import không làm gì. Cùng dòng mã với `gem-runtime.js` (một bảng trong `runtime.ts`), chỉ khác: mọi `x.addEventListener(` thành `__gemListen(x, ` để unmount gỡ được. `runtime-core.js` là phần dùng chung: `mountPlugin`, `createGemApi`, `gemApi` | `runtime.ts`, `runtime-modules.ts`, `runtime-core.js` |
|
|
15
15
|
|
|
@@ -93,6 +93,56 @@ 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.15.0** (11/10/2026): CHỈ THÊM, store đang chạy không đổi pixel nào (chưa trang nào mang các lớp mới). Gap #39: **nhấn
|
|
97
|
+
trong câu**. Chữ advertorial sống nhờ cách nhấn (kho GemFactory `research/advertorial-blocks-2026-10-10`: souleora 84 cụm đậm,
|
|
98
|
+
5 nghiêng, 4 đổi màu, 1 gạch chân trong 152 đoạn; 28pilates chốt thẻ "đừng mua" bằng câu đỏ, tô màu nhấn cụm kết quả trong
|
|
99
|
+
lời khách; 29% store advertorial dẫn đi mua bằng link trong đoạn), mà element `text` / `heading` chỉ đậm được, và ba section
|
|
100
|
+
của kho mỗi cái chép một bộ đọc dấu riêng.
|
|
101
|
+
- `…/sections` (và `@gemtheme/gem-sections`): `inlineMarkRuns(chuỗi)` đọc `**đậm**`, `*nghiêng*`, `***đậm nghiêng***`,
|
|
102
|
+
`__gạch chân__`, `==tô sáng==`, `^^đậm màu nhấn^^`, `!!màu cảnh báo!!`, `[link](url)`, lồng được hai chiều (đậm trong
|
|
103
|
+
nghiêng, nghiêng trong đậm). Dấu chỉ mở / đóng sát chữ (không sát khoảng trắng), nên dấu câu thường giữ nguyên ("Hurry!!
|
|
104
|
+
Only 3 left!!", "^^", "a == b"); `__` không mở giữa từ (URL `a__b__c`); `\` giữ ký tự dấu nguyên văn; đọc từng dòng (dấu không
|
|
105
|
+
vắt qua xuống dòng), một dòng dài hơn 20.000 ký tự vẽ trơn; tuyến tính trên chuỗi xấu (dấu `[` lặp, đậm không đóng). `phraseMarkRuns(chữ,
|
|
106
|
+
{ bold, italic, underline, highlight, accent, alert, links })` là cách của element: chữ vẫn trơn, mỗi dấu là danh sách
|
|
107
|
+
cụm từ (mỗi dòng một cụm, đặt ở chỗ xuất hiện ĐẦU TIÊN không chồng cụm cùng dấu, đúng luật `bold` đang dùng nên
|
|
108
|
+
`{ bold }` vẽ y như trước), link là dòng `cụm | url`. Cả hai trả về DỮ LIỆU (đoạn chữ + dấu + href), người gọi tự vẽ
|
|
109
|
+
`strong` / `em` / `span.gem-mark--…` / `a.gem-mark-link`: gói không kéo React, không chèn HTML; link chỉ giữ http(s),
|
|
110
|
+
đường dẫn trong site và #neo. `markClassName(marks)`, `inlineMarkText(chuỗi)` (chữ bỏ dấu), `safeMarkHref`.
|
|
111
|
+
- CSS (`GEM_INLINE_MARK_CSS`, trong `createPrimitiveCss`): `.gem-mark--underline`, `--highlight` (bút dạ: nền
|
|
112
|
+
`--gem-mark-highlight` #fff1a6, mực `--gem-mark-highlight-ink` #1f1d18, đọc được cả trên băng tối), `--accent`, `--alert`
|
|
113
|
+
(`--gem-color-danger`) trong `:where()` (độ ưu tiên 0: section muốn dáng riêng tự đè); `.gem-mark-link` (màu nhấn, gạch
|
|
114
|
+
chân) là một lớp thường, vì `a { color: inherit }` của framework và gemsite thắng mọi thứ trong `:where()`; link nằm
|
|
115
|
+
trong / chứa tô sáng giữ mực của bút dạ. Sáu lớp khai
|
|
116
|
+
trong manifest (nhóm content), nên section vẽ chúng không bị `unknown_gem_class`.
|
|
117
|
+
- Người dùng: gemboss phải thêm các tên mới vào requireShim của bản xem trước section; gemsite chép `inline-marks.ts`
|
|
118
|
+
byte-identical (cặp kiểm của `check-elements-copied-from-gemtheme`) rồi vẽ dấu cho element `text` / `heading`.
|
|
119
|
+
- **0.14.0** (11/10/2026): CHỈ THÊM, store đang chạy không đổi pixel nào (không section nào của gemtheme, gemsite, gemboss
|
|
120
|
+
đọc các tên dưới đây; 30 mục kho GemFactory tự khai thang dùng tên riêng theo mục). Hai gap của bộ interaction "Gọn"
|
|
121
|
+
(Lily 08/10/2026) mà kho đang chép tay vào từng mục:
|
|
122
|
+
(1) **Thang chuyển động (#32).** `createGemTokenCss` luôn phát `--gem-motion-fast` 100ms, `-base` 180ms, `-slow` 260ms,
|
|
123
|
+
`--gem-ease-enter` / `--gem-ease-exit`, `--gem-motion-distance` 8px, `-distance-lg` 16px, `-stagger` 30ms, `-blur` 8px
|
|
124
|
+
(`GEM_MOTION_SCALE_DEFAULTS`, `gemMotionScaleCssVars`); plan đổi được qua `motion.fast|base|slow|enter|exit|distance|
|
|
125
|
+
distanceLg|stagger|blur`, viết kiểu nào cũng được (`Slow`, `distance_lg`): mặc định chỉ phát cho biến plan chưa khai.
|
|
126
|
+
Plan có `motion.duration` bằng 0 (style pack `motion_level: "none"`) nhận thang tĩnh. Kèm một khối
|
|
127
|
+
`@media (prefers-reduced-motion: reduce)` cùng selector đưa thời lượng về 1ms (sự kiện end vẫn bắn), quãng, nhịp so le
|
|
128
|
+
và độ nhoè về 0 (`GEM_MOTION_REDUCED`), mang `!important` ở cuối khai báo để stylesheet đến sau khai thang trên `:root`
|
|
129
|
+
không đè được lựa chọn của người dùng. Bộ kiểm CSS nhận các biến này là biến chuẩn. Bí danh motion giờ tra theo slug
|
|
130
|
+
(`motionCssVarName(key)`, dùng hàm này thay vì tra `motionCssVarAliases` theo đúng chữ):
|
|
131
|
+
`easing_soft`, `ambient-slow`, `Enter` ra đúng `--gem-ease-soft`, `--gem-ambient-slow`, `--gem-ease-enter` (trước đó ra
|
|
132
|
+
`--gem-motion-easing-soft`…, biến không rule nào đọc).
|
|
133
|
+
(2) **Ảnh hiện dần (#38).** `data-gem-img-in` trên `<img>` hoặc trên khung chứa (áp cho mọi ảnh bên trong). Plugin mới
|
|
134
|
+
`img-in` (critical, `gem:img-in:ready` mang số ảnh đã gắn) đánh dấu TỪNG ảnh (`data-gem-bound-img-in`) chưa tải xong lúc
|
|
135
|
+
gắn và đặt `data-gem-img-in-state="loading"`; tải xong `"in"`, hết hoạt ảnh `gem-img-in` thì gỡ (ảnh hiện lại từ
|
|
136
|
+
display:none không chạy lại), lỗi thì gỡ. Tải xong mà trạng thái nhoè không thực sự hiện (luật section ưu tiên cao hơn,
|
|
137
|
+
reveal đặt sau) hoặc người dùng giảm chuyển động thì gỡ thẳng, không chạy hoạt ảnh. Đứng ngoài: ảnh đã tải sẵn, và ảnh
|
|
138
|
+
section đã tự đặt opacity / filter / animation (ảnh phụ chờ rê chuột, hero tối, `data-gem-ambient`). Một listener pha
|
|
139
|
+
capture trên document, nên ảnh nhân bản lúc đang tải (marquee, slider vô hạn) không kẹt nhoè. Khung lồng khung gắn một
|
|
140
|
+
lần. CSS (`GEM_IMG_IN_CSS`, trong `createPrimitiveCss`) nằm trong `:where()` (độ ưu tiên 0) và chỉ khớp ảnh còn dấu, nên
|
|
141
|
+
unmount lúc ảnh đang tải không để ảnh kẹt nhoè; mount lại xoá trạng thái sót trên ảnh đã tải. Gắn MỘT lần: `gem-runtime.js`
|
|
142
|
+
không quét lại, ảnh do plugin khác chèn sau hiện như thường; bản module gắn nội dung mới bằng `mount(gốc mới)`, kể cả
|
|
143
|
+
khi vùng bật là tổ tiên ngoài gốc. Không JS thì ảnh hiện như thường. `runtimePluginKeySchema` (`@gemtheme/schemas`) có
|
|
144
|
+
thêm `img-in`; runtime đủ 23 plugin. Planner chưa lên lịch img-in (danh sách khoá của planner là bản chép tay, thiếu cả
|
|
145
|
+
tabs, hero-slideshow, reveal-on-scroll, offer-engine): store nhận plugin qua `gem-runtime.js` hoặc module.
|
|
96
146
|
- **0.13.0** (11/10/2026): kiểu nút **`gradient`** (`plan.global_settings.button_style`, `<body data-gem-button-style="gradient">`).
|
|
97
147
|
GemFactory Global Style đo 339 trang store hàng đầu: nút mua chính là gradient ở 8,4% (10,6% trang landing), 16/21 cái đo
|
|
98
148
|
được là MỘT tông đi từ sáng sang sẫm, và 14/21 có chữ dưới 4,5:1. Đầu kia của gradient là màu nhấn dời 25%
|
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.15.0",
|
|
4
|
+
"gemtheme_sha": "c69c7c6e8d2c2dcf1543170990f6a644805c4dd8",
|
|
5
5
|
"gemtheme_dirty": false,
|
|
6
|
-
"framework_version": "0.
|
|
6
|
+
"framework_version": "0.15.0"
|
|
7
7
|
}
|
package/dist/editions.js
CHANGED
|
@@ -71,11 +71,24 @@ var PHONE_TARGET_BY_KEY = Object.fromEntries(
|
|
|
71
71
|
);
|
|
72
72
|
var GEM_LINE_HEIGHT_TOKEN_KEYS = ["headingLineHeight", "bodyLineHeight"];
|
|
73
73
|
var GEM_TYPE_RAMP_KEYS = ["base", "scale", "baseMobile"];
|
|
74
|
+
var GEM_MOTION_SCALE_DEFAULTS = {
|
|
75
|
+
fast: "100ms",
|
|
76
|
+
base: "180ms",
|
|
77
|
+
slow: "260ms",
|
|
78
|
+
enter: "cubic-bezier(.33, 1, .68, 1)",
|
|
79
|
+
exit: "cubic-bezier(.4, 0, 1, 1)",
|
|
80
|
+
distance: "8px",
|
|
81
|
+
distanceLg: "16px",
|
|
82
|
+
stagger: "30ms",
|
|
83
|
+
blur: "8px"
|
|
84
|
+
};
|
|
74
85
|
var optionalGemTokenKeys = {
|
|
75
86
|
typography: [...GEM_TYPE_RAMP_KEYS, ...GEM_LINE_HEIGHT_TOKEN_KEYS, ...GEM_BUTTON_TOKEN_KEYS.typography],
|
|
76
87
|
spacing: [...Object.keys(GEM_DERIVED_SPACE_STEPS), ...GEM_PHONE_TOKEN_TARGETS.spacing.map((k) => `${k}Mobile`)],
|
|
77
88
|
layout: GEM_PHONE_TOKEN_TARGETS.layout.map((k) => `${k}Mobile`),
|
|
78
|
-
radius: [...GEM_BUTTON_TOKEN_KEYS.radius]
|
|
89
|
+
radius: [...GEM_BUTTON_TOKEN_KEYS.radius],
|
|
90
|
+
// Khoá motion ngoài duration / easing: nền ambient (đã phát từ trước nhưng chưa khai ở đây) và thang "Gọn" (#32).
|
|
91
|
+
motion: ["easingSoft", "ambientSlow", "ambientMid", "ambientFast", "ambientAmp", ...Object.keys(GEM_MOTION_SCALE_DEFAULTS)]
|
|
79
92
|
};
|
|
80
93
|
var GEM_HEADING_SIZE_CSS_VARS = [
|
|
81
94
|
"--gem-section-heading-size: calc(var(--gem-h2) * var(--gem-typography-heading-scale, 1));",
|
|
@@ -83,6 +96,18 @@ var GEM_HEADING_SIZE_CSS_VARS = [
|
|
|
83
96
|
"--gem-section-heading-size-mobile: min(var(--gem-section-heading-size), var(--gem-heading-phone-cap, clamp(1.75rem, 9vw, 2.75rem)));",
|
|
84
97
|
"--gem-hero-heading-size-mobile: min(var(--gem-hero-heading-size), var(--gem-heading-phone-cap, clamp(1.75rem, 9vw, 2.75rem)));"
|
|
85
98
|
];
|
|
99
|
+
var motionCssVarAliases = {
|
|
100
|
+
easingSoft: "--gem-ease-soft",
|
|
101
|
+
enter: "--gem-ease-enter",
|
|
102
|
+
exit: "--gem-ease-exit",
|
|
103
|
+
ambientSlow: "--gem-ambient-slow",
|
|
104
|
+
ambientMid: "--gem-ambient-mid",
|
|
105
|
+
ambientFast: "--gem-ambient-fast",
|
|
106
|
+
ambientAmp: "--gem-ambient-amp"
|
|
107
|
+
};
|
|
108
|
+
var motionCssVarAliasBySlug = Object.fromEntries(
|
|
109
|
+
Object.entries(motionCssVarAliases).map(([key, name]) => [slugify(key), name])
|
|
110
|
+
);
|
|
86
111
|
function slugify(value) {
|
|
87
112
|
return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
88
113
|
}
|
package/dist/gem-framework.css
CHANGED
|
@@ -37,6 +37,15 @@
|
|
|
37
37
|
--gem-ambient-mid: 7s;
|
|
38
38
|
--gem-ambient-fast: 5.5s;
|
|
39
39
|
--gem-ambient-amp: 7px;
|
|
40
|
+
--gem-motion-fast: 100ms;
|
|
41
|
+
--gem-motion-base: 180ms;
|
|
42
|
+
--gem-motion-slow: 260ms;
|
|
43
|
+
--gem-ease-enter: cubic-bezier(.33, 1, .68, 1);
|
|
44
|
+
--gem-ease-exit: cubic-bezier(.4, 0, 1, 1);
|
|
45
|
+
--gem-motion-distance: 8px;
|
|
46
|
+
--gem-motion-distance-lg: 16px;
|
|
47
|
+
--gem-motion-stagger: 30ms;
|
|
48
|
+
--gem-motion-blur: 8px;
|
|
40
49
|
--gem-layout-page: 1180px;
|
|
41
50
|
--gem-layout-gutter: 32px;
|
|
42
51
|
--gem-layout-section: 64px;
|
|
@@ -68,6 +77,17 @@
|
|
|
68
77
|
--gem-border-subtle: calc(1px * var(--gem-style-border, 1)) solid var(--gem-color-border);
|
|
69
78
|
--gem-border-strong: calc(1px * var(--gem-style-border, 1)) solid color-mix(in srgb, var(--gem-color-text) 28%, transparent);
|
|
70
79
|
}
|
|
80
|
+
@media (prefers-reduced-motion: reduce) {
|
|
81
|
+
:root {
|
|
82
|
+
--gem-motion-fast: 1ms !important;
|
|
83
|
+
--gem-motion-base: 1ms !important;
|
|
84
|
+
--gem-motion-slow: 1ms !important;
|
|
85
|
+
--gem-motion-distance: 0px !important;
|
|
86
|
+
--gem-motion-distance-lg: 0px !important;
|
|
87
|
+
--gem-motion-stagger: 0ms !important;
|
|
88
|
+
--gem-motion-blur: 0px !important;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
71
91
|
|
|
72
92
|
*, *::before, *::after {
|
|
73
93
|
box-sizing: border-box;
|
|
@@ -1100,6 +1120,17 @@ a {
|
|
|
1100
1120
|
[data-gem-reveal='zoom'][data-gem-reveal-state='ready'] { opacity: 0; transform: scale(0.94); }
|
|
1101
1121
|
[data-gem-reveal='blur'][data-gem-reveal-state='ready'] { opacity: 0; filter: blur(10px); transform: none; }
|
|
1102
1122
|
@media (prefers-reduced-motion: reduce) { [data-gem-reveal] { opacity: 1 !important; transform: none !important; filter: none !important; transition: none; } }
|
|
1123
|
+
:where(img[data-gem-bound-img-in][data-gem-img-in-state='loading']) { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; }
|
|
1124
|
+
@media (prefers-reduced-motion: no-preference) { :where(img[data-gem-bound-img-in][data-gem-img-in-state='in']) { animation: gem-img-in var(--gem-motion-slow, 260ms) var(--gem-ease-enter, cubic-bezier(.33, 1, .68, 1)); } }
|
|
1125
|
+
@keyframes gem-img-in { from { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; } }
|
|
1126
|
+
:where(.gem-mark--underline) { text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }
|
|
1127
|
+
:where(.gem-mark--highlight) { background: var(--gem-mark-highlight, #fff1a6); color: var(--gem-mark-highlight-ink, #1f1d18); padding-inline: 0.12em; border-radius: 0.15em; -webkit-box-decoration-break: clone; box-decoration-break: clone; }
|
|
1128
|
+
:where(.gem-mark--accent) { color: var(--gem-color-accent); }
|
|
1129
|
+
:where(.gem-mark--alert) { color: var(--gem-color-danger, #b42318); }
|
|
1130
|
+
:where(.gem-mark--highlight.gem-mark--accent), :where(.gem-mark--highlight.gem-mark--alert) { color: var(--gem-mark-highlight-ink, #1f1d18); }
|
|
1131
|
+
.gem-mark-link { color: var(--gem-color-accent); text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }
|
|
1132
|
+
.gem-mark-link:hover { text-decoration-thickness: 2px; }
|
|
1133
|
+
.gem-mark-link.gem-mark--highlight, .gem-mark--highlight .gem-mark-link { color: var(--gem-mark-highlight-ink, #1f1d18); }
|
|
1103
1134
|
@keyframes gem-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(calc(-1 * var(--gem-ambient-amp, 7px))); } }
|
|
1104
1135
|
@keyframes gem-sheen { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
|
|
1105
1136
|
@keyframes gem-marquee-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
|
package/dist/gem-runtime.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
(() => {
|
|
2
|
-
const config = {"planId":"gem-framework-package","base":"gem-native","dawnAdapter":false,"plugins":[{"key":"cart-drawer","loadStrategy":"interaction","dependsOn":[]},{"key":"mobile-menu","loadStrategy":"interaction","dependsOn":[]},{"key":"quantity-input","loadStrategy":"interaction","dependsOn":[]},{"key":"variant-selects","loadStrategy":"interaction","dependsOn":[]},{"key":"predictive-search","loadStrategy":"critical","dependsOn":[]},{"key":"localization","loadStrategy":"interaction","dependsOn":[]},{"key":"disclosure","loadStrategy":"interaction","dependsOn":[]},{"key":"tabs","loadStrategy":"interaction","dependsOn":[]},{"key":"media-gallery","loadStrategy":"viewport","dependsOn":[]},{"key":"carousel","loadStrategy":"viewport","dependsOn":[]},{"key":"hero-slideshow","loadStrategy":"idle","dependsOn":[]},{"key":"quick-view","loadStrategy":"interaction","dependsOn":[]},{"key":"promo-popup","loadStrategy":"idle","dependsOn":[]},{"key":"before_after","loadStrategy":"viewport","dependsOn":[]},{"key":"runtime-slot-loader","loadStrategy":"idle","dependsOn":[]},{"key":"sticky-atc","loadStrategy":"viewport","dependsOn":[]},{"key":"recently-viewed","loadStrategy":"idle","dependsOn":[]},{"key":"countdown","loadStrategy":"viewport","dependsOn":[]},{"key":"reveal-on-scroll","loadStrategy":"viewport","dependsOn":[]},{"key":"seasonal-effects","loadStrategy":"idle","dependsOn":[]},{"key":"context-adapter","loadStrategy":"idle","dependsOn":[]},{"key":"offer-engine","loadStrategy":"interaction","dependsOn":[]}]};
|
|
2
|
+
const config = {"planId":"gem-framework-package","base":"gem-native","dawnAdapter":false,"plugins":[{"key":"cart-drawer","loadStrategy":"interaction","dependsOn":[]},{"key":"mobile-menu","loadStrategy":"interaction","dependsOn":[]},{"key":"quantity-input","loadStrategy":"interaction","dependsOn":[]},{"key":"variant-selects","loadStrategy":"interaction","dependsOn":[]},{"key":"predictive-search","loadStrategy":"critical","dependsOn":[]},{"key":"localization","loadStrategy":"interaction","dependsOn":[]},{"key":"disclosure","loadStrategy":"interaction","dependsOn":[]},{"key":"tabs","loadStrategy":"interaction","dependsOn":[]},{"key":"media-gallery","loadStrategy":"viewport","dependsOn":[]},{"key":"carousel","loadStrategy":"viewport","dependsOn":[]},{"key":"hero-slideshow","loadStrategy":"idle","dependsOn":[]},{"key":"quick-view","loadStrategy":"interaction","dependsOn":[]},{"key":"promo-popup","loadStrategy":"idle","dependsOn":[]},{"key":"before_after","loadStrategy":"viewport","dependsOn":[]},{"key":"runtime-slot-loader","loadStrategy":"idle","dependsOn":[]},{"key":"sticky-atc","loadStrategy":"viewport","dependsOn":[]},{"key":"recently-viewed","loadStrategy":"idle","dependsOn":[]},{"key":"countdown","loadStrategy":"viewport","dependsOn":[]},{"key":"reveal-on-scroll","loadStrategy":"viewport","dependsOn":[]},{"key":"img-in","loadStrategy":"critical","dependsOn":[]},{"key":"seasonal-effects","loadStrategy":"idle","dependsOn":[]},{"key":"context-adapter","loadStrategy":"idle","dependsOn":[]},{"key":"offer-engine","loadStrategy":"interaction","dependsOn":[]}]};
|
|
3
3
|
const target = window;
|
|
4
4
|
const existing = target.GemTheme || {};
|
|
5
5
|
const listeners = existing.listeners || {};
|
|
@@ -914,6 +914,37 @@
|
|
|
914
914
|
});
|
|
915
915
|
api.emit('gem:reveal:ready', { source: 'gem-runtime', count: bound });
|
|
916
916
|
});
|
|
917
|
+
registerPlugin('img-in', ({ api }) => {
|
|
918
|
+
const STATE = 'data-gem-img-in-state';
|
|
919
|
+
const BOUND = 'data-gem-bound-img-in';
|
|
920
|
+
const reduce = typeof target.matchMedia === 'function' && target.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
921
|
+
const style = (img) => (typeof target.getComputedStyle === 'function' ? target.getComputedStyle(img) : null);
|
|
922
|
+
const owned = (s) => Boolean(s && ((s.opacity && s.opacity !== '1') || (s.filter && s.filter !== 'none') || (s.animationName && s.animationName !== 'none')));
|
|
923
|
+
const overridden = (s) => Boolean(s && ((s.opacity && s.opacity !== '0.6') || (s.filter && s.filter.indexOf('blur') === -1)));
|
|
924
|
+
const settle = (event) => {
|
|
925
|
+
const img = event.target;
|
|
926
|
+
if (!img || img.tagName !== 'IMG' || !img.hasAttribute(BOUND)) return;
|
|
927
|
+
const state = img.getAttribute(STATE);
|
|
928
|
+
if (event.type === 'load' || event.type === 'error') {
|
|
929
|
+
if (state !== 'loading') return;
|
|
930
|
+
if (event.type === 'load' && !reduce && !overridden(style(img))) img.setAttribute(STATE, 'in');
|
|
931
|
+
else img.removeAttribute(STATE);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (state === 'in' && event.animationName === 'gem-img-in') img.removeAttribute(STATE);
|
|
935
|
+
};
|
|
936
|
+
['load', 'error', 'animationend', 'animationcancel'].forEach((type) => document.addEventListener(type, settle, true));
|
|
937
|
+
const candidates = [];
|
|
938
|
+
document.querySelectorAll('img').forEach((img) => {
|
|
939
|
+
if (!img.closest('[data-gem-img-in]')) return;
|
|
940
|
+
if (img.complete) { if (!img.hasAttribute(BOUND)) img.removeAttribute(STATE); return; }
|
|
941
|
+
if (mark(img, 'img-in')) candidates.push(img);
|
|
942
|
+
});
|
|
943
|
+
const own = candidates.map((img) => owned(style(img)));
|
|
944
|
+
let bound = 0;
|
|
945
|
+
candidates.forEach((img, i) => { if (own[i]) return; img.setAttribute(STATE, 'loading'); bound += 1; });
|
|
946
|
+
api.emit('gem:img-in:ready', { source: 'gem-runtime', count: bound });
|
|
947
|
+
});
|
|
917
948
|
registerPlugin('recently-viewed', ({ api }) => {
|
|
918
949
|
const storageKey = 'gem:recently-viewed-products';
|
|
919
950
|
const readItems = () => {
|
package/dist/index.js
CHANGED
|
@@ -280,7 +280,7 @@ function gemButtonGradientEndCss(accent = "var(--gem-color-accent)", accentText
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
// ../../packages/gem-framework/src/tokens.ts
|
|
283
|
-
var gemFrameworkVersion = "0.
|
|
283
|
+
var gemFrameworkVersion = "0.15.0";
|
|
284
284
|
var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
|
|
285
285
|
var GEM_TAP_MIN_CSS_VAR = "--gem-tap-min: 44px;";
|
|
286
286
|
var GEM_FOCUS_OFFSET_CSS_VAR = "--gem-focus-offset: 3px;";
|
|
@@ -501,11 +501,41 @@ var isLineHeightKey = (k) => GEM_LINE_HEIGHT_TOKEN_KEYS.some((l) => tokenKeyNorm
|
|
|
501
501
|
var badLineHeight = (k, v) => isLineHeightKey(k) && !LINE_HEIGHT_SHAPE.test(stripImportant(String(v ?? "")).trim());
|
|
502
502
|
var desktopEntries = (group, values) => values ? Object.fromEntries(Object.entries(values).filter(([key]) => gemPhoneTokenTarget(group, key) == null)) : values;
|
|
503
503
|
var GEM_TYPE_RAMP_KEYS = ["base", "scale", "baseMobile"];
|
|
504
|
+
var GEM_MOTION_SCALE_DEFAULTS = {
|
|
505
|
+
fast: "100ms",
|
|
506
|
+
base: "180ms",
|
|
507
|
+
slow: "260ms",
|
|
508
|
+
enter: "cubic-bezier(.33, 1, .68, 1)",
|
|
509
|
+
exit: "cubic-bezier(.4, 0, 1, 1)",
|
|
510
|
+
distance: "8px",
|
|
511
|
+
distanceLg: "16px",
|
|
512
|
+
stagger: "30ms",
|
|
513
|
+
blur: "8px"
|
|
514
|
+
};
|
|
515
|
+
var GEM_MOTION_REDUCED = {
|
|
516
|
+
fast: "1ms",
|
|
517
|
+
base: "1ms",
|
|
518
|
+
slow: "1ms",
|
|
519
|
+
distance: "0px",
|
|
520
|
+
distanceLg: "0px",
|
|
521
|
+
stagger: "0ms",
|
|
522
|
+
blur: "0px"
|
|
523
|
+
};
|
|
524
|
+
var cssVarName = (line) => line.slice(0, line.indexOf(":")).trim();
|
|
525
|
+
function gemMotionScaleCssVars(motion) {
|
|
526
|
+
const declared = new Set(toMotionCssVars(motion ?? {}).map(cssVarName));
|
|
527
|
+
const duration = Object.entries(motion ?? {}).find(([key]) => motionCssVarName(key) === "--gem-motion-duration")?.[1];
|
|
528
|
+
const still = /^0+(?:\.0+)?(?:m?s)?$/i.test(String(duration ?? "").trim());
|
|
529
|
+
const scale = still ? { ...GEM_MOTION_SCALE_DEFAULTS, ...GEM_MOTION_REDUCED } : GEM_MOTION_SCALE_DEFAULTS;
|
|
530
|
+
return toMotionCssVars(scale).filter((line) => !declared.has(cssVarName(line)));
|
|
531
|
+
}
|
|
504
532
|
var optionalGemTokenKeys = {
|
|
505
533
|
typography: [...GEM_TYPE_RAMP_KEYS, ...GEM_LINE_HEIGHT_TOKEN_KEYS, ...GEM_BUTTON_TOKEN_KEYS.typography],
|
|
506
534
|
spacing: [...Object.keys(GEM_DERIVED_SPACE_STEPS), ...GEM_PHONE_TOKEN_TARGETS.spacing.map((k) => `${k}Mobile`)],
|
|
507
535
|
layout: GEM_PHONE_TOKEN_TARGETS.layout.map((k) => `${k}Mobile`),
|
|
508
|
-
radius: [...GEM_BUTTON_TOKEN_KEYS.radius]
|
|
536
|
+
radius: [...GEM_BUTTON_TOKEN_KEYS.radius],
|
|
537
|
+
// Khoá motion ngoài duration / easing: nền ambient (đã phát từ trước nhưng chưa khai ở đây) và thang "Gọn" (#32).
|
|
538
|
+
motion: ["easingSoft", "ambientSlow", "ambientMid", "ambientFast", "ambientAmp", ...Object.keys(GEM_MOTION_SCALE_DEFAULTS)]
|
|
509
539
|
};
|
|
510
540
|
var GEM_HEADING_SIZE_CSS_VARS = [
|
|
511
541
|
"--gem-section-heading-size: calc(var(--gem-h2) * var(--gem-typography-heading-scale, 1));",
|
|
@@ -662,6 +692,8 @@ function createGemTokenCss(tokens, selector = ":root", options = {}) {
|
|
|
662
692
|
// and marquee primitives consume; unrecognised keys fall back to the --gem-motion-* namespace,
|
|
663
693
|
// so duration/easing keep emitting --gem-motion-duration/easing exactly as before (back-compat).
|
|
664
694
|
...toMotionCssVars(tokens.motion),
|
|
695
|
+
// Thang "Gọn" (#32): khoá plan không khai thì phát mặc định, sau các dòng của plan để thứ tự cũ không đổi.
|
|
696
|
+
...gemMotionScaleCssVars(tokens.motion),
|
|
665
697
|
...toCssVars("layout", desktopEntries("layout", tokens.layout)),
|
|
666
698
|
...phone.layout.section ? [`--gem-layout-section-mobile: ${phone.layout.section};`] : [],
|
|
667
699
|
...toCssVars("component", tokens.component),
|
|
@@ -694,6 +726,14 @@ function createGemTokenCss(tokens, selector = ":root", options = {}) {
|
|
|
694
726
|
"}",
|
|
695
727
|
...bandBorders.length ? [`:where(${bandScope}) {`, ...bandBorders.map((line) => ` ${line}`), "}"] : [],
|
|
696
728
|
...mobile.length ? [`@media (max-width: ${GEM_TYPE_MOBILE_MAX}) {`, ` ${selector} {`, ...mobile.map((line) => ` ${line}`), " }", "}"] : [],
|
|
729
|
+
"@media (prefers-reduced-motion: reduce) {",
|
|
730
|
+
` ${selector} {`,
|
|
731
|
+
// !important ở CUỐI khai báo (hợp lệ, khác cờ lọt vào giữa giá trị): stylesheet đến sau khai thang trên :root
|
|
732
|
+
// (gemsite theme-plan-style) không đè được lựa chọn giảm chuyển động của người dùng. Section tự khai thang trên chính
|
|
733
|
+
// nó vẫn giữ giá trị của nó, như mọi biến kế thừa.
|
|
734
|
+
...toMotionCssVars(GEM_MOTION_REDUCED).map((line) => ` ${line.replace(/;$/, " !important;")}`),
|
|
735
|
+
" }",
|
|
736
|
+
"}",
|
|
697
737
|
...formBlock ? [formBlock] : [],
|
|
698
738
|
...buttonCase ? [buttonCase] : []
|
|
699
739
|
].join("\n");
|
|
@@ -742,16 +782,22 @@ function toCssVars(namespace, values) {
|
|
|
742
782
|
}
|
|
743
783
|
var motionCssVarAliases = {
|
|
744
784
|
easingSoft: "--gem-ease-soft",
|
|
785
|
+
enter: "--gem-ease-enter",
|
|
786
|
+
exit: "--gem-ease-exit",
|
|
745
787
|
ambientSlow: "--gem-ambient-slow",
|
|
746
788
|
ambientMid: "--gem-ambient-mid",
|
|
747
789
|
ambientFast: "--gem-ambient-fast",
|
|
748
790
|
ambientAmp: "--gem-ambient-amp"
|
|
749
791
|
};
|
|
792
|
+
var motionCssVarAliasBySlug = Object.fromEntries(
|
|
793
|
+
Object.entries(motionCssVarAliases).map(([key, name]) => [slugify(key), name])
|
|
794
|
+
);
|
|
795
|
+
function motionCssVarName(key) {
|
|
796
|
+
return motionCssVarAliasBySlug[slugify(key)] ?? `--gem-motion-${slugify(key)}`;
|
|
797
|
+
}
|
|
750
798
|
function toMotionCssVars(values) {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
return `${name}: ${value};`;
|
|
754
|
-
});
|
|
799
|
+
if (!values) return [];
|
|
800
|
+
return Object.entries(values).map(([key, value]) => `${motionCssVarName(key)}: ${value};`);
|
|
755
801
|
}
|
|
756
802
|
function toScaledCssVars(namespace, values, knob) {
|
|
757
803
|
if (!values) return [];
|
|
@@ -895,7 +941,15 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
895
941
|
"gem-pull-quote",
|
|
896
942
|
"gem-byline",
|
|
897
943
|
"gem-disclaimer",
|
|
898
|
-
"gem-subhead"
|
|
944
|
+
"gem-subhead",
|
|
945
|
+
// Nhấn trong câu (gap #39): lớp @gemtheme/gem-sections inline-marks.ts bảo người gọi vẽ, GEM_INLINE_MARK_CSS tô.
|
|
946
|
+
// Không khai thì section vẽ đúng dấu của framework vẫn bị gem_framework_unknown_gem_class (review #740).
|
|
947
|
+
"gem-mark",
|
|
948
|
+
"gem-mark--underline",
|
|
949
|
+
"gem-mark--highlight",
|
|
950
|
+
"gem-mark--accent",
|
|
951
|
+
"gem-mark--alert",
|
|
952
|
+
"gem-mark-link"
|
|
899
953
|
]
|
|
900
954
|
},
|
|
901
955
|
{
|
|
@@ -1200,6 +1254,11 @@ var gemFrameworkRuntimePlugins = [
|
|
|
1200
1254
|
load_strategy: "viewport",
|
|
1201
1255
|
events: ["gem:reveal:ready", "gem:reveal:in"]
|
|
1202
1256
|
},
|
|
1257
|
+
{
|
|
1258
|
+
key: "img-in",
|
|
1259
|
+
load_strategy: "critical",
|
|
1260
|
+
events: ["gem:img-in:ready"]
|
|
1261
|
+
},
|
|
1203
1262
|
{
|
|
1204
1263
|
key: "seasonal-effects",
|
|
1205
1264
|
load_strategy: "idle",
|
|
@@ -1526,6 +1585,25 @@ var GEM_REVEAL_CSS_LINES = [
|
|
|
1526
1585
|
"@media (prefers-reduced-motion: reduce) { [data-gem-reveal] { opacity: 1 !important; transform: none !important; filter: none !important; transition: none; } }"
|
|
1527
1586
|
];
|
|
1528
1587
|
var GEM_REVEAL_CSS = GEM_REVEAL_CSS_LINES.join("\n");
|
|
1588
|
+
var GEM_IMG_IN_CSS_LINES = [
|
|
1589
|
+
":where(img[data-gem-bound-img-in][data-gem-img-in-state='loading']) { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; }",
|
|
1590
|
+
"@media (prefers-reduced-motion: no-preference) { :where(img[data-gem-bound-img-in][data-gem-img-in-state='in']) { animation: gem-img-in var(--gem-motion-slow, 260ms) var(--gem-ease-enter, cubic-bezier(.33, 1, .68, 1)); } }",
|
|
1591
|
+
"@keyframes gem-img-in { from { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; } }"
|
|
1592
|
+
];
|
|
1593
|
+
var GEM_IMG_IN_CSS = GEM_IMG_IN_CSS_LINES.join("\n");
|
|
1594
|
+
var GEM_INLINE_MARK_CSS_LINES = [
|
|
1595
|
+
":where(.gem-mark--underline) { text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }",
|
|
1596
|
+
":where(.gem-mark--highlight) { background: var(--gem-mark-highlight, #fff1a6); color: var(--gem-mark-highlight-ink, #1f1d18); padding-inline: 0.12em; border-radius: 0.15em; -webkit-box-decoration-break: clone; box-decoration-break: clone; }",
|
|
1597
|
+
":where(.gem-mark--accent) { color: var(--gem-color-accent); }",
|
|
1598
|
+
":where(.gem-mark--alert) { color: var(--gem-color-danger, #b42318); }",
|
|
1599
|
+
":where(.gem-mark--highlight.gem-mark--accent), :where(.gem-mark--highlight.gem-mark--alert) { color: var(--gem-mark-highlight-ink, #1f1d18); }",
|
|
1600
|
+
".gem-mark-link { color: var(--gem-color-accent); text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }",
|
|
1601
|
+
".gem-mark-link:hover { text-decoration-thickness: 2px; }",
|
|
1602
|
+
// Bút dạ thắng màu link: chữ trên nền vàng giữ mực đậm, dù span nằm trong link hay link nằm trong span (review #740 đo màu nhấn
|
|
1603
|
+
// trên vàng 4,32:1 ở nền trắng, 2,47:1 trên băng tối, 1,05:1 trên băng màu nhấn).
|
|
1604
|
+
".gem-mark-link.gem-mark--highlight, .gem-mark--highlight .gem-mark-link { color: var(--gem-mark-highlight-ink, #1f1d18); }"
|
|
1605
|
+
];
|
|
1606
|
+
var GEM_INLINE_MARK_CSS = GEM_INLINE_MARK_CSS_LINES.join("\n");
|
|
1529
1607
|
var GEM_AMBIENT_CSS_LINES = [
|
|
1530
1608
|
"@keyframes gem-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(calc(-1 * var(--gem-ambient-amp, 7px))); } }",
|
|
1531
1609
|
"@keyframes gem-sheen { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }",
|
|
@@ -2697,6 +2775,8 @@ function createPrimitiveCss() {
|
|
|
2697
2775
|
" opacity: 1;",
|
|
2698
2776
|
"}",
|
|
2699
2777
|
...GEM_REVEAL_CSS_LINES,
|
|
2778
|
+
...GEM_IMG_IN_CSS_LINES,
|
|
2779
|
+
...GEM_INLINE_MARK_CSS_LINES,
|
|
2700
2780
|
...GEM_AMBIENT_CSS_LINES,
|
|
2701
2781
|
...GEM_DIVIDER_CSS_LINES,
|
|
2702
2782
|
...GEM_CARD_CSS_LINES,
|
|
@@ -4605,6 +4685,7 @@ var runtimePluginReadyEvents = {
|
|
|
4605
4685
|
"recently-viewed": "gem:recently-viewed:ready",
|
|
4606
4686
|
countdown: "gem:countdown:ready",
|
|
4607
4687
|
"reveal-on-scroll": "gem:reveal:ready",
|
|
4688
|
+
"img-in": "gem:img-in:ready",
|
|
4608
4689
|
"seasonal-effects": "gem:seasonal:ready",
|
|
4609
4690
|
"context-adapter": "gem:context:ready",
|
|
4610
4691
|
"offer-engine": "gem:offer:ready"
|
|
@@ -5574,6 +5655,39 @@ var revealOnScrollPluginRegistrationLines = [
|
|
|
5574
5655
|
" api.emit('gem:reveal:ready', { source: 'gem-runtime', count: bound });",
|
|
5575
5656
|
" });"
|
|
5576
5657
|
];
|
|
5658
|
+
var imgInPluginRegistrationLines = [
|
|
5659
|
+
" registerPlugin('img-in', ({ api }) => {",
|
|
5660
|
+
" const STATE = 'data-gem-img-in-state';",
|
|
5661
|
+
" const BOUND = 'data-gem-bound-img-in';",
|
|
5662
|
+
" const reduce = typeof target.matchMedia === 'function' && target.matchMedia('(prefers-reduced-motion: reduce)').matches;",
|
|
5663
|
+
" const style = (img) => (typeof target.getComputedStyle === 'function' ? target.getComputedStyle(img) : null);",
|
|
5664
|
+
" const owned = (s) => Boolean(s && ((s.opacity && s.opacity !== '1') || (s.filter && s.filter !== 'none') || (s.animationName && s.animationName !== 'none')));",
|
|
5665
|
+
" const overridden = (s) => Boolean(s && ((s.opacity && s.opacity !== '0.6') || (s.filter && s.filter.indexOf('blur') === -1)));",
|
|
5666
|
+
" const settle = (event) => {",
|
|
5667
|
+
" const img = event.target;",
|
|
5668
|
+
" if (!img || img.tagName !== 'IMG' || !img.hasAttribute(BOUND)) return;",
|
|
5669
|
+
" const state = img.getAttribute(STATE);",
|
|
5670
|
+
" if (event.type === 'load' || event.type === 'error') {",
|
|
5671
|
+
" if (state !== 'loading') return;",
|
|
5672
|
+
" if (event.type === 'load' && !reduce && !overridden(style(img))) img.setAttribute(STATE, 'in');",
|
|
5673
|
+
" else img.removeAttribute(STATE);",
|
|
5674
|
+
" return;",
|
|
5675
|
+
" }",
|
|
5676
|
+
" if (state === 'in' && event.animationName === 'gem-img-in') img.removeAttribute(STATE);",
|
|
5677
|
+
" };",
|
|
5678
|
+
" ['load', 'error', 'animationend', 'animationcancel'].forEach((type) => document.addEventListener(type, settle, true));",
|
|
5679
|
+
" const candidates = [];",
|
|
5680
|
+
" document.querySelectorAll('img').forEach((img) => {",
|
|
5681
|
+
" if (!img.closest('[data-gem-img-in]')) return;",
|
|
5682
|
+
" if (img.complete) { if (!img.hasAttribute(BOUND)) img.removeAttribute(STATE); return; }",
|
|
5683
|
+
" if (mark(img, 'img-in')) candidates.push(img);",
|
|
5684
|
+
" });",
|
|
5685
|
+
" const own = candidates.map((img) => owned(style(img)));",
|
|
5686
|
+
" let bound = 0;",
|
|
5687
|
+
" candidates.forEach((img, i) => { if (own[i]) return; img.setAttribute(STATE, 'loading'); bound += 1; });",
|
|
5688
|
+
" api.emit('gem:img-in:ready', { source: 'gem-runtime', count: bound });",
|
|
5689
|
+
" });"
|
|
5690
|
+
];
|
|
5577
5691
|
var recentlyViewedPluginRegistrationLines = [
|
|
5578
5692
|
" registerPlugin('recently-viewed', ({ api }) => {",
|
|
5579
5693
|
" const storageKey = 'gem:recently-viewed-products';",
|
|
@@ -5670,6 +5784,7 @@ var runtimePluginSourceLines = [
|
|
|
5670
5784
|
["before_after", beforeAfterPluginRegistrationLines],
|
|
5671
5785
|
["countdown", countdownPluginRegistrationLines],
|
|
5672
5786
|
["reveal-on-scroll", revealOnScrollPluginRegistrationLines],
|
|
5787
|
+
["img-in", imgInPluginRegistrationLines],
|
|
5673
5788
|
["recently-viewed", recentlyViewedPluginRegistrationLines],
|
|
5674
5789
|
["runtime-slot-loader", runtimeSlotLoaderPluginRegistrationLines],
|
|
5675
5790
|
["offer-engine", offerEnginePluginRegistrationLines]
|
|
@@ -5971,6 +6086,8 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
|
5971
6086
|
GEM_FOCUS_RING_CSS_VAR.split(":")[0].trim(),
|
|
5972
6087
|
GEM_TAP_MIN_CSS_VAR.split(":")[0].trim(),
|
|
5973
6088
|
GEM_FOCUS_OFFSET_CSS_VAR.split(":")[0].trim(),
|
|
6089
|
+
// Thang chuyển động "Gọn" (tokens.ts GEM_MOTION_SCALE_DEFAULTS, #32): luôn phát, section đọc được.
|
|
6090
|
+
...toMotionCssVars(GEM_MOTION_SCALE_DEFAULTS).map((line) => line.split(":")[0].trim()),
|
|
5974
6091
|
// The derived spacing steps (tokens.ts GEM_DERIVED_SPACE_STEPS): always emitted, never required.
|
|
5975
6092
|
...GEM_DERIVED_SPACE_CSS_VARIABLES,
|
|
5976
6093
|
// Extension points a section SETS (gemtheme #650): icon size, drawer slide distance.
|
|
@@ -7316,7 +7433,11 @@ export {
|
|
|
7316
7433
|
GEM_FRAMEWORK_EDITIONS,
|
|
7317
7434
|
GEM_GRID_COLUMNS_ROW_KNOB_CSS,
|
|
7318
7435
|
GEM_HEADING_SIZE_CSS_VARS,
|
|
7436
|
+
GEM_IMG_IN_CSS,
|
|
7437
|
+
GEM_INLINE_MARK_CSS,
|
|
7319
7438
|
GEM_LINE_HEIGHT_TOKEN_KEYS,
|
|
7439
|
+
GEM_MOTION_REDUCED,
|
|
7440
|
+
GEM_MOTION_SCALE_DEFAULTS,
|
|
7320
7441
|
GEM_PART_CLASS,
|
|
7321
7442
|
GEM_POPUP_SHELL_CSS,
|
|
7322
7443
|
GEM_REVEAL_CSS,
|
|
@@ -7371,6 +7492,7 @@ export {
|
|
|
7371
7492
|
gemFrameworkValidationRules,
|
|
7372
7493
|
gemFrameworkVersion,
|
|
7373
7494
|
gemMobileTokenCssVars,
|
|
7495
|
+
gemMotionScaleCssVars,
|
|
7374
7496
|
gemPartClass,
|
|
7375
7497
|
gemPhoneTokenTarget,
|
|
7376
7498
|
gemPlanShapeKnobs,
|
|
@@ -7384,6 +7506,7 @@ export {
|
|
|
7384
7506
|
modernMineralFamilyLineage,
|
|
7385
7507
|
modernMineralHomepageLineageSections,
|
|
7386
7508
|
motionCssVarAliases,
|
|
7509
|
+
motionCssVarName,
|
|
7387
7510
|
normalizeGemTokens,
|
|
7388
7511
|
normalizeHex,
|
|
7389
7512
|
offerEngineRuntimeScript,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @gemboss/gem-framework/plugins/img-in: types of the ES module next to this file. Generated; do not edit here.
|
|
2
|
+
import type { MountOptions, Unmount } from "../runtime-core.js";
|
|
3
|
+
|
|
4
|
+
export type { MountOptions, Unmount };
|
|
5
|
+
export declare const key: "img-in";
|
|
6
|
+
export declare const readyEvent: "gem:img-in:ready";
|
|
7
|
+
/** Bind every "img-in" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
|
|
8
|
+
export declare function mount(root?: ParentNode | null, options?: MountOptions): Unmount;
|
|
9
|
+
export default mount;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @gemboss/gem-framework/plugins/img-in: the gem-runtime plugin "img-in" as an ES module. Generated from
|
|
2
|
+
// packages/gem-framework/src/runtime.ts, the same lines gem-runtime.js carries; do not edit here.
|
|
3
|
+
import { mountPlugin } from "../runtime-core.js";
|
|
4
|
+
|
|
5
|
+
export const key = "img-in";
|
|
6
|
+
export const readyEvent = "gem:img-in:ready";
|
|
7
|
+
|
|
8
|
+
function install({ document, window, target, mark, canListen, listen: __gemListen, registerPlugin, registerNoopPlugin }) {
|
|
9
|
+
registerPlugin('img-in', ({ api }) => {
|
|
10
|
+
const STATE = 'data-gem-img-in-state';
|
|
11
|
+
const BOUND = 'data-gem-bound-img-in';
|
|
12
|
+
const reduce = typeof target.matchMedia === 'function' && target.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
13
|
+
const style = (img) => (typeof target.getComputedStyle === 'function' ? target.getComputedStyle(img) : null);
|
|
14
|
+
const owned = (s) => Boolean(s && ((s.opacity && s.opacity !== '1') || (s.filter && s.filter !== 'none') || (s.animationName && s.animationName !== 'none')));
|
|
15
|
+
const overridden = (s) => Boolean(s && ((s.opacity && s.opacity !== '0.6') || (s.filter && s.filter.indexOf('blur') === -1)));
|
|
16
|
+
const settle = (event) => {
|
|
17
|
+
const img = event.target;
|
|
18
|
+
if (!img || img.tagName !== 'IMG' || !img.hasAttribute(BOUND)) return;
|
|
19
|
+
const state = img.getAttribute(STATE);
|
|
20
|
+
if (event.type === 'load' || event.type === 'error') {
|
|
21
|
+
if (state !== 'loading') return;
|
|
22
|
+
if (event.type === 'load' && !reduce && !overridden(style(img))) img.setAttribute(STATE, 'in');
|
|
23
|
+
else img.removeAttribute(STATE);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (state === 'in' && event.animationName === 'gem-img-in') img.removeAttribute(STATE);
|
|
27
|
+
};
|
|
28
|
+
['load', 'error', 'animationend', 'animationcancel'].forEach((type) => __gemListen(document, type, settle, true));
|
|
29
|
+
const candidates = [];
|
|
30
|
+
document.querySelectorAll('img').forEach((img) => {
|
|
31
|
+
if (!img.closest('[data-gem-img-in]')) return;
|
|
32
|
+
if (img.complete) { if (!img.hasAttribute(BOUND)) img.removeAttribute(STATE); return; }
|
|
33
|
+
if (mark(img, 'img-in')) candidates.push(img);
|
|
34
|
+
});
|
|
35
|
+
const own = candidates.map((img) => owned(style(img)));
|
|
36
|
+
let bound = 0;
|
|
37
|
+
candidates.forEach((img, i) => { if (own[i]) return; img.setAttribute(STATE, 'loading'); bound += 1; });
|
|
38
|
+
api.emit('gem:img-in:ready', { source: 'gem-runtime', count: bound });
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Bind every "img-in" hook inside root (default: the whole document). Returns unmount(). Mounting the same root twice binds once. */
|
|
43
|
+
export function mount(root, options) {
|
|
44
|
+
return mountPlugin(key, install, root, options);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default mount;
|
package/dist/primitives.css
CHANGED
|
@@ -1029,6 +1029,17 @@ a {
|
|
|
1029
1029
|
[data-gem-reveal='zoom'][data-gem-reveal-state='ready'] { opacity: 0; transform: scale(0.94); }
|
|
1030
1030
|
[data-gem-reveal='blur'][data-gem-reveal-state='ready'] { opacity: 0; filter: blur(10px); transform: none; }
|
|
1031
1031
|
@media (prefers-reduced-motion: reduce) { [data-gem-reveal] { opacity: 1 !important; transform: none !important; filter: none !important; transition: none; } }
|
|
1032
|
+
:where(img[data-gem-bound-img-in][data-gem-img-in-state='loading']) { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; }
|
|
1033
|
+
@media (prefers-reduced-motion: no-preference) { :where(img[data-gem-bound-img-in][data-gem-img-in-state='in']) { animation: gem-img-in var(--gem-motion-slow, 260ms) var(--gem-ease-enter, cubic-bezier(.33, 1, .68, 1)); } }
|
|
1034
|
+
@keyframes gem-img-in { from { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; } }
|
|
1035
|
+
:where(.gem-mark--underline) { text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }
|
|
1036
|
+
:where(.gem-mark--highlight) { background: var(--gem-mark-highlight, #fff1a6); color: var(--gem-mark-highlight-ink, #1f1d18); padding-inline: 0.12em; border-radius: 0.15em; -webkit-box-decoration-break: clone; box-decoration-break: clone; }
|
|
1037
|
+
:where(.gem-mark--accent) { color: var(--gem-color-accent); }
|
|
1038
|
+
:where(.gem-mark--alert) { color: var(--gem-color-danger, #b42318); }
|
|
1039
|
+
:where(.gem-mark--highlight.gem-mark--accent), :where(.gem-mark--highlight.gem-mark--alert) { color: var(--gem-mark-highlight-ink, #1f1d18); }
|
|
1040
|
+
.gem-mark-link { color: var(--gem-color-accent); text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }
|
|
1041
|
+
.gem-mark-link:hover { text-decoration-thickness: 2px; }
|
|
1042
|
+
.gem-mark-link.gem-mark--highlight, .gem-mark--highlight .gem-mark-link { color: var(--gem-mark-highlight-ink, #1f1d18); }
|
|
1032
1043
|
@keyframes gem-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(calc(-1 * var(--gem-ambient-amp, 7px))); } }
|
|
1033
1044
|
@keyframes gem-sheen { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
|
|
1034
1045
|
@keyframes gem-marquee-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
|
package/dist/section-gate.js
CHANGED
|
@@ -368,11 +368,41 @@ var isLineHeightKey = (k) => GEM_LINE_HEIGHT_TOKEN_KEYS.some((l) => tokenKeyNorm
|
|
|
368
368
|
var badLineHeight = (k, v) => isLineHeightKey(k) && !LINE_HEIGHT_SHAPE.test(stripImportant(String(v ?? "")).trim());
|
|
369
369
|
var desktopEntries = (group, values) => values ? Object.fromEntries(Object.entries(values).filter(([key]) => gemPhoneTokenTarget(group, key) == null)) : values;
|
|
370
370
|
var GEM_TYPE_RAMP_KEYS = ["base", "scale", "baseMobile"];
|
|
371
|
+
var GEM_MOTION_SCALE_DEFAULTS = {
|
|
372
|
+
fast: "100ms",
|
|
373
|
+
base: "180ms",
|
|
374
|
+
slow: "260ms",
|
|
375
|
+
enter: "cubic-bezier(.33, 1, .68, 1)",
|
|
376
|
+
exit: "cubic-bezier(.4, 0, 1, 1)",
|
|
377
|
+
distance: "8px",
|
|
378
|
+
distanceLg: "16px",
|
|
379
|
+
stagger: "30ms",
|
|
380
|
+
blur: "8px"
|
|
381
|
+
};
|
|
382
|
+
var GEM_MOTION_REDUCED = {
|
|
383
|
+
fast: "1ms",
|
|
384
|
+
base: "1ms",
|
|
385
|
+
slow: "1ms",
|
|
386
|
+
distance: "0px",
|
|
387
|
+
distanceLg: "0px",
|
|
388
|
+
stagger: "0ms",
|
|
389
|
+
blur: "0px"
|
|
390
|
+
};
|
|
391
|
+
var cssVarName = (line) => line.slice(0, line.indexOf(":")).trim();
|
|
392
|
+
function gemMotionScaleCssVars(motion) {
|
|
393
|
+
const declared = new Set(toMotionCssVars(motion ?? {}).map(cssVarName));
|
|
394
|
+
const duration = Object.entries(motion ?? {}).find(([key]) => motionCssVarName(key) === "--gem-motion-duration")?.[1];
|
|
395
|
+
const still = /^0+(?:\.0+)?(?:m?s)?$/i.test(String(duration ?? "").trim());
|
|
396
|
+
const scale = still ? { ...GEM_MOTION_SCALE_DEFAULTS, ...GEM_MOTION_REDUCED } : GEM_MOTION_SCALE_DEFAULTS;
|
|
397
|
+
return toMotionCssVars(scale).filter((line) => !declared.has(cssVarName(line)));
|
|
398
|
+
}
|
|
371
399
|
var optionalGemTokenKeys = {
|
|
372
400
|
typography: [...GEM_TYPE_RAMP_KEYS, ...GEM_LINE_HEIGHT_TOKEN_KEYS, ...GEM_BUTTON_TOKEN_KEYS.typography],
|
|
373
401
|
spacing: [...Object.keys(GEM_DERIVED_SPACE_STEPS), ...GEM_PHONE_TOKEN_TARGETS.spacing.map((k) => `${k}Mobile`)],
|
|
374
402
|
layout: GEM_PHONE_TOKEN_TARGETS.layout.map((k) => `${k}Mobile`),
|
|
375
|
-
radius: [...GEM_BUTTON_TOKEN_KEYS.radius]
|
|
403
|
+
radius: [...GEM_BUTTON_TOKEN_KEYS.radius],
|
|
404
|
+
// Khoá motion ngoài duration / easing: nền ambient (đã phát từ trước nhưng chưa khai ở đây) và thang "Gọn" (#32).
|
|
405
|
+
motion: ["easingSoft", "ambientSlow", "ambientMid", "ambientFast", "ambientAmp", ...Object.keys(GEM_MOTION_SCALE_DEFAULTS)]
|
|
376
406
|
};
|
|
377
407
|
var GEM_HEADING_SIZE_CSS_VARS = [
|
|
378
408
|
"--gem-section-heading-size: calc(var(--gem-h2) * var(--gem-typography-heading-scale, 1));",
|
|
@@ -486,6 +516,8 @@ function createGemTokenCss(tokens, selector = ":root", options = {}) {
|
|
|
486
516
|
// and marquee primitives consume; unrecognised keys fall back to the --gem-motion-* namespace,
|
|
487
517
|
// so duration/easing keep emitting --gem-motion-duration/easing exactly as before (back-compat).
|
|
488
518
|
...toMotionCssVars(tokens.motion),
|
|
519
|
+
// Thang "Gọn" (#32): khoá plan không khai thì phát mặc định, sau các dòng của plan để thứ tự cũ không đổi.
|
|
520
|
+
...gemMotionScaleCssVars(tokens.motion),
|
|
489
521
|
...toCssVars("layout", desktopEntries("layout", tokens.layout)),
|
|
490
522
|
...phone.layout.section ? [`--gem-layout-section-mobile: ${phone.layout.section};`] : [],
|
|
491
523
|
...toCssVars("component", tokens.component),
|
|
@@ -518,6 +550,14 @@ function createGemTokenCss(tokens, selector = ":root", options = {}) {
|
|
|
518
550
|
"}",
|
|
519
551
|
...bandBorders.length ? [`:where(${bandScope}) {`, ...bandBorders.map((line) => ` ${line}`), "}"] : [],
|
|
520
552
|
...mobile.length ? [`@media (max-width: ${GEM_TYPE_MOBILE_MAX}) {`, ` ${selector} {`, ...mobile.map((line) => ` ${line}`), " }", "}"] : [],
|
|
553
|
+
"@media (prefers-reduced-motion: reduce) {",
|
|
554
|
+
` ${selector} {`,
|
|
555
|
+
// !important ở CUỐI khai báo (hợp lệ, khác cờ lọt vào giữa giá trị): stylesheet đến sau khai thang trên :root
|
|
556
|
+
// (gemsite theme-plan-style) không đè được lựa chọn giảm chuyển động của người dùng. Section tự khai thang trên chính
|
|
557
|
+
// nó vẫn giữ giá trị của nó, như mọi biến kế thừa.
|
|
558
|
+
...toMotionCssVars(GEM_MOTION_REDUCED).map((line) => ` ${line.replace(/;$/, " !important;")}`),
|
|
559
|
+
" }",
|
|
560
|
+
"}",
|
|
521
561
|
...formBlock ? [formBlock] : [],
|
|
522
562
|
...buttonCase ? [buttonCase] : []
|
|
523
563
|
].join("\n");
|
|
@@ -566,16 +606,22 @@ function toCssVars(namespace, values) {
|
|
|
566
606
|
}
|
|
567
607
|
var motionCssVarAliases = {
|
|
568
608
|
easingSoft: "--gem-ease-soft",
|
|
609
|
+
enter: "--gem-ease-enter",
|
|
610
|
+
exit: "--gem-ease-exit",
|
|
569
611
|
ambientSlow: "--gem-ambient-slow",
|
|
570
612
|
ambientMid: "--gem-ambient-mid",
|
|
571
613
|
ambientFast: "--gem-ambient-fast",
|
|
572
614
|
ambientAmp: "--gem-ambient-amp"
|
|
573
615
|
};
|
|
616
|
+
var motionCssVarAliasBySlug = Object.fromEntries(
|
|
617
|
+
Object.entries(motionCssVarAliases).map(([key, name]) => [slugify(key), name])
|
|
618
|
+
);
|
|
619
|
+
function motionCssVarName(key) {
|
|
620
|
+
return motionCssVarAliasBySlug[slugify(key)] ?? `--gem-motion-${slugify(key)}`;
|
|
621
|
+
}
|
|
574
622
|
function toMotionCssVars(values) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
return `${name}: ${value};`;
|
|
578
|
-
});
|
|
623
|
+
if (!values) return [];
|
|
624
|
+
return Object.entries(values).map(([key, value]) => `${motionCssVarName(key)}: ${value};`);
|
|
579
625
|
}
|
|
580
626
|
function toScaledCssVars(namespace, values, knob) {
|
|
581
627
|
if (!values) return [];
|
|
@@ -700,7 +746,15 @@ var gemFrameworkPrimitiveGroups = [
|
|
|
700
746
|
"gem-pull-quote",
|
|
701
747
|
"gem-byline",
|
|
702
748
|
"gem-disclaimer",
|
|
703
|
-
"gem-subhead"
|
|
749
|
+
"gem-subhead",
|
|
750
|
+
// Nhấn trong câu (gap #39): lớp @gemtheme/gem-sections inline-marks.ts bảo người gọi vẽ, GEM_INLINE_MARK_CSS tô.
|
|
751
|
+
// Không khai thì section vẽ đúng dấu của framework vẫn bị gem_framework_unknown_gem_class (review #740).
|
|
752
|
+
"gem-mark",
|
|
753
|
+
"gem-mark--underline",
|
|
754
|
+
"gem-mark--highlight",
|
|
755
|
+
"gem-mark--accent",
|
|
756
|
+
"gem-mark--alert",
|
|
757
|
+
"gem-mark-link"
|
|
704
758
|
]
|
|
705
759
|
},
|
|
706
760
|
{
|
|
@@ -1192,6 +1246,25 @@ var GEM_REVEAL_CSS_LINES = [
|
|
|
1192
1246
|
"@media (prefers-reduced-motion: reduce) { [data-gem-reveal] { opacity: 1 !important; transform: none !important; filter: none !important; transition: none; } }"
|
|
1193
1247
|
];
|
|
1194
1248
|
var GEM_REVEAL_CSS = GEM_REVEAL_CSS_LINES.join("\n");
|
|
1249
|
+
var GEM_IMG_IN_CSS_LINES = [
|
|
1250
|
+
":where(img[data-gem-bound-img-in][data-gem-img-in-state='loading']) { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; }",
|
|
1251
|
+
"@media (prefers-reduced-motion: no-preference) { :where(img[data-gem-bound-img-in][data-gem-img-in-state='in']) { animation: gem-img-in var(--gem-motion-slow, 260ms) var(--gem-ease-enter, cubic-bezier(.33, 1, .68, 1)); } }",
|
|
1252
|
+
"@keyframes gem-img-in { from { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; } }"
|
|
1253
|
+
];
|
|
1254
|
+
var GEM_IMG_IN_CSS = GEM_IMG_IN_CSS_LINES.join("\n");
|
|
1255
|
+
var GEM_INLINE_MARK_CSS_LINES = [
|
|
1256
|
+
":where(.gem-mark--underline) { text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }",
|
|
1257
|
+
":where(.gem-mark--highlight) { background: var(--gem-mark-highlight, #fff1a6); color: var(--gem-mark-highlight-ink, #1f1d18); padding-inline: 0.12em; border-radius: 0.15em; -webkit-box-decoration-break: clone; box-decoration-break: clone; }",
|
|
1258
|
+
":where(.gem-mark--accent) { color: var(--gem-color-accent); }",
|
|
1259
|
+
":where(.gem-mark--alert) { color: var(--gem-color-danger, #b42318); }",
|
|
1260
|
+
":where(.gem-mark--highlight.gem-mark--accent), :where(.gem-mark--highlight.gem-mark--alert) { color: var(--gem-mark-highlight-ink, #1f1d18); }",
|
|
1261
|
+
".gem-mark-link { color: var(--gem-color-accent); text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }",
|
|
1262
|
+
".gem-mark-link:hover { text-decoration-thickness: 2px; }",
|
|
1263
|
+
// Bút dạ thắng màu link: chữ trên nền vàng giữ mực đậm, dù span nằm trong link hay link nằm trong span (review #740 đo màu nhấn
|
|
1264
|
+
// trên vàng 4,32:1 ở nền trắng, 2,47:1 trên băng tối, 1,05:1 trên băng màu nhấn).
|
|
1265
|
+
".gem-mark-link.gem-mark--highlight, .gem-mark--highlight .gem-mark-link { color: var(--gem-mark-highlight-ink, #1f1d18); }"
|
|
1266
|
+
];
|
|
1267
|
+
var GEM_INLINE_MARK_CSS = GEM_INLINE_MARK_CSS_LINES.join("\n");
|
|
1195
1268
|
var GEM_AMBIENT_CSS_LINES = [
|
|
1196
1269
|
"@keyframes gem-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(calc(-1 * var(--gem-ambient-amp, 7px))); } }",
|
|
1197
1270
|
"@keyframes gem-sheen { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }",
|
|
@@ -2356,6 +2429,8 @@ function createPrimitiveCss() {
|
|
|
2356
2429
|
" opacity: 1;",
|
|
2357
2430
|
"}",
|
|
2358
2431
|
...GEM_REVEAL_CSS_LINES,
|
|
2432
|
+
...GEM_IMG_IN_CSS_LINES,
|
|
2433
|
+
...GEM_INLINE_MARK_CSS_LINES,
|
|
2359
2434
|
...GEM_AMBIENT_CSS_LINES,
|
|
2360
2435
|
...GEM_DIVIDER_CSS_LINES,
|
|
2361
2436
|
...GEM_CARD_CSS_LINES,
|
|
@@ -4403,6 +4478,8 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
|
4403
4478
|
GEM_FOCUS_RING_CSS_VAR.split(":")[0].trim(),
|
|
4404
4479
|
GEM_TAP_MIN_CSS_VAR.split(":")[0].trim(),
|
|
4405
4480
|
GEM_FOCUS_OFFSET_CSS_VAR.split(":")[0].trim(),
|
|
4481
|
+
// Thang chuyển động "Gọn" (tokens.ts GEM_MOTION_SCALE_DEFAULTS, #32): luôn phát, section đọc được.
|
|
4482
|
+
...toMotionCssVars(GEM_MOTION_SCALE_DEFAULTS).map((line) => line.split(":")[0].trim()),
|
|
4406
4483
|
// The derived spacing steps (tokens.ts GEM_DERIVED_SPACE_STEPS): always emitted, never required.
|
|
4407
4484
|
...GEM_DERIVED_SPACE_CSS_VARIABLES,
|
|
4408
4485
|
// Extension points a section SETS (gemtheme #650): icon size, drawer slide distance.
|
package/dist/sections.js
CHANGED
|
@@ -104,9 +104,164 @@ function defaultPlan(plans) {
|
|
|
104
104
|
if (!plans.length) return null;
|
|
105
105
|
return plans.reduce((best, p) => p.amount < best.amount ? p : best, plans[0]);
|
|
106
106
|
}
|
|
107
|
+
|
|
108
|
+
// ../../packages/gem-sections/src/inline-marks.ts
|
|
109
|
+
var MARK_KINDS = ["bold", "italic", "underline", "highlight", "accent", "alert"];
|
|
110
|
+
var MAX_PHRASES = 12;
|
|
111
|
+
var MAX_PHRASE = 160;
|
|
112
|
+
var MAX_DEPTH = 3;
|
|
113
|
+
var MAX_INLINE = 2e4;
|
|
114
|
+
function safeMarkHref(href) {
|
|
115
|
+
if (typeof href !== "string") return void 0;
|
|
116
|
+
const h = href.trim();
|
|
117
|
+
return /^(https?:\/\/[^\s\\"<>]+|\/(?![/\\])[^\s\\"<>]*|#[^\s\\"<>]*)$/i.test(h) ? h : void 0;
|
|
118
|
+
}
|
|
119
|
+
function markPhrases(value) {
|
|
120
|
+
const raw = Array.isArray(value) ? value : typeof value === "string" ? value.split("\n") : [];
|
|
121
|
+
return raw.filter((p) => typeof p === "string").map((p) => p.trim()).filter((p) => p && p.length <= MAX_PHRASE).slice(0, MAX_PHRASES);
|
|
122
|
+
}
|
|
123
|
+
function linkPhrases(value) {
|
|
124
|
+
const raw = Array.isArray(value) ? value : typeof value === "string" ? value.split("\n") : [];
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const item of raw) {
|
|
127
|
+
let phrase, href;
|
|
128
|
+
if (typeof item === "string") {
|
|
129
|
+
const at = item.lastIndexOf("|");
|
|
130
|
+
if (at < 0) continue;
|
|
131
|
+
phrase = item.slice(0, at);
|
|
132
|
+
href = item.slice(at + 1);
|
|
133
|
+
} else if (item && typeof item === "object") {
|
|
134
|
+
phrase = item.text;
|
|
135
|
+
href = item.href;
|
|
136
|
+
}
|
|
137
|
+
const p = typeof phrase === "string" ? phrase.trim() : "";
|
|
138
|
+
const h = safeMarkHref(href);
|
|
139
|
+
if (p && p.length <= MAX_PHRASE && h) out.push({ phrase: p, href: h });
|
|
140
|
+
if (out.length >= MAX_PHRASES) break;
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
var sameMarks = (a, b) => a.href === b.href && a.marks.length === b.marks.length && a.marks.every((m, i) => m === b.marks[i]);
|
|
145
|
+
var ordered = (marks) => MARK_KINDS.filter((k) => new Set(marks).has(k));
|
|
146
|
+
function merge(runs) {
|
|
147
|
+
const out = [];
|
|
148
|
+
for (const r of runs) {
|
|
149
|
+
if (!r.text) continue;
|
|
150
|
+
const last = out[out.length - 1];
|
|
151
|
+
if (last && sameMarks(last, r)) last.text += r.text;
|
|
152
|
+
else out.push({ text: r.text, marks: [...r.marks], ...r.href ? { href: r.href } : {} });
|
|
153
|
+
}
|
|
154
|
+
return out.length ? out : [{ text: "", marks: [] }];
|
|
155
|
+
}
|
|
156
|
+
function place(text, phrases) {
|
|
157
|
+
const taken = [];
|
|
158
|
+
phrases.forEach((phrase, index) => {
|
|
159
|
+
for (let at = text.indexOf(phrase); at >= 0; at = text.indexOf(phrase, at + 1)) {
|
|
160
|
+
const end = at + phrase.length;
|
|
161
|
+
if (!taken.some(([s, e]) => at < e && s < end)) {
|
|
162
|
+
taken.push([at, end, index]);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
return taken;
|
|
168
|
+
}
|
|
169
|
+
function phraseMarkRuns(text, lists = {}) {
|
|
170
|
+
const copy = String(text ?? "");
|
|
171
|
+
const n = copy.length;
|
|
172
|
+
const marks = Array.from({ length: n });
|
|
173
|
+
const hrefs = Array.from({ length: n });
|
|
174
|
+
let any = false;
|
|
175
|
+
for (const kind of MARK_KINDS) {
|
|
176
|
+
for (const [s, e] of place(copy, markPhrases(lists[kind]))) {
|
|
177
|
+
for (let i = s; i < e; i++) (marks[i] ??= /* @__PURE__ */ new Set()).add(kind);
|
|
178
|
+
any = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const links = linkPhrases(lists.links);
|
|
182
|
+
for (const [s, e, index] of place(copy, links.map((l) => l.phrase))) {
|
|
183
|
+
for (let i = s; i < e; i++) hrefs[i] = links[index].href;
|
|
184
|
+
any = true;
|
|
185
|
+
}
|
|
186
|
+
if (!any) return [{ text: copy, marks: [] }];
|
|
187
|
+
const runs = [];
|
|
188
|
+
for (let i = 0; i < n; i++) runs.push({ text: copy[i], marks: ordered(marks[i] ?? []), ...hrefs[i] ? { href: hrefs[i] } : {} });
|
|
189
|
+
return merge(runs);
|
|
190
|
+
}
|
|
191
|
+
var NS = "(?=[^\\s*])";
|
|
192
|
+
var NE = "(?<=[^\\s*])";
|
|
193
|
+
var INLINE = new RegExp(
|
|
194
|
+
[
|
|
195
|
+
"\\[[^\\[\\]\\n]+\\]\\((?:[^()\\s]|\\([^()\\s]*\\))+\\)",
|
|
196
|
+
`\\*\\*\\*${NS}[^*\\n]+?${NE}\\*\\*\\*(?!\\*)`,
|
|
197
|
+
`\\*\\*${NS}(?:[^*\\n]|\\*\\*\\*[^*\\n]+?\\*\\*\\*|\\*(?!\\*))+?${NE}\\*\\*(?!\\*)|\\*\\*${NS}[^*\\n]*?\\*${NS}[^*\\n]+?${NE}\\*\\*\\*(?!\\*)`,
|
|
198
|
+
`\\*${NS}(?:[^*\\n]|\\*\\*\\*[^*\\n]+?\\*\\*\\*|\\*\\*[^*\\n]+?\\*\\*)+?${NE}\\*(?!\\*)`,
|
|
199
|
+
"==(?=[^\\s=])[^=\\n]+?(?<=[^\\s=])==",
|
|
200
|
+
"(?<![\\p{L}\\p{N}_])__(?=[^\\s_])[^_\\n]+?(?<=[^\\s_])__(?![\\p{L}\\p{N}_])",
|
|
201
|
+
"!!(?=[^\\s!])[^!\\n]+?(?<=[^\\s!])!!",
|
|
202
|
+
"\\^\\^(?=[^\\s^])[^\\^\\n]+?(?<=[^\\s^])\\^\\^"
|
|
203
|
+
].join("|"),
|
|
204
|
+
"gu"
|
|
205
|
+
);
|
|
206
|
+
var OPEN = [["***", ["bold", "italic"]], ["**", ["bold"]], ["==", ["highlight"]], ["__", ["underline"]], ["!!", ["alert"]], ["^^", ["bold", "accent"]], ["*", ["italic"]]];
|
|
207
|
+
var ESCAPABLE = "*_=!^[]()\\";
|
|
208
|
+
var STAND_IN = 57344;
|
|
209
|
+
var escapeMarkers = (src) => src.replace(/\\([*_=!^[\]()\\])/g, (_, c) => String.fromCharCode(STAND_IN + ESCAPABLE.indexOf(c)));
|
|
210
|
+
var unescapeMarkers = (text) => text.replace(/[-]/g, (c) => ESCAPABLE[c.charCodeAt(0) - STAND_IN]);
|
|
211
|
+
function read(src, inherited, href, depth) {
|
|
212
|
+
const out = [];
|
|
213
|
+
const at = (text, marks, h = href) => ({ text, marks: ordered(marks), ...h ? { href: h } : {} });
|
|
214
|
+
let last = 0;
|
|
215
|
+
for (const m of src.matchAll(INLINE)) {
|
|
216
|
+
const i = m.index ?? 0;
|
|
217
|
+
if (i > last) out.push(at(src.slice(last, i), inherited));
|
|
218
|
+
const tok = m[0];
|
|
219
|
+
if (tok.startsWith("[")) {
|
|
220
|
+
const lm = /^\[([^\]]+)\]\((.+)\)$/.exec(tok);
|
|
221
|
+
const safe = lm && !href ? safeMarkHref(unescapeMarkers(lm[2])) : void 0;
|
|
222
|
+
if (lm && safe && depth < MAX_DEPTH) out.push(...read(lm[1], inherited, safe, depth + 1));
|
|
223
|
+
else out.push(at(lm ? lm[1] : tok, inherited));
|
|
224
|
+
} else {
|
|
225
|
+
const [open, add] = OPEN.find(([o]) => tok.startsWith(o) && tok.endsWith(o) && tok.length > 2 * o.length) ?? OPEN.find(([o]) => tok.startsWith(o) && tok.length > 2 * o.length) ?? ["", []];
|
|
226
|
+
const close = tok.endsWith(open) ? open.length : 0;
|
|
227
|
+
const inner = open ? tok.slice(open.length, tok.length - close) : tok;
|
|
228
|
+
const marks = [...inherited, ...add];
|
|
229
|
+
if (open && depth < MAX_DEPTH) out.push(...read(inner, marks, href, depth + 1));
|
|
230
|
+
else out.push(at(open ? inner : tok, marks));
|
|
231
|
+
}
|
|
232
|
+
last = i + tok.length;
|
|
233
|
+
}
|
|
234
|
+
if (last < src.length) out.push(at(src.slice(last), inherited));
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
function inlineMarkRuns(src) {
|
|
238
|
+
const text = String(src ?? "");
|
|
239
|
+
const escapes = !/[\ue000-\ue009]/.test(text);
|
|
240
|
+
const runs = [];
|
|
241
|
+
text.split("\n").forEach((line, i) => {
|
|
242
|
+
if (i) runs.push({ text: "\n", marks: [] });
|
|
243
|
+
if (line.length > MAX_INLINE) return void runs.push({ text: line, marks: [] });
|
|
244
|
+
const read1 = read(escapes ? escapeMarkers(line) : line, [], void 0, 0);
|
|
245
|
+
runs.push(...escapes ? read1.map((r) => ({ ...r, text: unescapeMarkers(r.text) })) : read1);
|
|
246
|
+
});
|
|
247
|
+
return merge(runs);
|
|
248
|
+
}
|
|
249
|
+
function markClassName(marks) {
|
|
250
|
+
const span = marks.filter((m) => m !== "bold" && m !== "italic");
|
|
251
|
+
return span.length ? ["gem-mark", ...span.map((m) => `gem-mark--${m}`)].join(" ") : "";
|
|
252
|
+
}
|
|
253
|
+
function inlineMarkText(src) {
|
|
254
|
+
return inlineMarkRuns(src).map((r) => r.text).join("");
|
|
255
|
+
}
|
|
107
256
|
export {
|
|
257
|
+
MARK_KINDS,
|
|
108
258
|
defaultPlan,
|
|
109
259
|
elementOverrides,
|
|
110
260
|
imageSrcSet,
|
|
261
|
+
inlineMarkRuns,
|
|
262
|
+
inlineMarkText,
|
|
263
|
+
markClassName,
|
|
264
|
+
phraseMarkRuns,
|
|
265
|
+
safeMarkHref,
|
|
111
266
|
subscriptionPlans
|
|
112
267
|
};
|
package/dist/tokens.css
CHANGED
|
@@ -37,6 +37,15 @@
|
|
|
37
37
|
--gem-ambient-mid: 7s;
|
|
38
38
|
--gem-ambient-fast: 5.5s;
|
|
39
39
|
--gem-ambient-amp: 7px;
|
|
40
|
+
--gem-motion-fast: 100ms;
|
|
41
|
+
--gem-motion-base: 180ms;
|
|
42
|
+
--gem-motion-slow: 260ms;
|
|
43
|
+
--gem-ease-enter: cubic-bezier(.33, 1, .68, 1);
|
|
44
|
+
--gem-ease-exit: cubic-bezier(.4, 0, 1, 1);
|
|
45
|
+
--gem-motion-distance: 8px;
|
|
46
|
+
--gem-motion-distance-lg: 16px;
|
|
47
|
+
--gem-motion-stagger: 30ms;
|
|
48
|
+
--gem-motion-blur: 8px;
|
|
40
49
|
--gem-layout-page: 1180px;
|
|
41
50
|
--gem-layout-gutter: 32px;
|
|
42
51
|
--gem-layout-section: 64px;
|
|
@@ -68,4 +77,15 @@
|
|
|
68
77
|
--gem-border-subtle: calc(1px * var(--gem-style-border, 1)) solid var(--gem-color-border);
|
|
69
78
|
--gem-border-strong: calc(1px * var(--gem-style-border, 1)) solid color-mix(in srgb, var(--gem-color-text) 28%, transparent);
|
|
70
79
|
}
|
|
80
|
+
@media (prefers-reduced-motion: reduce) {
|
|
81
|
+
:root {
|
|
82
|
+
--gem-motion-fast: 1ms !important;
|
|
83
|
+
--gem-motion-base: 1ms !important;
|
|
84
|
+
--gem-motion-slow: 1ms !important;
|
|
85
|
+
--gem-motion-distance: 0px !important;
|
|
86
|
+
--gem-motion-distance-lg: 0px !important;
|
|
87
|
+
--gem-motion-stagger: 0ms !important;
|
|
88
|
+
--gem-motion-blur: 0px !important;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
71
91
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemboss/gem-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|