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 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) => ({ kind: 'heading', level, text });
87
- const p = (text) => ({ kind: 'text', text });
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([h('Tiêu đề chính', 1), p('Một câu nói rõ bạn bán gì và cho ai.'), cta('Mua ngay')], t, { alignItems: 'center', textAlign: 'center' }, { alignItems: 'center' }),
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([h('Sẵn sàng bắt đầu?'), p('Một câu nhắc lại lời hứa chính.'), cta('Mua ngay')], t, { alignItems: 'center', textAlign: 'center' }, { alignItems: 'center' }),
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
- shots.push({ kind: 'image', src: m.url, alt: m.name ?? 'Ảnh' });
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([
@@ -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.32.1",
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",