@gemboss/gem-framework 0.13.0 → 0.14.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 +28 -1
- package/dist/SOURCE.json +3 -3
- package/dist/editions.js +26 -1
- package/dist/gem-framework.css +23 -0
- package/dist/gem-runtime.js +32 -1
- package/dist/index.js +106 -6
- package/dist/plugins/img-in.d.ts +9 -0
- package/dist/plugins/img-in.js +47 -0
- package/dist/primitives.css +3 -0
- package/dist/section-gate.js +60 -5
- package/dist/tokens.css +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ gem-framework của GemTheme đóng thành package, cho các repo **ngoài** mon
|
|
|
9
9
|
| `…/sections` | 4 hàm `headless.tsx` được import (`elementOverrides`, `imageSrcSet`, `subscriptionPlans`, `defaultPlan`) | `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,33 @@ 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.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
|
|
97
|
+
đọ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"
|
|
98
|
+
(Lily 08/10/2026) mà kho đang chép tay vào từng mục:
|
|
99
|
+
(1) **Thang chuyển động (#32).** `createGemTokenCss` luôn phát `--gem-motion-fast` 100ms, `-base` 180ms, `-slow` 260ms,
|
|
100
|
+
`--gem-ease-enter` / `--gem-ease-exit`, `--gem-motion-distance` 8px, `-distance-lg` 16px, `-stagger` 30ms, `-blur` 8px
|
|
101
|
+
(`GEM_MOTION_SCALE_DEFAULTS`, `gemMotionScaleCssVars`); plan đổi được qua `motion.fast|base|slow|enter|exit|distance|
|
|
102
|
+
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.
|
|
103
|
+
Plan có `motion.duration` bằng 0 (style pack `motion_level: "none"`) nhận thang tĩnh. Kèm một khối
|
|
104
|
+
`@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
|
|
105
|
+
và độ nhoè về 0 (`GEM_MOTION_REDUCED`), mang `!important` ở cuối khai báo để stylesheet đến sau khai thang trên `:root`
|
|
106
|
+
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
|
|
107
|
+
(`motionCssVarName(key)`, dùng hàm này thay vì tra `motionCssVarAliases` theo đúng chữ):
|
|
108
|
+
`easing_soft`, `ambient-slow`, `Enter` ra đúng `--gem-ease-soft`, `--gem-ambient-slow`, `--gem-ease-enter` (trước đó ra
|
|
109
|
+
`--gem-motion-easing-soft`…, biến không rule nào đọc).
|
|
110
|
+
(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
|
|
111
|
+
`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
|
|
112
|
+
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ừ
|
|
113
|
+
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,
|
|
114
|
+
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
|
|
115
|
+
section đã tự đặt opacity / filter / animation (ảnh phụ chờ rê chuột, hero tối, `data-gem-ambient`). Một listener pha
|
|
116
|
+
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
|
|
117
|
+
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
|
|
118
|
+
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`
|
|
119
|
+
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ả
|
|
120
|
+
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ó
|
|
121
|
+
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ả
|
|
122
|
+
tabs, hero-slideshow, reveal-on-scroll, offer-engine): store nhận plugin qua `gem-runtime.js` hoặc module.
|
|
96
123
|
- **0.13.0** (11/10/2026): kiểu nút **`gradient`** (`plan.global_settings.button_style`, `<body data-gem-button-style="gradient">`).
|
|
97
124
|
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
125
|
đượ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.14.0",
|
|
4
|
+
"gemtheme_sha": "75954020da59dcca83cdae5676c0a9746e40ba4b",
|
|
5
5
|
"gemtheme_dirty": false,
|
|
6
|
-
"framework_version": "0.
|
|
6
|
+
"framework_version": "0.14.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,9 @@ 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; } }
|
|
1103
1126
|
@keyframes gem-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(calc(-1 * var(--gem-ambient-amp, 7px))); } }
|
|
1104
1127
|
@keyframes gem-sheen { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
|
|
1105
1128
|
@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.14.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 [];
|
|
@@ -1200,6 +1246,11 @@ var gemFrameworkRuntimePlugins = [
|
|
|
1200
1246
|
load_strategy: "viewport",
|
|
1201
1247
|
events: ["gem:reveal:ready", "gem:reveal:in"]
|
|
1202
1248
|
},
|
|
1249
|
+
{
|
|
1250
|
+
key: "img-in",
|
|
1251
|
+
load_strategy: "critical",
|
|
1252
|
+
events: ["gem:img-in:ready"]
|
|
1253
|
+
},
|
|
1203
1254
|
{
|
|
1204
1255
|
key: "seasonal-effects",
|
|
1205
1256
|
load_strategy: "idle",
|
|
@@ -1526,6 +1577,12 @@ var GEM_REVEAL_CSS_LINES = [
|
|
|
1526
1577
|
"@media (prefers-reduced-motion: reduce) { [data-gem-reveal] { opacity: 1 !important; transform: none !important; filter: none !important; transition: none; } }"
|
|
1527
1578
|
];
|
|
1528
1579
|
var GEM_REVEAL_CSS = GEM_REVEAL_CSS_LINES.join("\n");
|
|
1580
|
+
var GEM_IMG_IN_CSS_LINES = [
|
|
1581
|
+
":where(img[data-gem-bound-img-in][data-gem-img-in-state='loading']) { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; }",
|
|
1582
|
+
"@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)); } }",
|
|
1583
|
+
"@keyframes gem-img-in { from { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; } }"
|
|
1584
|
+
];
|
|
1585
|
+
var GEM_IMG_IN_CSS = GEM_IMG_IN_CSS_LINES.join("\n");
|
|
1529
1586
|
var GEM_AMBIENT_CSS_LINES = [
|
|
1530
1587
|
"@keyframes gem-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(calc(-1 * var(--gem-ambient-amp, 7px))); } }",
|
|
1531
1588
|
"@keyframes gem-sheen { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }",
|
|
@@ -2697,6 +2754,7 @@ function createPrimitiveCss() {
|
|
|
2697
2754
|
" opacity: 1;",
|
|
2698
2755
|
"}",
|
|
2699
2756
|
...GEM_REVEAL_CSS_LINES,
|
|
2757
|
+
...GEM_IMG_IN_CSS_LINES,
|
|
2700
2758
|
...GEM_AMBIENT_CSS_LINES,
|
|
2701
2759
|
...GEM_DIVIDER_CSS_LINES,
|
|
2702
2760
|
...GEM_CARD_CSS_LINES,
|
|
@@ -4605,6 +4663,7 @@ var runtimePluginReadyEvents = {
|
|
|
4605
4663
|
"recently-viewed": "gem:recently-viewed:ready",
|
|
4606
4664
|
countdown: "gem:countdown:ready",
|
|
4607
4665
|
"reveal-on-scroll": "gem:reveal:ready",
|
|
4666
|
+
"img-in": "gem:img-in:ready",
|
|
4608
4667
|
"seasonal-effects": "gem:seasonal:ready",
|
|
4609
4668
|
"context-adapter": "gem:context:ready",
|
|
4610
4669
|
"offer-engine": "gem:offer:ready"
|
|
@@ -5574,6 +5633,39 @@ var revealOnScrollPluginRegistrationLines = [
|
|
|
5574
5633
|
" api.emit('gem:reveal:ready', { source: 'gem-runtime', count: bound });",
|
|
5575
5634
|
" });"
|
|
5576
5635
|
];
|
|
5636
|
+
var imgInPluginRegistrationLines = [
|
|
5637
|
+
" registerPlugin('img-in', ({ api }) => {",
|
|
5638
|
+
" const STATE = 'data-gem-img-in-state';",
|
|
5639
|
+
" const BOUND = 'data-gem-bound-img-in';",
|
|
5640
|
+
" const reduce = typeof target.matchMedia === 'function' && target.matchMedia('(prefers-reduced-motion: reduce)').matches;",
|
|
5641
|
+
" const style = (img) => (typeof target.getComputedStyle === 'function' ? target.getComputedStyle(img) : null);",
|
|
5642
|
+
" const owned = (s) => Boolean(s && ((s.opacity && s.opacity !== '1') || (s.filter && s.filter !== 'none') || (s.animationName && s.animationName !== 'none')));",
|
|
5643
|
+
" const overridden = (s) => Boolean(s && ((s.opacity && s.opacity !== '0.6') || (s.filter && s.filter.indexOf('blur') === -1)));",
|
|
5644
|
+
" const settle = (event) => {",
|
|
5645
|
+
" const img = event.target;",
|
|
5646
|
+
" if (!img || img.tagName !== 'IMG' || !img.hasAttribute(BOUND)) return;",
|
|
5647
|
+
" const state = img.getAttribute(STATE);",
|
|
5648
|
+
" if (event.type === 'load' || event.type === 'error') {",
|
|
5649
|
+
" if (state !== 'loading') return;",
|
|
5650
|
+
" if (event.type === 'load' && !reduce && !overridden(style(img))) img.setAttribute(STATE, 'in');",
|
|
5651
|
+
" else img.removeAttribute(STATE);",
|
|
5652
|
+
" return;",
|
|
5653
|
+
" }",
|
|
5654
|
+
" if (state === 'in' && event.animationName === 'gem-img-in') img.removeAttribute(STATE);",
|
|
5655
|
+
" };",
|
|
5656
|
+
" ['load', 'error', 'animationend', 'animationcancel'].forEach((type) => document.addEventListener(type, settle, true));",
|
|
5657
|
+
" const candidates = [];",
|
|
5658
|
+
" document.querySelectorAll('img').forEach((img) => {",
|
|
5659
|
+
" if (!img.closest('[data-gem-img-in]')) return;",
|
|
5660
|
+
" if (img.complete) { if (!img.hasAttribute(BOUND)) img.removeAttribute(STATE); return; }",
|
|
5661
|
+
" if (mark(img, 'img-in')) candidates.push(img);",
|
|
5662
|
+
" });",
|
|
5663
|
+
" const own = candidates.map((img) => owned(style(img)));",
|
|
5664
|
+
" let bound = 0;",
|
|
5665
|
+
" candidates.forEach((img, i) => { if (own[i]) return; img.setAttribute(STATE, 'loading'); bound += 1; });",
|
|
5666
|
+
" api.emit('gem:img-in:ready', { source: 'gem-runtime', count: bound });",
|
|
5667
|
+
" });"
|
|
5668
|
+
];
|
|
5577
5669
|
var recentlyViewedPluginRegistrationLines = [
|
|
5578
5670
|
" registerPlugin('recently-viewed', ({ api }) => {",
|
|
5579
5671
|
" const storageKey = 'gem:recently-viewed-products';",
|
|
@@ -5670,6 +5762,7 @@ var runtimePluginSourceLines = [
|
|
|
5670
5762
|
["before_after", beforeAfterPluginRegistrationLines],
|
|
5671
5763
|
["countdown", countdownPluginRegistrationLines],
|
|
5672
5764
|
["reveal-on-scroll", revealOnScrollPluginRegistrationLines],
|
|
5765
|
+
["img-in", imgInPluginRegistrationLines],
|
|
5673
5766
|
["recently-viewed", recentlyViewedPluginRegistrationLines],
|
|
5674
5767
|
["runtime-slot-loader", runtimeSlotLoaderPluginRegistrationLines],
|
|
5675
5768
|
["offer-engine", offerEnginePluginRegistrationLines]
|
|
@@ -5971,6 +6064,8 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
|
5971
6064
|
GEM_FOCUS_RING_CSS_VAR.split(":")[0].trim(),
|
|
5972
6065
|
GEM_TAP_MIN_CSS_VAR.split(":")[0].trim(),
|
|
5973
6066
|
GEM_FOCUS_OFFSET_CSS_VAR.split(":")[0].trim(),
|
|
6067
|
+
// Thang chuyển động "Gọn" (tokens.ts GEM_MOTION_SCALE_DEFAULTS, #32): luôn phát, section đọc được.
|
|
6068
|
+
...toMotionCssVars(GEM_MOTION_SCALE_DEFAULTS).map((line) => line.split(":")[0].trim()),
|
|
5974
6069
|
// The derived spacing steps (tokens.ts GEM_DERIVED_SPACE_STEPS): always emitted, never required.
|
|
5975
6070
|
...GEM_DERIVED_SPACE_CSS_VARIABLES,
|
|
5976
6071
|
// Extension points a section SETS (gemtheme #650): icon size, drawer slide distance.
|
|
@@ -7316,7 +7411,10 @@ export {
|
|
|
7316
7411
|
GEM_FRAMEWORK_EDITIONS,
|
|
7317
7412
|
GEM_GRID_COLUMNS_ROW_KNOB_CSS,
|
|
7318
7413
|
GEM_HEADING_SIZE_CSS_VARS,
|
|
7414
|
+
GEM_IMG_IN_CSS,
|
|
7319
7415
|
GEM_LINE_HEIGHT_TOKEN_KEYS,
|
|
7416
|
+
GEM_MOTION_REDUCED,
|
|
7417
|
+
GEM_MOTION_SCALE_DEFAULTS,
|
|
7320
7418
|
GEM_PART_CLASS,
|
|
7321
7419
|
GEM_POPUP_SHELL_CSS,
|
|
7322
7420
|
GEM_REVEAL_CSS,
|
|
@@ -7371,6 +7469,7 @@ export {
|
|
|
7371
7469
|
gemFrameworkValidationRules,
|
|
7372
7470
|
gemFrameworkVersion,
|
|
7373
7471
|
gemMobileTokenCssVars,
|
|
7472
|
+
gemMotionScaleCssVars,
|
|
7374
7473
|
gemPartClass,
|
|
7375
7474
|
gemPhoneTokenTarget,
|
|
7376
7475
|
gemPlanShapeKnobs,
|
|
@@ -7384,6 +7483,7 @@ export {
|
|
|
7384
7483
|
modernMineralFamilyLineage,
|
|
7385
7484
|
modernMineralHomepageLineageSections,
|
|
7386
7485
|
motionCssVarAliases,
|
|
7486
|
+
motionCssVarName,
|
|
7387
7487
|
normalizeGemTokens,
|
|
7388
7488
|
normalizeHex,
|
|
7389
7489
|
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,9 @@ 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; } }
|
|
1032
1035
|
@keyframes gem-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(calc(-1 * var(--gem-ambient-amp, 7px))); } }
|
|
1033
1036
|
@keyframes gem-sheen { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
|
|
1034
1037
|
@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 [];
|
|
@@ -1192,6 +1238,12 @@ var GEM_REVEAL_CSS_LINES = [
|
|
|
1192
1238
|
"@media (prefers-reduced-motion: reduce) { [data-gem-reveal] { opacity: 1 !important; transform: none !important; filter: none !important; transition: none; } }"
|
|
1193
1239
|
];
|
|
1194
1240
|
var GEM_REVEAL_CSS = GEM_REVEAL_CSS_LINES.join("\n");
|
|
1241
|
+
var GEM_IMG_IN_CSS_LINES = [
|
|
1242
|
+
":where(img[data-gem-bound-img-in][data-gem-img-in-state='loading']) { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; }",
|
|
1243
|
+
"@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)); } }",
|
|
1244
|
+
"@keyframes gem-img-in { from { filter: blur(var(--gem-motion-blur, 8px)); opacity: 0.6; } }"
|
|
1245
|
+
];
|
|
1246
|
+
var GEM_IMG_IN_CSS = GEM_IMG_IN_CSS_LINES.join("\n");
|
|
1195
1247
|
var GEM_AMBIENT_CSS_LINES = [
|
|
1196
1248
|
"@keyframes gem-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(calc(-1 * var(--gem-ambient-amp, 7px))); } }",
|
|
1197
1249
|
"@keyframes gem-sheen { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }",
|
|
@@ -2356,6 +2408,7 @@ function createPrimitiveCss() {
|
|
|
2356
2408
|
" opacity: 1;",
|
|
2357
2409
|
"}",
|
|
2358
2410
|
...GEM_REVEAL_CSS_LINES,
|
|
2411
|
+
...GEM_IMG_IN_CSS_LINES,
|
|
2359
2412
|
...GEM_AMBIENT_CSS_LINES,
|
|
2360
2413
|
...GEM_DIVIDER_CSS_LINES,
|
|
2361
2414
|
...GEM_CARD_CSS_LINES,
|
|
@@ -4403,6 +4456,8 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
|
|
|
4403
4456
|
GEM_FOCUS_RING_CSS_VAR.split(":")[0].trim(),
|
|
4404
4457
|
GEM_TAP_MIN_CSS_VAR.split(":")[0].trim(),
|
|
4405
4458
|
GEM_FOCUS_OFFSET_CSS_VAR.split(":")[0].trim(),
|
|
4459
|
+
// Thang chuyển động "Gọn" (tokens.ts GEM_MOTION_SCALE_DEFAULTS, #32): luôn phát, section đọc được.
|
|
4460
|
+
...toMotionCssVars(GEM_MOTION_SCALE_DEFAULTS).map((line) => line.split(":")[0].trim()),
|
|
4406
4461
|
// The derived spacing steps (tokens.ts GEM_DERIVED_SPACE_STEPS): always emitted, never required.
|
|
4407
4462
|
...GEM_DERIVED_SPACE_CSS_VARIABLES,
|
|
4408
4463
|
// Extension points a section SETS (gemtheme #650): icon size, drawer slide distance.
|
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.14.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",
|