@gemboss/gem-framework 0.4.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.
@@ -0,0 +1,112 @@
1
+ // ../../packages/gem-sections/src/element-overrides.ts
2
+ function edgesToCss(v) {
3
+ if (!v || typeof v !== "object") return void 0;
4
+ const sides = [v.t, v.r, v.b, v.l];
5
+ if (!sides.some((x) => typeof x === "number")) return void 0;
6
+ const px = (x) => typeof x === "number" ? `${x}px` : "0px";
7
+ return `${px(v.t)} ${px(v.r)} ${px(v.b)} ${px(v.l)}`;
8
+ }
9
+ function elementOverrideStyle(section, elId) {
10
+ const store = section?.element_settings ?? {};
11
+ const o = store[elId];
12
+ if (!o || typeof o !== "object") return void 0;
13
+ const s = {};
14
+ if (typeof o.font_size === "number") s.fontSize = `${o.font_size}px`;
15
+ if (typeof o.text_color === "string" && o.text_color) s.color = o.text_color;
16
+ if (typeof o.font_weight === "number" || typeof o.font_weight === "string" && o.font_weight) s.fontWeight = String(o.font_weight);
17
+ if (o.text_align === "left" || o.text_align === "center" || o.text_align === "right") s.textAlign = o.text_align;
18
+ if (typeof o.line_height === "number") s.lineHeight = String(o.line_height);
19
+ if (typeof o.letter_spacing === "number") s.letterSpacing = `${o.letter_spacing}px`;
20
+ if (o.text_transform === "uppercase" || o.text_transform === "lowercase" || o.text_transform === "capitalize" || o.text_transform === "none") s.textTransform = o.text_transform;
21
+ if (o.font_style === "italic") s.fontStyle = "italic";
22
+ const pad = edgesToCss(o.padding);
23
+ if (pad) s.padding = pad;
24
+ const mar = edgesToCss(o.margin);
25
+ if (mar) s.margin = mar;
26
+ if (typeof o.background_color === "string" && o.background_color) s.background = o.background_color;
27
+ if (typeof o.border_radius === "number") s.borderRadius = `${o.border_radius}px`;
28
+ if (typeof o.border_width === "number" && o.border_width > 0) {
29
+ const bc = typeof o.border_color === "string" && o.border_color ? o.border_color : "currentColor";
30
+ s.border = `${o.border_width}px solid ${bc}`;
31
+ }
32
+ if (typeof o.opacity === "number" && o.opacity >= 0 && o.opacity <= 100) s.opacity = String(o.opacity / 100);
33
+ if (typeof o.max_width === "number" && o.max_width > 0) s.maxWidth = `${o.max_width}px`;
34
+ return Object.keys(s).length ? s : void 0;
35
+ }
36
+ function elementOverrides(section) {
37
+ return (elId) => elementOverrideStyle(section, elId);
38
+ }
39
+
40
+ // ../../packages/gem-sections/src/responsive-image.ts
41
+ var WIDTHS = [640, 828, 1080, 1920];
42
+ var OPTIMIZER = /^\/_next\/image\?url=[^&\s]+&w=(\d+)&q=(\d+)$/;
43
+ var SHOPIFY_WIDTHS = [256, 384, 512, 768, 1024, 1536];
44
+ var SHOPIFY_IMAGE = /^https:\/\/(?:cdn\.shopify\.com\/|[a-z0-9-]+\.myshopify\.com\/cdn\/)[^\s]+$/i;
45
+ function shopifySrcSet(src) {
46
+ let url;
47
+ try {
48
+ url = new URL(src);
49
+ } catch {
50
+ return void 0;
51
+ }
52
+ if (url.searchParams.has("width")) return void 0;
53
+ return SHOPIFY_WIDTHS.map((w) => {
54
+ const candidate = new URL(url.href);
55
+ candidate.searchParams.set("width", String(w));
56
+ return `${candidate.href} ${w}w`;
57
+ }).join(", ");
58
+ }
59
+ function imageSrcSet(src) {
60
+ if (typeof src !== "string") return void 0;
61
+ if (SHOPIFY_IMAGE.test(src)) return shopifySrcSet(src);
62
+ const match = OPTIMIZER.exec(src);
63
+ if (!match) return void 0;
64
+ const asked = Number(match[1]);
65
+ const widths = WIDTHS.filter((w) => w <= asked);
66
+ if (widths.length < 2) return void 0;
67
+ return widths.map((w) => `${src.replace(/&w=\d+/, `&w=${w}`)} ${w}w`).join(", ");
68
+ }
69
+
70
+ // ../../packages/gem-sections/src/selling-plans.ts
71
+ function num(value) {
72
+ const n = typeof value === "string" || typeof value === "number" ? Number(value) : NaN;
73
+ return Number.isFinite(n) ? n : null;
74
+ }
75
+ function subscriptionPlans(allocations, basePrice) {
76
+ const rows = Array.isArray(allocations) ? allocations : [];
77
+ const base = num(basePrice) ?? 0;
78
+ const seen = /* @__PURE__ */ new Set();
79
+ const out = [];
80
+ for (const row of rows) {
81
+ const id = typeof row?.sellingPlan?.id === "string" ? row.sellingPlan.id.trim() : "";
82
+ if (!id || seen.has(id)) continue;
83
+ const adjustments = Array.isArray(row?.priceAdjustments) ? row.priceAdjustments.filter(Boolean) : [];
84
+ const first = adjustments[0];
85
+ const money = first?.perDeliveryPrice ?? first?.price ?? null;
86
+ const amount = adjustments.length ? num(money?.amount) : base > 0 ? base : null;
87
+ if (amount === null) continue;
88
+ seen.add(id);
89
+ const cheaper = base > 0 && amount < base;
90
+ const discountPct = cheaper ? (1 - amount / base) * 100 : 0;
91
+ out.push({
92
+ id,
93
+ name: typeof row?.sellingPlan?.name === "string" && row.sellingPlan.name.trim() || "Subscription",
94
+ amount,
95
+ currencyCode: typeof money?.currencyCode === "string" && money.currencyCode || void 0,
96
+ savingPct: Math.round(discountPct),
97
+ discountPct,
98
+ introOnly: adjustments.length > 1
99
+ });
100
+ }
101
+ return out;
102
+ }
103
+ function defaultPlan(plans) {
104
+ if (!plans.length) return null;
105
+ return plans.reduce((best, p) => p.amount < best.amount ? p : best, plans[0]);
106
+ }
107
+ export {
108
+ defaultPlan,
109
+ elementOverrides,
110
+ imageSrcSet,
111
+ subscriptionPlans
112
+ };
@@ -0,0 +1,254 @@
1
+ // ../../scripts/lib/settings-schema-contract.mjs
2
+ var CONTROL_TYPES = Object.freeze([
3
+ "text",
4
+ "textarea",
5
+ "richtext",
6
+ "inline_richtext",
7
+ "select",
8
+ "radio",
9
+ "checkbox",
10
+ "number",
11
+ "range",
12
+ "color",
13
+ "color_scheme",
14
+ "image_picker",
15
+ "video",
16
+ "url",
17
+ "link_list",
18
+ "collection",
19
+ "product",
20
+ "edges",
21
+ // 4-side spacing box, value = {t,r,b,l}
22
+ "repeater",
23
+ // object-array rows
24
+ "header"
25
+ // non-input group label
26
+ ]);
27
+ var SETTING_GROUPS = Object.freeze(["content", "options", "design", "advanced"]);
28
+ var CONTROL_TYPE_SET = new Set(CONTROL_TYPES);
29
+ var GROUP_SET = new Set(SETTING_GROUPS);
30
+ var GEM_PREFIX = "gem_";
31
+ var ADVANCED_ID_RE = /(^|_)(visibility|custom_css|custom_html|custom_class|class_name|attribute|animation|reveal|position|opacity|z_index|anchor|css_id)(_|$)/i;
32
+ var DESIGN_ID_RE = /(^|_)(colou?r|scheme|radius|shadow|border|spacing|padding|margin|weight|font|typography|align|alignment|card|divider|overlay|tone|price)(_|$)/i;
33
+ var CONTENT_TYPES = /* @__PURE__ */ new Set([
34
+ "text",
35
+ "textarea",
36
+ "richtext",
37
+ "inline_richtext",
38
+ "image_picker",
39
+ "video",
40
+ "url",
41
+ "link_list",
42
+ "collection",
43
+ "product",
44
+ "repeater"
45
+ ]);
46
+ var HEX_RE = /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
47
+ function humanizeId(id = "") {
48
+ const base = String(id).replace(/^gem_/, "");
49
+ const words = base.replace(/[_-]+/g, " ").trim().split(/\s+/);
50
+ if (words.length === 0 || words[0] === "") return "";
51
+ return words.map((w, i) => i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : w).join(" ");
52
+ }
53
+ function inferStringControl(id = "", setting = {}) {
54
+ const key = String(id).toLowerCase();
55
+ if (typeof setting.default === "string" && HEX_RE.test(setting.default.trim())) return "color";
56
+ if (/(^|_)(colou?r)(_|$)/.test(key)) return "color";
57
+ if (/scheme/.test(key)) return "color_scheme";
58
+ if (/(url|link|href)$/.test(key)) return "url";
59
+ if (/(^|_)(image|photo|logo|avatar|icon|media)(_|$)/.test(key) && !/(url|link)$/.test(key)) return "image_picker";
60
+ return typeof setting.default === "string" && setting.default.length > 80 ? "textarea" : "text";
61
+ }
62
+ function coerceStubType(id = "", fallback = "text") {
63
+ const key = String(id).toLowerCase();
64
+ if (/^show_|^enable_|^hide_|^is_|_enabled$|^use_/.test(key)) return "checkbox";
65
+ if (key === "color_scheme" || /_scheme$|color_scheme/.test(key)) return "color_scheme";
66
+ if (/(^|_)(colou?r)(_|$)/.test(key)) return "color";
67
+ if (/^layout$|_layout$|position|alignment|_size$|variant|style$|columns?$/.test(key)) return "select";
68
+ if (/(^|_)(image|photo|media|logo|avatar)(_|$)/.test(key)) return "image_picker";
69
+ if (/(url|link|href)$/.test(key)) return "url";
70
+ if (/count$|limit$|number|_qty$|min$|max$/.test(key)) return "number";
71
+ return CONTROL_TYPE_SET.has(fallback) ? fallback : "text";
72
+ }
73
+ function resolveType(s, hasOptions) {
74
+ const rawType = String(s.type || "").toLowerCase();
75
+ if (CONTROL_TYPE_SET.has(rawType) && rawType !== "text" && rawType !== "textarea") return rawType;
76
+ if (hasOptions && rawType !== "radio") {
77
+ if (rawType === "" || rawType === "string" || rawType === "integer" || rawType === "number" || rawType === "setting" || !CONTROL_TYPE_SET.has(rawType)) {
78
+ return "select";
79
+ }
80
+ }
81
+ switch (rawType) {
82
+ case "text":
83
+ case "textarea":
84
+ return rawType;
85
+ case "boolean":
86
+ return "checkbox";
87
+ case "string":
88
+ return inferStringControl(s.id, s);
89
+ case "integer":
90
+ return "number";
91
+ case "array":
92
+ case "object":
93
+ return "repeater";
94
+ case "setting":
95
+ return coerceStubType(s.id);
96
+ default:
97
+ return coerceStubType(s.id, rawType);
98
+ }
99
+ }
100
+ function inferGroup(setting = {}) {
101
+ const id = String(setting.id || "");
102
+ const type = String(setting.type || "");
103
+ if (setting.group && GROUP_SET.has(setting.group)) return setting.group;
104
+ if (ADVANCED_ID_RE.test(id)) return "advanced";
105
+ if (id.startsWith(GEM_PREFIX)) return "design";
106
+ if (type === "color" || type === "color_scheme" || type === "edges") return "design";
107
+ if (type === "image_picker" || type === "video") return "content";
108
+ if (DESIGN_ID_RE.test(id)) return "design";
109
+ if (CONTENT_TYPES.has(type) && !/^show_|^enable_|^default_|^selected_/.test(id)) return "content";
110
+ return "options";
111
+ }
112
+ function normalizeSetting(s = {}) {
113
+ if (!s || typeof s !== "object") return null;
114
+ if (s.type === "header") {
115
+ return { type: "header", group: s.group && GROUP_SET.has(s.group) ? s.group : "content", content: s.content || s.label || "" };
116
+ }
117
+ const id = s.id;
118
+ if (!id) return null;
119
+ const options = Array.isArray(s.options) ? s.options : Array.isArray(s.enum) ? s.enum.map((v) => ({ value: String(v), label: humanizeId(String(v)) || String(v) })) : void 0;
120
+ const min = s.min !== void 0 ? s.min : s.minimum;
121
+ const max = s.max !== void 0 ? s.max : s.maximum;
122
+ const type = resolveType(s, Boolean(options));
123
+ const out = {
124
+ id,
125
+ type,
126
+ group: inferGroup({ ...s, type }),
127
+ label: s.label || humanizeId(id)
128
+ };
129
+ if (s.info) out.info = s.info;
130
+ if (s.default !== void 0) out.default = s.default;
131
+ if (options) out.options = options;
132
+ if (min !== void 0) out.min = min;
133
+ if (max !== void 0) out.max = max;
134
+ if (s.step !== void 0) out.step = s.step;
135
+ if (s.unit !== void 0) out.unit = s.unit;
136
+ if (s.variant) out.variant = s.variant;
137
+ if (s.requires && typeof s.requires === "object") out.requires = s.requires;
138
+ if (type === "repeater" && Array.isArray(s.item)) out.item = s.item;
139
+ return out;
140
+ }
141
+ function normalizeBlocks(blocks) {
142
+ if (!Array.isArray(blocks)) return void 0;
143
+ return blocks.map((b) => {
144
+ if (!b || typeof b !== "object") return b;
145
+ const out = { ...b };
146
+ if (Array.isArray(b.settings)) out.settings = b.settings.map(normalizeSetting).filter(Boolean);
147
+ return out;
148
+ });
149
+ }
150
+ function dedupe(rows) {
151
+ const seen = /* @__PURE__ */ new Set();
152
+ const out = [];
153
+ for (const r of rows) {
154
+ if (!r) continue;
155
+ if (r.type === "header") {
156
+ out.push(r);
157
+ continue;
158
+ }
159
+ if (r.id && seen.has(r.id)) continue;
160
+ if (r.id) seen.add(r.id);
161
+ out.push(r);
162
+ }
163
+ return out;
164
+ }
165
+ function normalizeSettingsSchema(raw) {
166
+ let rows = [];
167
+ if (raw && Array.isArray(raw.settings)) {
168
+ rows = raw.settings.map(normalizeSetting);
169
+ } else if (raw && raw.type === "object" && raw.properties && typeof raw.properties === "object") {
170
+ const required = new Set(Array.isArray(raw.required) ? raw.required : []);
171
+ rows = Object.entries(raw.properties).map(([id, prop]) => normalizeSetting({ id, ...prop, required: required.has(id) }));
172
+ } else if (Array.isArray(raw)) {
173
+ rows = raw.map(normalizeSetting);
174
+ }
175
+ const out = { settings: dedupe(rows) };
176
+ const blocks = normalizeBlocks(raw && raw.blocks);
177
+ if (blocks) out.blocks = blocks;
178
+ return out;
179
+ }
180
+ function validateRow(s, seen, push) {
181
+ if (!s || typeof s !== "object") {
182
+ push("error", "invalid_setting_row", "Setting row is not an object.");
183
+ return;
184
+ }
185
+ if (s.type === "header") return;
186
+ if (s.type === "setting") {
187
+ push("error", "stub_setting_type", `Stub dialect (type:"setting") carries no control info.`, s.id);
188
+ return;
189
+ }
190
+ if (!s.id) push("error", "missing_id", "Setting has no `id`.");
191
+ else if (seen.has(s.id)) push("error", "duplicate_id", `Duplicate setting id "${s.id}".`, s.id);
192
+ else seen.add(s.id);
193
+ if (!s.type) push("error", "missing_type", `Setting "${s.id}" has no \`type\`.`, s.id);
194
+ else if (!CONTROL_TYPE_SET.has(s.type)) push("error", "unknown_type", `Setting "${s.id}" uses non-canonical type "${s.type}".`, s.id);
195
+ if (!s.label) push("warn", "missing_label", `Setting "${s.id}" has no \`label\`.`, s.id);
196
+ if (!s.group) push("warn", "missing_group", `Setting "${s.id}" has no \`group\`.`, s.id);
197
+ else if (!GROUP_SET.has(s.group)) push("error", "unknown_group", `Setting "${s.id}" uses unknown group "${s.group}".`, s.id);
198
+ if ((s.type === "select" || s.type === "radio") && !Array.isArray(s.options))
199
+ push("warn", "choice_without_options", `Setting "${s.id}" (${s.type}) has no \`options\`.`, s.id);
200
+ if (s.requires !== void 0) {
201
+ const r = s.requires;
202
+ const ok = r && typeof r === "object" && typeof r.key === "string" && r.key && ("equals" in r && ["string", "number", "boolean"].includes(typeof r.equals) || r.set === true);
203
+ if (!ok) push("error", "invalid_requires", `Setting "${s.id}" has a malformed \`requires\` (want {key, equals} or {key, set: true}).`, s.id);
204
+ }
205
+ }
206
+ function validateRequires(rows, push) {
207
+ const byId = new Map(rows.filter((r) => r && r.id).map((r) => [r.id, r]));
208
+ for (const s of rows) {
209
+ const r = s && s.requires;
210
+ if (!r || typeof r !== "object" || typeof r.key !== "string") continue;
211
+ const gate = byId.get(r.key);
212
+ if (!gate || gate === s) {
213
+ push("error", "requires_unknown_key", `Setting "${s.id}" requires "${r.key}", which this schema does not declare.`, s.id);
214
+ continue;
215
+ }
216
+ if ("equals" in r && Array.isArray(gate.options) && !gate.options.some((o) => o && String(o.value) === String(r.equals)))
217
+ push("error", "requires_unknown_value", `Setting "${s.id}" requires ${r.key} = "${r.equals}", which is not one of its options.`, s.id);
218
+ }
219
+ }
220
+ function validateSettingsSchema(raw) {
221
+ const issues = [];
222
+ const push = (level, code, message, id) => issues.push({ level, code, message, id });
223
+ const isArrayShape = raw && Array.isArray(raw.settings);
224
+ const isJsonSchemaShape = raw && raw.type === "object" && raw.properties;
225
+ if (!isArrayShape) {
226
+ if (isJsonSchemaShape) push("error", "legacy_json_schema_dialect", "Uses the JSON-Schema dialect ({type:object,properties}); migrate to {settings:[\u2026]}.");
227
+ else push("error", "not_a_settings_schema", "Missing top-level `settings` array.");
228
+ return { issues };
229
+ }
230
+ const seen = /* @__PURE__ */ new Set();
231
+ for (const s of raw.settings) validateRow(s, seen, push);
232
+ validateRequires(raw.settings, push);
233
+ if (Array.isArray(raw.blocks)) {
234
+ for (const b of raw.blocks) {
235
+ if (b && Array.isArray(b.settings)) {
236
+ const blockSeen = /* @__PURE__ */ new Set();
237
+ for (const s of b.settings) validateRow(s, blockSeen, push);
238
+ }
239
+ }
240
+ }
241
+ return { issues };
242
+ }
243
+ function isCanonical(raw) {
244
+ return validateSettingsSchema(raw).issues.every((i) => i.level !== "error");
245
+ }
246
+ export {
247
+ CONTROL_TYPES,
248
+ SETTING_GROUPS,
249
+ humanizeId,
250
+ inferGroup,
251
+ isCanonical,
252
+ normalizeSettingsSchema,
253
+ validateSettingsSchema
254
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@gemboss/gem-framework",
3
+ "version": "0.4.0",
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
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "exports": {
8
+ ".": "./dist/index.js",
9
+ "./sections": "./dist/sections.js",
10
+ "./section-gate": "./dist/section-gate.js",
11
+ "./settings-schema": "./dist/settings-schema.js",
12
+ "./gem-framework.css": "./dist/gem-framework.css",
13
+ "./gem-runtime.js": "./dist/gem-runtime.js",
14
+ "./SOURCE.json": "./dist/SOURCE.json"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "node build.mjs",
22
+ "prepack": "node build.mjs"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/chriskoh26/gemtheme.git",
27
+ "directory": "dist-packages/gem-framework"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "license": "UNLICENSED",
33
+ "private": false
34
+ }