sbuilder-mcp 0.32.1 → 0.33.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/CHANGELOG.md +12 -0
- package/CHANGELOG.vi.md +12 -0
- package/dist/catalog/elements.generated.js +12 -5
- package/dist/catalog/storepages.generated.js +8 -0
- package/dist/domains/site/importmap.js +26 -0
- package/dist/domains/site/patterns.js +45 -8
- package/dist/vision/capture.js +19 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ All notable changes to this project are documented in this file.
|
|
|
6
6
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
7
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [0.33.0] - 2026-09-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- The catalog now declares seven config keys the platform's renderers always read but had never been listed anywhere: `image`'s `imageEager` (the override for a hero image that is not first, deciding the page's `<link rel=preload>` target), `menu`'s `activeColor`, `dataset-block`'s `contentWidthCustom`, `breadcrumb`'s `currentColor` and `maxItems`, `product-variants`' `optionGap`, and `list-dataset`'s `articleSourceType`. `sb_add`, `sb_set`, and `sb_traits_for` can now read and write all seven.
|
|
13
|
+
- `sb_traits_for`'s `config_values` vocabulary for `list-dataset`'s `articleSourceType` is now reachable: the value vocabulary already existed but the element had never listed the key, so no call could reach it.
|
|
14
|
+
|
|
15
|
+
## [0.32.2] - 2026-09-11
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- A centred section built by sb_import, sb_import_site, or the built-in layout patterns applied by sb_template_use no longer renders half left-aligned: the theme's heading preset overrides inherited text alignment, so headings and paragraphs now carry `textAlign` directly on the node when the source (or pattern) centers or right-aligns them.
|
|
19
|
+
- The gallery-wall layout pattern applied by sb_template_use now frames every photo to a shared aspect ratio measured as the median of the pictures actually picked, instead of leaving each tile its own shape, so a wall of portraits or landscapes no longer renders with mismatched row heights.
|
|
20
|
+
|
|
9
21
|
## [0.32.1] - 2026-09-11
|
|
10
22
|
|
|
11
23
|
### Fixed
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,18 @@ Mọi thay đổi đáng chú ý của dự án được ghi lại trong file n
|
|
|
6
6
|
Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
7
|
và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [0.33.0] - 2026-09-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Catalog giờ khai báo bảy config key mà renderer của nền tảng luôn đọc nhưng chưa từng được liệt kê ở đâu cả: `imageEager` của `image` (giá trị ghi đè cho ảnh hero không phải ảnh đầu tiên, quyết định đích `<link rel=preload>` của trang), `activeColor` của `menu`, `contentWidthCustom` của `dataset-block`, `currentColor` và `maxItems` của `breadcrumb`, `optionGap` của `product-variants`, và `articleSourceType` của `list-dataset`. sb_add, sb_set và sb_traits_for giờ đọc và ghi được cả bảy key này.
|
|
13
|
+
- Bảng từ vựng `config_values` của sb_traits_for cho `articleSourceType` thuộc `list-dataset` giờ có thể truy cập được: từ vựng giá trị này đã tồn tại từ trước nhưng element chưa từng liệt kê key này, nên không lệnh gọi nào chạm tới được nó.
|
|
14
|
+
|
|
15
|
+
## [0.32.2] - 2026-09-11
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Một section được căn giữa do sb_import, sb_import_site, hoặc các layout pattern có sẵn được sb_template_use áp dụng giờ không còn hiển thị nửa căn trái nữa: preset heading của theme ghi đè cách căn chữ được kế thừa, nên heading và đoạn văn giờ mang `textAlign` trực tiếp trên node khi trang nguồn (hoặc pattern) căn giữa hay căn phải chúng.
|
|
19
|
+
- Layout pattern dạng tường ảnh do sb_template_use áp dụng giờ đóng khung mọi ảnh theo một tỉ lệ khung hình chung, tính bằng trung vị của các ảnh thực sự được chọn, thay vì để mỗi ô giữ nguyên hình dạng riêng, nên một tường ảnh chân dung hay ảnh ngang không còn hiển thị với chiều cao các hàng lệch nhau.
|
|
20
|
+
|
|
9
21
|
## [0.32.1] - 2026-09-11
|
|
10
22
|
|
|
11
23
|
### Fixed
|
|
@@ -1608,7 +1608,8 @@ export const ELEMENTS = {
|
|
|
1608
1608
|
"config": {
|
|
1609
1609
|
"imageRatio": "auto",
|
|
1610
1610
|
"customImageRatioWidth": 16,
|
|
1611
|
-
"customImageRatioHeight": 9
|
|
1611
|
+
"customImageRatioHeight": 9,
|
|
1612
|
+
"imageEager": false
|
|
1612
1613
|
}
|
|
1613
1614
|
},
|
|
1614
1615
|
"inspector": [
|
|
@@ -12457,7 +12458,9 @@ export const ELEMENTS = {
|
|
|
12457
12458
|
"config": {
|
|
12458
12459
|
"iconSize": 16,
|
|
12459
12460
|
"iconColor": "#000000",
|
|
12460
|
-
"textGlobalStyle": "text-1"
|
|
12461
|
+
"textGlobalStyle": "text-1",
|
|
12462
|
+
"currentColor": "",
|
|
12463
|
+
"maxItems": 0
|
|
12461
12464
|
}
|
|
12462
12465
|
},
|
|
12463
12466
|
"inspector": [
|
|
@@ -12654,7 +12657,8 @@ export const ELEMENTS = {
|
|
|
12654
12657
|
"contentWidth": "fill_container",
|
|
12655
12658
|
"dropdownIcon": "down",
|
|
12656
12659
|
"expandType": "click",
|
|
12657
|
-
"submenuStyle": "dropdown"
|
|
12660
|
+
"submenuStyle": "dropdown",
|
|
12661
|
+
"activeColor": ""
|
|
12658
12662
|
},
|
|
12659
12663
|
"specials": {
|
|
12660
12664
|
"menuId": "",
|
|
@@ -19468,7 +19472,8 @@ export const ELEMENTS = {
|
|
|
19468
19472
|
"active": true,
|
|
19469
19473
|
"direction": "vertical",
|
|
19470
19474
|
"gap": 8
|
|
19471
|
-
}
|
|
19475
|
+
},
|
|
19476
|
+
"optionGap": ""
|
|
19472
19477
|
},
|
|
19473
19478
|
"specials": {
|
|
19474
19479
|
"displayType": "select",
|
|
@@ -30074,7 +30079,8 @@ export const ELEMENTS = {
|
|
|
30074
30079
|
"height": "fit-content"
|
|
30075
30080
|
},
|
|
30076
30081
|
"config": {
|
|
30077
|
-
"contentWidth": "fill_container"
|
|
30082
|
+
"contentWidth": "fill_container",
|
|
30083
|
+
"contentWidthCustom": 0
|
|
30078
30084
|
},
|
|
30079
30085
|
"bindings": [
|
|
30080
30086
|
{
|
|
@@ -31518,6 +31524,7 @@ export const ELEMENTS = {
|
|
|
31518
31524
|
},
|
|
31519
31525
|
"config": {
|
|
31520
31526
|
"datasetSource": "product",
|
|
31527
|
+
"articleSourceType": "category",
|
|
31521
31528
|
"loadingMode": "none",
|
|
31522
31529
|
"layout": "grid",
|
|
31523
31530
|
"quantity": 4,
|
|
@@ -86,6 +86,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
86
86
|
},
|
|
87
87
|
"config": {
|
|
88
88
|
"contentWidth": "fill_container",
|
|
89
|
+
"contentWidthCustom": 0,
|
|
89
90
|
"emptyStateId": "sppro_20"
|
|
90
91
|
},
|
|
91
92
|
"specials": {},
|
|
@@ -661,6 +662,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
661
662
|
"direction": "vertical",
|
|
662
663
|
"gap": 8
|
|
663
664
|
},
|
|
665
|
+
"optionGap": "",
|
|
664
666
|
"variantLabelId": "sppro_12",
|
|
665
667
|
"variantOptionId": "sppro_13"
|
|
666
668
|
},
|
|
@@ -1258,6 +1260,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
1258
1260
|
},
|
|
1259
1261
|
"config": {
|
|
1260
1262
|
"datasetSource": "product",
|
|
1263
|
+
"articleSourceType": "category",
|
|
1261
1264
|
"loadingMode": "none",
|
|
1262
1265
|
"layout": "grid",
|
|
1263
1266
|
"quantity": 4,
|
|
@@ -1341,6 +1344,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
1341
1344
|
},
|
|
1342
1345
|
"config": {
|
|
1343
1346
|
"contentWidth": "fill_container",
|
|
1347
|
+
"contentWidthCustom": 0,
|
|
1344
1348
|
"datasetSource": "product"
|
|
1345
1349
|
},
|
|
1346
1350
|
"specials": {},
|
|
@@ -1927,6 +1931,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
1927
1931
|
},
|
|
1928
1932
|
"config": {
|
|
1929
1933
|
"datasetSource": "product",
|
|
1934
|
+
"articleSourceType": "category",
|
|
1930
1935
|
"loadingMode": "none",
|
|
1931
1936
|
"layout": "grid",
|
|
1932
1937
|
"quantity": 4,
|
|
@@ -2010,6 +2015,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
2010
2015
|
},
|
|
2011
2016
|
"config": {
|
|
2012
2017
|
"contentWidth": "fill_container",
|
|
2018
|
+
"contentWidthCustom": 0,
|
|
2013
2019
|
"datasetSource": "product"
|
|
2014
2020
|
},
|
|
2015
2021
|
"specials": {},
|
|
@@ -2602,6 +2608,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
2602
2608
|
},
|
|
2603
2609
|
"config": {
|
|
2604
2610
|
"datasetSource": "article",
|
|
2611
|
+
"articleSourceType": "category",
|
|
2605
2612
|
"loadingMode": "none",
|
|
2606
2613
|
"layout": "grid",
|
|
2607
2614
|
"quantity": 4,
|
|
@@ -2686,6 +2693,7 @@ export const STORE_PAGE_SEEDS = {
|
|
|
2686
2693
|
},
|
|
2687
2694
|
"config": {
|
|
2688
2695
|
"contentWidth": "fill_container",
|
|
2696
|
+
"contentWidthCustom": 0,
|
|
2689
2697
|
"datasetSource": "article"
|
|
2690
2698
|
},
|
|
2691
2699
|
"specials": {},
|
|
@@ -155,6 +155,10 @@ function one(c, t) {
|
|
|
155
155
|
style: {
|
|
156
156
|
margin: '0',
|
|
157
157
|
...scale.style,
|
|
158
|
+
// ON THE NODE, never left to inheritance: the theme's heading preset
|
|
159
|
+
// declares its own textAlign, and a class rule beats an inherited
|
|
160
|
+
// value.
|
|
161
|
+
...(c.textAlign ? { textAlign: c.textAlign } : {}),
|
|
158
162
|
...(t.headingColor ? { color: t.headingColor } : {}),
|
|
159
163
|
...(t.headingWeight ? { fontWeight: t.headingWeight } : {}),
|
|
160
164
|
},
|
|
@@ -169,6 +173,7 @@ function one(c, t) {
|
|
|
169
173
|
specials: { htmlTag: 'p', text },
|
|
170
174
|
style: {
|
|
171
175
|
lineHeight: '1.7',
|
|
176
|
+
...(c.textAlign ? { textAlign: c.textAlign } : {}),
|
|
172
177
|
...(t.textColor ? { color: t.textColor } : {}),
|
|
173
178
|
...(t.textSize ? { fontSize: t.textSize } : {}),
|
|
174
179
|
},
|
|
@@ -288,6 +293,27 @@ function one(c, t) {
|
|
|
288
293
|
// a width/height ATTRIBUTE is a used height, and without this the cap below
|
|
289
294
|
// would be the thing ignored. `contain` because the source's crop is not
|
|
290
295
|
// ours to guess.
|
|
296
|
+
// A FRAME, WHEN THE CALLER MEASURED ONE. `height: auto` is what makes
|
|
297
|
+
// `aspect-ratio` work here at all — the platform writes the intrinsic
|
|
298
|
+
// width/height ATTRIBUTES onto every <img>, and a presentational height is
|
|
299
|
+
// a USED height, so the ratio would otherwise be ignored. That is the same
|
|
300
|
+
// fix the platform applied to its own media CSS.
|
|
301
|
+
//
|
|
302
|
+
// `cover` only inside a frame: filling one means cropping, which is the
|
|
303
|
+
// trade a wall of tiles makes on purpose. With no frame the answer stays
|
|
304
|
+
// `contain` — the source's crop is not ours to guess.
|
|
305
|
+
if (c.ratio) {
|
|
306
|
+
return {
|
|
307
|
+
type: 'image',
|
|
308
|
+
specials: { src: c.src, alt: c.alt ?? '' },
|
|
309
|
+
style: {
|
|
310
|
+
width: '100%',
|
|
311
|
+
height: 'auto',
|
|
312
|
+
aspectRatio: c.ratio,
|
|
313
|
+
objectFit: 'cover',
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
291
317
|
return {
|
|
292
318
|
type: 'image',
|
|
293
319
|
specials: { src: c.src, alt: c.alt ?? '' },
|
|
@@ -83,8 +83,17 @@ const row = (children, wrap = false, align) => ({
|
|
|
83
83
|
...(align ? { align } : {}),
|
|
84
84
|
children,
|
|
85
85
|
});
|
|
86
|
-
const h = (text, level = 2) => ({
|
|
87
|
-
|
|
86
|
+
const h = (text, level = 2, textAlign) => ({
|
|
87
|
+
kind: 'heading',
|
|
88
|
+
level,
|
|
89
|
+
text,
|
|
90
|
+
...(textAlign ? { textAlign } : {}),
|
|
91
|
+
});
|
|
92
|
+
const p = (text, textAlign) => ({
|
|
93
|
+
kind: 'text',
|
|
94
|
+
text,
|
|
95
|
+
...(textAlign ? { textAlign } : {}),
|
|
96
|
+
});
|
|
88
97
|
const cta = (text, href = '#') => ({ kind: 'button', variant: 'cta', text, href });
|
|
89
98
|
/**
|
|
90
99
|
* The picture slot: a real image when the site has one, and WORDS when it does
|
|
@@ -132,7 +141,10 @@ export const LAYOUT_PATTERNS = [
|
|
|
132
141
|
id: 'sb_hero_centered',
|
|
133
142
|
name: 'Hero — canh giữa',
|
|
134
143
|
use: 'Mở đầu trang khi chưa có ảnh: tiêu đề lớn, một câu, một nút',
|
|
135
|
-
build: (t) => section(
|
|
144
|
+
build: (t) => section(
|
|
145
|
+
// Centred on the NODES, not left to the section's inherited textAlign —
|
|
146
|
+
// the theme's heading preset declares its own and wins.
|
|
147
|
+
[h('Tiêu đề chính', 1, 'center'), p('Một câu nói rõ bạn bán gì và cho ai.', 'center'), cta('Mua ngay')], t, { alignItems: 'center', textAlign: 'center' }, { alignItems: 'center' }),
|
|
136
148
|
},
|
|
137
149
|
{
|
|
138
150
|
id: 'sb_feature_trio',
|
|
@@ -153,9 +165,9 @@ export const LAYOUT_PATTERNS = [
|
|
|
153
165
|
use: 'Ba đến bốn con số lớn với nhãn bên dưới — bằng chứng, không phải lời hứa',
|
|
154
166
|
build: (t) => section([
|
|
155
167
|
row([
|
|
156
|
-
{ kind: 'group', direction: 'column', children: [h('1.000+', 2), p('Khách hàng')] },
|
|
157
|
-
{ kind: 'group', direction: 'column', children: [h('4,9/5', 2), p('Đánh giá trung bình')] },
|
|
158
|
-
{ kind: 'group', direction: 'column', children: [h('24h', 2), p('Giao trong nội thành')] },
|
|
168
|
+
{ kind: 'group', direction: 'column', children: [h('1.000+', 2, 'center'), p('Khách hàng', 'center')] },
|
|
169
|
+
{ kind: 'group', direction: 'column', children: [h('4,9/5', 2, 'center'), p('Đánh giá trung bình', 'center')] },
|
|
170
|
+
{ kind: 'group', direction: 'column', children: [h('24h', 2, 'center'), p('Giao trong nội thành', 'center')] },
|
|
159
171
|
]),
|
|
160
172
|
], t, { alignItems: 'center', textAlign: 'center' }, { alignItems: 'center' }),
|
|
161
173
|
},
|
|
@@ -163,7 +175,11 @@ export const LAYOUT_PATTERNS = [
|
|
|
163
175
|
id: 'sb_cta_band',
|
|
164
176
|
name: 'Dải kêu gọi hành động',
|
|
165
177
|
use: 'Một câu và một nút, đặt cuối trang hoặc giữa hai band nội dung',
|
|
166
|
-
build: (t) => section(
|
|
178
|
+
build: (t) => section(
|
|
179
|
+
// CENTRED ON THE NODES. The section's own `textAlign` is inherited, and
|
|
180
|
+
// the theme's heading preset declares its own — so the band came out
|
|
181
|
+
// with its words hard left and only the button in the middle.
|
|
182
|
+
[h('Sẵn sàng bắt đầu?', 2, 'center'), p('Một câu nhắc lại lời hứa chính.', 'center'), cta('Mua ngay')], t, { alignItems: 'center', textAlign: 'center' }, { alignItems: 'center' }),
|
|
167
183
|
},
|
|
168
184
|
{
|
|
169
185
|
id: 'sb_gallery',
|
|
@@ -176,11 +192,32 @@ export const LAYOUT_PATTERNS = [
|
|
|
176
192
|
// SIX IS A WALL, THREE IS A ROW. Bounded because a gallery is a design
|
|
177
193
|
// decision and not a dump of the library — a merchant with 164 assets does
|
|
178
194
|
// not want 164 of them in one band.
|
|
195
|
+
const picked = [];
|
|
179
196
|
for (let i = 0; i < 6; i += 1) {
|
|
180
197
|
const m = pick(pool, used, 'any');
|
|
181
198
|
if (!m)
|
|
182
199
|
break;
|
|
183
|
-
|
|
200
|
+
picked.push(m);
|
|
201
|
+
}
|
|
202
|
+
// ONE FRAME FOR THE WALL, MEASURED RATHER THAN INVENTED.
|
|
203
|
+
//
|
|
204
|
+
// Rule 6 says match a frame's ratio to the ASSET, and a wall of
|
|
205
|
+
// photographs is the case it does not cover: there is no single asset.
|
|
206
|
+
// Left alone, every tile keeps its own shape and the grid's rows come out
|
|
207
|
+
// different heights — measured on a real build, four tiles at 330x220 and
|
|
208
|
+
// a fifth noticeably taller, which reads as unfinished.
|
|
209
|
+
//
|
|
210
|
+
// The MEDIAN of the pictures actually being shown is the honest frame:
|
|
211
|
+
// most crop by nothing, the outliers crop least, and a library of
|
|
212
|
+
// portraits gets a portrait wall rather than a landscape one imposed on
|
|
213
|
+
// it. A picture whose size the library did not report votes for nothing.
|
|
214
|
+
const ratios = picked
|
|
215
|
+
.map((m) => (m.width && m.height ? m.width / m.height : 0))
|
|
216
|
+
.filter((r) => r > 0)
|
|
217
|
+
.sort((a, b) => a - b);
|
|
218
|
+
const frame = ratios.length ? ratios[Math.floor(ratios.length / 2)].toFixed(2) : undefined;
|
|
219
|
+
for (const m of picked) {
|
|
220
|
+
shots.push({ kind: 'image', src: m.url, alt: m.name ?? 'Ảnh', ...(frame ? { ratio: frame } : {}) });
|
|
184
221
|
}
|
|
185
222
|
if (shots.length === 0) {
|
|
186
223
|
return section([
|
package/dist/vision/capture.js
CHANGED
|
@@ -274,6 +274,23 @@ function capturePage(limits) {
|
|
|
274
274
|
* loader is displaying until the real one arrives, which is precisely the
|
|
275
275
|
* placeholder this import must not ship.
|
|
276
276
|
*/
|
|
277
|
+
/**
|
|
278
|
+
* The SOURCE'S OWN text alignment, and only when it is not the default.
|
|
279
|
+
*
|
|
280
|
+
* A hero that centres its copy is making a decision; a copy that left-aligns
|
|
281
|
+
* it is not the same band. `start`/`left` is the browser's answer and saying
|
|
282
|
+
* it out loud would put a value on every imported paragraph for nothing —
|
|
283
|
+
* and worse, would override the target page's own centred preset with a
|
|
284
|
+
* literal.
|
|
285
|
+
*/
|
|
286
|
+
const alignOf = (el) => {
|
|
287
|
+
const a = getComputedStyle(el).textAlign;
|
|
288
|
+
if (a === 'center')
|
|
289
|
+
return { textAlign: 'center' };
|
|
290
|
+
if (a === 'right' || a === 'end')
|
|
291
|
+
return { textAlign: 'right' };
|
|
292
|
+
return {};
|
|
293
|
+
};
|
|
277
294
|
const LAZY_SRC = ['data-src', 'data-original', 'data-lazy-src', 'data-lazy', 'data-echo', 'data-url'];
|
|
278
295
|
const widest = (srcset) => {
|
|
279
296
|
let best = '';
|
|
@@ -657,7 +674,7 @@ function capturePage(limits) {
|
|
|
657
674
|
if (!text)
|
|
658
675
|
return [];
|
|
659
676
|
taken.nodes++;
|
|
660
|
-
return [{ kind: 'heading', level: Number(tag.slice(1)), text }];
|
|
677
|
+
return [{ kind: 'heading', level: Number(tag.slice(1)), text, ...alignOf(el) }];
|
|
661
678
|
}
|
|
662
679
|
if (tag === 'IMG') {
|
|
663
680
|
const src = realSrc(el);
|
|
@@ -801,7 +818,7 @@ function capturePage(limits) {
|
|
|
801
818
|
if (!text)
|
|
802
819
|
return [];
|
|
803
820
|
taken.nodes++;
|
|
804
|
-
return [{ kind: 'text', text }];
|
|
821
|
+
return [{ kind: 'text', text, ...alignOf(el) }];
|
|
805
822
|
}
|
|
806
823
|
const kids = walkChildren(el);
|
|
807
824
|
if (kids.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "MCP server that designs and operates a Store Builder site — pages, data, theme and publish — through the platform's own API and live-edit protocol.",
|
|
5
5
|
"mcpName": "io.github.vuluu2k/sbuilder-mcp",
|
|
6
6
|
"type": "module",
|